diff --git a/.changeset/consolidate-taskless-skills.md b/.changeset/consolidate-taskless-skills.md new file mode 100644 index 00000000..d309e447 --- /dev/null +++ b/.changeset/consolidate-taskless-skills.md @@ -0,0 +1,25 @@ +--- +"@taskless/cli": minor +--- + +Consolidate the 10 per-task Taskless skills into one. The `taskless` skill is now a small router whose body tells the agent to fetch the canonical recipe via `npx @taskless/cli help ` rather than carrying full per-task instructions inline. Recipes live in the CLI bundle so the agent always reads the version current to the installed CLI. This addresses customer reports of the Taskless plugin causing other skills to be evicted from the working set. + +**Breaking changes:** + +- **Skill names removed.** `taskless-check`, `taskless-ci`, `taskless-create-rule`, `taskless-create-rule-anonymous`, `taskless-delete-rule`, `taskless-improve-rule`, `taskless-improve-rule-anonymous`, `taskless-info`, `taskless-login`, `taskless-logout` no longer exist. The single `taskless` skill replaces them. Existing v0.6 installs auto-migrate when the user runs `npx @taskless/cli` (the install plumbing reads the manifest, deletes obsolete files, writes the consolidated skill). +- **Slash commands collapsed.** The 6 commands under `commands/tskl/` are replaced by a single `/tskl` router that accepts a free-form `$ARGUMENTS` ask. +- **CLI verb renamed: `rules` → `rule` (singular).** `taskless rule create`, `taskless rule improve`, `taskless rule delete`, `taskless rule verify`, `taskless rule meta`. The plural form is no longer recognized — there is no compatibility alias. Pipelines and scripts must update. +- **`--schema` flag removed.** Schemas are now embedded inline in `taskless help ` output via `z.toJSONSchema()` (zod 4 built-in). Agents that previously parsed `--schema` output should fetch the relevant `help` topic and read the embedded code-fenced JSON Schema block. +- **Telemetry rename (hard cut, no dual-emit).** `cli_help_*` events are renamed to `help_` (intent), `help_index` (no-args fetch), and `help_unknown` (unrecognized topic). Action commands now emit `cli_` (start) and `cli__completed` (with `success`, `durationMs`, `errorCode?` properties). PostHog dashboards keyed on the old names will need updates. + +**New features:** + +- **Global `--anonymous` flag.** Recognized on every command. Per-command behavior: `info` skips the API/auth probe; `auth login` errors with "auth commands cannot be anonymous"; `rule create`/`rule improve` exit with a pointer to `taskless help --anonymous` (the local-only flow runs in the agent per the architecture decision in the OpenSpec change). +- **`taskless help --anonymous`.** Variant lookup serves `.anonymous.txt` when present and falls back to the canonical recipe otherwise. Build-time map keeps lookup O(1). +- **Standardized JSON error envelope.** When `--json` is set, failures emit `{ ok: false, code: "", message: "<...>" }` with stable codes (`AUTH_REQUIRED`, `NO_GITHUB_REMOTE`, `RULE_GENERATION_FAILED`, `RULE_NOT_FOUND`, `INVALID_INPUT`, `NETWORK_ERROR`, `SCAN_FAILED`, `INTERNAL_ERROR`). Recipes reference these codes by name in their `## Errors` sections. +- **Recipe template.** Every help text follows the same shape: Goal / Preconditions / Steps / Input schema (where applicable) / Errors / See Also. Header line includes the CLI version and a topic version. `{{INPUT_SCHEMA}}` and `{{CLI_VERSION}}` placeholders are interpolated at runtime. +- **Bare `taskless` non-TTY routing.** Without a TTY, bare `taskless` now prints a short context preamble followed by the topic index (instead of citty's default usage screen). TTY behavior unchanged — still launches the wizard. + +**Migration:** + +Run `npx @taskless/cli` after upgrading. The wizard reads your existing manifest, computes the diff (10 obsolete skills + 6 obsolete commands removed, 1 new skill + 1 new command added), confirms with you, then applies. diff --git a/.claude/.gitignore b/.claude/.gitignore new file mode 100644 index 00000000..e40690f0 --- /dev/null +++ b/.claude/.gitignore @@ -0,0 +1,2 @@ +*.local.json +*.lock \ No newline at end of file diff --git a/.claude/skills/taskless b/.claude/skills/taskless new file mode 120000 index 00000000..7452edf4 --- /dev/null +++ b/.claude/skills/taskless @@ -0,0 +1 @@ +../../skills/taskless \ No newline at end of file diff --git a/.claude/skills/taskless-check b/.claude/skills/taskless-check deleted file mode 120000 index be2c6b31..00000000 --- a/.claude/skills/taskless-check +++ /dev/null @@ -1 +0,0 @@ -../../skills/taskless-check \ No newline at end of file diff --git a/.claude/skills/taskless-ci b/.claude/skills/taskless-ci deleted file mode 120000 index e6fb029f..00000000 --- a/.claude/skills/taskless-ci +++ /dev/null @@ -1 +0,0 @@ -../../skills/taskless-ci \ No newline at end of file diff --git a/.claude/skills/taskless-create-rule b/.claude/skills/taskless-create-rule deleted file mode 120000 index 9a58c1de..00000000 --- a/.claude/skills/taskless-create-rule +++ /dev/null @@ -1 +0,0 @@ -../../skills/taskless-create-rule \ No newline at end of file diff --git a/.claude/skills/taskless-create-rule-anonymous b/.claude/skills/taskless-create-rule-anonymous deleted file mode 120000 index 095571c5..00000000 --- a/.claude/skills/taskless-create-rule-anonymous +++ /dev/null @@ -1 +0,0 @@ -../../skills/taskless-create-rule-anonymous \ No newline at end of file diff --git a/.claude/skills/taskless-delete-rule b/.claude/skills/taskless-delete-rule deleted file mode 120000 index 9eb31c1e..00000000 --- a/.claude/skills/taskless-delete-rule +++ /dev/null @@ -1 +0,0 @@ -../../skills/taskless-delete-rule \ No newline at end of file diff --git a/.claude/skills/taskless-improve-rule b/.claude/skills/taskless-improve-rule deleted file mode 120000 index c247a2fb..00000000 --- a/.claude/skills/taskless-improve-rule +++ /dev/null @@ -1 +0,0 @@ -../../skills/taskless-improve-rule \ No newline at end of file diff --git a/.claude/skills/taskless-improve-rule-anonymous b/.claude/skills/taskless-improve-rule-anonymous deleted file mode 120000 index d3aaaf0e..00000000 --- a/.claude/skills/taskless-improve-rule-anonymous +++ /dev/null @@ -1 +0,0 @@ -../../skills/taskless-improve-rule-anonymous \ No newline at end of file diff --git a/.claude/skills/taskless-info b/.claude/skills/taskless-info deleted file mode 120000 index 6f1b11c7..00000000 --- a/.claude/skills/taskless-info +++ /dev/null @@ -1 +0,0 @@ -../../skills/taskless-info \ No newline at end of file diff --git a/.claude/skills/taskless-login b/.claude/skills/taskless-login deleted file mode 120000 index a6f813f6..00000000 --- a/.claude/skills/taskless-login +++ /dev/null @@ -1 +0,0 @@ -../../skills/taskless-login \ No newline at end of file diff --git a/.claude/skills/taskless-logout b/.claude/skills/taskless-logout deleted file mode 120000 index 6a6f6059..00000000 --- a/.claude/skills/taskless-logout +++ /dev/null @@ -1 +0,0 @@ -../../skills/taskless-logout \ No newline at end of file diff --git a/.taskless/taskless.json b/.taskless/taskless.json index 4c25d9e7..01bce72b 100644 --- a/.taskless/taskless.json +++ b/.taskless/taskless.json @@ -3,28 +3,11 @@ "install": { "targets": { ".claude": { - "skills": [ - "taskless-check", - "taskless-create-rule", - "taskless-create-rule-anonymous", - "taskless-delete-rule", - "taskless-improve-rule", - "taskless-improve-rule-anonymous", - "taskless-info", - "taskless-login", - "taskless-logout" - ], - "commands": [ - "check.md", - "improve.md", - "info.md", - "login.md", - "logout.md", - "rule.md" - ] + "skills": ["taskless"], + "commands": ["tskl.md"] } }, - "installedAt": "2026-04-17T21:24:28.613Z", - "cliVersion": "0.5.4" + "installedAt": "2026-05-11T16:38:34.273Z", + "cliVersion": "0.6.0" } } diff --git a/README.md b/README.md index f2757bec..c3271624 100644 --- a/README.md +++ b/README.md @@ -8,33 +8,28 @@ Includes a CLI that works with agentic systems (and humans too) at @taskless/cli ``` skills/ - taskless-info/SKILL.md # Confirms Taskless is working - taskless-login/SKILL.md # Explains auth login - taskless-logout/SKILL.md # Explains auth logout - taskless-rule-create/SKILL.md # Creates a Taskless rule - taskless-rule-delete/SKILL.md # Deletes a Taskless rule + taskless/SKILL.md # Single consolidated router skill commands/ - taskless/ # Generated commands (do not edit) + tskl/tskl.md # Single /tskl router command packages/ - cli/ # @taskless/cli + cli/ # @taskless/cli — recipes live in cli/src/help/ scripts/ - generate-commands.ts # Generates commands from skills link-skills.ts # Symlinks skills into .claude/skills/ sync-skill-versions.ts # Syncs metadata.version to CLI version .claude-plugin/ # Claude Code Plugin Marketplace manifest ``` -## Skills +## Skill -Skills follow the [Agent Skills Specification](https://agentskills.io) with additional hooks for Claude Code integration. +Starting in v0.7, Taskless ships a **single consolidated skill** (`taskless`) plus a single `/tskl` slash command. The skill body is a small router; per-task instructions live behind `npx @taskless/cli help ` and are fetched on demand. -| Skill | Command | Description | -| -------------------- | ------------------ | ------------------------------------------ | -| taskless-info | `/taskless:info` | Confirms Taskless is installed and working | -| taskless-login | `/taskless:login` | Explains how to authenticate | -| taskless-logout | `/taskless:logout` | Explains how to remove credentials | -| taskless-rule-create | `/taskless:rule` | Creates a new Taskless rule | -| taskless-rule-delete | — | Deletes a Taskless rule | +| Skill | Command | Description | +| ---------- | ------------- | ------------------------------------------------------ | +| `taskless` | `/tskl ` | Router for any Taskless action (create rule, improve, | +| | | delete, check, auth, CI). Fetches the canonical recipe | +| | | for the user's intent and follows it. | + +Available `taskless help` topics: `rule create`, `rule improve`, `rule delete`, `check`, `auth`, `ci`, `info`, `init`, `update`. Append `--anonymous` for the local-only flow on rule create/improve. ## CLI @@ -62,14 +57,17 @@ git commit -m "chore: Releases vx.y.z" # Commit with new version number pnpm release # Dry run — prints publish command when ready ``` -### Adding a new skill +### Adding a new topic recipe + +In v0.7+, new agent-facing instructions are added as **recipes**, not skills. To add a recipe: -1. Create `skills/taskless-/SKILL.md` with frontmatter including `metadata.commandName` -2. Set `commandName` to `"taskless:"` for a slash command, or `"-"` for no command -3. Run `pnpm build` — commands are generated automatically and embedded into the CLI +1. Create `packages/cli/src/help/.txt` following the canonical template (Goal / Preconditions / Steps / Input schema / Errors / See Also). +2. Use `{{CLI_VERSION}}` and `{{INPUT_SCHEMA}}` placeholders for runtime interpolation. +3. For topics with a substantively different local-only flow, add `.anonymous.txt`. The help command's variant lookup is automatic. +4. Update the topic table in `skills/taskless/SKILL.md` and `commands/tskl/tskl.md` so agents can discover the new topic. ### Distribution channels -- **`taskless init`** — CLI installs skills to `.claude/skills/` and commands to `.claude/commands/taskless/` +- **`taskless init`** — CLI installs the consolidated skill to `.claude/skills/taskless/` and the command to `.claude/commands/tskl/` - **Claude Code Plugin Marketplace** — `.claude-plugin/marketplace.json` and `plugin.json` - **Vercel Skills CLI** — `npx skills add` discovers skills from `skills/` directory diff --git a/commands/tskl/check.md b/commands/tskl/check.md deleted file mode 100644 index 58393700..00000000 --- a/commands/tskl/check.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: "Taskless: Check" -description: Checks a repository using the Taskless rules via the CLI. Use when the user wants to run a check, test rules, or validate code against taskless rules. Trigger on "check my code", "run taskless check", "test my rules", or "validate with taskless". -category: Taskless -tags: - - taskless -metadata: - author: taskless - version: 0.6.0 - commandName: tskl:check ---- - -# Taskless Check - -When this skill is invoked, perform a check of the codebase using the Taskless CLI and report the results. - -## Instructions - -**Package manager:** All commands below use `npx` as the default. If the project uses a different package manager (check for `pnpm-lock.yaml`, `yarn.lock`, or `bun.lockb`), prefer its equivalent: `pnpm dlx`, `yarn dlx` (Yarn Berry/2+ only), or `bunx`. - -1. **Read current command documentation.** Run `npx @taskless/cli@latest help check` and read the output. Use this to understand the command's options, output format, and exit codes. - -2. **Invoke the CLI with JSON output.** Run `npx @taskless/cli@latest check --json` and capture stdout. - -3. **Parse the response.** Parse the JSON output with `JSON.parse()`. Use the fields described in the help output to determine success or failure and report any issues found to the user. - -4. **Handle errors.** If the command exits with a non-zero code or the output is not valid JSON, report the error and suggest running `npx @taskless/cli@latest init` if configuration is missing. diff --git a/commands/tskl/improve.md b/commands/tskl/improve.md deleted file mode 100644 index e77f79bf..00000000 --- a/commands/tskl/improve.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -name: "Taskless: Improve" -description: Improves existing Taskless rules by iterating with guidance. Use when the user wants to refine, fix, or improve existing rules. Trigger on "improve rule", "fix my rule", "iterate on rule", "refine taskless rule", or "my rule isn't working". -category: Taskless -tags: - - taskless -metadata: - author: taskless - version: 0.6.0 - commandName: tskl:improve ---- - -# Taskless Improve - -When this skill is invoked, help the user improve an existing Taskless rule by determining the best approach and executing it. - -This is a decision-making skill. You must evaluate the situation and choose the right strategy — not every improvement is a simple iteration. - -## Instructions - -**Package manager:** All commands below use `npx` as the default. If the project uses a different package manager (check for `pnpm-lock.yaml`, `yarn.lock`, or `bun.lockb`), prefer its equivalent: `pnpm dlx`, `yarn dlx` (Yarn Berry/2+ only), or `bunx`. - -1. **Check authentication status.** Run `npx @taskless/cli@latest info --json` and parse the JSON output. Check the `loggedIn` field: - - If `loggedIn` is `true`: continue with step 2 below (API-backed flow). - - If `loggedIn` is `false`: **stop here** and invoke the `taskless-improve-rule-anonymous` skill instead. Pass along any context the user has already provided about which rule to improve and what changes they want. - -2. **Read current command documentation.** Run `npx @taskless/cli@latest help rules improve` and read the output. Use this to understand the improve command's `--from` JSON fields, options, and examples. - -3. **Inventory existing rules.** If the user has already named a specific rule, skip to that rule directly. Otherwise, scan the `.taskless/rules/` directory for `.yml` files and present a summary. For each rule, note: - - The rule ID (filename without `.yml`) - - The language it targets - - The pattern it detects (from the `message`, `note`, or `rule` fields) - - Any associated test files in `.taskless/rule-tests/` - - Once a rule is selected, check for its sidecar metadata by running `npx @taskless/cli@latest rules meta --json`. If metadata exists, note the `ticketId` — this is required for the iterate API. - -4. **Understand the improvement request.** Ask the user what they want to improve. Gather specifics: - - Which rule(s) are problematic? - - What is the rule doing wrong? (false positives, false negatives, wrong fix, missing edge cases, etc.) - - Can they show an example of the incorrect behavior? - - What would the correct behavior look like? - -5. **Search for evidence in the codebase.** Proactively scan the codebase for instances where the rule is triggering (or failing to trigger). Show the user what you found: - - "I found N places where this rule fires. Are any of these false positives?" - - "I found N places where this pattern exists but the rule doesn't catch it. Should it?" - -6. **Decide on approach.** Based on the user's feedback and your analysis, determine the best strategy: - - ### Option A — Iterate on a single rule (most common) - - Use this when the user wants to refine an existing rule that is fundamentally correct but needs adjustment. Examples: - - The rule has false positives that need to be excluded - - The rule misses certain variations of the pattern - - The fix suggestion is incorrect or incomplete - - The rule needs to handle edge cases better - - ### Option B — Replace an existing rule - - Use this when the rule is fundamentally wrong and needs a completely different approach. Examples: - - The rule's pattern matching strategy is incorrect (e.g., using string matching when AST matching is better suited to the task) - - The rule targets the wrong language construct entirely - - The user's requirements have changed significantly from the original rule - - For this approach: create a new rule (via the rule create flow) and then delete the old one. - - ### Option C — Create additional rules - - Use this when the user's need has expanded beyond what a single rule can cover. Examples: - - The user wants to detect the same pattern in multiple languages - - The pattern has distinct variants that are better handled by separate rules - - The user wants related but distinct checks - - For this approach: create new rules and optionally remove old ones that are being superseded. - - **Present your chosen approach to the user and get confirmation before proceeding.** - -7. **Execute the chosen approach.** - - ### For Option A (iterate): - - a. **Build the JSON payload.** Create a JSON object with: - - `ruleId`: The ticket ID from the rule's sidecar metadata. Retrieve it by running `npx @taskless/cli@latest rules meta --json` and reading the `ticketId` field. If no metadata file exists (rule was created before metadata support), fall back to using the rule filename as the identifier. Providing the ticket ID allows the API to understand the existing rule's logic and how to adjust it based on your guidance. - - `guidance`: A clear, specific description of what should change. Include: - - What the rule is doing wrong - - What it should do instead - - Specific examples of false positives/negatives - - Any exclusions or edge cases to handle - - `references` (optional): Include the current rule file and test file contents so the API has full context. Each reference is `{ "filename": "", "content": "" }`. - - Example payload (note: `ruleId` is the `ticketId` UUID from `rules meta --json`, not the rule filename): - - ```json - { - "ruleId": "d4f8e2a1-7b3c-4e9f-a5d6-1c2b3e4f5a6b", - "guidance": "The rule currently flags console.log statements inside catch blocks, but these are intentional error logging. Exclude console.log/console.error/console.warn calls that appear inside catch blocks. Also exclude any console calls in files under src/scripts/ as those are CLI tools where console output is expected.", - "references": [ - { - "filename": "rules/no-console-log.yml", - "content": "id: no-console-log\nlanguage: typescript\n..." - }, - { - "filename": "rule-tests/no-console-log-20260328-test.yml", - "content": "id: no-console-log\n..." - } - ] - } - ``` - - b. **Write the JSON to a temp file.** Write to `.taskless/.tmp-improve-request.json`. - - c. **Invoke the CLI.** Run `npx @taskless/cli@latest rules improve --from .taskless/.tmp-improve-request.json --json`. The command may take 30-60 seconds as it polls the API. - - d. **Clean up.** After the command completes (success or failure), delete `.taskless/.tmp-improve-request.json`. - - e. **Report results.** Show the updated file paths and suggest running `taskless-check` to test the changes. The CLI also updates the sidecar metadata in `.taskless/rule-metadata/`. - - ### For Option B (replace): - - a. Note the old rule ID for deletion. - b. Invoke the `taskless-create-rule` skill (command name `tskl:rule`) to create the replacement rule. This ensures the full enrichment workflow (examples, exclusions, confirmation) is followed. - c. After the new rule is generated, delete the old rule: `npx @taskless/cli@latest rules delete `. - d. Report results. - - ### For Option C (expand): - - a. For each new rule needed, invoke the `taskless-create-rule` skill (command name `tskl:rule`). - b. If any old rules are being superseded, delete them after the new rules are created: `npx @taskless/cli@latest rules delete `. - c. Report all changes. - -8. **Suggest testing.** After any approach, suggest running `taskless-check` to test the updated rules against the codebase. - -9. **Handle errors.** If the CLI fails: - - **Authentication required**: Suggest the `taskless-login` skill. - - **Missing organization info**: Suggest running `npx @taskless/cli@latest auth login` to re-authenticate. - - **Rule not found**: The ruleId may be incorrect. Check the rule's metadata or suggest creating a new rule instead. - - **API errors**: Report the error message and suggest trying again. diff --git a/commands/tskl/info.md b/commands/tskl/info.md deleted file mode 100644 index 6e765056..00000000 --- a/commands/tskl/info.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -name: "Taskless: Info" -description: Confirms that the Taskless skills plugin is installed and working. Use when the user wants to verify their Taskless setup, check plugin status, test the connection, or run a health check. Trigger on "is taskless working", "check taskless", "taskless status", or "taskless info". -category: Taskless -tags: - - taskless -metadata: - author: taskless - version: 0.6.0 - commandName: tskl:info ---- - -# Taskless Info - -When this skill is invoked, verify that the Taskless CLI is reachable and report its version. - -## Instructions - -**Package manager:** All commands below use `npx` as the default. If the project uses a different package manager (check for `pnpm-lock.yaml`, `yarn.lock`, or `bun.lockb`), prefer its equivalent: `pnpm dlx`, `yarn dlx` (Yarn Berry/2+ only), or `bunx`. - -1. **Read current command documentation.** Run `npx @taskless/cli@latest help info` and read the output. Use this to understand the command's output format and available options. - -2. **Invoke the CLI.** Run `npx @taskless/cli@latest info` and capture stdout. - -3. **Parse the response.** The CLI outputs JSON to stdout. Parse it with `JSON.parse()` and extract the fields described in the help output. Key fields to report: - - `version`: The version of the Taskless CLI. - - `tools`: An array of coding agent tools with their installed skills and versions. - - `loggedIn`: Indicates if the user is logged into Taskless. - -4. **Report the result.** Display a confirmation message with the version: - - ``` - Taskless skills plugin is installed and working. - CLI version: - - Tools: - - - - : Installed version , Current version , Up to date: - ... - ``` - -5. **Handle errors.** If the command fails (non-zero exit code) or the output is not valid JSON: - - Report that the Taskless CLI could not be reached. - - Suggest checking network connectivity and that npm/pnpm is available. - - Show the raw error output if available. - -6. **Report if Upgrade is Required** If any installed skill is not current, include a note that an upgrade is recommended. Offer to run `npx @taskless/cli@latest init` for them to reinitialize with the latest skills. - -## Example Output - -``` -Taskless skills plugin is installed and working. -CLI version: 0.0.1 -``` diff --git a/commands/tskl/login.md b/commands/tskl/login.md deleted file mode 100644 index e7e1d082..00000000 --- a/commands/tskl/login.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: "Taskless: Login" -description: Explains how to authenticate with Taskless. Use when the user wants to log in, authenticate, connect their account, or set up credentials. Trigger on "taskless login", "authenticate taskless", "taskless auth", or "connect to taskless". -category: Taskless -tags: - - taskless -metadata: - author: taskless - version: 0.6.0 - commandName: tskl:login ---- - -# Taskless Login - -When this skill is invoked, explain the authentication process and provide the CLI command the user needs to run. - -**Important:** Do NOT attempt to run the login command. The device flow requires interactive terminal input (displaying a URL and polling for browser-based authorization) that cannot be performed by an agent. - -## Instructions - -**Package manager:** All commands below use `npx` as the default. If the project uses a different package manager (check for `pnpm-lock.yaml`, `yarn.lock`, or `bun.lockb`), prefer its equivalent: `pnpm dlx`, `yarn dlx` (Yarn Berry/2+ only), or `bunx`. - -1. **Read current command documentation.** Run `npx @taskless/cli@latest help auth login` and read the output. Use this to understand the login flow, credential storage, and alternatives. - -2. **Present the login command and explain the process.** Using the information from the help output, display the command the user should run in their terminal and explain what will happen (device flow, credential storage, environment variable alternative). diff --git a/commands/tskl/logout.md b/commands/tskl/logout.md deleted file mode 100644 index 805ba9a3..00000000 --- a/commands/tskl/logout.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: "Taskless: Logout" -description: Explains how to remove saved Taskless authentication. Use when the user wants to log out, disconnect, remove credentials, or clear their Taskless session. Trigger on "taskless logout", "disconnect taskless", or "remove taskless auth". -category: Taskless -tags: - - taskless -metadata: - author: taskless - version: 0.6.0 - commandName: tskl:logout ---- - -# Taskless Logout - -When this skill is invoked, explain how to remove saved authentication and provide the CLI command. - -**Important:** Do NOT attempt to run the logout command. Provide the command for the user to run in their terminal. - -## Instructions - -**Package manager:** All commands below use `npx` as the default. If the project uses a different package manager (check for `pnpm-lock.yaml`, `yarn.lock`, or `bun.lockb`), prefer its equivalent: `pnpm dlx`, `yarn dlx` (Yarn Berry/2+ only), or `bunx`. - -1. **Read current command documentation.** Run `npx @taskless/cli@latest help auth logout` and read the output. Use this to understand what the command does, credential storage location, and any caveats. - -2. **Present the logout command and explain what it does.** Using the information from the help output, display the command the user should run and explain the effects (credential removal, environment variable note). diff --git a/commands/tskl/rule.md b/commands/tskl/rule.md deleted file mode 100644 index 12be1d7a..00000000 --- a/commands/tskl/rule.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -name: "Taskless: Rule" -description: Creates a new Taskless rule from a description. Use when the user wants to create a rule, add a lint rule, define a code pattern to detect, or generate an ast-grep rule. Trigger on "create a rule", "add a taskless rule", "new rule for", or "detect this pattern". -category: Taskless -tags: - - taskless -metadata: - author: taskless - version: 0.6.0 - commandName: tskl:rule ---- - -# Taskless Rule Create - -When this skill is invoked, work with the user to build a comprehensive rule request and generate a rule. - -Your goal is to produce the best possible rule by enriching the user's initial description with concrete examples, edge cases, and exclusions — not just pass their request through verbatim. - -## Instructions - -**Package manager:** All commands below use `npx` as the default. If the project uses a different package manager (check for `pnpm-lock.yaml`, `yarn.lock`, or `bun.lockb`), prefer its equivalent: `pnpm dlx`, `yarn dlx` (Yarn Berry/2+ only), or `bunx`. - -1. **Check authentication status.** Run `npx @taskless/cli@latest info --json` and parse the JSON output. Check the `loggedIn` field: - - If `loggedIn` is `true`: continue with step 2 below (API-backed flow). - - If `loggedIn` is `false`: **stop here** and invoke the `taskless-create-rule-anonymous` skill instead. Pass along any context the user has already provided about the rule they want to create. - -2. **Read current command documentation.** Run `npx @taskless/cli@latest help rules create` and read the output. Use this to understand the command's `--from` JSON fields, options, and examples. - -3. **Gather the rule description.** Even if the user provided a description with their command, you MUST ask clarifying questions before proceeding. Do NOT skip to rule generation. Ask what specific code pattern should be flagged, with concrete examples. This becomes the `prompt` field (required). - -4. **Check for existing similar rules.** Once you have the user's description, scan `.taskless/rules/` for existing rule files. Read each rule's `message`, `note`, and `rule` fields to understand what patterns are already covered. If any existing rule appears to overlap with the user's request: - - Show the user the similar rule(s) and explain the overlap. - - Ask: "It looks like you already have a rule that covers something similar. Would you like to **improve the existing rule** instead of creating a new one?" - - If the user wants to improve, stop this skill and invoke the `taskless-improve-rule` skill (command name `tskl:improve`) with context about which rule to iterate on. - - If the user confirms they want a separate new rule, proceed with creation. - -5. **Enrich the request.** After receiving the initial description, actively work with the user to strengthen the rule. Do all of the following: - - a. **Search the codebase for real examples.** Proactively scan the codebase for instances of the pattern they want to detect. Show them what you found and ask: - - "I found N instances of this pattern in your codebase. Should I include some as examples in the rule request?" - - If you find variations of the pattern, highlight them as potential edge cases. - - b. **Ask for success and failure cases.** Even if the user provided examples, ask if there are other cases to consider: - - "Are there edge cases or variations of this pattern that should also be caught?" - - "Can you show me an example of the _correct_ way to write this code?" - - Use any examples the user provided in their description as a starting point, but look for more. - - c. **Collect default ignores from the project.** Before asking the user about exclusions, check the project for existing ignore patterns that inform what files the rule should skip. Look at: - - `.gitignore` — files already excluded from version control (e.g., `node_modules/`, `dist/`, build artifacts) - - Linter configs (e.g., `eslint.config.js`, `.eslintignore`) — files or directories already excluded from linting - - `tsconfig.json` `exclude` field — files excluded from type checking - - Any other relevant config that signals "these files are not authored source code" - - Use these to build a baseline set of ignores. Present them to the user as defaults that will be included in the rule prompt. - - d. **Ask about additional exclusions.** Beyond the defaults, ask the user if there are files, directories, or contexts where the pattern is acceptable: - - "Are there any files or directories where this pattern should be allowed? (e.g., `.d.ts` files, test files, generated code)" - - Incorporate both the default ignores and user-specified exclusions into the `prompt` field so the rule generator understands the boundaries. - - e. **Infer the language.** Detect the primary language from the codebase or the user's examples. Confirm your assumption with the user. Include the target language in the `prompt` field (e.g., "Detect X in TypeScript files") so the rule generator knows what language to target. - -6. **Confirm the enriched request.** Before submitting, present a summary of what you'll send to the API: - - The full prompt (including language and any exclusion notes) - - The success case(s) - - The failure case(s) - - Ask the user to confirm or adjust before proceeding. - -7. **Write the JSON payload to a file.** Build a JSON object with the gathered fields. Write the JSON to `.taskless/.tmp-rule-request.json`. - - **Multiple examples:** The `successCases` and `failureCases` fields are arrays of strings. Each example is a separate array element: - - ```json - { - "prompt": "...", - "failureCases": [ - "/// \nexport class MyWorker { ... }", - "/// \nconst x = import.meta.env.FOO;" - ], - "successCases": [ - "import type { DurableObjectState } from 'cloudflare:workers';\nexport class MyWorker { ... }", - "// .d.ts files are exempt — triple-slash is idiomatic there\n/// " - ] - } - ``` - -8. **Invoke the CLI.** Run `npx @taskless/cli@latest rules create --from .taskless/.tmp-rule-request.json --json`. The command may take 30-60 seconds as it polls the API. - -9. **Clean up.** After the command completes (success or failure), delete the `.taskless/.tmp-rule-request.json` file. - -10. **Report the results.** When the CLI completes, show the generated file paths and suggest running `taskless-check` to test the new rule. The CLI also writes sidecar metadata to `.taskless/rule-metadata/.yml` containing the `ticketId` used for future iterations. You can retrieve this with `npx @taskless/cli@latest rules meta --json`. - -11. **Handle errors.** If the CLI fails: - - **Authentication required**: Suggest the `taskless-login` skill. - - **Missing organization info**: Suggest running `npx @taskless/cli@latest auth login` to re-authenticate. - - **API errors**: Report the error message and suggest trying again. diff --git a/commands/tskl/tskl.md b/commands/tskl/tskl.md new file mode 100644 index 00000000..6d759319 --- /dev/null +++ b/commands/tskl/tskl.md @@ -0,0 +1,43 @@ +--- +name: "Taskless" +description: Run any Taskless action — create/improve/delete a rule, run check, manage auth, or wire CI. Routes via `npx @taskless/cli help ` to fetch the canonical recipe and follow it. +category: Taskless +argument-hint: +tags: + - taskless +metadata: + author: taskless + version: 0.6.0 + commandName: tskl +--- + +# Taskless + +The user invoked Taskless via `/tskl` with: $ARGUMENTS + +If `$ARGUMENTS` is empty or ambiguous, ask the user what they want to do +with Taskless before proceeding. + +Otherwise, follow the same flow as the `taskless` skill: + +1. Identify the topic from `$ARGUMENTS` using the table below. +2. Fetch the canonical recipe with `npx @taskless/cli help ` (or + `npx @taskless/cli help --anonymous` if the user is offline or + explicitly asked for anonymous mode). +3. Follow the recipe step-by-step. The recipe is canonical for the + currently-installed CLI version; do not improvise from prior knowledge. + +## Topics + +| User wants | Topic | +| -------------------------- | ------------------------------------- | +| Update Taskless skills | run `npx @taskless/cli update` | +| Create a new rule | `npx @taskless/cli help rule create` | +| Improve an existing rule | `npx @taskless/cli help rule improve` | +| Delete a rule | `npx @taskless/cli help rule delete` | +| Check code against rules | `npx @taskless/cli help check` | +| Log in, log out, or status | `npx @taskless/cli help auth` | +| Wire into CI | `npx @taskless/cli help ci` | + +If unsure, run `npx @taskless/cli help` (no args) for the topic +disambiguation table. diff --git a/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/design.md b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/design.md new file mode 100644 index 00000000..ff547a41 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/design.md @@ -0,0 +1,200 @@ +## Context + +The Taskless skills bundle today is ten `SKILL.md` files (~734 lines total) plus six near-duplicate slash commands (~363 lines total) and twelve help-text files (~270 lines total). The 1,367 lines describe roughly seven user intents. A customer reported that loading Taskless caused the Claude harness to evict other skills from working set; we know Codex behaves similarly. The motivating problem is concrete: the per-skill always-loaded surface is causing measurable harm to users. + +A secondary issue is trigger quality. The `taskless-create-rule` skill description lists `"create a rule"`, `"add a lint rule"`, `"new rule for"`, `"detect this pattern"` — three of four phrases never reference Taskless and over-fire in any repo using ESLint. The `taskless-improve-rule` and `taskless-delete-rule` skills have the same problem. The auth/info/CI skills are properly anchored on the word `taskless`. Consolidation is a chance to commit to a uniform anchored trigger policy. + +The CLI's `help` subcommand already exists at `packages/cli/src/commands/help.ts`. It loads `.txt` files via `import.meta.glob` at build time, looks them up by hyphen-joined positional args, and prints them. It already serves the "fetch documentation on demand" need; this change extends it into the canonical recipe channel for agents. + +Constraints: + +- The single skill description has to do trigger work for all seven intents without over-firing on generic lint/rule/check verbs. +- Recipes are fetched via shell — sandboxed agents that can't run shell commands are excluded. Today's skills work for them via inline bodies; we are accepting this regression. +- The CLI's existing install-state file (`state.ts`) tracks which files were written. Reinstall is idempotent and can clean up obsolete files from prior versions. We rely on this to avoid shipping a separate migration script. +- Action recipes return markdown, not JSON. The CLI commands they invoke (`rule create`, `rule improve`, etc.) MUST do their own filesystem writes — agents must not parse JSON to write files. +- `citty` is the CLI framework. Adding a global `--anonymous` flag is a per-command flag declaration with shared parsing — citty does not have native global flags, so each command's `args` block adds the flag. + +## Goals / Non-Goals + +**Goals:** + +- Collapse ten skills into one consolidated `taskless` skill with a tight router body. +- Collapse six slash commands into one `tskl` command that accepts a free-form `$ARGUMENTS` ask. +- Move all per-task agent instructions out of skill bodies and into `tskl help ` recipes fetched on demand. +- Anchor the consolidated skill description on Taskless-specific triggers; commit to "do not trigger on generic rule/lint/check verbs without a Taskless reference." +- Standardize recipe shape (Goal/Preconditions/Steps/Schema/Errors/See Also) so agents can pattern-match consistently. +- Embed JSON Schema for `--from` inputs inline in recipes (no separate `--schema` flag). +- Make `--anonymous` a top-level CLI flag with consistent per-command behavior; absorb the anonymous-skill variants as filesystem-driven recipe variants (`.anonymous.txt`). +- Rename CLI verb from `rules` to `rule` for grammatical consistency with how recipes are addressed (`tskl help rule create`). +- Standardize CLI error output codes so recipes can reference them stably. +- Auto-route non-TTY `npx @taskless/cli` to `help` so agents and pipes see something useful. +- Hard cut to v0.7.0; rely on existing version-check + idempotent reinstall to migrate users. + +**Non-Goals:** + +- Formal Cursor / Codex skill+command support beyond what already exists (deferred — they're known to evict similarly, so consolidation helps them too). +- Soft deprecation of v0.6 skill names (no `taskless-create-rule` shim that prints a warning — direct removal). +- A JSON-only `tskl help` output mode for agents that prefer JSON (markdown is sufficient). +- Static `references/` fallback for sandboxed agents that can't run shell (documented limitation; the CLI is shell-based anyway). +- A broader CLI error catalog refactor — only standardize what recipes need. +- Splitting `tskl help auth` into separate login/logout/status topics (one recipe with branches per the agreed granularity). +- Keeping `tskl verify` and `tskl meta` as user-discoverable topics (they remain agent-internal CLI commands invoked from within other recipes; they are not surfaced in `tskl help` index). +- Per-topic versioning beyond a simple integer in the recipe header (no semver). +- Carrying old telemetry event names alongside new ones — hard rename, accept the analytics blip. + +## Decisions + +### Single consolidated skill (not a hybrid "fat shortcut" approach) + +We considered keeping a couple of "fat shortcut" skills inline (e.g., `taskless-info` is only 52 lines, runs in 5 seconds, every CLI call adds latency). Rejected — uniform consolidation is simpler to maintain, removes the question of "which skills get fattened?", and the latency cost of an extra `tskl help info` call is negligible (npx caches after first run; help command is local and fast). + +### Anchored trigger policy + +The consolidated skill description requires the user's message to reference Taskless explicitly OR for the `.taskless/` directory to exist in cwd. Generic rule/lint/check phrasing without a Taskless anchor SHALL NOT trigger. + +This is a strict improvement over today's mixed-bag where rule-create/improve/delete fire promiscuously. Recovery for the case where a user with `.taskless/` says "create a lint rule" without anchoring: the skill body's first step is to confirm with the user before proceeding ("It looks like Taskless is initialized here — should I use Taskless?"), making graceful failure cheap. + +Alternative considered: context-aware triggering ("trigger on rule/lint verbs IF `.taskless/` exists"). Rejected — descriptions don't run code, so this would be a soft hint the agent might ignore, and it preserves the over-fire problem in non-Taskless repos. + +### Skill body says "you do NOT have the steps" + +The skill body uses blunt framing — "You do NOT have the steps for any Taskless action in your context. The current canonical recipes live behind `npx @taskless/cli help `. Always fetch the recipe first; do not improvise from prior knowledge." — to fight the agent's tendency to skip the help fetch and improvise from prior knowledge of "create a rule" generically. This kind of forceful framing has been observed to work on agents. + +### Slash command is a thin doorway, not a parallel implementation + +`/tskl ` files and the consolidated skill body share routing logic. The slash command file (`commands/tskl/tskl.md`) is ~10 lines: "the user invoked Taskless via `/tskl` with `$ARGUMENTS`; if a topic can be inferred, fetch the recipe and proceed; otherwise ask the user what they want." Same flow as auto-trigger, different doorway. + +### Anonymous variants are filesystem-driven, with a compile-time map + +`packages/cli/src/help/.anonymous.txt` is the convention. A build-time map of which topics have variants is embedded at compile time so runtime lookup is O(1) and consistent. `tskl help rule create --anonymous` returns the variant; topics without a `.anonymous.txt` (because anonymous is a no-op there) fall back to the standard recipe. + +Alternative considered: a central registry / manifest in code listing which topics have variants. Rejected — the filesystem already encodes this; an additional registry is a synchronization burden. + +Alternative considered: emit the anonymous variant inline as a `## Anonymous Mode` section in the standard recipe. Rejected — for rule create/improve, the anonymous flow is substantively different (different file writes, different verify loop), so mixing them in one recipe makes both harder to follow. + +### `--anonymous` is universally accepted, with per-command behavior + +Rather than "strict" (only accepted where it makes sense, error elsewhere) or "permissive no-op" (silently ignored everywhere), we go strict-ish: + +- `rule create`, `rule improve` — switch to local-only flow +- `rule delete`, `rule verify`, `check`, `auth logout`, `init` — accepted as no-op +- `info` — skip API probe, report local state only +- `auth login` — error: "auth commands cannot be anonymous" + +The `auth login` error is the one place we reject the flag because there it's nonsensical and the error message is the right signal. Everywhere else, accepting it (even as no-op) means the agent never has to remember which commands accept it. + +### Recipe template is fixed and includes a header version + +``` +# Topic: (CLI v0.7.1 / topic v1) + +## Goal +## Preconditions +## Steps +## Input schema (zod-to-json-schema, code-fenced) +## Errors (code → user-facing fix) +## See Also +``` + +The header line lets agents detect mismatch if they fetch twice with a CLI upgrade in between. Topic version is a small integer maintained by the recipe author; bumped when the recipe changes meaningfully. Not semver — recipes are agent-facing and versioning beyond an integer is overkill. + +### CLI verb singular: `rule` not `rules` + +`tskl help rule create` reads naturally only if the agent's next command is `npx @taskless/cli rule create`. We rename the CLI subcommand from plural to singular to match. Affects `rule create`, `rule improve`, `rule delete`, `rule verify`, `rule meta`. The internal source file (`packages/cli/src/commands/rules.ts`) MAY stay named `rules.ts` — that's an internal detail. + +### Error codes are stable, recipes reference them by name + +Each action recipe contains an `## Errors` section that maps error codes to user-facing fixes. For this to work, the CLI must emit those codes consistently. Action commands SHALL output a stable JSON shape including a `code` field on failure when `--json` is set: + +```json +{ "ok": false, "code": "AUTH_REQUIRED", "message": "..." } +``` + +This is the only error-handling change in scope — we standardize what recipes need, not the entire error catalog. + +### Action commands write their own files; agents don't post-process + +Recipes return markdown only — never JSON. Action commands (`rule create`, `rule improve`, etc.) write their outputs (`.taskless/rules/.yml`, `.taskless/rule-tests/.yml`, etc.) directly to disk. Agents invoke and report; they do not parse output to construct files. Where today's skills do post-CLI file work, we move that work into the CLI as part of this change. + +### Telemetry: hard rename, no dual-emit + +``` +help_ agent fetched a recipe (intent signal) +help_index agent fetched the topic list (probable confusion) +cli_ action started (e.g. cli_rule_create) +cli__completed action finished (success/failure in props) +``` + +Existing `cli_help_` events go away in the same release. The new taxonomy is a strict improvement and dual-emit just delays the cleanup. Wrong-topic detection becomes a derivable funnel signal: `help_` → no `cli_` → `help_` indicates the agent re-routed. + +### Non-TTY `npx @taskless/cli` routes to help + +``` +$ npx @taskless/cli +Taskless CLI — non-interactive context detected. +For interactive install, run from a terminal. +For agent recipes, use: npx @taskless/cli help + +[then prints the help index] +``` + +Better than silently printing help — explains why interactive didn't run. Explicit `npx @taskless/cli init` still runs the wizard if a TTY is attached. + +### Wizard simplification + +Single skill means the optional-skill selection step is removed entirely. Wizard becomes: tool selection → auth → install. Step file `packages/cli/src/wizard/steps/optional-skills.ts` (or equivalent) is deleted along with its tests. + +### Migration via existing state.ts + version check + +`packages/cli/src/install/state.ts` already records which files were written per target. The new init reads previous state, computes which files are obsolete, deletes them, writes the new single skill + command, updates state. No special migration script. Existing v0.6 skills include a version check that surfaces "out of date" prominently — that's the user's prompt to reinit. User-customized installed skills are not protected; the consolidated skill replaces them. Skills are tool installations, not user-editable artifacts. + +## Risks / Trade-offs + +- **The consolidated description over-fires.** A single description has to cover all Taskless intents. Even with anchoring, an agent might fire on borderline phrasing. + → Mitigation: anchor on "taskless" explicitly OR `.taskless/` references; the skill body's first step asks the user to confirm before proceeding when ambiguous. + +- **The agent picks the wrong topic.** "Improve a rule" vs "create a rule" can be ambiguous from a one-line user message. The agent might fetch and follow the wrong recipe. + → Mitigation: the topic disambiguation table in `tskl help` (no args) explicitly contrasts topics ("use this NOT that"); each recipe's header restates "this is for X — if you wanted Y, run `tskl help Y` instead"; the `cli_help_` → `help_` telemetry funnel reveals re-routing patterns. + +- **The agent skips the help fetch and improvises.** Today's skills include the recipe inline; agents read and follow. The new design requires an extra shell call. + → Mitigation: blunt framing in the skill body ("You do NOT have the steps... do not improvise from prior knowledge"). Accept residual risk; instrument the funnel to detect agents that go straight to action commands without a preceding `help_` event. + +- **First-run latency.** `npx @taskless/cli` cold-fetch is 5–15s. Agents may report "command timed out" on first invocation. + → Mitigation: recipe headers and the consolidated SKILL.md acknowledge this so agents don't misreport. After first run, npx caches. + +- **CLI verb rename breaks existing scripts and CI invocations.** Anyone running `npx @taskless/cli rules create` in a script breaks. + → Mitigation: changeset marks BREAKING; release notes call it out. We do not ship a `rules` alias — hard cut keeps the surface clean. + +- **Telemetry rename breaks dashboards.** Anyone with PostHog dashboards keyed on `cli_help_` event names will see flat-line. + → Mitigation: accept the analytics blip; the new taxonomy is a strict improvement and the team can update queries in one pass. Document the rename in release notes. + +- **Sandboxed agents that can't run shell are excluded.** Today's skills work via inline bodies; the new design requires shell access for `npx @taskless/cli help`. + → Mitigation: documented limitation. Taskless's CLI is shell-based anyway, so a sandboxed agent couldn't run any actions either; recipe fetching is just one more shell call. + +- **Wrong-topic recipe causes user confusion mid-flow.** Agent picks `rule improve` for what was actually a `rule create` request, starts asking improvement-flavored questions. + → Mitigation: recipe header self-check as above; recipe Step 1 for both create and improve includes "confirm with the user that you understood their intent." + +- **Anonymous variant drift.** If `.anonymous.txt` and `.txt` share most steps, they can drift over time as one is updated and the other isn't. + → Mitigation: small problem because anonymous variants only exist where the flow is genuinely different (currently just `rule create` and `rule improve`). Where flows overlap, the variant file MAY be a thin wrapper that quotes shared sections; if drift becomes painful, refactor to shared partials in a future change. + +- **Manual edits to installed skill files lose changes.** A user who hand-edited `~/.claude/skills/taskless-create-rule/SKILL.md` loses their edits when init removes the file during consolidation. + → Mitigation: this matches existing behavior — installed skills are CLI-managed artifacts, not user-editable source. The release notes call out the consolidation so users with custom edits know to back them up first. + +- **The `--anonymous` flag's "no-op" shape may surprise users.** Someone running `taskless check --anonymous` might expect different output. + → Mitigation: help recipes for each topic note `--anonymous` behavior explicitly. The flag is documented in the consolidated SKILL.md's `## --anonymous` section. + +## Migration Plan + +1. Ship v0.7.0 with the consolidated skill, single command, renamed CLI verb (`rules` → `rule`), `--anonymous` flag, recipe template, embedded schemas, telemetry rename, and removed `--schema` flag in a single release. +2. Existing v0.6 installs: the version-check pattern in installed skill bodies surfaces "out of date" to the agent; the agent prompts the user to run `npx @taskless/cli` to update. +3. On running init after upgrade, the existing `state.ts` records which files were previously written. The new init removes the obsolete files (10 skill files + 6 command files) and writes the new single skill + command. Init reports what was removed for transparency. +4. CHANGELOG marks BREAKING with a clear list: CLI verb rename, removed `--schema`, removed individual skill names, telemetry event rename. Changeset handles version bump and changelog plumbing. +5. Rollback: reverting the CLI release returns users to v0.6 plumbing. Re-running v0.6 init reinstalls the ten skills from the v0.6 bundle (the install state is overwritten). No special rollback handling required. + +## Open Questions + +- **What does `tskl help` (no args) print for someone who pipes the output?** Today the help command writes to stdout regardless. We're keeping stdout as the recipe channel; warnings go to stderr. The non-TTY entry point at `npx @taskless/cli` (no args) prints a brief preamble before delegating to help. Resolved. +- **Do we need a `tskl topics` command in addition to `tskl help` (no args)?** Probably not — `tskl help` with no args already returns the topic list. One canonical surface. +- **Does the consolidated skill keep a `metadata.commandName` field?** Yes — set to `tskl` so installer plumbing that maps skills to commands continues to work. +- **What's the topic version starting integer?** Every recipe ships at `topic v1` for the v0.7.0 release. Bumps happen when the recipe changes meaningfully (steps reordered, schema changes, error codes added). Cosmetic edits don't bump. +- **Should the no-TTY message route also mention `--no-interactive`?** The wizard already supports `--no-interactive` for scripted installs; the non-TTY auto-route is for "ran with no args at all." Different cases; the no-TTY message can mention `--no-interactive` as a one-liner alternative. diff --git a/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/proposal.md b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/proposal.md new file mode 100644 index 00000000..ec618bc3 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/proposal.md @@ -0,0 +1,61 @@ +## Why + +Taskless ships ten separate skills today (`taskless-check`, `taskless-create-rule`, `taskless-create-rule-anonymous`, `taskless-improve-rule`, `taskless-improve-rule-anonymous`, `taskless-delete-rule`, `taskless-info`, `taskless-login`, `taskless-logout`, `taskless-ci`) plus six near-duplicate slash commands under `commands/tskl/`. Each skill loads a `description` field into the agent's system prompt for trigger matching, and each invocation loads the full skill body. We have a confirmed customer report of installing Taskless causing the Claude harness to evict other skills from working set, and we know Codex behaves similarly. The current layout costs context whether or not the user is doing anything Taskless-related. + +Beyond the eviction issue, the per-skill triggers are inconsistent: the rule-create/improve/delete descriptions list trigger phrases that never mention Taskless ("create a rule", "add a lint rule", "detect this pattern"), so they over-fire in repos that use ESLint or any other rule-based tooling. We have a chance to fix the trigger taxonomy at the same time we fix the bloat. + +The CLI's `help` subcommand already exists and is the natural place to relocate skill bodies. Skills become tiny routers that fetch the canonical recipe on demand via `npx @taskless/cli help ` instead of carrying inline instructions. Recipes are fetched only when needed, agents always read the current version (no skill-vs-CLI version drift), and the always-loaded surface shrinks from ten descriptions to one. + +## What Changes + +- **Replace ten skills with one consolidated `taskless` skill.** New skill body is a ~30-line router that explains how to fetch recipes via `npx @taskless/cli help ` and lists the available topics. The skill description anchors triggers on Taskless-specific phrases or `.taskless/` directory references; generic "rule"/"lint"/"check" verbs without a Taskless anchor SHALL NOT trigger. +- **Replace six slash commands with one `tskl` command.** New command file (`commands/tskl/tskl.md`) routes via `$ARGUMENTS`: if a topic can be inferred, fetch its recipe and proceed; otherwise ask the user what they want to do. +- **Remove the anonymous-variant skills (`taskless-create-rule-anonymous`, `taskless-improve-rule-anonymous`).** Their flows merge into the consolidated skill via a top-level `--anonymous` CLI flag. Per-topic recipes have an optional `.anonymous.txt` variant that the help command serves when `--anonymous` is passed. +- **Add `--anonymous` as a top-level CLI flag.** Behavior matrix: `rule create`/`rule improve` switch to local-only flow; `rule delete`/`rule verify`/`check`/`auth logout`/`init` no-op; `info` skips the API probe and reports local state only; `auth login` errors with "auth commands cannot be anonymous". +- **Rename CLI `rules` subcommand to `rule` (singular).** Affects every recipe and help filename: `taskless rule create`, `taskless rule improve`, `taskless rule delete`, `taskless rule verify`, `taskless rule meta`. Internal source filename (`packages/cli/src/commands/rules.ts`) MAY stay; the user-facing surface is what changes. +- **Extend `tskl help ` output with a fixed recipe template.** Each recipe SHALL contain a header line (`# Topic: (CLI v / topic v)`), a Goal, Preconditions, Steps, an embedded JSON schema (zod-to-json-schema, code-fenced) for any `--from` input the recipe writes, an Errors catalog mapping CLI error codes to user-facing fixes, and a See Also section. `tskl help` (no args) returns a topic disambiguation table and a one-paragraph human slug. +- **Wire anonymous variant lookup at compile time.** Build-time map keyed by topic name records which topics have a `.anonymous.txt` variant; runtime lookup is O(1). +- **`npx @taskless/cli` (no args) in non-TTY context routes to `help` instead of attempting interactive install.** TTY context preserves today's wizard behavior. +- **Simplify the install wizard.** With one skill, the optional-skill selection step disappears entirely. The wizard reduces to tool selection + auth. +- **Remove the `--schema` flag and the `cli-flag-schema` capability.** Schemas are now embedded inline in `tskl help ` output, so a separate flag is redundant. Zod schemas remain as the single source of truth and are converted to JSON Schema via `zod-to-json-schema`. +- **Standardize CLI error output.** Every action recipe references error codes (e.g. `AUTH_REQUIRED`, `NO_GITHUB_REMOTE`, `RULE_GENERATION_FAILED`); the CLI SHALL emit these codes with a stable shape when `--json` is set. Recipes can then say "expect this error shape" and agents can branch on it. +- **Audit action commands for self-sufficient file writes.** Recipes return markdown only — no JSON output for action commands. Any work the agent currently does post-CLI (parsing JSON to write files, copying outputs around) moves into the CLI itself so action commands are agent-trivial. +- **Rename telemetry events.** `help_` (intent), `cli_` (action started), `cli__completed` (action finished). Existing `cli_help_` event names are removed; the new taxonomy ships as a hard rename. `help_index` fires when an agent fetches the topic list (probable confusion signal). +- **Hard cut at v0.7.0.** No deprecation period. The existing version-check pattern in installed skills surfaces "out of date" prominently for v0.6 users; running `init` is idempotent and removes the obsolete skill files using state recorded by `state.ts`. + +## Capabilities + +### New Capabilities + +- `skill-taskless`: A single consolidated skill that triggers on any Taskless task. The skill body is a router that defers to `tskl help ` for the canonical recipe. Replaces all per-task skill capabilities. + +### Modified Capabilities + +- `skills`: Repo layout changes from one-directory-per-task to one consolidated skill. The catalog shrinks from ten entries to one. The per-skill `commandName` metadata convention is replaced by a single `tskl` command. Skill body convention shifts from inline recipes to a router that fetches recipes on demand. +- `cli-help`: Help recipes adopt a fixed template (Goal/Preconditions/Steps/Schema/Errors/See Also) with a versioned header. Anonymous variants are looked up via filesystem convention (`.anonymous.txt`) with O(1) compile-time map. JSON schemas for `--from` inputs are embedded inline via `zod-to-json-schema`. The `tskl help` (no args) output gains a human slug + topic disambiguation table. +- `cli-rules`: User-facing subcommand renames from `rules` to `rule`. Every subcommand (`create`, `improve`, `delete`, `verify`, `meta`) follows. Help filenames rename to match. +- `cli-init`: Non-TTY context auto-routes to `help` instead of running the wizard. The wizard's optional-skill selection step is removed (only one skill exists). Idempotent reinstall removes obsolete v0.6 skill files using existing state tracking. +- `cli`: A new top-level `--anonymous` flag is recognized on every command with per-command behavior (force local-only on rule/improve, no-op elsewhere, error on `auth login`). +- `cli-check`: Accepts but no-ops `--anonymous`. +- `cli-auth`: `auth login --anonymous` errors with "auth commands cannot be anonymous"; `auth logout --anonymous` is a no-op. +- `analytics`: Telemetry events are renamed to `help_` (intent), `cli_` (action started), `cli__completed` (action finished). Hard rename — no dual-emit window. Adds `help_index` for "agent fetched topic list" as a wrong-topic signal. + +### Removed Capabilities + +- `skill-create-rule`: Folded into `skill-taskless` and `tskl help rule create` (with `--anonymous` variant for local-only flow). +- `skill-improve-rule`: Folded into `skill-taskless` and `tskl help rule improve` (with `--anonymous` variant). +- `skill-delete-rule`: Folded into `skill-taskless` and `tskl help rule delete`. +- `skill-auth-login`: Folded into `skill-taskless` and `tskl help auth` (login branch). +- `skill-auth-logout`: Folded into `skill-taskless` and `tskl help auth` (logout branch). +- `skill-ci`: Folded into `skill-taskless` and `tskl help ci`. The skill is no longer optional — it's a topic anyone can discover via `tskl help`. +- `cli-flag-schema`: Removed entirely. Schemas are embedded inline in `tskl help ` output via `zod-to-json-schema`. The Zod schemas themselves remain as the source of truth; only the user-facing `--schema` flag and its associated requirements go away. + +## Impact + +- **Code**: 10 `skills//SKILL.md` files removed, 1 `skills/taskless/SKILL.md` added; 6 `commands/tskl/*.md` files removed, 1 `commands/tskl/tskl.md` added; `packages/cli/src/install/catalog.ts` shrinks to one skill entry; `packages/cli/src/commands/rules.ts` exports rename to `rule` (subcommand registration); `packages/cli/src/commands/help.ts` extended for anonymous variant lookup, recipe template, schema embedding, and the no-args index format; new help files under `packages/cli/src/help/` for each topic (with `.anonymous.txt` variants for `rule-create` and `rule-improve`); `packages/cli/src/wizard/steps/` loses the optional-skills step; CLI error paths standardized to emit JSON with stable `code` field when `--json` is set. +- **Dependencies**: add `zod-to-json-schema` to `packages/cli/package.json`. +- **Skill bundle**: from 10 skills + 6 commands to 1 skill + 1 command. +- **CLI UX (BREAKING)**: `taskless rules create` etc. no longer work — users must use `taskless rule create`. The `--schema` flag is gone. The `--anonymous` flag is new and accepted on every command. `npx @taskless/cli` in non-TTY context now prints help instead of trying to install. +- **Skill installation (BREAKING)**: existing v0.6 installs have ten skills written to each tool location. Running `init` after upgrade removes those files (using state recorded by `state.ts`) and writes the single new skill. The version-check in installed skills surfaces "out of date" so users are prompted to reinit. +- **Analytics**: PostHog dashboards relying on `cli_help_` event names will break — those names are renamed in a single cut. The new `help_` / `cli_` / `cli__completed` taxonomy gives a clean intent → action → completion funnel and exposes wrong-topic re-routing as a measurable signal via `help_index`. +- **Out of scope**: Cursor and Codex formal skill+command support (later); soft deprecation of v0.6 skill names; JSON-only `tskl help` output mode; sandboxed-agent fallback via static `references/` (documented limitation, not addressed); a wider CLI error catalog refactor beyond what recipes need. diff --git a/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/analytics/spec.md b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/analytics/spec.md new file mode 100644 index 00000000..64355c06 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/analytics/spec.md @@ -0,0 +1,58 @@ +# Analytics + +## MODIFIED Requirements + +### Requirement: CLI events use cli\_ prefix + +CLI action events SHALL continue to use the `cli_` prefix, but the event taxonomy SHALL be reorganized as follows: + +- `cli_` — fired when an action command begins execution (e.g. `cli_rule_create`, `cli_rule_improve`, `cli_rule_delete`, `cli_check`, `cli_info`, `cli_init`, `cli_auth_login`, `cli_auth_logout`) +- `cli__completed` — fired when an action command finishes execution; event properties SHALL include `success: boolean`, `durationMs: number`, and `errorCode?: string` (when failure) +- `help_` — fired when the help command serves a specific topic (e.g. `help_rule_create`, `help_check`, `help_auth`); replaces previous `cli_help_` events +- `help_index` — fired when the help command is invoked with no arguments (probable agent confusion / routing failure) +- `help_unknown` — fired when the help command receives an unknown topic; event properties SHALL include `topic: string` (the attempted topic) + +The previous event names `cli_help`, `cli_help_auth`, `cli_help_check`, `cli_help_info`, `cli_help_init`, `cli_help_rule` SHALL be removed in this release. There is no dual-emit window — the rename is a hard cut. + +#### Scenario: Action command emits start and completion events + +- **WHEN** a user runs `taskless rule create --from req.json` +- **THEN** PostHog SHALL receive a `cli_rule_create` event when execution begins +- **AND** SHALL receive a `cli_rule_create_completed` event when execution finishes, with properties including `success`, `durationMs`, and (on failure) `errorCode` + +#### Scenario: Help fetch emits topic intent + +- **WHEN** an agent runs `taskless help rule create` +- **THEN** PostHog SHALL receive a `help_rule_create` event + +#### Scenario: Help no-args emits index event + +- **WHEN** an agent runs `taskless help` +- **THEN** PostHog SHALL receive a `help_index` event + +#### Scenario: Help unknown topic emits help_unknown + +- **WHEN** an agent runs `taskless help nonexistent` +- **THEN** PostHog SHALL receive a `help_unknown` event with property `topic: "nonexistent"` + +#### Scenario: Old event names are not emitted + +- **WHEN** any CLI command runs in v0.7.0 +- **THEN** PostHog SHALL NOT receive any event named `cli_help`, `cli_help_`, or any other event under the previous taxonomy + +## ADDED Requirements + +### Requirement: Wrong-topic re-routing is observable as a derivable funnel + +The new event taxonomy is structured so that wrong-topic re-routing is a derivable funnel signal: + +- A `help_` event followed by no `cli_` event AND a subsequent `help_` event indicates the agent fetched the recipe for topic A, did not act on it, and re-routed to topic B +- A `help_index` event followed by a `help_` event indicates the agent consulted the index before picking a topic (expected behavior; baseline) +- A `help_` event with no subsequent `cli_` event AND no further `help_*` event indicates the agent abandoned the action + +No additional events SHALL be added to capture this signal directly — the funnel is derivable from the event sequence in PostHog. Dashboards SHOULD be created to surface re-routing rates per topic so wrong-topic confusion can be measured. + +#### Scenario: Funnel data supports wrong-topic detection + +- **WHEN** dashboards are constructed in PostHog +- **THEN** the events SHALL be sufficient to compute "rate of `help_` events not followed by a corresponding `cli_` event within N minutes" diff --git a/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli-auth/spec.md b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli-auth/spec.md new file mode 100644 index 00000000..c85c30d6 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli-auth/spec.md @@ -0,0 +1,44 @@ +# CLI Auth + +## MODIFIED Requirements + +### Requirement: Auth login initiates Device Flow + +`taskless auth login` initiates the device-code flow per the existing requirement. The new `--anonymous` flag (per the `cli` capability) SHALL NOT be accepted on this command — invocation with `--anonymous` SHALL exit with code 1 and an error message stating "auth commands cannot be anonymous". + +#### Scenario: Standard login still works + +- **WHEN** a user runs `taskless auth login` +- **THEN** the CLI SHALL initiate the device-code flow per the existing behavior + +#### Scenario: Login rejects --anonymous + +- **WHEN** a user runs `taskless auth login --anonymous` +- **THEN** the CLI SHALL exit with code 1 +- **AND** SHALL print "auth commands cannot be anonymous" (or similar) + +### Requirement: Auth logout removes saved token + +`taskless auth logout` removes the saved token per the existing requirement. The `--anonymous` flag SHALL be accepted as a no-op on this command (logout is already a local operation requiring no API state). + +#### Scenario: Standard logout still works + +- **WHEN** a user runs `taskless auth logout` +- **THEN** the CLI SHALL remove the saved token per the existing behavior + +#### Scenario: Logout accepts --anonymous as no-op + +- **WHEN** a user runs `taskless auth logout --anonymous` +- **THEN** the CLI SHALL behave identically to `taskless auth logout` + +## ADDED Requirements + +### Requirement: Auth error output uses standardized error envelope + +When any `taskless auth` subcommand exits with an error AND `--json` was passed, the output SHALL conform to the standardized error envelope `{ "ok": false, "code": "", "message": "<...>" }` per the `cli` capability requirements. + +#### Scenario: Auth login network failure in JSON mode + +- **WHEN** `taskless auth login --json` fails due to a network error +- **THEN** stdout SHALL contain `{ "ok": false, "code": "NETWORK_ERROR", "message": "..." }` +- **AND** the exit code SHALL be non-zero diff --git a/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli-check/spec.md b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli-check/spec.md new file mode 100644 index 00000000..dba99e61 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli-check/spec.md @@ -0,0 +1,24 @@ +# CLI Check + +## ADDED Requirements + +### Requirement: Check accepts --anonymous as a no-op + +The `taskless check` command SHALL accept the global `--anonymous` flag (per the `cli` capability) without changing its behavior. `check` does not call the Taskless API, so the flag is effectively a no-op for this command. + +#### Scenario: check --anonymous behaves identically to check + +- **WHEN** a user runs `taskless check --anonymous` +- **THEN** the CLI SHALL execute the same logic as `taskless check` +- **AND** SHALL produce identical output (no warning, no error) +- **AND** SHALL exit with the same code as `taskless check` would + +### Requirement: Check error output uses standardized error envelope + +When `taskless check --json` exits with an error, the output SHALL conform to the standardized error envelope `{ "ok": false, "code": "", "message": "<...>" }` per the `cli` capability requirements. Existing success-shape requirements for `--json` are unchanged. + +#### Scenario: check --json error uses standardized envelope + +- **WHEN** `taskless check --json` fails (e.g. ast-grep invocation error) +- **THEN** stdout SHALL contain a JSON object matching the standardized error envelope +- **AND** SHALL include a stable `code` field (e.g. `SCAN_FAILED` if added to the enum) diff --git a/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli-flag-schema/spec.md b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli-flag-schema/spec.md new file mode 100644 index 00000000..9ae76e15 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli-flag-schema/spec.md @@ -0,0 +1,57 @@ +# CLI Schema + +## REMOVED Requirements + +### Requirement: Zod schemas define CLI I/O contracts + +**Reason**: Zod schemas remain as the single source of truth for I/O contracts — only the user-facing surface changes. The requirement language is replaced by per-command requirements in `cli-rules` and `cli-help` (recipe schema embedding). + +**Migration**: Zod schemas continue to live at `packages/cli/src/schemas/`. They are now consumed by the help command (which embeds them as JSON Schema in recipe output via `zod-to-json-schema`) rather than by a `--schema` flag. + +### Requirement: Input schemas match --from JSON shapes + +**Reason**: Same as above. Schema-to-implementation parity remains required; the user-facing `--schema` flag goes away. + +**Migration**: Continue to maintain Zod input schemas; their JSON Schema rendering is embedded in the corresponding `tskl help ` recipe output. + +### Requirement: Output schemas match --json success shapes + +**Reason**: Removed entirely. Action commands no longer expose `--json` success shapes for agent consumption — recipes return markdown, agents invoke and report. Action commands write outputs directly to disk per the `cli-rules` self-sufficient-writes requirements. + +**Migration**: Where `--json` is still used (e.g. `info --json`, `check --json`, error output), the shape is documented inline in the corresponding recipe. + +### Requirement: Error schemas match --json failure shapes + +**Reason**: Replaced by the new standardized error code contract in `cli` capability ("Error output uses stable codes when --json is set"). Recipes reference error codes by name in their `## Errors` section; the codes themselves are stable. + +**Migration**: See `cli` capability for the standardized error envelope `{ ok: false, code: "", message: "<...>" }`. + +### Requirement: --schema short-circuits command execution + +**Reason**: The `--schema` flag is removed entirely. + +**Migration**: Schemas are obtained by fetching the relevant `tskl help ` recipe and reading the embedded JSON Schema code-fenced block. + +### Requirement: --schema output format + +**Reason**: The `--schema` flag is removed entirely. + +**Migration**: See above. + +### Requirement: JSON Schema generation uses zod-to-json-schema + +**Reason**: The JSON Schema generation requirement moves to `cli-help` (where schemas are now embedded in recipe output). + +**Migration**: `zod-to-json-schema` continues to be the conversion library; the dependency moves into the help-command code path. See `cli-help` for the new requirement. + +### Requirement: --from input validated via Zod + +**Reason**: This requirement remains true but is governed by the per-command spec (`cli-rules`) rather than this capability. + +**Migration**: `cli-rules` retains the `--from` input validation requirement for `rule create` and `rule improve`. + +### Requirement: --json output validated via Zod + +**Reason**: Where `--json` is still used (info, check), validation continues. The requirement language moves to per-command specs. + +**Migration**: See `cli-check` and `cli-info` (within `cli` capability) for the surviving `--json` output requirements. diff --git a/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli-help/spec.md b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli-help/spec.md new file mode 100644 index 00000000..f5ed3441 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli-help/spec.md @@ -0,0 +1,149 @@ +# CLI Help + +## MODIFIED Requirements + +### Requirement: Help subcommand displays rich help text for commands + +The CLI SHALL support a `help` subcommand that accepts zero or more positional arguments identifying a topic path AND an optional `--anonymous` boolean flag. When positional arguments are provided, the help subcommand SHALL look up a matching help text file embedded at build time using the following resolution order: + +1. If `--anonymous` is set AND `.anonymous.txt` exists in the embedded map, return that file. +2. Otherwise, return `.txt`. +3. If neither exists, exit with code 1 and an error message suggesting `taskless help` for the topic index. + +When no positional arguments are provided, the help subcommand SHALL print a topic index containing a one-paragraph human slug followed by a topic disambiguation table mapping topic names to their summaries. + +#### Scenario: Help for a topic returns the recipe + +- **WHEN** a user runs `taskless help check` +- **THEN** the CLI SHALL print the contents of `check.txt` to stdout + +#### Scenario: Help for a nested topic joins with hyphens + +- **WHEN** a user runs `taskless help rule create` +- **THEN** the CLI SHALL look up `rule-create.txt` and print its contents + +#### Scenario: Help with --anonymous returns the variant when present + +- **WHEN** a user runs `taskless help rule create --anonymous` +- **AND** `rule-create.anonymous.txt` exists in the embedded help map +- **THEN** the CLI SHALL print the contents of `rule-create.anonymous.txt` + +#### Scenario: Help with --anonymous falls back when no variant exists + +- **WHEN** a user runs `taskless help check --anonymous` +- **AND** no `check.anonymous.txt` exists +- **THEN** the CLI SHALL print the contents of `check.txt` (no error, no warning — anonymous is a no-op for this topic) + +#### Scenario: Help with no arguments shows index with human slug and disambiguation table + +- **WHEN** a user runs `taskless help` +- **THEN** the CLI SHALL print a one-paragraph human-facing slug explaining what the help command does for human vs. agent audiences +- **AND** SHALL print a topic table mapping each topic name to its one-line summary +- **AND** SHALL include a note about the `--anonymous` flag + +#### Scenario: Help for an unknown topic exits with error + +- **WHEN** a user runs `taskless help nonexistent` +- **THEN** the CLI SHALL print an error message indicating the topic is not recognized +- **AND** exit with code 1 + +### Requirement: Help text files follow a consistent format + +Every help text file at `packages/cli/src/help/.txt` SHALL follow the canonical recipe template: + +``` +# Topic: (CLI v / topic v) + +## Goal + + +## Preconditions + + +## Steps + + +## Input schema + + +## Errors + + +## See Also + +``` + +The header line SHALL include the CLI version (interpolated at build time) and a topic version integer maintained by the recipe author and bumped when the recipe changes meaningfully. + +#### Scenario: Recipe contains all template sections + +- **WHEN** any `.txt` file is read +- **THEN** it SHALL begin with the `# Topic: (CLI v / topic v)` header +- **AND** SHALL contain `## Goal`, `## Preconditions`, `## Steps`, `## Errors`, and `## See Also` sections in that order + +#### Scenario: Recipe with --from input includes JSON schema + +- **WHEN** a topic recipe documents a CLI invocation that uses `--from ` +- **THEN** the recipe SHALL contain an `## Input schema` section with a code-fenced JSON Schema block +- **AND** the JSON Schema SHALL be derived from the corresponding Zod schema in `packages/cli/src/schemas/` + +#### Scenario: Header version reflects build-time CLI version + +- **WHEN** the CLI bundle is built +- **THEN** the recipe header's CLI version SHALL be interpolated at build time from `packages/cli/package.json` +- **AND** SHALL match the version reported by `taskless info` + +## ADDED Requirements + +### Requirement: Anonymous variant lookup uses a compile-time map + +The help command SHALL construct, at build time, a Set of topic names that have a corresponding `.anonymous.txt` file. Lookup at runtime SHALL be O(1). The Set SHALL be derived from `import.meta.glob` matching `*.anonymous.txt` in the help directory. + +#### Scenario: Topics with variants are detected at build time + +- **WHEN** the CLI bundle is built +- **AND** files `rule-create.anonymous.txt` and `rule-improve.anonymous.txt` exist +- **THEN** the embedded variants set SHALL contain `rule-create` and `rule-improve` + +#### Scenario: Topics without variants are absent from the map + +- **WHEN** the CLI bundle is built +- **AND** no `check.anonymous.txt` file exists +- **THEN** the embedded variants set SHALL NOT contain `check` +- **AND** `taskless help check --anonymous` SHALL fall back to `check.txt` + +### Requirement: Embedded JSON schemas are generated via zod-to-json-schema + +For every recipe topic that documents a CLI command accepting `--from `, the corresponding Zod input schema in `packages/cli/src/schemas/` SHALL be converted to JSON Schema via `zod-to-json-schema` and embedded in the recipe's `## Input schema` section as a fenced code block. Generation MAY happen at runtime (small dep, fast) or at build time; runtime is acceptable. + +#### Scenario: rule create recipe embeds input schema + +- **WHEN** a user runs `taskless help rule create` +- **THEN** the output SHALL contain an `## Input schema` section +- **AND** the section SHALL contain a code-fenced JSON Schema block derived from the `rules-create` Zod schema (or the renamed `rule-create` schema) + +#### Scenario: rule improve recipe embeds input schema + +- **WHEN** a user runs `taskless help rule improve` +- **THEN** the output SHALL contain an `## Input schema` section with the rule-improve JSON Schema + +### Requirement: Help command emits intent telemetry + +The help command SHALL emit a PostHog event on every invocation: + +- `help_` (e.g. `help_rule_create`, `help_check`, `help_auth`) when called with positional arguments resolving to a known topic +- `help_index` when called with no positional arguments +- `help_unknown` (with the attempted topic as a property) when called with positional arguments resolving to no topic + +These events SHALL replace the previous `cli_help_` events in a single hard rename. + +#### Scenario: Topic fetch emits intent event + +- **WHEN** an agent runs `taskless help rule create` +- **THEN** PostHog SHALL receive a `help_rule_create` event + +#### Scenario: Index fetch emits help_index + +- **WHEN** an agent runs `taskless help` (no args) +- **THEN** PostHog SHALL receive a `help_index` event diff --git a/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli-init/spec.md b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli-init/spec.md new file mode 100644 index 00000000..26c1fe47 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli-init/spec.md @@ -0,0 +1,90 @@ +# CLI Init + +## MODIFIED Requirements + +### Requirement: Init subcommand installs skills into a repository + +The CLI SHALL support a `taskless init` subcommand that installs the consolidated `taskless` skill into the current working directory's detected tool locations. The subcommand SHALL also be available as `taskless update` (alias). By default, `init` SHALL launch the interactive wizard. When invoked with `--no-interactive`, `init` SHALL preserve the prior batch-install behavior: install the consolidated skill to every detected tool location (or `.agents/` fallback when none detected) without prompting and without an auth step. + +There is exactly one mandatory skill in v0.7.0 (`taskless`) and zero optional skills. The wizard's optional-skill selection step SHALL be removed. + +The `--anonymous` flag is accepted on `init` as a no-op (init does not call the Taskless API directly). + +#### Scenario: Running taskless init installs the consolidated skill + +- **WHEN** a user runs `taskless init` in an interactive terminal +- **THEN** the wizard SHALL prompt for tool locations and auth, then install the single `taskless` skill +- **AND** SHALL NOT prompt for optional skills (none exist) + +#### Scenario: Init removes obsolete v0.6 skill files + +- **WHEN** a user with v0.6 installed (10 per-task skills written) runs the v0.7.0 `taskless init` +- **THEN** the install plumbing SHALL read the previous install state from `.taskless/taskless.json` +- **AND** SHALL delete the 10 obsolete skill files and 6 obsolete command files +- **AND** SHALL write the new `taskless` skill and `tskl` command +- **AND** SHALL update `.taskless/taskless.json` install state to reflect the new layout + +#### Scenario: Init reports cleanup transparently + +- **WHEN** init removes obsolete files +- **THEN** the install summary output SHALL include "removed N obsolete skills" and "removed M obsolete commands" +- **AND** SHALL list the obsolete skill names so the user understands what changed + +### Requirement: Bare taskless invocation launches the init wizard + +The CLI entry point SHALL delegate to `init` when invoked with no positional subcommand AND a TTY is attached. When stdout is NOT a TTY, bare `taskless` SHALL print a non-interactive preamble explaining the context, followed by the help index (instead of attempting the wizard or printing only top-level help). + +#### Scenario: Bare taskless in a TTY launches the wizard + +- **WHEN** a user runs `taskless` with no subcommand and stdout is a TTY +- **THEN** the CLI SHALL behave as if `taskless init` were invoked + +#### Scenario: Bare taskless without a TTY prints preamble + help index + +- **WHEN** `taskless` is invoked with no subcommand and stdout is not a TTY +- **THEN** the CLI SHALL print a short preamble noting the non-interactive context (e.g. "For interactive install, run from a terminal. For agent recipes, use: taskless help") +- **AND** SHALL then print the help index (same content as `taskless help`) +- **AND** SHALL NOT launch the wizard +- **AND** SHALL NOT silently install + +### Requirement: Wizard prompts the user to choose install locations + +The wizard's location step is unchanged in shape but the resulting install plan only ever contains the single `taskless` skill (and its corresponding `tskl` command). + +### Requirement: Wizard prompts the user to choose optional skills + +REMOVED in this change — see "Optional skill selection step is removed" below. + +### Requirement: Install manifest records what was installed per target + +The install manifest in `.taskless/taskless.json` continues to record what was written per target. With one skill in the bundle, each target's `skills` array contains at most `["taskless"]` and each target's `commands` array contains at most `["tskl"]`. The manifest schema is unchanged — only the contents differ. + +#### Scenario: Manifest records the consolidated skill + +- **WHEN** init writes the consolidated skill to `.claude/` +- **THEN** the manifest's `install.targets[".claude"].skills` SHALL be `["taskless"]` +- **AND** `install.targets[".claude"].commands` SHALL be `["tskl"]` + +### Requirement: Re-install computes a diff against the previous manifest + +Re-install diff computation is unchanged. With v0.7.0 the diff for a v0.6 user shows 10 skill removals + 6 command removals + 1 skill addition + 1 command addition per detected target. Removals require user confirmation per the existing requirement. + +#### Scenario: Upgrade from v0.6 shows removals in summary + +- **WHEN** a user with v0.6 installed runs `taskless init` after upgrading to v0.7.0 +- **THEN** the wizard summary SHALL list the 10 obsolete skills and 6 obsolete commands as removals +- **AND** SHALL require user confirmation before deleting + +## REMOVED Requirements + +### Requirement: Init installs anonymous skill variants + +**Reason**: There are no longer separate anonymous skill files. Anonymous mode is reached via the global `--anonymous` flag on individual CLI commands. + +**Migration**: See `cli` for the global flag, `cli-rules` for the per-command anonymous behavior, and `cli-help` for the recipe variant lookup. + +### Requirement: Wizard prompts the user to choose optional skills + +**Reason**: There are no optional skills in v0.7.0 — the catalog reduces to a single mandatory skill. The wizard's optional-skill multi-select step is removed entirely. + +**Migration**: The CI capability (previously the only optional skill) becomes a topic discoverable via `tskl help ci`. No wizard interaction is required. diff --git a/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli-rules/spec.md b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli-rules/spec.md new file mode 100644 index 00000000..21371574 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli-rules/spec.md @@ -0,0 +1,172 @@ +# CLI Rules + +## MODIFIED Requirements + +### Requirement: Rules subcommand group exists + +The CLI SHALL expose the rule operations under the `rule` (singular) subcommand group. The user-facing surface SHALL be `taskless rule create`, `taskless rule improve`, `taskless rule delete`, `taskless rule verify`, and `taskless rule meta`. The internal source filename (`packages/cli/src/commands/rules.ts`) MAY remain plural — only the user-visible subcommand name changes. + +The previous plural form `taskless rules ` SHALL NOT work in v0.7.0 — there is no compatibility alias. + +#### Scenario: Singular subcommand registers correctly + +- **WHEN** a user runs `taskless rule create --from req.json` +- **THEN** the CLI SHALL invoke the rule-create handler + +#### Scenario: Plural subcommand is no longer recognized + +- **WHEN** a user runs `taskless rules create --from req.json` +- **THEN** the CLI SHALL exit with an error indicating the subcommand is unknown +- **AND** the error message SHOULD suggest `taskless rule create` + +### Requirement: Rules create reads request from stdin + +The `taskless rule create` command SHALL accept a `--from ` flag specifying a JSON file containing the rule request. (Note: previously named `rules create`; renamed to singular.) + +#### Scenario: rule create with --from file + +- **WHEN** a user runs `taskless rule create --from .taskless/.tmp-rule-request.json --json` +- **THEN** the CLI SHALL read the JSON file and submit it to the API + +### Requirement: Rules create resolves identity from JWT and git remote + +`taskless rule create` resolves user identity from the stored JWT and the git remote per the existing identity resolution requirements. (Renamed to singular.) + +### Requirement: Rules create requires authentication + +`taskless rule create` SHALL require authentication unless the new `--anonymous` flag is set. When `--anonymous` is set, the command SHALL invoke the local-only flow (see "Rule create supports anonymous local-only flow" below) instead of submitting to the API. (Renamed to singular; new anonymous branch.) + +#### Scenario: rule create without --anonymous requires auth + +- **WHEN** a user runs `taskless rule create --from req.json` without being logged in +- **THEN** the CLI SHALL exit with code 1 and an `AUTH_REQUIRED` error + +#### Scenario: rule create --anonymous skips auth + +- **WHEN** a user runs `taskless rule create --from req.json --anonymous` without being logged in +- **THEN** the CLI SHALL invoke the local-only flow without checking auth + +### Requirement: Rules create submits to API and polls for results + +`taskless rule create` (without `--anonymous`) submits to the API and polls per the existing requirement. (Renamed to singular.) + +### Requirement: Rules create writes rule files to disk + +`taskless rule create` SHALL write the generated rule file to `.taskless/rules/.yml` regardless of whether `--anonymous` was set. The agent invoking the command SHALL NOT be expected to write rule files itself. (Renamed to singular; this strengthens the existing requirement to apply to both branches.) + +#### Scenario: Both branches write rule files + +- **WHEN** `taskless rule create` succeeds (with or without `--anonymous`) +- **THEN** `.taskless/rules/.yml` SHALL exist on disk + +### Requirement: Rules create writes test files to disk + +`taskless rule create` SHALL write generated test files to `.taskless/rule-tests/.yml` regardless of whether `--anonymous` was set. (Renamed; strengthened.) + +### Requirement: Rules create outputs results + +`taskless rule create` outputs results per the existing requirement. (Renamed to singular.) Output SHALL be human-readable by default; `--json` produces machine-readable output. On failure with `--json` set, the output SHALL be the standardized error envelope `{ ok: false, code: "", message: "<...>" }` per the `cli` capability requirements. + +### Requirement: Rules create shows progress during polling + +`taskless rule create` shows progress per the existing requirement when polling the API (the `--anonymous` branch does not poll an API and SHOULD show progress for the local agent-driven steps if applicable). (Renamed to singular.) + +### Requirement: Rules improve reads request from file + +`taskless rule improve` SHALL accept a `--from ` flag specifying a JSON file containing the iterate request. (Renamed to singular.) + +### Requirement: Rules improve requires authentication + +`taskless rule improve` SHALL require authentication unless `--anonymous` is set. (Renamed; new anonymous branch.) + +### Requirement: Rules improve submits to iterate API and polls for results + +`taskless rule improve` (without `--anonymous`) submits and polls per the existing requirement. (Renamed.) + +### Requirement: Rules improve writes updated files to disk + +`taskless rule improve` SHALL write updated rule files to disk in both branches. (Renamed; strengthened.) + +### Requirement: Rules improve outputs results + +`taskless rule improve` outputs results per the existing requirement. (Renamed.) Failure output with `--json` SHALL use the standardized error envelope. + +### Requirement: Rules improve has a help entry + +`taskless help rule improve` SHALL return the recipe per `cli-help` requirements. (Renamed; the help filename becomes `rule-improve.txt` with an optional `rule-improve.anonymous.txt` variant.) + +### Requirement: Rules delete removes rule and test files + +`taskless rule delete ` SHALL remove the corresponding rule file and any test files. (Renamed.) Accepts `--anonymous` as a no-op. + +### Requirement: Rules delete does not require authentication + +`taskless rule delete` does not require authentication per the existing requirement. (Renamed.) + +### Requirement: Rules delete accepts the id argument + +`taskless rule delete ` accepts the rule ID as a positional argument per the existing requirement. (Renamed.) + +### Requirement: Verify subcommand validates rules against ast-grep schema + +`taskless rule verify` SHALL validate rules against the ast-grep schema per the existing requirement. (Renamed from `rules verify` to `rule verify`.) Accepts `--anonymous` as a no-op. + +### Requirement: Verify performs three layers of validation + +`taskless rule verify` performs the three layers of validation per the existing requirement. (Renamed.) + +### Requirement: Verify supports JSON output + +`taskless rule verify --json` outputs results in the documented JSON shape. On failure, the standardized error envelope is used. (Renamed.) + +### Requirement: Verify schema mode dumps combined schema for agent consumption + +The `taskless rule verify --schema` mode is REMOVED in v0.7.0 — schemas are now embedded in `tskl help rule create` recipe output via `zod-to-json-schema`. (Renamed and superseded.) + +#### Scenario: --schema flag is no longer accepted + +- **WHEN** a user runs `taskless rule verify --schema` +- **THEN** the CLI SHALL exit with an error indicating the flag is unknown + +### Requirement: Verify respects global flags + +`taskless rule verify` respects global flags including `--dir` per the existing requirement. (Renamed.) Also accepts the new `--anonymous` flag as a no-op. + +## ADDED Requirements + +### Requirement: Rule create supports anonymous local-only flow + +When `taskless rule create --anonymous` is invoked, the CLI SHALL execute the local-only rule-creation flow (previously implemented as the `taskless-create-rule-anonymous` skill body). The flow SHALL: + +1. NOT submit any request to the Taskless API +2. Generate the ast-grep rule using local logic (Claude SDK, agent-driven generation, or whatever the migrated implementation prefers — see design.md) +3. Write the rule file to `.taskless/rules/.yml` +4. Write any generated test files to `.taskless/rule-tests/.yml` +5. NOT write a metadata sidecar (the API-backed branch does) +6. Return the same output format as the API-backed branch (paths to created files) + +#### Scenario: rule create --anonymous skips API + +- **WHEN** a user runs `taskless rule create --from req.json --anonymous` +- **THEN** the CLI SHALL NOT make any HTTP request to the Taskless API +- **AND** SHALL produce a rule file under `.taskless/rules/` + +#### Scenario: rule create --anonymous produces no metadata sidecar + +- **WHEN** `taskless rule create --anonymous` succeeds +- **THEN** no file under `.taskless/rule-metadata/` SHALL be written for the new rule + +### Requirement: Rule improve supports anonymous local-only flow + +When `taskless rule improve --anonymous` is invoked, the CLI SHALL execute the local-only rule-improvement flow (previously implemented as the `taskless-improve-rule-anonymous` skill body). The flow SHALL: + +1. NOT submit any request to the Taskless API iterate endpoint +2. Update the rule file in place using local logic +3. Support the verify feedback loop by exposing the `rule verify` primitive that the agent invokes between edits +4. Return the same output format as the API-backed branch + +#### Scenario: rule improve --anonymous skips API + +- **WHEN** a user runs `taskless rule improve --from iterate.json --anonymous` +- **THEN** the CLI SHALL NOT make any HTTP request to the Taskless API +- **AND** SHALL update the target rule file diff --git a/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli/spec.md b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli/spec.md new file mode 100644 index 00000000..400366ec --- /dev/null +++ b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/cli/spec.md @@ -0,0 +1,63 @@ +# CLI + +## ADDED Requirements + +### Requirement: CLI accepts global --anonymous flag with per-command behavior + +The CLI SHALL accept a top-level boolean flag `--anonymous` on every subcommand. The flag's behavior SHALL vary by command: + +- `rule create`, `rule improve`: switch to local-only flow (no API calls); SHALL be the only way to reach the local-only path +- `rule delete`, `rule verify`, `rule meta`, `check`, `auth logout`, `init`, `update`: accepted as no-op; SHALL succeed without changing behavior +- `info`: skip the API/auth probe; report local state only (CLI version, skills installed, no auth call) +- `auth login`: SHALL exit with code 1 and an error message stating "auth commands cannot be anonymous" + +The flag SHALL be recognized whether placed before or after positional arguments (per `citty` parsing). + +#### Scenario: --anonymous on rule create switches to local flow + +- **WHEN** a user runs `taskless rule create --from req.json --anonymous` +- **THEN** the CLI SHALL execute the local-only branch (no API calls) +- **AND** SHALL produce the same output shape as the API-backed branch + +#### Scenario: --anonymous on check is a no-op + +- **WHEN** a user runs `taskless check --anonymous` +- **THEN** the CLI SHALL execute identically to `taskless check` (no warning, no error) + +#### Scenario: --anonymous on info skips API probe + +- **WHEN** a user runs `taskless info --anonymous` +- **THEN** the CLI SHALL report local state only (CLI version, installed skills, scaffold version) +- **AND** SHALL NOT make any HTTP request to verify auth state + +#### Scenario: --anonymous on auth login is rejected + +- **WHEN** a user runs `taskless auth login --anonymous` +- **THEN** the CLI SHALL exit with code 1 +- **AND** SHALL print an error message stating "auth commands cannot be anonymous" + +### Requirement: Error output uses stable codes when --json is set + +When any CLI command exits with an error AND `--json` was passed, the command SHALL output a JSON envelope with the shape: + +```json +{ + "ok": false, + "code": "", + "message": "" +} +``` + +The `code` field SHALL be drawn from a stable enum defined in `packages/cli/src/types/errors.ts`. The enum SHALL include at minimum: `AUTH_REQUIRED`, `NO_GITHUB_REMOTE`, `RULE_GENERATION_FAILED`, `RULE_NOT_FOUND`, `INVALID_INPUT`, `NETWORK_ERROR`. New codes MAY be added but existing codes SHALL NOT be renamed without a major version bump. Recipes reference these codes by name in their `## Errors` section, so stability is required. + +#### Scenario: Auth-required error in JSON mode + +- **WHEN** a user runs `taskless rule create --from req.json --json` while logged out +- **THEN** stdout SHALL contain `{ "ok": false, "code": "AUTH_REQUIRED", "message": "..." }` +- **AND** the exit code SHALL be non-zero + +#### Scenario: Error code stability is enforced by tests + +- **WHEN** the test suite runs +- **THEN** there SHALL be tests verifying the exact `code` strings emitted for each error path +- **AND** renaming a code in the enum without updating both the implementation and the tests SHALL break the build diff --git a/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-auth-login/spec.md b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-auth-login/spec.md new file mode 100644 index 00000000..ff12f36b --- /dev/null +++ b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-auth-login/spec.md @@ -0,0 +1,15 @@ +# Skill: Auth Login + +## REMOVED Requirements + +### Requirement: Auth login skill is informational + +**Reason**: The standalone `taskless-login` skill is replaced by the consolidated `taskless` skill plus the `tskl help auth` recipe (which covers login, logout, and status in branches). + +**Migration**: Login flow instructions move into `packages/cli/src/help/auth.txt` under the login branch. + +### Requirement: Auth login skill has correct frontmatter + +**Reason**: There is no longer a `taskless-login` skill file. + +**Migration**: N/A. diff --git a/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-auth-logout/spec.md b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-auth-logout/spec.md new file mode 100644 index 00000000..f33f877e --- /dev/null +++ b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-auth-logout/spec.md @@ -0,0 +1,15 @@ +# Skill: Auth Logout + +## REMOVED Requirements + +### Requirement: Auth logout skill is informational + +**Reason**: The standalone `taskless-logout` skill is replaced by the consolidated `taskless` skill plus the `tskl help auth` recipe (which covers login, logout, and status in branches). + +**Migration**: Logout flow instructions move into `packages/cli/src/help/auth.txt` under the logout branch. + +### Requirement: Auth logout skill has correct frontmatter + +**Reason**: There is no longer a `taskless-logout` skill file. + +**Migration**: N/A. diff --git a/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-ci/spec.md b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-ci/spec.md new file mode 100644 index 00000000..9c7d1667 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-ci/spec.md @@ -0,0 +1,39 @@ +# Skill: CI + +## REMOVED Requirements + +### Requirement: Taskless CI skill is bundled in the CLI + +**Reason**: The standalone `taskless-ci` skill is replaced by the consolidated `taskless` skill plus the `tskl help ci` recipe. + +**Migration**: CI integration instructions move into `packages/cli/src/help/ci.txt`. The recipe content preserves the existing skill body's full-scan and diff-scan patterns. + +### Requirement: Taskless CI skill is marked optional in the skill catalog + +**Reason**: The catalog reduces to a single mandatory skill (`taskless`); there are no optional skills in the new design. + +**Migration**: CI is now a topic discoverable via `tskl help` rather than an optional installation. Anyone with the consolidated skill installed can fetch the CI recipe; nothing to opt into separately. + +### Requirement: Taskless CI skill teaches full-scan and diff-scan patterns + +**Reason**: The pattern teaching moves into the recipe text. + +**Migration**: `ci.txt` preserves the full-scan / diff-scan pattern descriptions, the per-CI diff-target variable table, the GitHub Actions reference template, and the translation guidance for other CI systems. + +### Requirement: Taskless CI skill generates non-destructive configuration + +**Reason**: The non-destructive constraint moves into the recipe's Steps section. + +**Migration**: `ci.txt` Steps SHALL include "generate standalone config files; never edit the user's existing CI config" and reference canonical paths per CI system. + +### Requirement: Taskless CI skill requires no authentication + +**Reason**: This remains true — the `ci` topic recipe does not invoke any authenticated CLI commands. + +**Migration**: Documented in `ci.txt` Preconditions section. + +### Requirement: Taskless CI skill gates CI setup on rule presence + +**Reason**: The gate moves into the recipe's Preconditions and first Step. + +**Migration**: `ci.txt` SHALL gate setup on `npx @taskless/cli check` returning a non-empty rule set; if no rules exist, the recipe SHALL instruct the agent to invoke the rule-create flow first by recommending the user say "create a taskless rule for X". diff --git a/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-create-rule/spec.md b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-create-rule/spec.md new file mode 100644 index 00000000..69099aea --- /dev/null +++ b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-create-rule/spec.md @@ -0,0 +1,75 @@ +# Skill: Rules Create + +## REMOVED Requirements + +### Requirement: Rules create skill gathers input conversationally + +**Reason**: The standalone `taskless-create-rule` skill is replaced by the consolidated `taskless` skill plus the `tskl help rule create` recipe. + +**Migration**: The conversational input-gathering instructions move into `packages/cli/src/help/rule-create.txt` (API-backed flow) and `packages/cli/src/help/rule-create.anonymous.txt` (local-only flow). Agents discover these via the consolidated `taskless` skill's topic table. + +### Requirement: Rules create skill invokes CLI with JSON stdin + +**Reason**: Same as above. Recipe steps now live in the help text files, not in a per-skill SKILL.md. + +**Migration**: The CLI invocation `npx @taskless/cli rule create --from --json` (note the verb rename `rules` → `rule`) is documented in `rule-create.txt`. The flag set and JSON input shape are unchanged. + +### Requirement: Rules create skill constructs valid JSON payload + +**Reason**: Same as above. + +**Migration**: The JSON payload shape is documented in the embedded JSON schema (zod-to-json-schema output) within `rule-create.txt` per the new recipe template requirement in `cli-help`. + +### Requirement: Rules create skill handles stale config errors + +**Reason**: Same as above. + +**Migration**: Error handling moves to the recipe's `## Errors` section, which references stable error codes emitted by the CLI's `--json` failure mode (per `cli` capability standardization). + +### Requirement: Rules create skill has correct frontmatter + +**Reason**: There is no longer a `taskless-create-rule` skill file with frontmatter — the file itself is removed. + +**Migration**: The consolidated `taskless` skill's frontmatter is governed by `skill-taskless` capability requirements. + +### Requirement: Rules create skill routes by authentication status + +**Reason**: Auth-state branching no longer routes between two skills; it is handled inside the recipe via `--anonymous` variant lookup. + +**Migration**: When the user is logged out OR the agent is told to use anonymous mode, it fetches `tskl help rule create --anonymous`, which returns the local-only recipe. The `--anonymous` flag is also passed to the CLI invocation, where it dispatches the local-only flow per the `cli` capability requirements. + +### Requirement: Anonymous create skill derives rules locally via agent + +**Reason**: The `taskless-create-rule-anonymous` skill is removed; the local-only flow moves into the CLI's `rule create --anonymous` branch and the `rule-create.anonymous.txt` recipe. + +**Migration**: See `cli-rules` for the `--anonymous` branch behavior; see `cli-help` for the recipe variant lookup. + +### Requirement: Anonymous create skill writes rule and test files + +**Reason**: Same as above. File writes now happen in the CLI, not orchestrated by the agent. + +**Migration**: The CLI's `rule create --anonymous` branch SHALL write `.taskless/rules/.yml` and `.taskless/rule-tests/.yml` directly. The recipe's Steps section reflects this — the agent invokes and reports. + +### Requirement: Anonymous create skill uses verify feedback loop + +**Reason**: The verify loop remains in the recipe; the agent still owns the iteration. Only the skill file is removed. + +**Migration**: The `rule-create.anonymous.txt` recipe documents the verify loop step-by-step, invoking `npx @taskless/cli rule verify` (note rename) between agent edits. + +### Requirement: Anonymous create skill produces no metadata sidecar + +**Reason**: This behavior remains true of the `--anonymous` branch but is governed by the CLI command's behavior, not the skill. + +**Migration**: Documented in `rule-create.anonymous.txt` and enforced by the CLI `rule create --anonymous` implementation. + +### Requirement: Anonymous create skill is not directly invocable + +**Reason**: There is no anonymous skill to be invocable in the new design. + +**Migration**: Anonymous mode is reached by passing `--anonymous` to any compatible action; there is no separate skill the user or agent can invoke directly. + +### Requirement: Anonymous create skill has correct frontmatter + +**Reason**: There is no longer a `taskless-create-rule-anonymous` skill file with frontmatter. + +**Migration**: N/A. diff --git a/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-delete-rule/spec.md b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-delete-rule/spec.md new file mode 100644 index 00000000..6fa7a4bc --- /dev/null +++ b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-delete-rule/spec.md @@ -0,0 +1,21 @@ +# Skill: Rules Delete + +## REMOVED Requirements + +### Requirement: Rules delete skill identifies rules conversationally + +**Reason**: The standalone `taskless-delete-rule` skill is replaced by the consolidated `taskless` skill plus the `tskl help rule delete` recipe. + +**Migration**: The conversational rule-identification flow moves into `packages/cli/src/help/rule-delete.txt`. + +### Requirement: Rules delete skill invokes CLI with rule ID + +**Reason**: Same as above. + +**Migration**: The CLI invocation `npx @taskless/cli rule delete ` (note verb rename `rules` → `rule`) is documented in `rule-delete.txt`. + +### Requirement: Rules delete skill has correct frontmatter + +**Reason**: There is no longer a `taskless-delete-rule` skill file. + +**Migration**: N/A. The consolidated `taskless` skill's frontmatter is governed by `skill-taskless`. diff --git a/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-improve-rule/spec.md b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-improve-rule/spec.md new file mode 100644 index 00000000..3f30bb27 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-improve-rule/spec.md @@ -0,0 +1,69 @@ +# Skill: Improve Rule + +## REMOVED Requirements + +### Requirement: Skill inventories existing rules + +**Reason**: The standalone `taskless-improve-rule` skill is replaced by the consolidated `taskless` skill plus the `tskl help rule improve` recipe. + +**Migration**: The instruction to inventory existing rules under `.taskless/rules/` moves into `packages/cli/src/help/rule-improve.txt` (API-backed flow) and `packages/cli/src/help/rule-improve.anonymous.txt` (local-only flow). + +### Requirement: Skill determines improvement approach + +**Reason**: Same as above. + +**Migration**: The decision tree (target a specific rule, broad refactor, or merge multiple rules) lives in `rule-improve.txt`'s Steps section. + +### Requirement: Skill builds iterate payload with references + +**Reason**: Same as above. + +**Migration**: The payload shape is documented in the embedded JSON schema within `rule-improve.txt` (zod-to-json-schema output per the new recipe template). + +### Requirement: Skill cross-references use skill names + +**Reason**: There are no longer multiple skill names to cross-reference. + +**Migration**: Recipes cross-reference each other via topic names (`tskl help rule create`, `tskl help check`, etc.) using the `## See Also` section in the recipe template. + +### Requirement: Improve rule skill routes by authentication status + +**Reason**: Auth-state branching no longer routes between two skills; it is handled inside the recipe via `--anonymous` variant lookup and the CLI's `rule improve --anonymous` flag. + +**Migration**: When the user is logged out OR anonymous mode is requested, the agent fetches `tskl help rule improve --anonymous` and invokes the CLI with `--anonymous`. + +### Requirement: Anonymous improve skill iterates on rules locally via agent + +**Reason**: The `taskless-improve-rule-anonymous` skill is removed; the local-only flow moves into the CLI's `rule improve --anonymous` branch and the `rule-improve.anonymous.txt` recipe. + +**Migration**: See `cli-rules` for the `--anonymous` branch; see `cli-help` for the variant lookup. + +### Requirement: Anonymous improve skill writes updated files + +**Reason**: File writes now happen in the CLI, not orchestrated by the agent. + +**Migration**: The CLI's `rule improve --anonymous` branch SHALL write the updated rule file directly. The recipe documents this — the agent invokes and reports. + +### Requirement: Anonymous improve skill uses verify feedback loop + +**Reason**: The verify loop remains; only the skill file is removed. + +**Migration**: The `rule-improve.anonymous.txt` recipe documents the verify loop step-by-step. The agent owns the iteration; the CLI provides the `rule verify` primitive. + +### Requirement: Anonymous improve skill supports all improvement approaches + +**Reason**: The branch logic for "specific rule", "broad refactor", "merge rules" moves into the recipe. + +**Migration**: Documented in `rule-improve.anonymous.txt` Steps section. + +### Requirement: Anonymous improve skill is not directly invocable + +**Reason**: No standalone anonymous skill in the new design. + +**Migration**: Anonymous mode is reached via the `--anonymous` flag. + +### Requirement: Anonymous improve skill has correct frontmatter + +**Reason**: There is no longer a skill file with frontmatter. + +**Migration**: N/A. diff --git a/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-taskless/spec.md b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-taskless/spec.md new file mode 100644 index 00000000..35f3b536 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skill-taskless/spec.md @@ -0,0 +1,91 @@ +# Skill: Taskless + +## ADDED Requirements + +### Requirement: Single consolidated taskless skill replaces per-task skills + +The skills bundle SHALL contain exactly one skill named `taskless`. This skill SHALL replace the per-task skills `taskless-check`, `taskless-create-rule`, `taskless-create-rule-anonymous`, `taskless-improve-rule`, `taskless-improve-rule-anonymous`, `taskless-delete-rule`, `taskless-info`, `taskless-login`, `taskless-logout`, and `taskless-ci`. The skill SHALL be installed into every detected tool location (Claude Code, OpenCode, Cursor, etc.) per the existing install plumbing. + +#### Scenario: Bundle contains exactly one skill + +- **WHEN** the CLI bundle is built +- **THEN** `import.meta.glob("../../../../skills/**/SKILL.md")` SHALL match exactly one file at `skills/taskless/SKILL.md` + +#### Scenario: Skill catalog has one entry + +- **WHEN** `getMandatorySkillNames()` is called +- **THEN** it SHALL return `["taskless"]` +- **AND** `getOptionalSkillNames()` SHALL return `[]` + +#### Scenario: Old skill directories are removed + +- **WHEN** the v0.7.0 release is built +- **THEN** none of the directories `skills/taskless-check`, `skills/taskless-ci`, `skills/taskless-create-rule`, `skills/taskless-create-rule-anonymous`, `skills/taskless-delete-rule`, `skills/taskless-improve-rule`, `skills/taskless-improve-rule-anonymous`, `skills/taskless-info`, `skills/taskless-login`, or `skills/taskless-logout` SHALL exist in the repository + +### Requirement: Skill description anchors triggers on Taskless-specific phrases + +The consolidated skill's `description` frontmatter field SHALL anchor triggers on either an explicit reference to "Taskless" in the user's message OR a reference to the `.taskless/` directory or files within it (rules, rule-tests, rule-metadata). The description SHALL explicitly instruct the agent NOT to trigger on generic ESLint, linting, or rule requests that don't reference Taskless. + +#### Scenario: Description includes anchored trigger phrases + +- **WHEN** the skill `description` field is read +- **THEN** it SHALL include trigger phrases such as "create/add/write a taskless rule", "improve/fix/iterate on this taskless rule", "run taskless", "taskless login", "add taskless to CI" +- **AND** SHALL include an explicit "Do NOT trigger on" clause covering generic ESLint and linting requests + +#### Scenario: Description is at most 1024 characters + +- **WHEN** the skill `description` field length is measured +- **THEN** it SHALL be at most 1024 characters (Agent Skills spec limit) + +### Requirement: Skill body is a router, not an inline recipe + +The consolidated skill body SHALL NOT contain step-by-step instructions for any individual Taskless task. The body SHALL be a router that: + +1. States explicitly that the agent does NOT have the steps for any Taskless action in its context +2. Instructs the agent to fetch the canonical recipe via `npx @taskless/cli help ` before proceeding +3. Provides a topic disambiguation table mapping user intents to topic names +4. Includes a `## --anonymous` section explaining the global flag's behavior +5. Includes a first-step `.taskless/` presence check with graceful failure ("ask the user to confirm they meant Taskless") + +The body SHALL be no more than 60 lines of markdown to keep the always-loaded surface small. + +#### Scenario: Skill body warns against improvising + +- **WHEN** the skill body is read by an agent +- **THEN** it SHALL contain explicit framing such as "You do NOT have the steps... do not improvise from prior knowledge" + +#### Scenario: Skill body lists available topics + +- **WHEN** the skill body is read by an agent +- **THEN** it SHALL include a table or list mapping user intents (create rule, improve rule, delete rule, check, auth, ci) to the corresponding `tskl help ` invocations + +#### Scenario: Skill body checks for .taskless directory + +- **WHEN** the skill is invoked +- **THEN** the body's first step SHALL instruct the agent to check whether `.taskless/` exists in the working directory +- **AND** to ask the user to confirm Taskless is what they meant if the directory is absent + +### Requirement: Skill maps to a single tskl command + +The consolidated skill's frontmatter SHALL include `metadata.commandName: tskl` so that command-installation plumbing maps the skill to the new single command file at `commands/tskl/tskl.md`. The command file SHALL be a thin doorway that accepts a free-form `$ARGUMENTS` ask, infers a topic if possible, and otherwise asks the user what they want to do. + +#### Scenario: Frontmatter declares the command mapping + +- **WHEN** the skill frontmatter is parsed +- **THEN** `metadata.commandName` SHALL equal `tskl` + +#### Scenario: Old command files are removed + +- **WHEN** the v0.7.0 release is built +- **THEN** none of `commands/tskl/check.md`, `commands/tskl/improve.md`, `commands/tskl/info.md`, `commands/tskl/login.md`, `commands/tskl/logout.md`, or `commands/tskl/rule.md` SHALL exist in the repository +- **AND** exactly one file `commands/tskl/tskl.md` SHALL exist + +#### Scenario: Slash command accepts free-form arguments + +- **WHEN** a user invokes `/tskl` with arguments (e.g. `/tskl create a rule for no console.log`) +- **THEN** the command body SHALL instruct the agent to infer the topic from `$ARGUMENTS`, fetch the recipe via `npx @taskless/cli help `, and proceed + +#### Scenario: Slash command without arguments asks the user + +- **WHEN** a user invokes `/tskl` with no arguments +- **THEN** the command body SHALL instruct the agent to ask the user what they want to do with Taskless before proceeding diff --git a/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skills/spec.md b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skills/spec.md new file mode 100644 index 00000000..f848c651 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/specs/skills/spec.md @@ -0,0 +1,98 @@ +# Skills + +## MODIFIED Requirements + +### Requirement: Skills use SKILL.md format with YAML frontmatter + +The single skill SHALL be defined at `skills/taskless/SKILL.md` with YAML frontmatter (`name`, `description`, `metadata`) followed by markdown instructions. The `name` field SHALL be exactly `taskless` (no per-task prefix). The `metadata` field SHALL include `author`, `version`, and `commandName: tskl` keys. The `version` SHALL be used for staleness detection when the skill is installed into target repositories. + +The skill body SHALL begin by instructing the agent that it does NOT have step-by-step instructions for any Taskless action and that recipes must be fetched via `npx @taskless/cli help ` before proceeding. The body SHALL NOT contain inline step-by-step recipes for any individual task — those live in `packages/cli/src/help/.txt` files served by the help subcommand. + +#### Scenario: Skill directory contains valid SKILL.md + +- **WHEN** the skill is built +- **THEN** `skills/taskless/SKILL.md` SHALL exist +- **AND** SHALL contain YAML frontmatter with `name: taskless` and `description` (up to 1024 chars) +- **AND** the `metadata` field SHALL include `author: taskless`, `version` (string matching CLI package version), and `commandName: tskl` +- **AND** the markdown body SHALL contain the router instructions described above (no per-task recipes inline) + +#### Scenario: Skill body delegates to CLI help + +- **WHEN** the skill body is read +- **THEN** it SHALL instruct the agent to fetch the canonical recipe via `npx @taskless/cli help ` before performing any Taskless action +- **AND** SHALL NOT duplicate recipe content inline + +### Requirement: Skill names are globally qualified with taskless prefix + +The single skill name SHALL be `taskless` (without a per-task suffix). When installed into a target tool, the skill SHALL be installed at `/skills/taskless/SKILL.md` (e.g. `.claude/skills/taskless/SKILL.md`). + +#### Scenario: Skill name is exactly taskless + +- **WHEN** the skill is installed into any tool location +- **THEN** the directory name SHALL be `taskless` +- **AND** SHALL NOT use a `taskless-` per-task name + +### Requirement: Commands directory contains Claude Code command files + +The `commands/tskl/` directory SHALL contain exactly one command file (`tskl.md`) that maps to the consolidated skill. The command body SHALL accept a free-form `$ARGUMENTS` ask and route via the same flow as the skill (fetch `npx @taskless/cli help `, follow the recipe). When `$ARGUMENTS` is empty or ambiguous, the command body SHALL instruct the agent to ask the user what they want to do. + +#### Scenario: Single command file exists + +- **WHEN** the v0.7.0 release is built +- **THEN** `commands/tskl/tskl.md` SHALL exist +- **AND** no other command files SHALL exist in `commands/tskl/` + +#### Scenario: Command file is a router + +- **WHEN** the command body is read +- **THEN** it SHALL instruct the agent to handle `$ARGUMENTS` by inferring a topic, fetching its recipe, and proceeding +- **AND** SHALL specify behavior when `$ARGUMENTS` is empty (ask the user) + +### Requirement: Plugin manifest declares skills and commands + +The `.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json` SHALL declare the single consolidated skill and the single command. The plugin version SHALL be `0.7.0` for this release. The plugin description MAY be updated to reflect the consolidation. + +#### Scenario: Plugin manifest reflects consolidated bundle + +- **WHEN** `plugin.json` is read +- **THEN** `version` SHALL be `0.7.0` +- **AND** `commands` SHALL point to `./commands/tskl/` +- **AND** the bundled commands directory SHALL contain only `tskl.md` + +## REMOVED Requirements + +### Requirement: Info skill confirms Taskless is working + +**Reason**: The `taskless-info` skill is removed; "info" is now a topic accessed via `tskl help info`. + +**Migration**: Info instructions move into `packages/cli/src/help/info.txt`. The CLI command `taskless info` is unchanged. + +### Requirement: Check skill uses --json flag for machine-readable output + +**Reason**: The `taskless-check` skill is removed; check is now a topic accessed via `tskl help check`. + +**Migration**: Check recipe instructions move into `packages/cli/src/help/check.txt`. The CLI command and its `--json` flag are unchanged. + +### Requirement: Login skill delegates documentation to CLI help + +**Reason**: The `taskless-login` skill is removed; login is now a branch within `tskl help auth`. + +**Migration**: Login instructions move into `packages/cli/src/help/auth.txt` under the login branch. + +### Requirement: Logout skill delegates documentation to CLI help + +**Reason**: The `taskless-logout` skill is removed; logout is now a branch within `tskl help auth`. + +**Migration**: Logout instructions move into `packages/cli/src/help/auth.txt` under the logout branch. + +### Requirement: Rule create skill uses --json flag and delegates docs to help + +**Reason**: The `taskless-create-rule` skill is removed; rule create is now a topic accessed via `tskl help rule create`. + +**Migration**: Recipe instructions move into `packages/cli/src/help/rule-create.txt` (API-backed) and `packages/cli/src/help/rule-create.anonymous.txt` (local-only). See `skill-create-rule` for full migration notes. + +### Requirement: Rule delete skill delegates docs to help + +**Reason**: The `taskless-delete-rule` skill is removed; rule delete is now a topic accessed via `tskl help rule delete`. + +**Migration**: Recipe instructions move into `packages/cli/src/help/rule-delete.txt`. See `skill-delete-rule`. diff --git a/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/tasks.md b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/tasks.md new file mode 100644 index 00000000..78a9f052 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-consolidate-taskless-skills/tasks.md @@ -0,0 +1,142 @@ +## 1. Dependencies and catalog + +- [x] 1.1 Schema embedding uses zod 4 built-in `z.toJSONSchema()` — no separate dep needed +- [x] 1.2 Shrink `packages/cli/src/install/catalog.ts` to a single `{ name: "taskless", optional: false }` entry +- [x] 1.3 Keep `getOptionalSkillNames()` and `isOptionalSkill()` returning empty for back-compat — there are no optional skills in the new design +- [x] 1.4 Build-time guard (Vite `assertSkillVersions`) already verifies catalog ↔ source match; no change required since the existing logic handles any catalog size + +## 2. CLI verb rename: `rules` → `rule` + +- [x] 2.1 Rename the citty subcommand definition in `packages/cli/src/commands/rules.ts` so it registers under `rule` (singular). The source file MAY stay named `rules.ts` +- [x] 2.2 Update `packages/cli/src/index.ts` (or wherever subcommands are wired) to register `rule` instead of `rules` +- [x] 2.3 Rename help files: `packages/cli/src/help/rules-create.txt` → `rule-create.txt`, `rules-improve.txt` → `rule-improve.txt`, `rules-delete.txt` → `rule-delete.txt`, `rules-verify.txt` → `rule-verify.txt`, `rules-meta.txt` → `rule-meta.txt`, `rules.txt` → `rule.txt` +- [x] 2.4 Update help-file content to reference `taskless rule create` etc. throughout (no `rules` in body text) +- [x] 2.5 Search the repo for stale `rules create`/`rules improve`/etc. references and update them: docs, comments, error messages +- [x] 2.6 Update tests in `packages/cli/test/` to invoke the renamed subcommand +- [x] 2.7 Confirm `pnpm typecheck` and `pnpm lint` are clean after rename + +## 3. Global `--anonymous` flag + +- [x] 3.1 Add `anonymous: { type: "boolean", default: false }` to every action command's `args` block in `packages/cli/src/commands/{auth,check,info,rules}.ts` +- [x] 3.2 In `auth login`: when `args.anonymous` is true, exit 1 with a clear "auth commands cannot be anonymous" message +- [x] 3.3 In `auth logout`: accept and no-op +- [x] 3.4 In `info`: when `args.anonymous` is true, skip the API/auth probe and report local state only +- [x] 3.5 In `check`, `rule delete`, `rule verify`, `rule meta`, `init`: accept and no-op +- [x] 3.6 In `rule create` and `rule improve`: when `args.anonymous` is true, exit with a pointer to `taskless help --anonymous`. Per Option A, generation runs in the agent (not the CLI); the recipe variant guides the agent +- [x] 3.7 Per-command behavior matrix covered in `anonymous-flag.test.ts` (info skip, auth login reject, rule create/improve recipe pointer, no-op on the rest) +- [x] 3.8 Documented in consolidated SKILL.md `## --anonymous` section, the `/tskl` command body, and the per-topic anonymous variants (`rule-create.anonymous.txt`, `rule-improve.anonymous.txt`) + +## 4. Audit action commands for self-sufficient file writes + +- [x] 4.1 Audited each command: + - `rule create` (API) — CLI writes rule + test + metadata files via writeRuleFile/writeRuleTestFile/writeRuleMetaFiles. Agent only invokes and reports. + - `rule improve` (API) — CLI writes updated rule + test files. Same pattern. + - `rule create --anonymous` / `rule improve --anonymous` — Per Option A, the CLI rejects with a pointer to the local-only recipe. The AGENT writes files via its own tools per the recipe. Self-write doesn't apply (intentional design choice). + - `rule delete` — CLI deletes files. Self-writes. + - `rule verify` — Read-only; no writes. + - `rule meta` — Read-only. + - `check` — Read-only scanner. + - `info` — Read-only state report. + - `auth login` — CLI writes the token to `.taskless/.env.local.json`. + - `auth logout` — CLI removes the token. +- [x] 4.2 No gaps identified — every action command that produces artifacts writes them itself +- [x] 4.3 Existing write-path coverage in `apply-install-plan.test.ts`, `login-interactive.test.ts`, `rule-from.test.ts` — each action command writes its own outputs end-to-end. Explicit "agent does nothing post-CLI" framing is implicit in the recipes (Steps say "invoke and report") rather than enforced as a test assertion +- [x] 4.4 Recipes (task 8) describe the agent flow as "invoke and report" wherever applicable + +## 5. Anonymous flow absorption into CLI + +**Superseded by Option A architecture decision** (recorded in design.md +"Decisions" section). The anonymous rule-creation and rule-improvement +flows stay agent-driven rather than being absorbed into the CLI. The +CLI exposes `rule verify` as the primitive; the agent owns the loop and +writes the rule files itself per the `.anonymous.txt` recipe. +The CLI's `--anonymous` flag on `rule create` / `rule improve` exits +cleanly with a pointer to the recipe (see task 3.6). + +- [x] 5.1 ~~Move the local-only rule-creation flow into the CLI~~ — superseded by Option A; flow stays agent-driven via `rule-create.anonymous.txt` +- [x] 5.2 ~~Move the local-only rule-improvement flow into the CLI~~ — superseded by Option A; flow stays agent-driven via `rule-improve.anonymous.txt` +- [x] 5.3 ~~Unit tests for the absorbed branches~~ — N/A; behavior tested in `anonymous-flag.test.ts` (CLI exits with pointer) +- [x] 5.4 The verify feedback loop stays agent-driven; the CLI provides `rule verify` as the primitive; the agent owns the loop per the recipe + +## 6. Standardize CLI error output for recipe references + +- [x] 6.1 Define a stable error-code enum in `packages/cli/src/types/errors.ts` (or extend the existing `GeneratorErrorCode`) covering at minimum: `AUTH_REQUIRED`, `NO_GITHUB_REMOTE`, `RULE_GENERATION_FAILED`, `RULE_NOT_FOUND`, `INVALID_INPUT`, `NETWORK_ERROR` +- [x] 6.2 When any action command exits with an error AND `--json` was set, output `{ "ok": false, "code": "", "message": "" }` to stdout and a non-zero exit code +- [x] 6.3 Update existing error-throwing sites to use the standardized codes +- [x] 6.4 Tests covering rule create/improve/meta/verify error envelope codes in `error-envelope.test.ts` + +## 7. `tskl help` extensions: template, schemas, variants, no-args index + +- [x] 7.1 Recipe template (Goal/Preconditions/Steps/Schema/Errors/See Also) + versioned header applied to every help file in task 8 +- [x] 7.2 Extend `packages/cli/src/commands/help.ts` to recognize a `--anonymous` flag; when set, look up `.anonymous.txt` first and fall back to `.txt` +- [x] 7.3 Add a build-time map of which topics have anonymous variants — derived from `import.meta.glob` matching `*.anonymous.txt` +- [x] 7.4 Add JSON schema embedding via the `{{INPUT_SCHEMA}}` placeholder. Recipes that include the marker get the JSON Schema rendered from the corresponding Zod input via `z.toJSONSchema()` (zod 4 built-in; no extra dep). `{{CLI_VERSION}}` placeholder also supported for the recipe header +- [x] 7.5 Update `packages/cli/src/commands/help.ts` no-args output to include a human slug (paragraph explaining what the command does for human vs. agent) followed by a topic disambiguation table +- [x] 7.6 Emit `help_` telemetry event on every topic fetch; emit `help_index` on no-args fetch (already done in task 11; this task adds the `anonymous` property to the topic event) +- [x] 7.7 Tests in `help-extensions.test.ts` cover: no-args slug + topic table, `{{CLI_VERSION}}` and `{{INPUT_SCHEMA}}` interpolation, anonymous variant lookup + fallback, unknown topic exit, bare-taskless non-TTY routing + +## 8. Author the seven topic recipes + +- [x] 8.1 Author `packages/cli/src/help/rule-create.txt` (API-backed flow) — Goal/Preconditions/Steps/Schema/Errors/See Also; embeds the JSON schema via `{{INPUT_SCHEMA}}` +- [x] 8.2 Author `packages/cli/src/help/rule-create.anonymous.txt` (local-only flow) — distinct steps, no API references, includes the verify loop the agent owns +- [x] 8.3 Author `packages/cli/src/help/rule-improve.txt` (API-backed flow) — preserves the verify loop end-to-end +- [x] 8.4 Author `packages/cli/src/help/rule-improve.anonymous.txt` (local-only flow) +- [x] 8.5 Author `packages/cli/src/help/rule-delete.txt` — short; no schema needed; deletes by rule ID +- [x] 8.6 Author `packages/cli/src/help/check.txt` — restructured into the template format +- [x] 8.7 Author `packages/cli/src/help/auth.txt` — combined login/logout/status with branches; replaces the per-subcommand `auth-login.txt` and `auth-logout.txt` (deleted) +- [x] 8.8 Author `packages/cli/src/help/info.txt` — local state report, version, auth state +- [x] 8.9 Author `packages/cli/src/help/ci.txt` — ported from the v0.6 `taskless-ci` skill body into the recipe template +- [x] 8.10 Author `packages/cli/src/help/init.txt` — directs the user to run `npx @taskless/cli` themselves; describes wizard behavior + the v0.6→v0.7 cleanup +- [x] 8.11 For each recipe, include the `## Errors` section listing the error codes from task 6 and a user-facing fix per code + +## 9. Consolidated skill and command + +- [x] 9.1 Create `skills/taskless/SKILL.md` with the new ~30-line router body. Frontmatter description: anchored on Taskless-specific phrases or `.taskless/` references; explicitly says "do NOT trigger on generic ESLint, linting, or rule requests that don't reference Taskless" +- [x] 9.2 SKILL.md frontmatter SHALL include `metadata.commandName: tskl` so command-installation plumbing maps the skill to the new command +- [x] 9.3 SKILL.md body SHALL include the "you do NOT have the steps" framing, the `.taskless/` presence check as the first step, the topic table, and the `## --anonymous` section +- [x] 9.4 Create `commands/tskl/tskl.md` with the new ~10-line router body that handles `$ARGUMENTS`. Argument-hint: `` +- [x] 9.5 Delete the old skill directories: `skills/taskless-check`, `skills/taskless-ci`, `skills/taskless-create-rule`, `skills/taskless-create-rule-anonymous`, `skills/taskless-delete-rule`, `skills/taskless-improve-rule`, `skills/taskless-improve-rule-anonymous`, `skills/taskless-info`, `skills/taskless-login`, `skills/taskless-logout` +- [x] 9.6 Delete the old command files: `commands/tskl/check.md`, `commands/tskl/improve.md`, `commands/tskl/info.md`, `commands/tskl/login.md`, `commands/tskl/logout.md`, `commands/tskl/rule.md` +- [x] 9.7 Verify the consolidated skill builds via the existing `import.meta.glob` pattern in `packages/cli/src/install/install.ts` + +## 10. Wizard simplification and non-TTY routing + +- [x] 10.1 Delete the optional-skills wizard step file (`packages/cli/src/wizard/steps/optional-skills.ts` or equivalent) and its tests +- [x] 10.2 Update `packages/cli/src/wizard/index.ts` to remove the optional-skills step from the `runWizard()` composition +- [x] 10.3 Update bare `taskless` (in `packages/cli/src/index.ts`) so that when invoked with no args AND no TTY, it prints the non-TTY preamble + `help` index. Explicit `npx @taskless/cli init` preserves the existing `--no-interactive` fallback behavior +- [x] 10.4 Non-TTY routing covered in `help-extensions.test.ts` ("bare taskless (non-TTY) routes to help index" — asserts the non-interactive preamble + topic index) +- [x] 10.5 Update `packages/cli/src/commands/init.ts` install reporting to print "removed N obsolete skills" and "removed M obsolete commands" alongside "installed 1 skill" so users see the cleanup + +## 11. Telemetry rename + +- [x] 11.1 Rename existing capture sites in `packages/cli/src/commands/help.ts` from `cli_help_` to `help_`; emit `help_index` on no-args fetch +- [x] 11.2 Rename action-start events to `cli_` (e.g. `cli_rule_create`, `cli_rule_improve`, `cli_check`); add corresponding `cli__completed` events with success/failure properties +- [x] 11.3 Remove the old `cli_help`, `cli_help_`, `cli_init_completed` (renamed to `cli_init_completed`), etc. event names where superseded +- [x] 11.4 Update unit tests covering telemetry to expect the new event names + +## 12. Remove `--schema` flag and capability + +- [x] 12.1 Remove `--schema` flag from every command's `args` block in `packages/cli/src/commands/*.ts` +- [x] 12.2 Delete any code paths that handle `--schema` (likely in command run() bodies) +- [x] 12.3 Delete or repurpose the `cli-flag-schema` capability spec file (handled by the spec delta, but verify no orphan code references) +- [x] 12.4 Update tests that exercised `--schema` — they get replaced by tests in task 7.4 that verify schemas are embedded in `tskl help` output + +## 13. Migration cleanup via existing state + +- [x] 13.1 Verify `packages/cli/src/install/state.ts` already records every skill file written per target (verified — `applyInstallPlan` writes a fresh state on every run including all skills/commands per target) +- [x] 13.2 Verify `applyInstallPlan()` deletes files recorded in previous state but absent from current plan (verified — `computeInstallDiff()` produces removals which `applyInstallPlan` deletes before writing) +- [x] 13.3 Add an integration test: simulate a v0.6 install (state file lists the 10 old skills + 6 commands), run new install, assert all 16 are deleted and 1 new skill + 1 new command are written. Test added in `apply-install-plan.test.ts` +- [x] 13.4 The version-check via `metadata.version` in installed skill bodies vs. `__VERSION__` in `checkStaleness()` surfaces "out of date" via `info` (existing behavior, no change required) + +## 14. Capability spec deletion (filesystem cleanup) + +- [x] ~~14.1 After this change is archived, the spec deltas will guide moving these to the `archive` state. The deletion of `openspec/specs/skill-create-rule/`, `skill-improve-rule/`, `skill-delete-rule/`, `skill-auth-login/`, `skill-auth-logout/`, `skill-ci/`, `cli-flag-schema/` happens at archive time, not implementation time. No action required during apply~~ + +## 15. Release hygiene + +- [x] 15.1 Add a changeset noting BREAKING: CLI verb rename (`rules` → `rule`), removal of `--schema` flag, removal of individual skill names, telemetry event rename. v0.x semver — minor bump (0.6 → 0.7) per project convention +- [x] 15.2 Update `packages/cli/README.md` reflecting the consolidated skill, single command, `--anonymous` flag, and new `taskless help` flow +- [x] 15.3 `init.txt` updated as part of task 8 recipe authoring (uses the new template) +- [x] 15.4 Update root `README.md` reflecting the user-facing changes +- [x] 15.6 `pnpm typecheck` and `pnpm lint` clean +- [x] 15.7 Recipe rendering smoke-tested via `node packages/cli/dist/index.js help ` (canonical, anonymous variants, version interpolation, schema embedding all verified). Full end-to-end install + rule create + check flow against a scratch directory deferred to a separate validation task — covered piecemeal by existing CLI integration tests diff --git a/openspec/specs/analytics/spec.md b/openspec/specs/analytics/spec.md index 1a175836..6f1173c1 100644 --- a/openspec/specs/analytics/spec.md +++ b/openspec/specs/analytics/spec.md @@ -134,49 +134,56 @@ Every `capture()` call SHALL include the `cli` property (anonymous UUID), the `c ### Requirement: CLI events use cli\_ prefix -All CLI events SHALL use the `cli_` prefix and `snake_case` naming. The following events SHALL be emitted: - -| Event | Command | -| -------------------------- | ---------------------------------------------------------- | -| `cli_help` | `help` (top-level) | -| `cli_help_auth` | `help auth [subcommand]` | -| `cli_help_check` | `help check` | -| `cli_help_info` | `help info` | -| `cli_help_init` | `help init` | -| `cli_help_rule` | `help rules [subcommand]` | -| `cli_auth_login` | `auth login` (initiated) | -| `cli_auth_login_completed` | `auth login` (succeeded) | -| `cli_auth_logout` | `auth logout` | -| `cli_check` | `check` | -| `cli_init` | `init` (initiated) | -| `cli_init_completed` | `init` (completed successfully, wizard or non-interactive) | -| `cli_init_cancelled` | `init` (wizard cancelled before completion) | -| `cli_info` | `info` | -| `cli_rule_create` | `rules create` | -| `cli_rule_improve` | `rules improve` | -| `cli_rule_delete` | `rules delete` | -| `cli_rule_verify` | `rules verify` | -| `cli_rule_meta` | `rules meta` | - -#### Scenario: Each command emits its event - -- **WHEN** a user runs `taskless check` -- **THEN** the CLI SHALL emit a `cli_check` event before the command logic executes - -#### Scenario: Help for specific topic emits scoped event - -- **WHEN** a user runs `taskless help rules` -- **THEN** the CLI SHALL emit a `cli_help_rule` event - -#### Scenario: Init completion event carries wizard selections - -- **WHEN** an init run completes successfully -- **THEN** the CLI SHALL emit `cli_init_completed` with the properties `locations` (array of selected target paths), `optionalSkills` (array of optional skill names selected), `authPromptShown` (boolean), `authCompleted` (boolean), `nonInteractive` (boolean), and `durationMs` (number) - -#### Scenario: Init cancellation event identifies the step - -- **WHEN** the user cancels the wizard before completion -- **THEN** the CLI SHALL emit `cli_init_cancelled` with an `atStep` property whose value is one of `"intro"`, `"locations"`, `"optionalSkills"`, `"auth"`, or `"summary"` +CLI action events SHALL continue to use the `cli_` prefix, but the event taxonomy SHALL be reorganized as follows: + +- `cli_` — fired when an action command begins execution (e.g. `cli_rule_create`, `cli_rule_improve`, `cli_rule_delete`, `cli_check`, `cli_info`, `cli_init`, `cli_auth_login`, `cli_auth_logout`) +- `cli__completed` — fired when an action command finishes execution; event properties SHALL include `success: boolean`, `durationMs: number`, and `errorCode?: string` (when failure) +- `help_` — fired when the help command serves a specific topic (e.g. `help_rule_create`, `help_check`, `help_auth`); replaces previous `cli_help_` events +- `help_index` — fired when the help command is invoked with no arguments (probable agent confusion / routing failure) +- `help_unknown` — fired when the help command receives an unknown topic; event properties SHALL include `topic: string` (the attempted topic) + +The previous event names `cli_help`, `cli_help_auth`, `cli_help_check`, `cli_help_info`, `cli_help_init`, `cli_help_rule` SHALL be removed in this release. There is no dual-emit window — the rename is a hard cut. + +#### Scenario: Action command emits start and completion events + +- **WHEN** a user runs `taskless rule create --from req.json` +- **THEN** PostHog SHALL receive a `cli_rule_create` event when execution begins +- **AND** SHALL receive a `cli_rule_create_completed` event when execution finishes, with properties including `success`, `durationMs`, and (on failure) `errorCode` + +#### Scenario: Help fetch emits topic intent + +- **WHEN** an agent runs `taskless help rule create` +- **THEN** PostHog SHALL receive a `help_rule_create` event + +#### Scenario: Help no-args emits index event + +- **WHEN** an agent runs `taskless help` +- **THEN** PostHog SHALL receive a `help_index` event + +#### Scenario: Help unknown topic emits help_unknown + +- **WHEN** an agent runs `taskless help nonexistent` +- **THEN** PostHog SHALL receive a `help_unknown` event with property `topic: "nonexistent"` + +#### Scenario: Old event names are not emitted + +- **WHEN** any CLI command runs in v0.7.0 +- **THEN** PostHog SHALL NOT receive any event named `cli_help`, `cli_help_`, or any other event under the previous taxonomy + +### Requirement: Wrong-topic re-routing is observable as a derivable funnel + +The new event taxonomy is structured so that wrong-topic re-routing is a derivable funnel signal: + +- A `help_` event followed by no `cli_` event AND a subsequent `help_` event indicates the agent fetched the recipe for topic A, did not act on it, and re-routed to topic B +- A `help_index` event followed by a `help_` event indicates the agent consulted the index before picking a topic (expected behavior; baseline) +- A `help_` event with no subsequent `cli_` event AND no further `help_*` event indicates the agent abandoned the action + +No additional events SHALL be added to capture this signal directly — the funnel is derivable from the event sequence in PostHog. Dashboards SHOULD be created to surface re-routing rates per topic so wrong-topic confusion can be measured. + +#### Scenario: Funnel data supports wrong-topic detection + +- **WHEN** dashboards are constructed in PostHog +- **THEN** the events SHALL be sufficient to compute "rate of `help_` events not followed by a corresponding `cli_` event within N minutes" ### Requirement: Telemetry failures are silent diff --git a/openspec/specs/cli-auth/spec.md b/openspec/specs/cli-auth/spec.md index b537a15f..9106ebf9 100644 --- a/openspec/specs/cli-auth/spec.md +++ b/openspec/specs/cli-auth/spec.md @@ -17,58 +17,32 @@ The CLI SHALL register an `auth` subcommand group with `login` and `logout` as n ### Requirement: Auth login initiates Device Flow -The `taskless auth login` command SHALL initiate an OAuth Device Flow (RFC 8628). It SHALL display a verification URI and user code to stdout, then poll for authorization until the user completes the flow, the code expires, or the user cancels with Ctrl+C. +`taskless auth login` initiates the device-code flow per the existing requirement. The new `--anonymous` flag (per the `cli` capability) SHALL NOT be accepted on this command — invocation with `--anonymous` SHALL exit with code 1 and an error message stating "auth commands cannot be anonymous". -#### Scenario: Successful login +#### Scenario: Standard login still works - **WHEN** a user runs `taskless auth login` -- **THEN** the CLI SHALL display a verification URI and user code -- **AND** the CLI SHALL poll for authorization at the server-specified interval -- **AND** when authorization succeeds, the CLI SHALL save the access token and print a success message +- **THEN** the CLI SHALL initiate the device-code flow per the existing behavior -#### Scenario: Login when already authenticated +#### Scenario: Login rejects --anonymous -- **WHEN** a user runs `taskless auth login` and a valid token already exists -- **THEN** the CLI SHALL inform the user they are already logged in -- **AND** the CLI SHALL suggest running `taskless auth logout` first to re-authenticate - -#### Scenario: User cancels login - -- **WHEN** a user presses Ctrl+C during the polling loop -- **THEN** the CLI SHALL exit cleanly without saving any token - -#### Scenario: Device code expires - -- **WHEN** the device code expires before the user completes authorization -- **THEN** the CLI SHALL print an error message indicating the code has expired -- **AND** the CLI SHALL exit with a non-zero exit code +- **WHEN** a user runs `taskless auth login --anonymous` +- **THEN** the CLI SHALL exit with code 1 +- **AND** SHALL print "auth commands cannot be anonymous" (or similar) ### Requirement: Auth logout removes saved token -The `taskless auth logout` command SHALL remove the saved authentication token from all locations: the per-repo `.taskless/.env.local.json` (if present) and the global XDG config auth file. If no token exists in any location, it SHALL inform the user they are not logged in. - -#### Scenario: Successful logout removes per-repo and global tokens - -- **WHEN** a user runs `taskless auth logout` and both `.taskless/.env.local.json` and global auth file exist -- **THEN** the CLI SHALL delete both files -- **AND** the CLI SHALL print a confirmation message +`taskless auth logout` removes the saved token per the existing requirement. The `--anonymous` flag SHALL be accepted as a no-op on this command (logout is already a local operation requiring no API state). -#### Scenario: Logout removes only per-repo token +#### Scenario: Standard logout still works -- **WHEN** a user runs `taskless auth logout` and only `.taskless/.env.local.json` exists -- **THEN** the CLI SHALL delete `.taskless/.env.local.json` -- **AND** the CLI SHALL print a confirmation message +- **WHEN** a user runs `taskless auth logout` +- **THEN** the CLI SHALL remove the saved token per the existing behavior -#### Scenario: Logout removes only global token +#### Scenario: Logout accepts --anonymous as no-op -- **WHEN** a user runs `taskless auth logout` and only the global auth file exists -- **THEN** the CLI SHALL delete the global auth file -- **AND** the CLI SHALL print a confirmation message - -#### Scenario: Logout when not logged in - -- **WHEN** a user runs `taskless auth logout` and no token files exist -- **THEN** the CLI SHALL print a message indicating the user is not logged in +- **WHEN** a user runs `taskless auth logout --anonymous` +- **THEN** the CLI SHALL behave identically to `taskless auth logout` ### Requirement: Token is stored in XDG config directory @@ -227,3 +201,13 @@ On any command that reads from `.taskless/.env.local.json`, the CLI SHALL check - **WHEN** the CLI reads `.taskless/.env.local.json` and the file is not tracked by git - **THEN** the CLI SHALL NOT print any warning + +### Requirement: Auth error output uses standardized error envelope + +When any `taskless auth` subcommand exits with an error AND `--json` was passed, the output SHALL conform to the standardized error envelope `{ "ok": false, "code": "", "message": "<...>" }` per the `cli` capability requirements. + +#### Scenario: Auth login network failure in JSON mode + +- **WHEN** `taskless auth login --json` fails due to a network error +- **THEN** stdout SHALL contain `{ "ok": false, "code": "NETWORK_ERROR", "message": "..." }` +- **AND** the exit code SHALL be non-zero diff --git a/openspec/specs/cli-check/spec.md b/openspec/specs/cli-check/spec.md index f5bfea3e..599e3975 100644 --- a/openspec/specs/cli-check/spec.md +++ b/openspec/specs/cli-check/spec.md @@ -202,3 +202,24 @@ Before forwarding, the CLI SHALL silently drop any path that does not exist on d - **AND** `src/` is a directory - **THEN** the CLI SHALL forward `src/` to `sg scan` - **AND** the scan SHALL cover files under that directory + +### Requirement: Check accepts --anonymous as a no-op + +The `taskless check` command SHALL accept the global `--anonymous` flag (per the `cli` capability) without changing its behavior. `check` does not call the Taskless API, so the flag is effectively a no-op for this command. + +#### Scenario: check --anonymous behaves identically to check + +- **WHEN** a user runs `taskless check --anonymous` +- **THEN** the CLI SHALL execute the same logic as `taskless check` +- **AND** SHALL produce identical output (no warning, no error) +- **AND** SHALL exit with the same code as `taskless check` would + +### Requirement: Check error output uses standardized error envelope + +When `taskless check --json` exits with an error, the output SHALL conform to the standardized error envelope `{ "ok": false, "code": "", "message": "<...>" }` per the `cli` capability requirements. Existing success-shape requirements for `--json` are unchanged. + +#### Scenario: check --json error uses standardized envelope + +- **WHEN** `taskless check --json` fails (e.g. ast-grep invocation error) +- **THEN** stdout SHALL contain a JSON object matching the standardized error envelope +- **AND** SHALL include a stable `code` field (e.g. `SCAN_FAILED` if added to the enum) diff --git a/openspec/specs/cli-flag-schema/spec.md b/openspec/specs/cli-flag-schema/spec.md deleted file mode 100644 index ffb70ca3..00000000 --- a/openspec/specs/cli-flag-schema/spec.md +++ /dev/null @@ -1,172 +0,0 @@ -# CLI Schema - -## Purpose - -TBD -- Defines the `--schema` flag behavior, Zod schema infrastructure, per-command schema definitions, and runtime validation for the `@taskless/cli` package. - -## Requirements - -### Requirement: Zod schemas define CLI I/O contracts - -Each CLI command that supports `--json` SHALL have Zod schemas defined in `packages/cli/src/schemas/` that describe: - -- Input schema (for commands accepting `--from` JSON) -- Output schema (the success `--json` shape) -- Error schema (the failure `--json` shape) - -These Zod schemas SHALL be the single source of truth for validation and schema generation. - -#### Scenario: rules create has input, output, and error schemas - -- **WHEN** inspecting `packages/cli/src/schemas/rules-create.ts` -- **THEN** it SHALL export `inputSchema`, `outputSchema`, and `errorSchema` as Zod objects - -#### Scenario: rules improve has input, output, and error schemas - -- **WHEN** inspecting `packages/cli/src/schemas/rules-improve.ts` -- **THEN** it SHALL export `inputSchema`, `outputSchema`, and `errorSchema` as Zod objects - -#### Scenario: check has output and error schemas only - -- **WHEN** inspecting `packages/cli/src/schemas/check.ts` -- **THEN** it SHALL export `outputSchema` and `errorSchema` as Zod objects -- **AND** it SHALL NOT export `inputSchema` - -#### Scenario: update-engine has output and error schemas only - -- **WHEN** inspecting `packages/cli/src/schemas/update-engine.ts` -- **THEN** it SHALL export `outputSchema` and `errorSchema` as Zod objects -- **AND** it SHALL NOT export `inputSchema` - -### Requirement: Input schemas match --from JSON shapes - -The input Zod schema for each command SHALL describe exactly the JSON shape the agent writes to the `--from` file. It SHALL NOT include fields the CLI fills in from project config (e.g., `orgId`, `repositoryUrl`). - -#### Scenario: rules create input schema - -- **WHEN** the `inputSchema` from `rules-create.ts` is inspected -- **THEN** it SHALL require `prompt` (string) and optionally accept `successCases` (string array) and `failureCases` (string array) -- **AND** it SHALL NOT include `orgId` or `repositoryUrl` - -#### Scenario: rules improve input schema - -- **WHEN** the `inputSchema` from `rules-improve.ts` is inspected -- **THEN** it SHALL require `ruleId` (string) and `guidance` (string), and optionally accept `references` (array of `{ filename: string, content: string }`) - -### Requirement: Output schemas match --json success shapes - -The output Zod schema for each command SHALL describe the JSON object written to stdout when the command succeeds with `--json`. - -#### Scenario: rules create output schema - -- **WHEN** the `outputSchema` from `rules-create.ts` is inspected -- **THEN** it SHALL describe `{ success: true, ruleId: string, rules: string[], files: string[] }` - -#### Scenario: rules improve output schema - -- **WHEN** the `outputSchema` from `rules-improve.ts` is inspected -- **THEN** it SHALL describe `{ success: true, requestId: string, rules: string[], files: string[] }` - -#### Scenario: check output schema - -- **WHEN** the `outputSchema` from `check.ts` is inspected -- **THEN** it SHALL describe `{ success: boolean, results: CheckResult[] }` where CheckResult includes `source`, `ruleId`, `severity`, `message`, `file`, `range`, `matchedText`, and optional `note` and `fix` - -#### Scenario: update-engine output schema - -- **WHEN** the `outputSchema` from `update-engine.ts` is inspected -- **THEN** it SHALL describe the union of status shapes: `{ status: "current" }`, `{ status: "exists", requestId, prUrl }`, `{ status: "open", prUrl }`, `{ status: "merged", prUrl }`, `{ status: "closed", prUrl }` - -### Requirement: Error schemas match --json failure shapes - -The error Zod schema for each command SHALL describe the JSON object written to stdout when the command fails with `--json` and a non-zero exit code. - -#### Scenario: check error schema - -- **WHEN** the `errorSchema` from `check.ts` is inspected -- **THEN** it SHALL describe `{ success: false, error: string, results: CheckResult[] }` - -#### Scenario: rules create error schema - -- **WHEN** the `errorSchema` from `rules-create.ts` is inspected -- **THEN** it SHALL describe `{ error: string }` - -#### Scenario: update-engine error schema - -- **WHEN** the `errorSchema` from `update-engine.ts` is inspected -- **THEN** it SHALL describe `{ error: string }` - -### Requirement: --schema short-circuits command execution - -When `--schema` is passed, the command SHALL print schema information to stdout and exit with code 0. No authentication, configuration reading, network requests, or other side effects SHALL occur. - -#### Scenario: --schema on rules create requires no auth - -- **WHEN** a user runs `taskless rules create --schema` without being authenticated -- **THEN** the CLI SHALL print the schema blocks and exit 0 -- **AND** SHALL NOT attempt to read a token or project config - -#### Scenario: --schema on check requires no .taskless directory - -- **WHEN** a user runs `taskless check --schema` in a directory without `.taskless/` -- **THEN** the CLI SHALL print the schema blocks and exit 0 - -#### Scenario: --schema ignores other flags - -- **WHEN** a user runs `taskless rules create --schema --from request.json` -- **THEN** the CLI SHALL print schema blocks and exit 0 -- **AND** SHALL NOT read or process the `--from` file - -### Requirement: --schema output format - -When `--schema` is passed, the CLI SHALL output three labeled sections to stdout. Each section SHALL have a label line followed by either a valid JSON Schema object or a descriptive message. - -#### Scenario: Command with input, output, and error schemas - -- **WHEN** a user runs `taskless rules create --schema` -- **THEN** stdout SHALL contain: - - A line `Input Schema:` followed by a JSON Schema object - - A blank line separator - - A line `Output Schema:` followed by a JSON Schema object - - A blank line separator - - A line `Error Schema:` followed by a JSON Schema object - -#### Scenario: Command with no input schema - -- **WHEN** a user runs `taskless check --schema` -- **THEN** the `Input Schema:` section SHALL read `This command does not accept JSON input.` -- **AND** the `Output Schema:` and `Error Schema:` sections SHALL contain JSON Schema objects - -### Requirement: JSON Schema generation uses zod-to-json-schema - -The `--schema` output SHALL be generated by converting Zod schemas to JSON Schema format using the `zod-to-json-schema` library. - -#### Scenario: Output is valid JSON Schema - -- **WHEN** a user runs `taskless rules create --schema` -- **THEN** each JSON block in the output SHALL be a valid JSON Schema document parseable by `JSON.parse()` - -### Requirement: --from input validated via Zod - -Commands that accept `--from` JSON input SHALL validate the parsed JSON using the corresponding Zod input schema's `.parse()` method, replacing manual `typeof` checks. - -#### Scenario: rules create validates input with Zod - -- **WHEN** a user runs `taskless rules create --from request.json` -- **AND** the file contains `{ "prompt": 123 }` (wrong type) -- **THEN** the CLI SHALL fail with a Zod validation error message - -#### Scenario: rules improve validates input with Zod - -- **WHEN** a user runs `taskless rules improve --from request.json` -- **AND** the file is missing the required `guidance` field -- **THEN** the CLI SHALL fail with a Zod validation error message - -### Requirement: --json output validated via Zod - -Commands that support `--json` SHALL pass their output through the corresponding Zod output schema's `.parse()` method before serializing with `JSON.stringify`. - -#### Scenario: Output validation catches shape drift - -- **WHEN** a command constructs a `--json` output object that is missing a required field -- **THEN** the Zod `.parse()` call SHALL throw, preventing malformed output from reaching stdout diff --git a/openspec/specs/cli-help/spec.md b/openspec/specs/cli-help/spec.md index 67f6d860..0ac482de 100644 --- a/openspec/specs/cli-help/spec.md +++ b/openspec/specs/cli-help/spec.md @@ -8,34 +8,48 @@ TBD — Defines the help subcommand for the `@taskless/cli` package, including h ### Requirement: Help subcommand displays rich help text for commands -The CLI SHALL support a `help` subcommand that accepts zero or more positional arguments identifying a command path. When arguments are provided, the help subcommand SHALL look up a matching help text file embedded at build time and print its contents to stdout. When no arguments are provided, the help subcommand SHALL print a command index listing all top-level commands with their descriptions. +The CLI SHALL support a `help` subcommand that accepts zero or more positional arguments identifying a topic path AND an optional `--anonymous` boolean flag. When positional arguments are provided, the help subcommand SHALL look up a matching help text file embedded at build time using the following resolution order: -#### Scenario: Help for a top-level command +1. If `--anonymous` is set AND `.anonymous.txt` exists in the embedded map, return that file. +2. Otherwise, return `.txt`. +3. If neither exists, exit with code 1 and an error message suggesting `taskless help` for the topic index. + +When no positional arguments are provided, the help subcommand SHALL print a topic index containing a one-paragraph human slug followed by a topic disambiguation table mapping topic names to their summaries. + +#### Scenario: Help for a topic returns the recipe - **WHEN** a user runs `taskless help check` -- **THEN** the CLI SHALL print the contents of the `check` help file to stdout +- **THEN** the CLI SHALL print the contents of `check.txt` to stdout + +#### Scenario: Help for a nested topic joins with hyphens -#### Scenario: Help for a nested subcommand +- **WHEN** a user runs `taskless help rule create` +- **THEN** the CLI SHALL look up `rule-create.txt` and print its contents -- **WHEN** a user runs `taskless help auth login` -- **THEN** the CLI SHALL join the arguments with `-` to form the key `auth-login` -- **AND** print the contents of the `auth-login` help file to stdout +#### Scenario: Help with --anonymous returns the variant when present -#### Scenario: Help for a command group +- **WHEN** a user runs `taskless help rule create --anonymous` +- **AND** `rule-create.anonymous.txt` exists in the embedded help map +- **THEN** the CLI SHALL print the contents of `rule-create.anonymous.txt` -- **WHEN** a user runs `taskless help auth` -- **THEN** the CLI SHALL print the contents of the `auth` help file to stdout +#### Scenario: Help with --anonymous falls back when no variant exists -#### Scenario: Help with no arguments lists all commands +- **WHEN** a user runs `taskless help check --anonymous` +- **AND** no `check.anonymous.txt` exists +- **THEN** the CLI SHALL print the contents of `check.txt` (no error, no warning — anonymous is a no-op for this topic) + +#### Scenario: Help with no arguments shows index with human slug and disambiguation table - **WHEN** a user runs `taskless help` -- **THEN** the CLI SHALL print a command index listing each top-level command name and its description +- **THEN** the CLI SHALL print a one-paragraph human-facing slug explaining what the help command does for human vs. agent audiences +- **AND** SHALL print a topic table mapping each topic name to its one-line summary +- **AND** SHALL include a note about the `--anonymous` flag -#### Scenario: Help for unknown command shows error +#### Scenario: Help for an unknown topic exits with error - **WHEN** a user runs `taskless help nonexistent` -- **THEN** the CLI SHALL print an error message indicating the command is not recognized -- **AND** suggest running `taskless help` for available commands +- **THEN** the CLI SHALL print an error message indicating the topic is not recognized +- **AND** exit with code 1 ### Requirement: Help text files are embedded at build time @@ -53,10 +67,99 @@ Help text files SHALL be located at `packages/cli/src/help/` as plain `.txt` fil ### Requirement: Help text files follow a consistent format -Each help text file SHALL begin with a one-line summary of the command, followed by a blank line, then structured sections. The sections MAY include any of: a longer description, Prerequisites, Usage, Options, Output, Exit Codes, and Examples. Section headers SHALL be followed by a colon and content on subsequent indented lines. +Every help text file at `packages/cli/src/help/.txt` SHALL follow the canonical recipe template: + +``` +# Topic: (CLI v / topic v) + +## Goal + + +## Preconditions + + +## Steps + + +## Input schema + + +## Errors +
+ +## See Also + +``` + +The header line SHALL include the CLI version (interpolated at build time) and a topic version integer maintained by the recipe author and bumped when the recipe changes meaningfully. + +#### Scenario: Recipe contains all template sections + +- **WHEN** any `.txt` file is read +- **THEN** it SHALL begin with the `# Topic: (CLI v / topic v)` header +- **AND** SHALL contain `## Goal`, `## Preconditions`, `## Steps`, `## Errors`, and `## See Also` sections in that order + +#### Scenario: Recipe with --from input includes JSON schema + +- **WHEN** a topic recipe documents a CLI invocation that uses `--from ` +- **THEN** the recipe SHALL contain an `## Input schema` section with a code-fenced JSON Schema block +- **AND** the JSON Schema SHALL be derived from the corresponding Zod schema in `packages/cli/src/schemas/` + +#### Scenario: Header version reflects build-time CLI version + +- **WHEN** the CLI bundle is built +- **THEN** the recipe header's CLI version SHALL be interpolated at build time from `packages/cli/package.json` +- **AND** SHALL match the version reported by `taskless info` + +### Requirement: Anonymous variant lookup uses a compile-time map + +The help command SHALL construct, at build time, a Set of topic names that have a corresponding `.anonymous.txt` file. Lookup at runtime SHALL be O(1). The Set SHALL be derived from `import.meta.glob` matching `*.anonymous.txt` in the help directory. + +#### Scenario: Topics with variants are detected at build time + +- **WHEN** the CLI bundle is built +- **AND** files `rule-create.anonymous.txt` and `rule-improve.anonymous.txt` exist +- **THEN** the embedded variants set SHALL contain `rule-create` and `rule-improve` + +#### Scenario: Topics without variants are absent from the map + +- **WHEN** the CLI bundle is built +- **AND** no `check.anonymous.txt` file exists +- **THEN** the embedded variants set SHALL NOT contain `check` +- **AND** `taskless help check --anonymous` SHALL fall back to `check.txt` + +### Requirement: Embedded JSON schemas are generated via zod-to-json-schema + +For every recipe topic that documents a CLI command accepting `--from `, the corresponding Zod input schema in `packages/cli/src/schemas/` SHALL be converted to JSON Schema via `zod-to-json-schema` and embedded in the recipe's `## Input schema` section as a fenced code block. Generation MAY happen at runtime (small dep, fast) or at build time; runtime is acceptable. + +#### Scenario: rule create recipe embeds input schema + +- **WHEN** a user runs `taskless help rule create` +- **THEN** the output SHALL contain an `## Input schema` section +- **AND** the section SHALL contain a code-fenced JSON Schema block derived from the `rules-create` Zod schema (or the renamed `rule-create` schema) + +#### Scenario: rule improve recipe embeds input schema + +- **WHEN** a user runs `taskless help rule improve` +- **THEN** the output SHALL contain an `## Input schema` section with the rule-improve JSON Schema + +### Requirement: Help command emits intent telemetry + +The help command SHALL emit a PostHog event on every invocation: + +- `help_` (e.g. `help_rule_create`, `help_check`, `help_auth`) when called with positional arguments resolving to a known topic +- `help_index` when called with no positional arguments +- `help_unknown` (with the attempted topic as a property) when called with positional arguments resolving to no topic + +These events SHALL replace the previous `cli_help_` events in a single hard rename. + +#### Scenario: Topic fetch emits intent event + +- **WHEN** an agent runs `taskless help rule create` +- **THEN** PostHog SHALL receive a `help_rule_create` event -#### Scenario: Help file has summary and usage +#### Scenario: Index fetch emits help_index -- **WHEN** a help text file is read -- **THEN** the first line SHALL be a brief summary of the command -- **AND** the file SHALL contain a `Usage:` section showing the invocation pattern +- **WHEN** an agent runs `taskless help` (no args) +- **THEN** PostHog SHALL receive a `help_index` event diff --git a/openspec/specs/cli-init/spec.md b/openspec/specs/cli-init/spec.md index b254dad8..43678bbf 100644 --- a/openspec/specs/cli-init/spec.md +++ b/openspec/specs/cli-init/spec.md @@ -8,31 +8,31 @@ TBD — Defines the `taskless init` subcommand that installs Taskless skills int ### Requirement: Init subcommand installs skills into a repository -The CLI SHALL support a `taskless init` subcommand that installs Taskless skills into the current working directory. The subcommand SHALL also be available as `taskless update` (alias with identical behavior). By default, `init` SHALL launch the interactive wizard. When invoked with `--no-interactive`, `init` SHALL preserve the prior batch-install behavior: install every mandatory skill to every detected tool location, with no prompts and no auth step. When no tool directories are detected AND the CLI runs with `--no-interactive` (or the wizard user explicitly selects none of the detected locations and falls back), the CLI SHALL install skills to `.agents/skills//SKILL.md` as a fallback. +The CLI SHALL support a `taskless init` subcommand that installs the consolidated `taskless` skill into the current working directory's detected tool locations. The subcommand SHALL also be available as `taskless update` (alias). By default, `init` SHALL launch the interactive wizard. When invoked with `--no-interactive`, `init` SHALL preserve the prior batch-install behavior: install the consolidated skill to every detected tool location (or `.agents/` fallback when none detected) without prompting and without an auth step. -#### Scenario: Running taskless init launches the wizard by default +There is exactly one mandatory skill in v0.7.0 (`taskless`) and zero optional skills. The wizard's optional-skill selection step SHALL be removed. -- **WHEN** a user runs `taskless init` without `--no-interactive` in an interactive terminal -- **THEN** the CLI SHALL launch the interactive wizard instead of silently installing +The `--anonymous` flag is accepted on `init` as a no-op (init does not call the Taskless API directly). -#### Scenario: Running taskless init --no-interactive installs mandatory skills to all detected locations +#### Scenario: Running taskless init installs the consolidated skill -- **WHEN** a user runs `taskless init --no-interactive` in a repository with at least one detected AI tool -- **THEN** the CLI SHALL write every mandatory skill into each detected tool's directory without prompting -- **AND** SHALL NOT write any optional skills -- **AND** SHALL NOT prompt for authentication -- **AND** SHALL report which tools were updated and how many skills were installed +- **WHEN** a user runs `taskless init` in an interactive terminal +- **THEN** the wizard SHALL prompt for tool locations and auth, then install the single `taskless` skill +- **AND** SHALL NOT prompt for optional skills (none exist) -#### Scenario: Running taskless update behaves identically to init +#### Scenario: Init removes obsolete v0.6 skill files -- **WHEN** a user runs `taskless update` -- **THEN** the behavior SHALL be identical to `taskless init` +- **WHEN** a user with v0.6 installed (10 per-task skills written) runs the v0.7.0 `taskless init` +- **THEN** the install plumbing SHALL read the previous install state from `.taskless/taskless.json` +- **AND** SHALL delete the 10 obsolete skill files and 6 obsolete command files +- **AND** SHALL write the new `taskless` skill and `tskl` command +- **AND** SHALL update `.taskless/taskless.json` install state to reflect the new layout -#### Scenario: Running init --no-interactive with no detected tools uses fallback +#### Scenario: Init reports cleanup transparently -- **WHEN** a user runs `taskless init --no-interactive` in a repository with no detected AI tool signals -- **THEN** the CLI SHALL install mandatory skills to `.agents/skills/` -- **AND** report that the fallback location was used +- **WHEN** init removes obsolete files +- **THEN** the install summary output SHALL include "removed N obsolete skills" and "removed M obsolete commands" +- **AND** SHALL list the obsolete skill names so the user understands what changed ### Requirement: Tool detection via filesystem inspection @@ -267,44 +267,22 @@ The `init` subcommand SHALL use the resolved working directory from the global ` - **WHEN** a user runs `taskless init` without `-d` - **THEN** tool detection and skill installation SHALL operate on `process.cwd()` -### Requirement: Init installs anonymous skill variants - -The `taskless init` subcommand SHALL install the `taskless-create-rule-anonymous` and `taskless-improve-rule-anonymous` skills alongside existing skills. These skills SHALL be bundled into the CLI at build time using the same `import.meta.glob` pattern as existing skills. - -#### Scenario: Anonymous skills are installed for Claude Code - -- **WHEN** a user runs `taskless init` in a repository with a `.claude/` directory -- **THEN** the CLI SHALL write `taskless-create-rule-anonymous/SKILL.md` and `taskless-improve-rule-anonymous/SKILL.md` to `.claude/skills/` - -#### Scenario: Anonymous skills have no command files - -- **WHEN** the CLI installs skills and commands -- **THEN** no command `.md` files SHALL be created for the anonymous skill variants - -#### Scenario: Build includes anonymous skills - -- **WHEN** `pnpm build` is run in `packages/cli/` -- **THEN** the `taskless-create-rule-anonymous` and `taskless-improve-rule-anonymous` SKILL.md files SHALL be embedded in the output bundle - ### Requirement: Bare taskless invocation launches the init wizard -The CLI entry point SHALL delegate to `init` when invoked with no positional subcommand and a TTY is attached. When stdout is not a TTY (e.g., piped, non-interactive shell), bare `taskless` SHALL instead print the top-level help as it does today. Users SHALL continue to be able to view top-level help explicitly via `taskless help`. +The CLI entry point SHALL delegate to `init` when invoked with no positional subcommand AND a TTY is attached. When stdout is NOT a TTY, bare `taskless` SHALL print a non-interactive preamble explaining the context, followed by the help index (instead of attempting the wizard or printing only top-level help). #### Scenario: Bare taskless in a TTY launches the wizard - **WHEN** a user runs `taskless` with no subcommand and stdout is a TTY - **THEN** the CLI SHALL behave as if `taskless init` were invoked -#### Scenario: Bare taskless without a TTY prints help +#### Scenario: Bare taskless without a TTY prints preamble + help index - **WHEN** `taskless` is invoked with no subcommand and stdout is not a TTY -- **THEN** the CLI SHALL print the top-level help text +- **THEN** the CLI SHALL print a short preamble noting the non-interactive context (e.g. "For interactive install, run from a terminal. For agent recipes, use: taskless help") +- **AND** SHALL then print the help index (same content as `taskless help`) - **AND** SHALL NOT launch the wizard - -#### Scenario: Explicit help command still works - -- **WHEN** a user runs `taskless help` -- **THEN** the CLI SHALL print the top-level help text regardless of TTY state +- **AND** SHALL NOT silently install ### Requirement: Wizard renders an intro banner @@ -322,44 +300,7 @@ The wizard SHALL begin by rendering an ASCII rendition of the Taskless wordmark ### Requirement: Wizard prompts the user to choose install locations -The wizard SHALL present a multi-select prompt listing all four known install locations (`.claude/`, `.opencode/`, `.cursor/`, `.agents/`) regardless of which are detected. Detected locations SHALL be pre-checked. Undetected locations SHALL be shown unchecked with a visual indicator that they are not currently present. The wizard SHALL NOT allow the user to confirm zero selections; if the user unchecks all locations, the wizard SHALL re-prompt with an inline validation error until at least one location is selected. - -#### Scenario: Detected locations are pre-checked - -- **WHEN** the wizard reaches the locations step in a repository with `.claude/` and `.cursor/` -- **THEN** the multi-select SHALL show `.claude/` and `.cursor/` as pre-checked -- **AND** SHALL show `.opencode/` and `.agents/` as unchecked - -#### Scenario: All four locations are always offered - -- **WHEN** the wizard reaches the locations step -- **THEN** the multi-select SHALL include `.claude/`, `.opencode/`, `.cursor/`, and `.agents/` as choices - -#### Scenario: Zero selections is rejected - -- **WHEN** the user confirms the locations step with zero selections -- **THEN** the wizard SHALL display a validation error -- **AND** SHALL re-prompt without advancing - -### Requirement: Wizard prompts the user to choose optional skills - -The wizard SHALL present a multi-select prompt listing every skill marked `optional` in the skill catalog. All optional skills SHALL be unchecked by default. The user MAY confirm the step with zero selections, in which case only mandatory skills SHALL be installed. Mandatory skills SHALL NOT appear in this prompt and SHALL always be installed. - -#### Scenario: Optional skills appear unchecked - -- **WHEN** the wizard reaches the optional-skills step and the catalog contains `taskless-ci` -- **THEN** the multi-select SHALL include `taskless-ci` as an unchecked option - -#### Scenario: Zero optional selections is permitted - -- **WHEN** the user confirms the optional-skills step with zero selections -- **THEN** the wizard SHALL advance without error -- **AND** only mandatory skills SHALL be installed in the subsequent write step - -#### Scenario: Mandatory skills are not shown - -- **WHEN** the wizard renders the optional-skills step -- **THEN** skills classified as `mandatory` SHALL NOT appear in the prompt +The wizard's location step is unchanged in shape but the resulting install plan only ever contains the single `taskless` skill (and its corresponding `tskl` command). ### Requirement: Wizard explains the auth tradeoff and offers to log in @@ -452,44 +393,20 @@ If the user cancels the wizard at any step (Ctrl-C, Esc, or equivalent clack can ### Requirement: Install manifest records what was installed per target -On every successful install (wizard or `--no-interactive`), the CLI SHALL update `.taskless/taskless.json` with an `install` object keyed by target location. For each target, the CLI SHALL record the list of skill names written and the list of command filenames written. The CLI SHALL also record `installedAt` (ISO-8601 timestamp) and `cliVersion` (the `@taskless/cli` package version) at the top level of the `install` object. +The install manifest in `.taskless/taskless.json` continues to record what was written per target. With one skill in the bundle, each target's `skills` array contains at most `["taskless"]` and each target's `commands` array contains at most `["tskl"]`. The manifest schema is unchanged — only the contents differ. -#### Scenario: Manifest records skills per target +#### Scenario: Manifest records the consolidated skill -- **WHEN** `taskless init` completes writing to `.claude/` with skills `taskless-check` and `taskless-ci` -- **THEN** `taskless.json` SHALL contain `install.targets[".claude"].skills` equal to `["taskless-check", "taskless-ci"]` (order not significant) - -#### Scenario: Manifest records commands for Claude Code - -- **WHEN** `taskless init` completes writing command files to `.claude/commands/tskl/` -- **THEN** `taskless.json` SHALL contain `install.targets[".claude"].commands` listing each command filename written - -#### Scenario: Manifest records install metadata - -- **WHEN** `taskless init` completes successfully -- **THEN** `taskless.json` SHALL contain `install.installedAt` (ISO-8601 string) -- **AND** `install.cliVersion` SHALL equal the running CLI's package version +- **WHEN** init writes the consolidated skill to `.claude/` +- **THEN** the manifest's `install.targets[".claude"].skills` SHALL be `["taskless"]` +- **AND** `install.targets[".claude"].commands` SHALL be `["tskl"]` ### Requirement: Re-install computes a diff against the previous manifest -On every interactive run, the wizard SHALL read the existing `install` object from `.taskless/taskless.json` (if present) and use it to compute the diff summary described in the "Wizard shows a diff-style summary" requirement. When a previously-recorded target or skill is not selected in the current session, it SHALL be classified as a removal in the summary. On confirmed writes, the CLI SHALL delete the previously-written files for each removed target or skill, then write the new manifest reflecting the current selection only. - -#### Scenario: Previously installed skill not selected is removed - -- **WHEN** the previous manifest recorded `.claude/skills/taskless-ci` -- **AND** the user deselects `taskless-ci` in the current wizard -- **THEN** the summary SHALL list `taskless-ci` as a removal under `.claude/` -- **AND** on confirm, `.claude/skills/taskless-ci/SKILL.md` SHALL be deleted - -#### Scenario: Previously installed target not selected is removed - -- **WHEN** the previous manifest recorded `.cursor/` as a target -- **AND** the user deselects `.cursor/` in the current wizard -- **THEN** the summary SHALL list every `.cursor/` skill as a removal -- **AND** on confirm, the Taskless skill files under `.cursor/skills/` SHALL be deleted +Re-install diff computation is unchanged. With v0.7.0 the diff for a v0.6 user shows 10 skill removals + 6 command removals + 1 skill addition + 1 command addition per detected target. Removals require user confirmation per the existing requirement. -#### Scenario: Manifest only reflects current selection after write +#### Scenario: Upgrade from v0.6 shows removals in summary -- **WHEN** a wizard run completes writing -- **THEN** `install.targets` SHALL contain exactly the targets selected in that run -- **AND** for each target, `skills` SHALL contain exactly the skills written in that run +- **WHEN** a user with v0.6 installed runs `taskless init` after upgrading to v0.7.0 +- **THEN** the wizard summary SHALL list the 10 obsolete skills and 6 obsolete commands as removals +- **AND** SHALL require user confirmation before deleting diff --git a/openspec/specs/cli-rules/spec.md b/openspec/specs/cli-rules/spec.md index d74a5490..301a588c 100644 --- a/openspec/specs/cli-rules/spec.md +++ b/openspec/specs/cli-rules/spec.md @@ -8,125 +8,51 @@ Defines the `rules` subcommand group for the Taskless CLI, including `create`, ` ### Requirement: Rules subcommand group exists -The CLI SHALL register a `rules` subcommand group with `create`, `improve`, and `delete` as nested subcommands. Running `taskless rules` with no subcommand SHALL display help text listing the available rules subcommands. +The CLI SHALL expose the rule operations under the `rule` (singular) subcommand group. The user-facing surface SHALL be `taskless rule create`, `taskless rule improve`, `taskless rule delete`, `taskless rule verify`, and `taskless rule meta`. The internal source filename (`packages/cli/src/commands/rules.ts`) MAY remain plural — only the user-visible subcommand name changes. -#### Scenario: Rules help is displayed +The previous plural form `taskless rules ` SHALL NOT work in v0.7.0 — there is no compatibility alias. -- **WHEN** a user runs `taskless rules` -- **THEN** the CLI SHALL print help text listing `create`, `improve`, and `delete` subcommands +#### Scenario: Singular subcommand registers correctly -### Requirement: Rules create reads request from stdin - -The `taskless rules create` command SHALL read a JSON request payload from a file specified by the `--from ` argument. The payload SHALL conform to the shape `{ prompt: string, successCases?: string[], failureCases?: string[] }`. The `prompt` field is required. If `--from` is not provided, the CLI SHALL print an error message with usage examples and exit with a non-zero exit code. If the file does not exist or contains invalid JSON, the CLI SHALL print an appropriate error and exit with a non-zero exit code. - -#### Scenario: Valid JSON from file - -- **WHEN** a user runs `taskless rules create --from request.json` and `request.json` contains valid JSON with a `prompt` field -- **THEN** the CLI SHALL read the file, parse the JSON, and proceed to submit it to the API +- **WHEN** a user runs `taskless rule create --from req.json` +- **THEN** the CLI SHALL invoke the rule-create handler -#### Scenario: Missing --from flag +#### Scenario: Plural subcommand is no longer recognized -- **WHEN** a user runs `taskless rules create` without the `--from` flag -- **THEN** the CLI SHALL print an error indicating `--from ` is required with a usage example -- **AND** the CLI SHALL exit with a non-zero exit code +- **WHEN** a user runs `taskless rules create --from req.json` +- **THEN** the CLI SHALL exit with an error indicating the subcommand is unknown +- **AND** the error message SHOULD suggest `taskless rule create` -#### Scenario: File not found - -- **WHEN** a user runs `taskless rules create --from missing.json` and the file does not exist -- **THEN** the CLI SHALL print an error indicating the file was not found -- **AND** the CLI SHALL exit with a non-zero exit code +### Requirement: Rules create reads request from stdin -#### Scenario: Invalid JSON in file +The `taskless rule create` command SHALL accept a `--from ` flag specifying a JSON file containing the rule request. (Note: previously named `rules create`; renamed to singular.) -- **WHEN** a user runs `taskless rules create --from bad.json` and the file contains invalid JSON -- **THEN** the CLI SHALL print an error indicating the file is not valid JSON -- **AND** the CLI SHALL exit with a non-zero exit code +#### Scenario: rule create with --from file -#### Scenario: Missing required fields - -- **WHEN** a user provides a file missing the `prompt` field -- **THEN** the CLI SHALL print an error indicating the missing field -- **AND** the CLI SHALL exit with a non-zero exit code +- **WHEN** a user runs `taskless rule create --from .taskless/.tmp-rule-request.json --json` +- **THEN** the CLI SHALL read the JSON file and submit it to the API ### Requirement: Rules create resolves identity from JWT and git remote -The `taskless rules create` command SHALL resolve `orgId` and `repositoryUrl` using the `resolveIdentity()` function. `orgId` SHALL be extracted from the JWT's `orgId` claim (decoded via `jose`). `repositoryUrl` SHALL be inferred from `git remote get-url origin`, canonicalized to `https://github.com/{owner}/{repo}`. If identity resolution fails, the CLI SHALL print a descriptive error and exit with a non-zero exit code. - -#### Scenario: Identity resolved from JWT and git remote - -- **WHEN** the stored JWT contains an `orgId` claim and the repository has a valid GitHub `origin` remote -- **THEN** the CLI SHALL use the JWT's `orgId` and the inferred `repositoryUrl` in the API request - -#### Scenario: JWT lacks orgId (stale token) - -- **WHEN** the stored JWT does not contain an `orgId` claim -- **THEN** the CLI SHALL print an error: "Your auth token is missing organization info. Run `taskless auth login` to re-authenticate." -- **AND** the CLI SHALL exit with a non-zero exit code - -#### Scenario: Git remote not available - -- **WHEN** `git remote get-url origin` fails -- **THEN** the CLI SHALL print an error about the missing git remote -- **AND** the CLI SHALL exit with a non-zero exit code +`taskless rule create` resolves user identity from the stored JWT and the git remote per the existing identity resolution requirements. (Renamed to singular.) ### Requirement: Rules create requires authentication -The `taskless rules create` command SHALL require a valid auth token. The token SHALL be resolved using the existing `getToken()` utility (env var first, then file). If no token is available, the CLI SHALL print an error directing the user to run `taskless auth login` and exit with a non-zero exit code. - -#### Scenario: No token available - -- **WHEN** a user runs `taskless rules create` with no `TASKLESS_TOKEN` env var and no token file -- **THEN** the CLI SHALL print an error indicating authentication is required -- **AND** the CLI SHALL suggest running `taskless auth login` -- **AND** the CLI SHALL exit with a non-zero exit code +`taskless rule create` SHALL require authentication unless the new `--anonymous` flag is set. When `--anonymous` is set, the command SHALL invoke the local-only flow (see "Rule create supports anonymous local-only flow" below) instead of submitting to the API. (Renamed to singular; new anonymous branch.) -#### Scenario: Token from env var +#### Scenario: rule create without --anonymous requires auth -- **WHEN** `TASKLESS_TOKEN` is set -- **THEN** the CLI SHALL use it as the bearer token for API requests +- **WHEN** a user runs `taskless rule create --from req.json` without being logged in +- **THEN** the CLI SHALL exit with code 1 and an `AUTH_REQUIRED` error -#### Scenario: Token from file +#### Scenario: rule create --anonymous skips auth -- **WHEN** `TASKLESS_TOKEN` is not set and a token file exists -- **THEN** the CLI SHALL use the file-based token for API requests +- **WHEN** a user runs `taskless rule create --from req.json --anonymous` without being logged in +- **THEN** the CLI SHALL invoke the local-only flow without checking auth ### Requirement: Rules create submits to API and polls for results -The `taskless rules create` command SHALL POST to `POST /cli/api/request` with `orgId`, `repositoryUrl`, `prompt`, and optional `language`, `successCase`, `failureCase`. It SHALL receive a `requestId` in the response and poll `GET /cli/api/request/:requestId` at a 15-second interval until the status reaches `generated` or `failed`. - -#### Scenario: Successful rule generation - -- **WHEN** the API accepts the request and rule generation completes -- **THEN** the CLI SHALL receive a `requestId`, poll until status is `generated`, and proceed to write files - -#### Scenario: Rule generation fails - -- **WHEN** the request status returns `failed` with an error message -- **THEN** the CLI SHALL print the error message -- **AND** the CLI SHALL exit with a non-zero exit code - -#### Scenario: API returns validation error - -- **WHEN** the API returns HTTP 400 with `error: "validation_error"` and a `details` array -- **THEN** the CLI SHALL print the validation details -- **AND** the CLI SHALL exit with a non-zero exit code - -#### Scenario: Repository not accessible - -- **WHEN** the API returns HTTP 403 with `error: "repository_not_accessible"` -- **THEN** the CLI SHALL print an error indicating the repository is not accessible to the organization -- **AND** the CLI SHALL exit with a non-zero exit code - -#### Scenario: Organization not found - -- **WHEN** the API returns HTTP 404 with `error: "organization_not_found"` -- **THEN** the CLI SHALL print an error indicating the organization was not found -- **AND** the CLI SHALL exit with a non-zero exit code - -#### Scenario: Polling shows progressive status - -- **WHEN** the CLI is polling and the status transitions from `accepted` to `building` -- **THEN** the CLI SHALL update the progress message to reflect the current status +`taskless rule create` (without `--anonymous`) submits to the API and polls per the existing requirement. (Renamed to singular.) ### Requirement: Rules create uses a network interface with stub @@ -144,185 +70,60 @@ The API calls for rule generation (`POST /cli/api/request` and `GET /cli/api/req ### Requirement: Rules create writes rule files to disk -When rule generation completes, the CLI SHALL write each generated rule to `.taskless/rules/{kebab-id}.yml`. The file content SHALL be the `content` field of the generated rule serialized as YAML. If a file with the same name already exists, it SHALL be overwritten. - -#### Scenario: Single rule generated - -- **WHEN** the API returns one rule with id `no-console-log` -- **THEN** the CLI SHALL write `.taskless/rules/no-console-log.yml` containing the rule serialized as YAML +`taskless rule create` SHALL write the generated rule file to `.taskless/rules/.yml` regardless of whether `--anonymous` was set. The agent invoking the command SHALL NOT be expected to write rule files itself. (Renamed to singular; this strengthens the existing requirement to apply to both branches.) -#### Scenario: Multiple rules generated +#### Scenario: Both branches write rule files -- **WHEN** the API returns two rules with ids `no-console-log` and `no-inner-html` -- **THEN** the CLI SHALL write `.taskless/rules/no-console-log.yml` and `.taskless/rules/no-inner-html.yml` - -#### Scenario: Existing rule file is overwritten - -- **WHEN** `.taskless/rules/no-console-log.yml` already exists and the API returns a rule with id `no-console-log` -- **THEN** the CLI SHALL overwrite the existing file with the new content +- **WHEN** `taskless rule create` succeeds (with or without `--anonymous`) +- **THEN** `.taskless/rules/.yml` SHALL exist on disk ### Requirement: Rules create writes test files to disk -When rule generation completes and a rule includes test cases, the CLI SHALL write test files to `.taskless/rule-tests/{kebab-id}-{timestamp}-test.yml`. The timestamp SHALL be the current date formatted as `YYYYMMDD`. The test file SHALL contain the rule id, valid snippets, and invalid snippets serialized as YAML. - -#### Scenario: Rule with test cases - -- **WHEN** the API returns a rule with id `no-console-log` that includes test cases -- **THEN** the CLI SHALL write `.taskless/rule-tests/no-console-log-20260302-test.yml` -- **AND** the file SHALL contain `id`, `valid`, and `invalid` fields - -#### Scenario: Rule without test cases - -- **WHEN** the API returns a rule with no `tests` field -- **THEN** the CLI SHALL NOT write a test file for that rule - -#### Scenario: Rules directory is created if missing - -- **WHEN** `.taskless/rules/` or `.taskless/rule-tests/` does not exist -- **THEN** the CLI SHALL create the directory before writing files +`taskless rule create` SHALL write generated test files to `.taskless/rule-tests/.yml` regardless of whether `--anonymous` was set. (Renamed; strengthened.) ### Requirement: Rules create outputs results -After writing files, the CLI SHALL output a summary to stdout. In text mode, the summary SHALL include a list of files written. In JSON mode (`--json`), the full generation result SHALL be output along with the file paths written. - -#### Scenario: Text output - -- **WHEN** `taskless rules create` completes without `--json` -- **THEN** stdout SHALL include a list of written file paths - -#### Scenario: JSON output - -- **WHEN** `taskless rules create` completes with `--json` -- **THEN** stdout SHALL contain a JSON object with the full result and an array of written file paths +`taskless rule create` outputs results per the existing requirement. (Renamed to singular.) Output SHALL be human-readable by default; `--json` produces machine-readable output. On failure with `--json` set, the output SHALL be the standardized error envelope `{ ok: false, code: "", message: "<...>" }` per the `cli` capability requirements. ### Requirement: Rules create shows progress during polling -While polling for the request result, the CLI SHALL display a waiting message to stderr so the user knows the command is active. The message SHALL reflect the current status (`accepted`, `building`). - -#### Scenario: Polling shows progress - -- **WHEN** the CLI is polling for a request result -- **THEN** the CLI SHALL print a waiting/progress message to stderr indicating the current status +`taskless rule create` shows progress per the existing requirement when polling the API (the `--anonymous` branch does not poll an API and SHOULD show progress for the local agent-driven steps if applicable). (Renamed to singular.) ### Requirement: Rules improve reads request from file -The `taskless rules improve` command SHALL read a JSON request payload from a file specified by the `--from ` argument. The payload SHALL conform to the shape `{ ruleId: string, guidance: string, references?: Array<{ filename: string, content: string }> }`. The `ruleId` and `guidance` fields are required. If `--from` is not provided, the CLI SHALL print an error message with usage examples and exit with a non-zero exit code. - -#### Scenario: Valid JSON from file - -- **WHEN** a user runs `taskless rules improve --from request.json` and `request.json` contains valid JSON with `ruleId` and `guidance` fields -- **THEN** the CLI SHALL read the file, parse the JSON, and proceed to submit it to the API - -#### Scenario: Missing --from flag - -- **WHEN** a user runs `taskless rules improve` without the `--from` flag -- **THEN** the CLI SHALL print an error indicating `--from ` is required with a usage example -- **AND** the CLI SHALL exit with a non-zero exit code - -#### Scenario: Missing required ruleId field - -- **WHEN** a user provides a file missing the `ruleId` field -- **THEN** the CLI SHALL print an error indicating the missing field -- **AND** the CLI SHALL exit with a non-zero exit code - -#### Scenario: Missing required guidance field - -- **WHEN** a user provides a file missing the `guidance` field -- **THEN** the CLI SHALL print an error indicating the missing field -- **AND** the CLI SHALL exit with a non-zero exit code +`taskless rule improve` SHALL accept a `--from ` flag specifying a JSON file containing the iterate request. (Renamed to singular.) ### Requirement: Rules improve requires authentication -The `taskless rules improve` command SHALL require a valid auth token resolved via `getToken()`. If no token is available, the CLI SHALL print an error directing the user to run `taskless auth login` and exit with a non-zero exit code. - -#### Scenario: No token available - -- **WHEN** a user runs `taskless rules improve` with no token available -- **THEN** the CLI SHALL print an error indicating authentication is required -- **AND** the CLI SHALL exit with a non-zero exit code +`taskless rule improve` SHALL require authentication unless `--anonymous` is set. (Renamed; new anonymous branch.) ### Requirement: Rules improve submits to iterate API and polls for results -The `taskless rules improve` command SHALL POST to `/cli/api/rule/{ruleId}/iterate` with `orgId` resolved from the JWT's `orgId` claim (via `resolveIdentity()`), `guidance`, and optional `references`. It SHALL receive a `requestId` in the response and poll `GET /cli/api/rule/{requestId}` at a 15-second interval until the status reaches `generated` or `failed`. - -#### Scenario: Successful rule iteration - -- **WHEN** the API accepts the iterate request and generation completes -- **THEN** the CLI SHALL receive a `requestId`, poll until status is `generated`, and proceed to write files - -#### Scenario: Rule iteration fails - -- **WHEN** the request status returns `failed` with an error message -- **THEN** the CLI SHALL print the error message -- **AND** the CLI SHALL exit with a non-zero exit code - -#### Scenario: Identity resolved from JWT and git remote - -- **WHEN** the `rules improve` command resolves identity -- **THEN** it SHALL use `resolveIdentity()` to obtain `orgId` from the JWT claim -- **AND** it SHALL NOT read `orgId` from `taskless.json` +`taskless rule improve` (without `--anonymous`) submits and polls per the existing requirement. (Renamed.) ### Requirement: Rules improve writes updated files to disk -When iteration completes, the CLI SHALL write each rule to `.taskless/rules/{kebab-id}.yml` and test files to `.taskless/rule-tests/{kebab-id}-{timestamp}-test.yml`, overwriting existing files. This uses the same file-writing logic as `rules create`. - -#### Scenario: Updated rule overwrites existing file - -- **WHEN** the API returns an updated rule with id `no-console-log` -- **THEN** the CLI SHALL overwrite `.taskless/rules/no-console-log.yml` with the new content +`taskless rule improve` SHALL write updated rule files to disk in both branches. (Renamed; strengthened.) ### Requirement: Rules improve outputs results -After writing files, the CLI SHALL output a summary. In text mode, it SHALL list written file paths. In JSON mode (`--json`), it SHALL output a JSON object with `requestId`, `rules` array, and `files` array. - -#### Scenario: JSON output - -- **WHEN** `taskless rules improve` completes with `--json` -- **THEN** stdout SHALL contain a JSON object with `success`, `requestId`, `rules`, and `files` fields +`taskless rule improve` outputs results per the existing requirement. (Renamed.) Failure output with `--json` SHALL use the standardized error envelope. ### Requirement: Rules improve has a help entry -The `rules improve` subcommand SHALL have a help file at `packages/cli/src/help/rules-improve.txt` describing usage, options, and JSON file fields. The rules help index SHALL list `improve` alongside `create` and `delete`. - -#### Scenario: Help is accessible - -- **WHEN** a user runs `taskless help rules improve` -- **THEN** the CLI SHALL display the improve help text with usage, options, and JSON field descriptions +`taskless help rule improve` SHALL return the recipe per `cli-help` requirements. (Renamed; the help filename becomes `rule-improve.txt` with an optional `rule-improve.anonymous.txt` variant.) ### Requirement: Rules delete removes rule and test files -The `taskless rules delete ` command SHALL remove `.taskless/rules/{id}.yml` and any matching files in `.taskless/rule-tests/` that begin with `{id}-`. If the rule file does not exist, the CLI SHALL print an error and exit with a non-zero exit code. - -#### Scenario: Successful deletion - -- **WHEN** a user runs `taskless rules delete no-console-log` and `.taskless/rules/no-console-log.yml` exists -- **THEN** the CLI SHALL delete `.taskless/rules/no-console-log.yml` -- **AND** the CLI SHALL delete any files matching `.taskless/rule-tests/no-console-log-*-test.yml` -- **AND** the CLI SHALL print a confirmation message - -#### Scenario: Rule not found - -- **WHEN** a user runs `taskless rules delete no-console-log` and `.taskless/rules/no-console-log.yml` does not exist -- **THEN** the CLI SHALL print an error indicating the rule was not found -- **AND** the CLI SHALL exit with a non-zero exit code +`taskless rule delete ` SHALL remove the corresponding rule file and any test files. (Renamed.) Accepts `--anonymous` as a no-op. ### Requirement: Rules delete does not require authentication -The `taskless rules delete` command SHALL NOT require an auth token. It operates only on local files. - -#### Scenario: Delete works without auth - -- **WHEN** a user runs `taskless rules delete ` with no token available -- **THEN** the CLI SHALL proceed with the deletion without checking for authentication +`taskless rule delete` does not require authentication per the existing requirement. (Renamed.) ### Requirement: Rules delete accepts the id argument -The `taskless rules delete` command SHALL accept a positional argument specifying the rule ID to delete. The ID SHALL match the filename stem (without `.yml` extension) in `.taskless/rules/`. - -#### Scenario: ID matches filename - -- **WHEN** a user runs `taskless rules delete no-console-log` -- **THEN** the CLI SHALL look for `.taskless/rules/no-console-log.yml` +`taskless rule delete ` accepts the rule ID as a positional argument per the existing requirement. (Renamed.) ### Requirement: Codegen script fetches official ast-grep rule schema @@ -371,112 +172,65 @@ The generated JSON Schema file SHALL be importable by the CLI bundle via Vite. T ### Requirement: Verify subcommand validates rules against ast-grep schema -The CLI SHALL support a `taskless rules verify` subcommand that validates a rule file against the ast-grep Zod schema, applies Taskless-specific requirements, and runs test cases. The subcommand SHALL accept a positional `` argument identifying the rule to verify. - -#### Scenario: Verify a valid rule with passing tests - -- **WHEN** a user runs `taskless rules verify no-eval` -- **THEN** the CLI SHALL read `.taskless/rules/no-eval.yml` -- **AND** validate it against the ast-grep Zod schema -- **AND** check Taskless-specific requirements -- **AND** run `sg test` for the rule's test cases -- **AND** report success with a summary of checks passed - -#### Scenario: Verify a rule that fails schema validation - -- **WHEN** a user runs `taskless rules verify bad-rule` and the rule YAML contains unknown fields or invalid types -- **THEN** the CLI SHALL report the specific schema validation errors -- **AND** exit with code 1 - -#### Scenario: Verify a rule with no test file - -- **WHEN** a user runs `taskless rules verify orphan-rule` and no matching test file exists in `.taskless/rule-tests/` -- **THEN** the CLI SHALL report a Taskless requirement failure: missing test file -- **AND** skip the test execution layer - -#### Scenario: Verify a nonexistent rule - -- **WHEN** a user runs `taskless rules verify nonexistent` -- **THEN** the CLI SHALL report that `.taskless/rules/nonexistent.yml` was not found -- **AND** exit with code 1 +`taskless rule verify` SHALL validate rules against the ast-grep schema per the existing requirement. (Renamed from `rules verify` to `rule verify`.) Accepts `--anonymous` as a no-op. ### Requirement: Verify performs three layers of validation -The verify subcommand SHALL execute validation in three sequential layers: (1) Zod schema validation against the ast-grep rule schema, (2) Taskless requirement checks, and (3) test execution via `sg test`. If an earlier layer fails, subsequent layers SHALL still execute to provide complete feedback. - -#### Scenario: Layer 1 — Schema validation - -- **WHEN** the rule file is parsed as YAML -- **THEN** the CLI SHALL validate the resulting object against the ast-grep Zod schema -- **AND** report all validation errors with field paths - -#### Scenario: Layer 2 — Taskless requirement checks - -- **WHEN** the rule passes or fails schema validation -- **THEN** the CLI SHALL additionally check that `id`, `language`, `severity`, `message`, and `rule` fields are present -- **AND** check that any rule using `regex` also specifies `kind` at the same level -- **AND** check that a matching test file exists in `.taskless/rule-tests/` - -#### Scenario: Layer 3 — Test execution via sg test - -- **WHEN** a matching test file exists -- **THEN** the CLI SHALL generate `sgconfig.yml` via `generateSgConfig()` -- **AND** run `sg test --config .taskless/sgconfig.yml` using the existing `findSgBinary()` resolver -- **AND** parse the output to report pass/fail counts for valid and invalid test cases - -#### Scenario: All layers run regardless of earlier failures - -- **WHEN** Layer 1 reports schema errors -- **THEN** Layer 2 and Layer 3 SHALL still execute -- **AND** the output SHALL include results from all three layers +`taskless rule verify` performs the three layers of validation per the existing requirement. (Renamed.) ### Requirement: Verify supports JSON output -The verify subcommand SHALL support the global `--json` flag. When enabled, the output SHALL be a JSON object with per-layer results. +`taskless rule verify --json` outputs results in the documented JSON shape. On failure, the standardized error envelope is used. (Renamed.) -#### Scenario: JSON output for successful verification +### Requirement: Verify schema mode dumps combined schema for agent consumption -- **WHEN** a user runs `taskless rules verify no-eval --json` -- **THEN** the CLI SHALL output a JSON object with structure: `{ "success": true, "ruleId": "no-eval", "schema": { "valid": true, "errors": [] }, "requirements": { "valid": true, "checks": [...] }, "tests": { "valid": true, "passed": , "failed": 0 } }` +The `taskless rule verify --schema` mode is REMOVED in v0.7.0 — schemas are now embedded in `tskl help rule create` recipe output via `zod-to-json-schema`. (Renamed and superseded.) -#### Scenario: JSON output for failed verification +#### Scenario: --schema flag is no longer accepted -- **WHEN** a user runs `taskless rules verify bad-rule --json` and the rule fails Layer 2 -- **THEN** the CLI SHALL output a JSON object with `"success": false` and the failing layer SHALL have `"valid": false` with descriptive error entries +- **WHEN** a user runs `taskless rule verify --schema` +- **THEN** the CLI SHALL exit with an error indicating the flag is unknown -### Requirement: Verify schema mode dumps combined schema for agent consumption +### Requirement: Verify respects global flags -The verify subcommand SHALL support a `--schema` flag that dumps the combined ast-grep schema, Taskless requirements, and annotated examples as JSON. When `--schema` is provided, no rule ID is required and no validation is performed. +`taskless rule verify` respects global flags including `--dir` per the existing requirement. (Renamed.) Also accepts the new `--anonymous` flag as a no-op. -#### Scenario: Schema output with --schema flag +### Requirement: Rule create supports anonymous local-only flow -- **WHEN** a user runs `taskless rules verify --schema --json` -- **THEN** the CLI SHALL output a JSON object with three top-level keys: `astGrepSchema` (the full official ast-grep rule JSON Schema for agent reference), `tasklessRequirements` (required fields and additional rules), and `examples` (curated annotated rule examples) -- **AND** exit with code 0 +When `taskless rule create --anonymous` is invoked, the CLI SHALL execute the local-only rule-creation flow (previously implemented as the `taskless-create-rule-anonymous` skill body). The flow SHALL: -#### Scenario: Schema output includes curated examples +1. NOT submit any request to the Taskless API +2. Generate the ast-grep rule using local logic (Claude SDK, agent-driven generation, or whatever the migrated implementation prefers — see design.md) +3. Write the rule file to `.taskless/rules/.yml` +4. Write any generated test files to `.taskless/rule-tests/.yml` +5. NOT write a metadata sidecar (the API-backed branch does) +6. Return the same output format as the API-backed branch (paths to created files) -- **WHEN** the `--schema` output is examined -- **THEN** the `examples` array SHALL include at least: a simple pattern match example, a regex-with-kind example, and a composite rule using `any`/`all` +#### Scenario: rule create --anonymous skips API -#### Scenario: Schema flag does not require auth +- **WHEN** a user runs `taskless rule create --from req.json --anonymous` +- **THEN** the CLI SHALL NOT make any HTTP request to the Taskless API +- **AND** SHALL produce a rule file under `.taskless/rules/` -- **WHEN** a user runs `taskless rules verify --schema` without being authenticated -- **THEN** the command SHALL succeed without requiring a token +#### Scenario: rule create --anonymous produces no metadata sidecar -### Requirement: Verify respects global flags +- **WHEN** `taskless rule create --anonymous` succeeds +- **THEN** no file under `.taskless/rule-metadata/` SHALL be written for the new rule -The verify subcommand SHALL respect the global `-d` (working directory) and `--schema` (print Zod schemas) flags consistent with other CLI subcommands. +### Requirement: Rule improve supports anonymous local-only flow -#### Scenario: Verify uses custom directory +When `taskless rule improve --anonymous` is invoked, the CLI SHALL execute the local-only rule-improvement flow (previously implemented as the `taskless-improve-rule-anonymous` skill body). The flow SHALL: -- **WHEN** a user runs `taskless rules verify no-eval -d /path/to/repo` -- **THEN** the CLI SHALL look for `.taskless/rules/no-eval.yml` in `/path/to/repo` +1. NOT submit any request to the Taskless API iterate endpoint +2. Update the rule file in place using local logic +3. Support the verify feedback loop by exposing the `rule verify` primitive that the agent invokes between edits +4. Return the same output format as the API-backed branch -#### Scenario: Verify --schema prints Zod schemas +#### Scenario: rule improve --anonymous skips API -- **WHEN** a user runs `taskless rules verify --schema` (without `--json`) -- **THEN** the CLI SHALL print the combined schema payload in the standard `--schema` output format +- **WHEN** a user runs `taskless rule improve --from iterate.json --anonymous` +- **THEN** the CLI SHALL NOT make any HTTP request to the Taskless API +- **AND** SHALL update the target rule file ## API Contract diff --git a/openspec/specs/cli/spec.md b/openspec/specs/cli/spec.md index 2bbde3c0..5e9e5521 100644 --- a/openspec/specs/cli/spec.md +++ b/openspec/specs/cli/spec.md @@ -283,3 +283,63 @@ The CLI SHALL use `citty` as its sole argument parsing dependency. Each subcomma - **WHEN** inspecting `packages/cli/package.json` - **THEN** `citty` SHALL be listed in `dependencies` + +### Requirement: CLI accepts global --anonymous flag with per-command behavior + +The CLI SHALL accept a top-level boolean flag `--anonymous` on every subcommand. The flag's behavior SHALL vary by command: + +- `rule create`, `rule improve`: switch to local-only flow (no API calls); SHALL be the only way to reach the local-only path +- `rule delete`, `rule verify`, `rule meta`, `check`, `auth logout`, `init`, `update`: accepted as no-op; SHALL succeed without changing behavior +- `info`: skip the API/auth probe; report local state only (CLI version, skills installed, no auth call) +- `auth login`: SHALL exit with code 1 and an error message stating "auth commands cannot be anonymous" + +The flag SHALL be recognized whether placed before or after positional arguments (per `citty` parsing). + +#### Scenario: --anonymous on rule create switches to local flow + +- **WHEN** a user runs `taskless rule create --from req.json --anonymous` +- **THEN** the CLI SHALL execute the local-only branch (no API calls) +- **AND** SHALL produce the same output shape as the API-backed branch + +#### Scenario: --anonymous on check is a no-op + +- **WHEN** a user runs `taskless check --anonymous` +- **THEN** the CLI SHALL execute identically to `taskless check` (no warning, no error) + +#### Scenario: --anonymous on info skips API probe + +- **WHEN** a user runs `taskless info --anonymous` +- **THEN** the CLI SHALL report local state only (CLI version, installed skills, scaffold version) +- **AND** SHALL NOT make any HTTP request to verify auth state + +#### Scenario: --anonymous on auth login is rejected + +- **WHEN** a user runs `taskless auth login --anonymous` +- **THEN** the CLI SHALL exit with code 1 +- **AND** SHALL print an error message stating "auth commands cannot be anonymous" + +### Requirement: Error output uses stable codes when --json is set + +When any CLI command exits with an error AND `--json` was passed, the command SHALL output a JSON envelope with the shape: + +```json +{ + "ok": false, + "code": "", + "message": "" +} +``` + +The `code` field SHALL be drawn from a stable enum defined in `packages/cli/src/types/errors.ts`. The enum SHALL include at minimum: `AUTH_REQUIRED`, `NO_GITHUB_REMOTE`, `RULE_GENERATION_FAILED`, `RULE_NOT_FOUND`, `INVALID_INPUT`, `NETWORK_ERROR`. New codes MAY be added but existing codes SHALL NOT be renamed without a major version bump. Recipes reference these codes by name in their `## Errors` section, so stability is required. + +#### Scenario: Auth-required error in JSON mode + +- **WHEN** a user runs `taskless rule create --from req.json --json` while logged out +- **THEN** stdout SHALL contain `{ "ok": false, "code": "AUTH_REQUIRED", "message": "..." }` +- **AND** the exit code SHALL be non-zero + +#### Scenario: Error code stability is enforced by tests + +- **WHEN** the test suite runs +- **THEN** there SHALL be tests verifying the exact `code` strings emitted for each error path +- **AND** renaming a code in the enum without updating both the implementation and the tests SHALL break the build diff --git a/openspec/specs/infrastructure/spec.md b/openspec/specs/infrastructure/spec.md index 4887103a..185251e0 100644 --- a/openspec/specs/infrastructure/spec.md +++ b/openspec/specs/infrastructure/spec.md @@ -39,26 +39,16 @@ A `scripts/sync-skill-versions.ts` script SHALL read the version from `packages/ - **WHEN** 5 SKILL.md files exist under `skills/` - **THEN** the script SHALL update all 5 files' `metadata.version` fields -### Requirement: Command generation script derives commands from skills +### Requirement: Slash command files are hand-authored -A `scripts/generate-commands.ts` script SHALL read all `skills/taskless-*/SKILL.md` files, transform them into command format, and write to `commands/taskless/`. The output filename SHALL strip the `taskless-` prefix from the skill directory name. +Since the v0.7 consolidation, the single `commands/tskl/tskl.md` slash command is hand-authored rather than generated from a `SKILL.md` body. The command body intentionally differs from the skill body (it is a `$ARGUMENTS`-aware router), so the prior "copy SKILL.md body to command" generation script no longer applies. -#### Scenario: Command is generated from skill +#### Scenario: Single hand-authored command file exists -- **WHEN** `skills/taskless-auth-login/SKILL.md` exists with name `taskless-auth-login` and description `"Explains how to log in"` -- **THEN** running the script SHALL write `commands/taskless/auth-login.md` -- **AND** the command frontmatter SHALL have `name: "Taskless: Auth Login"`, `description: "Explains how to log in"`, `category: "Taskless"`, and `tags: ["taskless"]` -- **AND** the command body SHALL match the skill body - -#### Scenario: All skills produce commands - -- **WHEN** 5 skill directories exist under `skills/` -- **THEN** running the script SHALL produce 5 command files under `commands/taskless/` - -#### Scenario: Metadata is preserved in commands - -- **WHEN** a skill has `metadata: { author: "taskless", version: "0.1.0" }` -- **THEN** the generated command SHALL include the same `metadata` field in frontmatter +- **WHEN** inspecting the repository +- **THEN** `commands/tskl/tskl.md` SHALL exist as a hand-authored file +- **AND** there SHALL be no `scripts/generate-commands.ts` script +- **AND** the root `package.json` SHALL NOT reference `build:generate-commands` ### Requirement: Version sync runs as part of changeset version diff --git a/openspec/specs/skill-auth-login/spec.md b/openspec/specs/skill-auth-login/spec.md deleted file mode 100644 index 5c5dea6e..00000000 --- a/openspec/specs/skill-auth-login/spec.md +++ /dev/null @@ -1,38 +0,0 @@ -# Skill: Auth Login - -## Purpose - -Defines the `taskless-auth-login` skill that informs users how to authenticate with Taskless via the CLI. - -## Requirements - -### Requirement: Auth login skill is informational - -The `taskless-auth-login` skill SHALL exist at `skills/taskless-auth-login/SKILL.md`. When invoked, the agent SHALL explain the authentication process and provide the CLI command to run. The agent SHALL NOT attempt to execute the login command itself, as the device flow requires interactive terminal input. - -#### Scenario: Skill provides login command for pnpm projects - -- **WHEN** the auth login skill is invoked in a project with a `pnpm-lock.yaml` file -- **THEN** the agent SHALL display the command `pnpm dlx @taskless/cli@latest auth login` -- **AND** explain that the command will display a URL and code for browser-based authentication - -#### Scenario: Skill provides login command for npm projects - -- **WHEN** the auth login skill is invoked in a project without a `pnpm-lock.yaml` file -- **THEN** the agent SHALL display the command `npx @taskless/cli@latest auth login` - -#### Scenario: Skill explains the device flow - -- **WHEN** the auth login skill is invoked -- **THEN** the agent SHALL explain that the user needs to open the displayed URL in a browser, enter the code, and authorize the CLI - -### Requirement: Auth login skill has correct frontmatter - -The skill's YAML frontmatter SHALL include `name: taskless-auth-login`, a description mentioning authentication and login, and `metadata` with `author: taskless` and `version` matching the CLI version. - -#### Scenario: Frontmatter is valid - -- **WHEN** inspecting `skills/taskless-auth-login/SKILL.md` -- **THEN** the frontmatter SHALL have `name: taskless-auth-login` -- **AND** `metadata.author` SHALL be `taskless` -- **AND** `metadata.version` SHALL match the CLI package version diff --git a/openspec/specs/skill-auth-logout/spec.md b/openspec/specs/skill-auth-logout/spec.md deleted file mode 100644 index 1c1e3aa6..00000000 --- a/openspec/specs/skill-auth-logout/spec.md +++ /dev/null @@ -1,37 +0,0 @@ -# Skill: Auth Logout - -## Purpose - -Defines the `taskless-auth-logout` skill that informs users how to remove saved authentication. - -## Requirements - -### Requirement: Auth logout skill is informational - -The `taskless-auth-logout` skill SHALL exist at `skills/taskless-auth-logout/SKILL.md`. When invoked, the agent SHALL provide the CLI command to remove saved authentication. The agent SHALL NOT attempt to execute the logout command itself. - -#### Scenario: Skill provides logout command for pnpm projects - -- **WHEN** the auth logout skill is invoked in a project with a `pnpm-lock.yaml` file -- **THEN** the agent SHALL display the command `pnpm dlx @taskless/cli@latest auth logout` - -#### Scenario: Skill provides logout command for npm projects - -- **WHEN** the auth logout skill is invoked in a project without a `pnpm-lock.yaml` file -- **THEN** the agent SHALL display the command `npx @taskless/cli@latest auth logout` - -#### Scenario: Skill explains what logout does - -- **WHEN** the auth logout skill is invoked -- **THEN** the agent SHALL explain that the command removes the locally saved authentication token - -### Requirement: Auth logout skill has correct frontmatter - -The skill's YAML frontmatter SHALL include `name: taskless-auth-logout`, a description mentioning logout or removing authentication, and `metadata` with `author: taskless` and `version` matching the CLI version. - -#### Scenario: Frontmatter is valid - -- **WHEN** inspecting `skills/taskless-auth-logout/SKILL.md` -- **THEN** the frontmatter SHALL have `name: taskless-auth-logout` -- **AND** `metadata.author` SHALL be `taskless` -- **AND** `metadata.version` SHALL match the CLI package version diff --git a/openspec/specs/skill-ci/spec.md b/openspec/specs/skill-ci/spec.md deleted file mode 100644 index 5b339333..00000000 --- a/openspec/specs/skill-ci/spec.md +++ /dev/null @@ -1,116 +0,0 @@ -# Skill: CI - -## Purpose - -Defines the `taskless-ci` skill that helps invoking agents wire `taskless check` into a project's CI pipeline. The skill is optional, bundled into the CLI, and teaches both full-scan and diff-scan patterns without modifying user-owned CI configuration in place. - -## Requirements - -### Requirement: Taskless CI skill is bundled in the CLI - -The CLI build SHALL embed a `taskless-ci` skill file at `skills/taskless-ci/SKILL.md` into the compiled bundle using the same `import.meta.glob` pattern as all other skills. The skill SHALL NOT register a slash command — its frontmatter `metadata.commandName` SHALL be `"-"` (the repo convention for skills with no slash command, matching `taskless-create-rule-anonymous`, `taskless-improve-rule-anonymous`, and `taskless-delete-rule`), and no companion file in `commands/tskl/` SHALL be installed for it. - -#### Scenario: Build includes taskless-ci skill - -- **WHEN** `pnpm build` is run in `packages/cli/` -- **THEN** the `skills/taskless-ci/SKILL.md` file SHALL be embedded in the output bundle - -#### Scenario: Skill name matches convention - -- **WHEN** the `taskless-ci` skill is installed -- **THEN** the `name` field in its SKILL.md frontmatter SHALL be `taskless-ci` - -### Requirement: Taskless CI skill is marked optional in the skill catalog - -The CLI SHALL classify each bundled skill as either `mandatory` or `optional` via an exported catalog. The `taskless-ci` skill SHALL be classified as `optional`. All other skills currently in the bundle SHALL be classified as `mandatory`. The init wizard and `--no-interactive` code paths SHALL use this classification to decide whether a skill is installed automatically or requires explicit opt-in. - -#### Scenario: CI skill is optional - -- **WHEN** the skill catalog is loaded -- **THEN** the `taskless-ci` entry SHALL have `optional: true` - -#### Scenario: Existing skills are mandatory - -- **WHEN** the skill catalog is loaded -- **THEN** every bundled skill other than `taskless-ci` SHALL have `optional: false` - -#### Scenario: Non-interactive install skips optional skills - -- **WHEN** a user runs `taskless init --no-interactive` -- **THEN** the CLI SHALL install every skill classified as `mandatory` -- **AND** the CLI SHALL NOT install any skill classified as `optional` - -#### Scenario: Wizard offers optional skills as opt-in - -- **WHEN** the init wizard renders its optional-skills step -- **THEN** every skill classified as `optional` SHALL appear in the multi-select -- **AND** every optional skill SHALL be unchecked by default - -### Requirement: Taskless CI skill teaches full-scan and diff-scan patterns - -The `taskless-ci` SKILL.md SHALL instruct invoking agents on two reusable CI patterns that work with any CI system: - -- **Full scan**: `taskless check` without path arguments, for runs on the main/default branch. -- **Diff scan**: `taskless check ` where `` is the output of `git diff --name-only ...HEAD`, for pull-request runs. - -The skill body SHALL: - -1. Explain how to compute the diff target for common CI systems (GitHub Actions, GitLab CI, CircleCI, Jenkins, Azure Pipelines, Bitbucket Pipelines) AND instruct agents to apply the same pattern to CI systems not explicitly listed. -2. Recommend a default of "diff scan on PRs, full scan on pushes to main" but allow the user to override. -3. Document that `taskless check` silently filters non-existent paths, so raw `git diff --name-only` output can be piped in directly without pre-filtering deleted files. - -#### Scenario: Skill teaches both scan patterns - -- **WHEN** an agent invokes the `taskless-ci` skill -- **THEN** the skill body SHALL describe both the full-scan invocation (`taskless check`) and the diff-scan invocation (`taskless check `) - -#### Scenario: Skill provides CI system hints without restricting to a fixed list - -- **WHEN** an agent reads the skill body -- **THEN** the skill SHALL list common CI systems with their detection signals as hints -- **AND** the skill SHALL direct the agent to apply the same patterns to unlisted CI systems that the agent recognizes - -#### Scenario: Skill recommends a default scan pattern - -- **WHEN** the agent asks the user how they want CI to run -- **THEN** the skill SHALL recommend diff-scan on pull requests and full-scan on main-branch pushes as the default -- **AND** the skill SHALL allow the user to override (e.g., full-scan everywhere, or diff-scan only) - -### Requirement: Taskless CI skill generates non-destructive configuration - -The `taskless-ci` skill SHALL instruct invoking agents to generate configuration without modifying files the user already owns. The skill body SHALL: - -1. Require the agent to write a new, standalone file rather than editing an existing CI config. -2. For CI systems with native include/import support, write a standalone snippet and tell the user the single line to add to their main config. -3. For CI systems without include support, write a standalone snippet to a canonical `.taskless/ci/` path and provide explicit instructions for where to paste it. -4. Before overwriting any target file that already exists, require the agent to ask the user for confirmation. - -#### Scenario: Skill instructs agent to write standalone files - -- **WHEN** an agent invokes the skill to set up CI -- **THEN** the skill body SHALL direct the agent to write a new standalone file rather than modifying an existing CI config - -#### Scenario: Skill requires confirmation before overwriting - -- **WHEN** the target CI config path already exists -- **THEN** the skill body SHALL direct the agent to prompt the user before overwriting - -### Requirement: Taskless CI skill requires no authentication - -The `taskless-ci` skill SHALL state that `taskless check` does not require authentication and that the generated CI configuration therefore needs no secrets or environment variables. The skill body SHALL only mention authentication if the user explicitly asks about running authenticated commands (e.g., `rules create`) in CI. - -#### Scenario: Skill documents no-auth requirement - -- **WHEN** an agent reads the skill body -- **THEN** the skill SHALL state that `taskless check` requires no authentication -- **AND** the skill SHALL state that the generated CI config needs no secrets - -### Requirement: Taskless CI skill gates CI setup on rule presence - -The `taskless-ci` skill SHALL direct the agent to verify `taskless check` runs successfully locally before writing any CI configuration. If `taskless check` reports that no rules are configured, the agent SHALL stop and invoke the `taskless-create-rule` skill (or ask the user to create rules) instead of writing a CI config that would produce an always-green check with no coverage. - -#### Scenario: Skill refuses to write CI with no rules - -- **WHEN** `taskless check` reports "No rules configured" during the local verification step -- **THEN** the skill SHALL direct the agent NOT to write any CI configuration -- **AND** the skill SHALL direct the agent to invoke `taskless-create-rule` or ask the user to create rules first diff --git a/openspec/specs/skill-create-rule/spec.md b/openspec/specs/skill-create-rule/spec.md deleted file mode 100644 index 557b84b9..00000000 --- a/openspec/specs/skill-create-rule/spec.md +++ /dev/null @@ -1,196 +0,0 @@ -# Skill: Rules Create - -## Purpose - -Defines the `taskless-create-rule` skill that conversationally gathers rule details and invokes the CLI to create rules. - -## Requirements - -### Requirement: Rules create skill gathers input conversationally - -The `taskless-create-rule` skill SHALL exist at `skills/taskless-create-rule/SKILL.md`. When invoked, the agent SHALL gather the required information from the user through conversation, construct a JSON payload, and pipe it to the CLI's `rules create` command. - -#### Scenario: Agent gathers minimal input - -- **WHEN** the rules create skill is invoked and the user provides a prompt like "detect console.log usage" -- **THEN** the agent SHALL construct a JSON payload with `{ "prompt": "detect console.log usage" }` -- **AND** pipe it to the CLI via stdin - -#### Scenario: Agent asks for clarification when needed - -- **WHEN** the user's request is ambiguous (e.g., "create a rule") -- **THEN** the agent SHALL ask clarifying questions about what the rule should detect - -#### Scenario: Agent may analyze codebase for context - -- **WHEN** the user's request could benefit from codebase context (e.g., language, patterns) -- **THEN** the agent MAY analyze the codebase to populate the `language`, `successCase`, and `failureCase` fields -- **AND** the agent SHALL confirm its assumptions with the user before proceeding - -### Requirement: Rules create skill invokes CLI with JSON stdin - -The skill SHALL write the constructed JSON payload to a temporary file and invoke the CLI using the `--from` flag. The skill SHALL clean up the temporary file after the CLI completes, regardless of success or failure. - -#### Scenario: Invocation with pnpm - -- **WHEN** the skill is invoked in a project with `pnpm-lock.yaml` -- **THEN** the agent SHALL write the JSON payload to `.taskless/.tmp-rule-request.json` -- **AND** run `pnpm dlx @taskless/cli@latest rules create --from .taskless/.tmp-rule-request.json --json` -- **AND** delete `.taskless/.tmp-rule-request.json` after the command completes - -#### Scenario: Invocation with npm - -- **WHEN** the skill is invoked in a project without `pnpm-lock.yaml` -- **THEN** the agent SHALL write the JSON payload to `.taskless/.tmp-rule-request.json` -- **AND** run `npx @taskless/cli@latest rules create --from .taskless/.tmp-rule-request.json --json` -- **AND** delete `.taskless/.tmp-rule-request.json` after the command completes - -#### Scenario: CLI output is reported to user - -- **WHEN** the CLI completes rule generation -- **THEN** the agent SHALL report the generated file paths to the user - -#### Scenario: CLI error is reported to user - -- **WHEN** the CLI exits with a non-zero exit code -- **THEN** the agent SHALL report the error message to the user -- **AND** suggest corrective actions (e.g., run `taskless auth login`, run `taskless update-engine`) - -### Requirement: Rules create skill constructs valid JSON payload - -The JSON payload piped to stdin SHALL conform to `{ prompt: string, language?: string, successCase?: string, failureCase?: string }`. The `prompt` field is required. Optional fields SHALL only be included if the agent has gathered them from the user or inferred them from context. - -#### Scenario: Minimal payload - -- **WHEN** only a prompt is provided -- **THEN** the JSON payload SHALL be `{ "prompt": "" }` - -#### Scenario: Full payload with all fields - -- **WHEN** the agent has gathered prompt, language, success case, and failure case -- **THEN** the JSON payload SHALL include all four fields - -### Requirement: Rules create skill handles stale config errors - -When the CLI reports a stale spec version error, the skill SHALL suggest running `taskless update-engine` instead of `taskless init`. - -#### Scenario: Stale config error from CLI - -- **WHEN** the CLI exits with an error about a stale spec version -- **THEN** the agent SHALL suggest running `taskless update-engine` to upgrade the project scaffold -- **AND** SHALL NOT suggest running `taskless init` - -### Requirement: Rules create skill has correct frontmatter - -The skill's YAML frontmatter SHALL include `name: taskless-create-rule`, a description mentioning rule creation, and `metadata` with `author: taskless` and `version` matching the CLI version. - -#### Scenario: Frontmatter is valid - -- **WHEN** inspecting `skills/taskless-create-rule/SKILL.md` -- **THEN** the frontmatter SHALL have `name: taskless-create-rule` -- **AND** `metadata.author` SHALL be `taskless` -- **AND** `metadata.version` SHALL match the CLI package version - -### Requirement: Rules create skill routes by authentication status - -The `taskless-create-rule` skill SHALL check authentication status before proceeding with rule creation. If the user is authenticated, the skill SHALL proceed with the existing API-backed flow. If the user is not authenticated, the skill SHALL delegate to the `taskless-create-rule-anonymous` skill. - -#### Scenario: Authenticated user gets API flow - -- **WHEN** the rules create skill is invoked -- **AND** `taskless info --json` returns `"loggedIn": true` -- **THEN** the skill SHALL proceed with the existing behavior: gather input, construct JSON payload, and invoke `taskless rules create --from --json` - -#### Scenario: Unauthenticated user gets anonymous flow - -- **WHEN** the rules create skill is invoked -- **AND** `taskless info --json` returns `"loggedIn": false` -- **THEN** the skill SHALL delegate to the `taskless-create-rule-anonymous` skill - -#### Scenario: Auth check happens before input gathering - -- **WHEN** the rules create skill is invoked -- **THEN** the auth check via `taskless info --json` SHALL happen before gathering any rule input from the user - -### Requirement: Anonymous create skill derives rules locally via agent - -The `taskless-create-rule-anonymous` skill SHALL exist at `skills/taskless-create-rule-anonymous/SKILL.md`. When invoked, the agent SHALL use the ast-grep schema from `taskless rules verify --schema --json` to understand rule syntax, derive a rule from the user's description, and validate it using the verify feedback loop. This skill SHALL NOT make any API calls or require authentication. - -#### Scenario: Agent learns ast-grep syntax from schema - -- **WHEN** the anonymous create skill is invoked -- **THEN** the agent SHALL run `taskless rules verify --schema --json` -- **AND** use the `astGrepSchema`, `tasklessRequirements`, and `examples` from the output to understand how to write valid ast-grep rules - -#### Scenario: Agent gathers input conversationally - -- **WHEN** the user provides a description of the rule to create -- **THEN** the agent SHALL gather sufficient context (language, patterns to detect, examples of good/bad code) -- **AND** derive an ast-grep rule YAML based on the schema and examples - -### Requirement: Anonymous create skill writes rule and test files - -The agent SHALL write the derived rule to `.taskless/rules/.yml` and test cases to `.taskless/rule-tests/--test.yml` following the same naming conventions as API-generated rules. - -#### Scenario: Rule file is written - -- **WHEN** the agent has derived a rule -- **THEN** it SHALL write the rule YAML to `.taskless/rules/.yml` -- **AND** the rule SHALL include all Taskless-required fields: `id`, `language`, `severity`, `message`, and `rule` - -#### Scenario: Test file is written - -- **WHEN** the agent has derived a rule -- **THEN** it SHALL also write a test file to `.taskless/rule-tests/--test.yml` -- **AND** the test file SHALL contain `id`, `valid` (code that should NOT trigger), and `invalid` (code that SHOULD trigger) fields - -### Requirement: Anonymous create skill uses verify feedback loop - -After writing the rule and test files, the agent SHALL run `taskless rules verify --json` and fix any reported errors. The agent SHALL repeat the verify-fix cycle until verification passes or a reasonable attempt limit is reached. - -#### Scenario: Verify passes on first attempt - -- **WHEN** the agent writes a rule and runs `taskless rules verify --json` -- **AND** the result has `"success": true` -- **THEN** the agent SHALL report success to the user with the created file paths - -#### Scenario: Verify fails and agent fixes errors - -- **WHEN** `taskless rules verify --json` returns `"success": false` -- **THEN** the agent SHALL read the error details from each validation layer -- **AND** fix the rule and/or test files accordingly -- **AND** re-run `taskless rules verify --json` - -#### Scenario: Verify loop gives up after reasonable attempts - -- **WHEN** the agent has attempted to fix errors multiple times without achieving a passing verify -- **THEN** the agent SHALL inform the user of the remaining issues and suggest manual review - -### Requirement: Anonymous create skill produces no metadata sidecar - -Rules created by the anonymous skill SHALL NOT create files in `.taskless/rule-metadata/`. There is no ticket ID or installation ID to record for locally-derived rules. - -#### Scenario: No metadata files created - -- **WHEN** the anonymous skill completes rule creation -- **THEN** no files SHALL be written to `.taskless/rule-metadata/` - -### Requirement: Anonymous create skill is not directly invocable - -The `taskless-create-rule-anonymous` skill SHALL NOT have a corresponding `/tskl:` command. It SHALL only be invoked by the `taskless-create-rule` router skill when the user is not authenticated. - -#### Scenario: No command file exists - -- **WHEN** the skills are installed -- **THEN** there SHALL be no command file in `commands/tskl/` for the anonymous create skill - -### Requirement: Anonymous create skill has correct frontmatter - -The skill's YAML frontmatter SHALL include `name: taskless-create-rule-anonymous`, a description mentioning anonymous/local rule creation, and `metadata` with `author: taskless` and `version` matching the CLI version. - -#### Scenario: Frontmatter is valid - -- **WHEN** inspecting `skills/taskless-create-rule-anonymous/SKILL.md` -- **THEN** the frontmatter SHALL have `name: taskless-create-rule-anonymous` -- **AND** `metadata.author` SHALL be `taskless` -- **AND** `metadata.version` SHALL match the CLI package version diff --git a/openspec/specs/skill-delete-rule/spec.md b/openspec/specs/skill-delete-rule/spec.md deleted file mode 100644 index 6d30312e..00000000 --- a/openspec/specs/skill-delete-rule/spec.md +++ /dev/null @@ -1,62 +0,0 @@ -# Skill: Rules Delete - -## Purpose - -Defines the `taskless-rules-delete` skill that conversationally identifies a rule and invokes the CLI to delete it. - -## Requirements - -### Requirement: Rules delete skill identifies rules conversationally - -The `taskless-rules-delete` skill SHALL exist at `skills/taskless-rules-delete/SKILL.md`. When invoked, the agent SHALL help the user identify which rule to delete by listing available rules and confirming the target before executing the delete. - -#### Scenario: Agent lists available rules - -- **WHEN** the rules delete skill is invoked -- **THEN** the agent SHALL scan `.taskless/rules/` for `.yml` files -- **AND** present the available rule IDs to the user - -#### Scenario: Agent confirms before deleting - -- **WHEN** the user identifies a rule to delete -- **THEN** the agent SHALL confirm the rule ID with the user before running the delete command - -#### Scenario: No rules found - -- **WHEN** `.taskless/rules/` does not exist or contains no `.yml` files -- **THEN** the agent SHALL inform the user that no rules were found - -### Requirement: Rules delete skill invokes CLI with rule ID - -After confirming the target rule, the skill SHALL detect the package manager and invoke the CLI's `rules delete` command with the rule ID as a positional argument. - -#### Scenario: Invocation with pnpm - -- **WHEN** the skill is invoked in a project with `pnpm-lock.yaml` -- **THEN** the agent SHALL run `pnpm dlx @taskless/cli@latest rules delete ` - -#### Scenario: Invocation with npm - -- **WHEN** the skill is invoked in a project without `pnpm-lock.yaml` -- **THEN** the agent SHALL run `npx @taskless/cli@latest rules delete ` - -#### Scenario: Successful deletion is reported - -- **WHEN** the CLI completes deletion successfully -- **THEN** the agent SHALL confirm which rule and test files were removed - -#### Scenario: Rule not found error is reported - -- **WHEN** the CLI exits with an error indicating the rule was not found -- **THEN** the agent SHALL report the error and suggest checking the rule ID - -### Requirement: Rules delete skill has correct frontmatter - -The skill's YAML frontmatter SHALL include `name: taskless-rules-delete`, a description mentioning rule deletion, and `metadata` with `author: taskless` and `version` matching the CLI version. - -#### Scenario: Frontmatter is valid - -- **WHEN** inspecting `skills/taskless-rules-delete/SKILL.md` -- **THEN** the frontmatter SHALL have `name: taskless-rules-delete` -- **AND** `metadata.author` SHALL be `taskless` -- **AND** `metadata.version` SHALL match the CLI package version diff --git a/openspec/specs/skill-improve-rule/spec.md b/openspec/specs/skill-improve-rule/spec.md deleted file mode 100644 index edb6a507..00000000 --- a/openspec/specs/skill-improve-rule/spec.md +++ /dev/null @@ -1,165 +0,0 @@ -# Skill: Improve Rule - -## Purpose - -TBD — Defines the `taskless-improve-rule` skill that guides agents through improving existing Taskless rules by choosing between iterating, replacing, or expanding. - -## Requirements - -### Requirement: Skill inventories existing rules - -The `taskless-improve-rule` skill SHALL scan `.taskless/rules/` for `.yml` files and present a summary of existing rules to the user if they have not indicated a specific rule to improve. - -#### Scenario: Multiple rules exist - -- **WHEN** the skill is invoked and `.taskless/rules/` contains multiple rule files -- **THEN** the skill SHALL list each rule's ID, language, and detected pattern - -#### Scenario: No rules exist - -- **WHEN** the skill is invoked and `.taskless/rules/` is empty -- **THEN** the skill SHALL inform the user there are no rules to improve - -### Requirement: Skill determines improvement approach - -The skill SHALL evaluate the user's feedback and choose one of three approaches: (A) iterate on the existing rule via the improve CLI command, (B) replace the rule by creating a new one and deleting the old, or (C) expand by creating additional rules. The skill SHALL present the chosen approach to the user for confirmation before proceeding. - -#### Scenario: Iterate approach selected - -- **WHEN** the user wants to refine an existing rule (e.g., fix false positives) -- **THEN** the skill SHALL choose Option A and use the `rules improve` CLI command - -#### Scenario: Replace approach selected - -- **WHEN** the rule is fundamentally wrong and needs a different approach -- **THEN** the skill SHALL choose Option B and use `taskless-create-rule` followed by `rules delete` - -#### Scenario: Expand approach selected - -- **WHEN** the user's need has expanded beyond a single rule -- **THEN** the skill SHALL choose Option C and create additional rules via `taskless-create-rule` - -### Requirement: Skill builds iterate payload with references - -When using the iterate approach, the skill SHALL build a JSON payload containing `ruleId`, `guidance`, and optionally `references` (current rule and test file contents). The skill SHALL write this to `.taskless/.tmp-improve-request.json`, invoke the CLI, and clean up the temp file. - -#### Scenario: Payload includes references - -- **WHEN** the skill iterates on a rule that has associated test files -- **THEN** the payload SHALL include both the rule file and test file as references - -### Requirement: Skill cross-references use skill names - -The skill SHALL reference other skills by their skill name (e.g., `taskless-create-rule`, `taskless-check`, `taskless-login`) rather than command names, for compatibility with non-command agentic systems. - -#### Scenario: Suggesting follow-up actions - -- **WHEN** the skill suggests testing after improvement -- **THEN** the skill SHALL reference `taskless-check` not `tskl:check` - -### Requirement: Improve rule skill routes by authentication status - -The `taskless-improve-rule` skill SHALL check authentication status before proceeding with rule improvement. If the user is authenticated, the skill SHALL proceed with the existing API-backed flow. If the user is not authenticated, the skill SHALL delegate to the `taskless-improve-rule-anonymous` skill. - -#### Scenario: Authenticated user gets API flow - -- **WHEN** the improve rule skill is invoked -- **AND** `taskless info --json` returns `"loggedIn": true` -- **THEN** the skill SHALL proceed with the existing behavior: inventory rules, determine approach, and invoke the CLI - -#### Scenario: Unauthenticated user gets anonymous flow - -- **WHEN** the improve rule skill is invoked -- **AND** `taskless info --json` returns `"loggedIn": false` -- **THEN** the skill SHALL delegate to the `taskless-improve-rule-anonymous` skill - -#### Scenario: Auth check happens before rule inventory - -- **WHEN** the improve rule skill is invoked -- **THEN** the auth check via `taskless info --json` SHALL happen before inventorying rules or gathering improvement guidance - -### Requirement: Anonymous improve skill iterates on rules locally via agent - -The `taskless-improve-rule-anonymous` skill SHALL exist at `skills/taskless-improve-rule-anonymous/SKILL.md`. When invoked, the agent SHALL read the existing rule and test files, understand the user's improvement guidance, modify the rule locally, and validate changes using the verify feedback loop. This skill SHALL NOT make any API calls or require authentication. - -#### Scenario: Agent reads existing rule and tests - -- **WHEN** the anonymous improve skill is invoked for rule `` -- **THEN** the agent SHALL read `.taskless/rules/.yml` and any matching test file in `.taskless/rule-tests/` -- **AND** run `taskless rules verify --schema --json` to understand ast-grep rule syntax - -#### Scenario: Agent gathers improvement guidance - -- **WHEN** the user describes what to improve (e.g., "reduce false positives on arrow functions") -- **THEN** the agent SHALL analyze the existing rule against the guidance -- **AND** modify the rule YAML accordingly - -### Requirement: Anonymous improve skill writes updated files - -The agent SHALL overwrite the existing rule file at `.taskless/rules/.yml` and write a new test file at `.taskless/rule-tests/--test.yml` reflecting the improved rule. - -#### Scenario: Rule file is updated in place - -- **WHEN** the agent improves a rule -- **THEN** it SHALL overwrite `.taskless/rules/.yml` with the updated rule YAML - -#### Scenario: New test file is created - -- **WHEN** the agent improves a rule -- **THEN** it SHALL write a new test file to `.taskless/rule-tests/--test.yml` -- **AND** the test file SHALL include cases that exercise the improved behavior - -### Requirement: Anonymous improve skill uses verify feedback loop - -After writing the updated files, the agent SHALL run `taskless rules verify --json` and fix any reported errors, repeating until verification passes. - -#### Scenario: Verify passes after improvement - -- **WHEN** the agent updates a rule and runs `taskless rules verify --json` -- **AND** the result has `"success": true` -- **THEN** the agent SHALL report success to the user - -#### Scenario: Verify fails and agent fixes errors - -- **WHEN** `taskless rules verify --json` returns `"success": false` -- **THEN** the agent SHALL fix the issues and re-verify - -### Requirement: Anonymous improve skill supports all improvement approaches - -The anonymous improve skill SHALL support the same three approaches as the authenticated improve skill: (A) iterate on the existing rule, (B) replace the rule by creating a new one and deleting the old, or (C) expand by creating additional rules. For approaches B and C, the skill SHALL delegate to `taskless-create-rule-anonymous` for new rule creation. - -#### Scenario: Iterate approach - -- **WHEN** the user wants to refine an existing rule -- **THEN** the skill SHALL modify the rule in place and verify - -#### Scenario: Replace approach - -- **WHEN** the rule needs a fundamentally different approach -- **THEN** the skill SHALL invoke `taskless-create-rule-anonymous` for the new rule -- **AND** delete the old rule via `taskless rules delete ` - -#### Scenario: Expand approach - -- **WHEN** the user's need requires additional rules -- **THEN** the skill SHALL invoke `taskless-create-rule-anonymous` for each new rule - -### Requirement: Anonymous improve skill is not directly invocable - -The `taskless-improve-rule-anonymous` skill SHALL NOT have a corresponding `/tskl:` command. It SHALL only be invoked by the `taskless-improve-rule` router skill when the user is not authenticated. - -#### Scenario: No command file exists - -- **WHEN** the skills are installed -- **THEN** there SHALL be no command file in `commands/tskl/` for the anonymous improve skill - -### Requirement: Anonymous improve skill has correct frontmatter - -The skill's YAML frontmatter SHALL include `name: taskless-improve-rule-anonymous`, a description mentioning anonymous/local rule improvement, and `metadata` with `author: taskless` and `version` matching the CLI version. - -#### Scenario: Frontmatter is valid - -- **WHEN** inspecting `skills/taskless-improve-rule-anonymous/SKILL.md` -- **THEN** the frontmatter SHALL have `name: taskless-improve-rule-anonymous` -- **AND** `metadata.author` SHALL be `taskless` -- **AND** `metadata.version` SHALL match the CLI package version diff --git a/openspec/specs/skill-taskless/spec.md b/openspec/specs/skill-taskless/spec.md new file mode 100644 index 00000000..4aad4dbe --- /dev/null +++ b/openspec/specs/skill-taskless/spec.md @@ -0,0 +1,95 @@ +# Skill: Taskless + +## Purpose + +Defines the single consolidated `taskless` skill that replaced the per-task skills (`taskless-check`, `taskless-create-rule`, `taskless-create-rule-anonymous`, `taskless-improve-rule`, `taskless-improve-rule-anonymous`, `taskless-delete-rule`, `taskless-info`, `taskless-login`, `taskless-logout`, `taskless-ci`) in v0.7. The skill body is a small router whose canonical recipes live behind `npx @taskless/cli help `, fetched on demand by the agent rather than always loaded. + +## Requirements + +### Requirement: Single consolidated taskless skill replaces per-task skills + +The skills bundle SHALL contain exactly one skill named `taskless`. This skill SHALL replace the per-task skills `taskless-check`, `taskless-create-rule`, `taskless-create-rule-anonymous`, `taskless-improve-rule`, `taskless-improve-rule-anonymous`, `taskless-delete-rule`, `taskless-info`, `taskless-login`, `taskless-logout`, and `taskless-ci`. The skill SHALL be installed into every detected tool location (Claude Code, OpenCode, Cursor, etc.) per the existing install plumbing. + +#### Scenario: Bundle contains exactly one skill + +- **WHEN** the CLI bundle is built +- **THEN** `import.meta.glob("../../../../skills/**/SKILL.md")` SHALL match exactly one file at `skills/taskless/SKILL.md` + +#### Scenario: Skill catalog has one entry + +- **WHEN** `getMandatorySkillNames()` is called +- **THEN** it SHALL return `["taskless"]` +- **AND** `getOptionalSkillNames()` SHALL return `[]` + +#### Scenario: Old skill directories are removed + +- **WHEN** the v0.7.0 release is built +- **THEN** none of the directories `skills/taskless-check`, `skills/taskless-ci`, `skills/taskless-create-rule`, `skills/taskless-create-rule-anonymous`, `skills/taskless-delete-rule`, `skills/taskless-improve-rule`, `skills/taskless-improve-rule-anonymous`, `skills/taskless-info`, `skills/taskless-login`, or `skills/taskless-logout` SHALL exist in the repository + +### Requirement: Skill description anchors triggers on Taskless-specific phrases + +The consolidated skill's `description` frontmatter field SHALL anchor triggers on either an explicit reference to "Taskless" in the user's message OR a reference to the `.taskless/` directory or files within it (rules, rule-tests, rule-metadata). The description SHALL explicitly instruct the agent NOT to trigger on generic ESLint, linting, or rule requests that don't reference Taskless. + +#### Scenario: Description includes anchored trigger phrases + +- **WHEN** the skill `description` field is read +- **THEN** it SHALL include trigger phrases such as "create/add/write a taskless rule", "improve/fix/iterate on this taskless rule", "run taskless", "taskless login", "add taskless to CI" +- **AND** SHALL include an explicit "Do NOT trigger on" clause covering generic ESLint and linting requests + +#### Scenario: Description is at most 1024 characters + +- **WHEN** the skill `description` field length is measured +- **THEN** it SHALL be at most 1024 characters (Agent Skills spec limit) + +### Requirement: Skill body is a router, not an inline recipe + +The consolidated skill body SHALL NOT contain step-by-step instructions for any individual Taskless task. The body SHALL be a router that: + +1. States explicitly that the agent does NOT have the steps for any Taskless action in its context +2. Instructs the agent to fetch the canonical recipe via `npx @taskless/cli help ` before proceeding +3. Provides a topic disambiguation table mapping user intents to topic names +4. Includes a `## --anonymous` section explaining the global flag's behavior +5. Includes a first-step `.taskless/` presence check with graceful failure ("ask the user to confirm they meant Taskless") + +The body SHALL be no more than 60 lines of markdown to keep the always-loaded surface small. + +#### Scenario: Skill body warns against improvising + +- **WHEN** the skill body is read by an agent +- **THEN** it SHALL contain explicit framing such as "You do NOT have the steps... do not improvise from prior knowledge" + +#### Scenario: Skill body lists available topics + +- **WHEN** the skill body is read by an agent +- **THEN** it SHALL include a table or list mapping user intents (create rule, improve rule, delete rule, check, auth, ci) to the corresponding `tskl help ` invocations + +#### Scenario: Skill body checks for .taskless directory + +- **WHEN** the skill is invoked +- **THEN** the body's first step SHALL instruct the agent to check whether `.taskless/` exists in the working directory +- **AND** to ask the user to confirm Taskless is what they meant if the directory is absent + +### Requirement: Skill maps to a single tskl command + +The consolidated skill's frontmatter SHALL include `metadata.commandName: tskl` so that command-installation plumbing maps the skill to the new single command file at `commands/tskl/tskl.md`. The command file SHALL be a thin doorway that accepts a free-form `$ARGUMENTS` ask, infers a topic if possible, and otherwise asks the user what they want to do. + +#### Scenario: Frontmatter declares the command mapping + +- **WHEN** the skill frontmatter is parsed +- **THEN** `metadata.commandName` SHALL equal `tskl` + +#### Scenario: Old command files are removed + +- **WHEN** the v0.7.0 release is built +- **THEN** none of `commands/tskl/check.md`, `commands/tskl/improve.md`, `commands/tskl/info.md`, `commands/tskl/login.md`, `commands/tskl/logout.md`, or `commands/tskl/rule.md` SHALL exist in the repository +- **AND** exactly one file `commands/tskl/tskl.md` SHALL exist + +#### Scenario: Slash command accepts free-form arguments + +- **WHEN** a user invokes `/tskl` with arguments (e.g. `/tskl create a rule for no console.log`) +- **THEN** the command body SHALL instruct the agent to infer the topic from `$ARGUMENTS`, fetch the recipe via `npx @taskless/cli help `, and proceed + +#### Scenario: Slash command without arguments asks the user + +- **WHEN** a user invokes `/tskl` with no arguments +- **THEN** the command body SHALL instruct the agent to ask the user what they want to do with Taskless before proceeding diff --git a/openspec/specs/skills/spec.md b/openspec/specs/skills/spec.md index d0317d68..0b8dc956 100644 --- a/openspec/specs/skills/spec.md +++ b/openspec/specs/skills/spec.md @@ -8,118 +8,23 @@ Defines the structure, conventions, and distribution model for Taskless skills, ### Requirement: Skills use SKILL.md format with YAML frontmatter -Each skill SHALL be defined in its own directory under `skills//` with a `SKILL.md` file containing YAML frontmatter (`name`, `description`, `metadata`) followed by markdown instructions. The `name` field SHALL use the `taskless-` prefix (e.g., `taskless-info`). The `metadata` field SHALL include `author` and `version` keys. The `version` key SHALL be used for staleness detection when skills are installed into target repositories. +The single skill SHALL be defined at `skills/taskless/SKILL.md` with YAML frontmatter (`name`, `description`, `metadata`) followed by markdown instructions. The `name` field SHALL be exactly `taskless` (no per-task prefix). The `metadata` field SHALL include `author`, `version`, and `commandName: tskl` keys. The `version` SHALL be used for staleness detection when the skill is installed into target repositories. -Each skill's instructions SHALL begin by invoking `taskless help ` to retrieve the command's current usage documentation. The agent SHALL use this help output — including usage patterns, options, and examples — when constructing CLI invocations. Skills SHALL NOT hardcode CLI option lists, output format descriptions, or example commands that duplicate the help output. Skills SHALL prefer `pnpm dlx` when available but MAY use `npx` — the CLI works identically either way. +The skill body SHALL begin by instructing the agent that it does NOT have step-by-step instructions for any Taskless action and that recipes must be fetched via `npx @taskless/cli help ` before proceeding. The body SHALL NOT contain inline step-by-step recipes for any individual task — those live in `packages/cli/src/help/.txt` files served by the help subcommand. #### Scenario: Skill directory contains valid SKILL.md -- **WHEN** a new skill is added at `skills//SKILL.md` -- **THEN** it SHALL contain YAML frontmatter with `name` (kebab-case, 1-64 chars, starting with `taskless-`) and `description` (up to 1024 chars) -- **AND** the `metadata` field SHALL include `author: taskless` and `version` (string matching CLI package version) -- **AND** the markdown body SHALL contain instructions for the agent +- **WHEN** the skill is built +- **THEN** `skills/taskless/SKILL.md` SHALL exist +- **AND** SHALL contain YAML frontmatter with `name: taskless` and `description` (up to 1024 chars) +- **AND** the `metadata` field SHALL include `author: taskless`, `version` (string matching CLI package version), and `commandName: tskl` +- **AND** the markdown body SHALL contain the router instructions described above (no per-task recipes inline) -#### Scenario: Skill name matches directory name +#### Scenario: Skill body delegates to CLI help -- **WHEN** a skill exists at `skills/taskless-info/SKILL.md` -- **THEN** the `name` field in frontmatter SHALL be `taskless-info` - -#### Scenario: Skills are bundled into CLI at build time - -- **WHEN** the CLI is built -- **THEN** all SKILL.md files under `skills/` SHALL be embedded into the CLI bundle -- **AND** the skill content SHALL be available at runtime without filesystem or network access - -#### Scenario: Skill instructions begin with help invocation - -- **WHEN** any skill's instructions are followed by an agent -- **THEN** the agent SHALL first run `taskless help ` to retrieve current command documentation -- **AND** the agent SHALL use the help output to understand usage, options, and examples - -#### Scenario: Skills do not duplicate CLI help content - -- **WHEN** a skill references CLI options, output formats, or example invocations -- **THEN** those details SHALL come from the `taskless help` output, not from hardcoded text in the SKILL.md - -### Requirement: Info skill confirms Taskless is working - -The `taskless-info` skill SHALL exist at `skills/taskless-info/SKILL.md`. When invoked, it SHALL run `taskless help info` to read current command documentation, invoke the CLI via `pnpm dlx @taskless/cli@latest info` (or `npx`), parse the JSON response, and report the CLI version. The skill SHALL confirm that Taskless is operational by displaying the version received from the CLI. - -#### Scenario: Info skill reads help before invoking CLI - -- **WHEN** the info skill is invoked -- **THEN** the agent SHALL run `taskless help info` to understand the command's output format -- **AND** then run `taskless info` to get the actual data - -#### Scenario: Info skill invokes CLI and parses response - -- **WHEN** the info skill runs the CLI -- **THEN** the agent SHALL run `pnpm dlx @taskless/cli@latest info` (preferring pnpm, falling back to npx) -- **AND** parse the JSON stdout to extract the `version` field -- **AND** report the version to the user - -#### Scenario: Info skill handles CLI failure - -- **WHEN** the CLI invocation fails (non-zero exit code or unparseable output) -- **THEN** the agent SHALL report that it could not reach the Taskless CLI and suggest troubleshooting steps - -### Requirement: Check skill uses --json flag for machine-readable output - -The `taskless-check` skill SHALL invoke the CLI with `--json` to get machine-readable output. The skill SHALL run `taskless help check` first to read current command documentation, then invoke `taskless check --json` and parse the JSON response. - -#### Scenario: Check skill reads help and uses --json - -- **WHEN** the check skill is invoked -- **THEN** the agent SHALL run `taskless help check` to understand the command -- **AND** then run `taskless check --json` to get results as JSON -- **AND** parse the JSON to determine success/failure and report results - -### Requirement: Login skill delegates documentation to CLI help - -The `taskless-login` skill SHALL run `taskless help auth login` to retrieve the current command documentation and present it to the user. The skill SHALL NOT attempt to run the login command itself, as it requires interactive terminal input. - -#### Scenario: Login skill reads help and presents command - -- **WHEN** the login skill is invoked -- **THEN** the agent SHALL run `taskless help auth login` to get current documentation -- **AND** present the login command for the user to run in their terminal -- **AND** SHALL NOT attempt to run the login command - -### Requirement: Logout skill delegates documentation to CLI help - -The `taskless-logout` skill SHALL run `taskless help auth logout` to retrieve the current command documentation and present it to the user. The skill SHALL NOT attempt to run the logout command itself. - -#### Scenario: Logout skill reads help and presents command - -- **WHEN** the logout skill is invoked -- **THEN** the agent SHALL run `taskless help auth logout` to get current documentation -- **AND** present the logout command for the user to run in their terminal -- **AND** SHALL NOT attempt to run the logout command - -### Requirement: Rule create skill uses --json flag and delegates docs to help - -The `taskless-rule-create` skill SHALL run `taskless help rules create` to read current command documentation, gather input from the user for the JSON payload, and invoke the CLI with `--json` for machine-readable output. The skill SHALL use examples from the help output to construct the CLI invocation. - -#### Scenario: Rule create skill reads help before gathering input - -- **WHEN** the rule create skill is invoked -- **THEN** the agent SHALL run `taskless help rules create` to understand the command's input format and options -- **AND** use the help output to understand required and optional JSON fields - -#### Scenario: Rule create skill uses --json - -- **WHEN** the rule create skill invokes the CLI -- **THEN** it SHALL pipe the JSON payload to `taskless rules create --json` - -### Requirement: Rule delete skill delegates docs to help - -The `taskless-rule-delete` skill SHALL run `taskless help rules delete` to read current command documentation before executing the delete command. The skill SHALL use the help output to understand the command's arguments and options. - -#### Scenario: Rule delete skill reads help before executing - -- **WHEN** the rule delete skill is invoked -- **THEN** the agent SHALL run `taskless help rules delete` to understand the command -- **AND** use the help output to construct the correct invocation with the rule ID +- **WHEN** the skill body is read +- **THEN** it SHALL instruct the agent to fetch the canonical recipe via `npx @taskless/cli help ` before performing any Taskless action +- **AND** SHALL NOT duplicate recipe content inline ## Distribution @@ -144,38 +49,30 @@ All Taskless skills SHALL be located at `skills//SKILL.md` in the reposito ### Requirement: Skill names are globally qualified with taskless prefix -All Taskless skill directories and frontmatter `name` fields SHALL use the `taskless-` prefix (e.g., `taskless-info`, `taskless-auth-login`). The prefix is part of the source name, not applied at install time. - -#### Scenario: Skill directory name matches frontmatter name +The single skill name SHALL be `taskless` (without a per-task suffix). When installed into a target tool, the skill SHALL be installed at `/skills/taskless/SKILL.md` (e.g. `.claude/skills/taskless/SKILL.md`). -- **WHEN** a skill exists at `skills/taskless-info/SKILL.md` -- **THEN** the frontmatter `name` field SHALL be `taskless-info` +#### Scenario: Skill name is exactly taskless -#### Scenario: All skills use the taskless prefix - -- **WHEN** listing all skill directories under `skills/` -- **THEN** every directory name SHALL start with `taskless-` +- **WHEN** the skill is installed into any tool location +- **THEN** the directory name SHALL be `taskless` +- **AND** SHALL NOT use a `taskless-` per-task name ### Requirement: Commands directory contains Claude Code command files -A `commands/taskless/` directory SHALL exist at the repository root containing command `.md` files for Claude Code. Each command file SHALL correspond to a skill with the `taskless-` prefix stripped from the filename. - -#### Scenario: Command filename strips taskless prefix +The `commands/tskl/` directory SHALL contain exactly one command file (`tskl.md`) that maps to the consolidated skill. The command body SHALL accept a free-form `$ARGUMENTS` ask and route via the same flow as the skill (fetch `npx @taskless/cli help `, follow the recipe). When `$ARGUMENTS` is empty or ambiguous, the command body SHALL instruct the agent to ask the user what they want to do. -- **WHEN** a skill exists at `skills/taskless-auth-login/SKILL.md` -- **THEN** a corresponding command SHALL exist at `commands/taskless/auth-login.md` +#### Scenario: Single command file exists -#### Scenario: Command frontmatter uses display name +- **WHEN** the v0.7.0 release is built -- **WHEN** a command is generated for skill `taskless-auth-login` -- **THEN** the command frontmatter `name` SHALL be `"Taskless: Auth Login"` -- **AND** the `category` SHALL be `"Taskless"` -- **AND** the `tags` SHALL include `"taskless"` +- **THEN** `commands/tskl/tskl.md` SHALL exist +- **AND** no other command files SHALL exist in `commands/tskl/` -#### Scenario: Command body matches skill body +#### Scenario: Command file is a router -- **WHEN** a command is generated from a skill -- **THEN** the command markdown body SHALL be identical to the skill's markdown body +- **WHEN** the command body is read +- **THEN** it SHALL instruct the agent to handle `$ARGUMENTS` by inferring a topic, fetching its recipe, and proceeding +- **AND** SHALL specify behavior when `$ARGUMENTS` is empty (ask the user) ### Requirement: Claude Code Plugin Marketplace manifest exists @@ -194,14 +91,14 @@ A `.claude-plugin/marketplace.json` file SHALL exist at the repository root defi ### Requirement: Plugin manifest declares skills and commands -A `.claude-plugin/plugin.json` file SHALL exist declaring the `taskless` plugin with `skills` and `commands` paths pointing to the repo root directories. +The `.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json` SHALL declare the single consolidated skill and the single command. The plugin version SHALL be `0.7.0` for this release. The plugin description MAY be updated to reflect the consolidation. -#### Scenario: Plugin manifest declares component paths +#### Scenario: Plugin manifest reflects consolidated bundle -- **WHEN** inspecting `.claude-plugin/plugin.json` -- **THEN** it SHALL declare `name: "taskless"`, a `description`, and a `version` -- **AND** it SHALL include `skills` pointing to `"./skills/"` or listing skill paths -- **AND** it SHALL include `commands` pointing to `"./commands/taskless/"` or listing command paths +- **WHEN** `plugin.json` is read +- **THEN** `version` SHALL be `0.7.0` +- **AND** `commands` SHALL point to `./commands/tskl/` +- **AND** the bundled commands directory SHALL contain only `tskl.md` ### Requirement: Three distribution channels are supported diff --git a/package.json b/package.json index 06b75019..932214d5 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,10 @@ "license": "MIT", "repository": "taskless/skills.git", "scripts": { - "build": "run-s build:generate-commands build:link-skills build:compile", + "build": "run-s build:link-skills build:compile", "build:compile": "turbo run build", - "build:generate-commands": "tsx scripts/generate-commands.ts", "build:link-skills": "tsx scripts/link-skills.ts", - "bump": "run-s bump:version bump:sync build:generate-commands", + "bump": "run-s bump:version bump:sync", "bump:sync": "tsx scripts/sync-skill-versions.ts", "bump:version": "changeset version", "changeset": "changeset", diff --git a/packages/cli/README.md b/packages/cli/README.md index 34a03e60..3f53129b 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -33,13 +33,15 @@ Outputs CLI version, tool status, and login info as JSON to stdout: Launches an interactive wizard that detects supported tool directories in the current project (`.claude/`, `.opencode/`, `.cursor/`, `.agents/`), lets you -pick which ones to install into, optionally includes the `taskless-ci` skill, -and walks through the auth tradeoff before writing anything. Running `taskless` -with no subcommand in a TTY also launches this wizard. +pick which ones to install into, and walks through the auth tradeoff before +writing anything. Running `taskless` with no subcommand in a TTY also launches +this wizard. Without a TTY, bare `taskless` prints a short context preamble +followed by the topic index from `taskless help`. -For CI and scripted installs, pass `--no-interactive` to skip all prompts. -This installs every mandatory skill to every detected tool, or falls back to -`.agents/skills/` when no tools are detected: +In v0.7+, there is exactly one skill (`taskless`) and one command (`tskl`) — +no opt-in selection needed. + +For CI and scripted installs, pass `--no-interactive` to skip all prompts: ```bash taskless init # interactive wizard (default in a TTY) @@ -48,8 +50,9 @@ taskless init --no-interactive # scripted install, no prompts The wizard records what it installed in `.taskless/taskless.json` so later runs can compute a diff and surgically remove files that are no longer -selected. Cancelling the wizard at any step (Ctrl-C) aborts cleanly with no -filesystem changes. +selected. Upgrading from v0.6 automatically removes the obsolete per-task +skills and commands during this diff. Cancelling the wizard at any step +(Ctrl-C) aborts cleanly with no filesystem changes. ### `taskless check` @@ -77,29 +80,52 @@ If every supplied path is missing, the command exits 0 with empty results. Authenticate with taskless.io using the device flow. Tokens are stored in `~/.config/taskless/auth.json`. -### `taskless rules create` +### `taskless rule create` Generate ast-grep rules via the taskless.io API. Reads a JSON request from stdin, submits it, polls for results, and writes rule and test files to `.taskless/rules/` and `.taskless/rule-tests/`. ```bash -echo '{"prompt": "detect console.log usage"}' | taskless rules create -echo '{"prompt": "find innerHTML assignments", "language": "typescript"}' | taskless rules create --json +echo '{"prompt": "detect console.log usage"}' | taskless rule create +echo '{"prompt": "find innerHTML assignments", "language": "typescript"}' | taskless rule create --json ``` Requires authentication and a `.taskless/taskless.json` with `orgId` and `repositoryUrl`. -### `taskless rules delete ` +### `taskless rule delete ` Remove a rule file and its associated test files from disk. No authentication required. ```bash -taskless rules delete no-console-log +taskless rule delete no-console-log ``` ### `taskless --help` Lists available subcommands. +### `taskless help [topic]` + +Returns agent-facing recipes. With no args, prints the topic index. With a +topic (e.g. `taskless help rule create`), prints the full step-by-step recipe +for that operation, including an embedded JSON Schema for any `--from` input +and a table of stable error codes. Append `--anonymous` to fetch the +local-only variant where one exists (currently `rule create`/`rule improve`). + +Recipes are how the consolidated `taskless` skill stays small while still +covering every operation — the skill body is a router that fetches the +relevant recipe on demand. + +### `--anonymous` flag + +Recognized on every command. Behavior matrix: + +- `rule create` / `rule improve` — exits with a pointer to + `taskless help --anonymous`. The local-only flow runs in the agent + per the recipe variant. +- `info` — skips the API/auth probe; reports local state only. +- `auth login` — rejected (auth commands cannot be anonymous). +- All others — accepted as no-op. + ## For skill authors Skills should detect the package manager by checking for lock files and invoke the CLI accordingly: diff --git a/packages/cli/src/commands/auth.ts b/packages/cli/src/commands/auth.ts index 22d0ac03..a8d96321 100644 --- a/packages/cli/src/commands/auth.ts +++ b/packages/cli/src/commands/auth.ts @@ -5,6 +5,7 @@ import { loginInteractive } from "../auth/login-interactive"; import { getToken, removeToken } from "../auth/token"; import { fetchWhoami } from "../auth/whoami"; import { getTelemetry } from "../telemetry"; +import { type CliErrorCode, writeJsonError } from "../types/errors"; const loginCommand = defineCommand({ meta: { @@ -17,28 +18,90 @@ const loginCommand = defineCommand({ alias: "d", description: "Working directory", }, + anonymous: { + type: "boolean", + description: "Rejected: auth commands cannot be anonymous", + default: false, + }, + json: { + type: "boolean", + description: + "On error, write the standardized { ok:false, code, message } envelope to stdout instead of human text on stderr", + default: false, + }, }, async run({ args }) { const cwd = resolve(args.dir ?? process.cwd()); const telemetry = await getTelemetry(cwd); + const startedAt = Date.now(); telemetry.capture("cli_auth_login"); - const result = await loginInteractive({ cwd }); + /** Tracks the last emitted error code so the completion event can include it. */ + let lastErrorCode: CliErrorCode | undefined; - switch (result.status) { - case "ok": { - telemetry.capture("cli_auth_login_completed"); - return; + /** Emit an error in the right channel and set exit code. */ + const fail = (code: CliErrorCode, message: string): void => { + lastErrorCode = code; + if (args.json) { + writeJsonError(code, message); + } else { + console.error(`Error: ${message}`); } - case "already_logged_in": { - console.log("You are already logged in."); - console.log("Run `taskless auth logout` first to re-authenticate."); - return; - } - case "cancelled": { - process.exitCode = 1; - return; + process.exitCode = 1; + }; + + if (args.anonymous) { + fail("INVALID_INPUT", "auth commands cannot be anonymous."); + telemetry.capture("cli_auth_login_completed", { + success: false, + durationMs: Date.now() - startedAt, + errorCode: lastErrorCode, + }); + return; + } + + let success = false; + try { + // In --json mode the user is an agent / pipe; suppress the device-flow + // chatter and only emit a single structured line on error. + const noop = (): void => {}; + const result = await loginInteractive( + args.json ? { cwd, out: noop, err: noop } : { cwd } + ); + + switch (result.status) { + case "ok": { + success = true; + return; + } + case "already_logged_in": { + if (!args.json) { + console.log("You are already logged in."); + console.log("Run `taskless auth logout` first to re-authenticate."); + } + success = true; + return; + } + case "cancelled": { + const code: CliErrorCode = + result.reason === "denied" ? "AUTH_REQUIRED" : "NETWORK_ERROR"; + const message = + result.message ?? + (result.reason === "denied" + ? "Authorization denied." + : result.reason === "expired" + ? "Device code expired. Please try again." + : "Authentication failed."); + fail(code, message); + return; + } } + } finally { + telemetry.capture("cli_auth_login_completed", { + success, + durationMs: Date.now() - startedAt, + ...(success ? {} : { errorCode: lastErrorCode }), + }); } }, }); @@ -54,17 +117,36 @@ const logoutCommand = defineCommand({ alias: "d", description: "Working directory", }, + anonymous: { + type: "boolean", + description: "Accepted for compatibility; logout is already local", + default: false, + }, + json: { + type: "boolean", + description: + "On error, write the standardized { ok:false, code, message } envelope to stdout. Success is silent on stdout in --json mode.", + default: false, + }, }, async run({ args }) { const cwd = resolve(args.dir ?? process.cwd()); const telemetry = await getTelemetry(cwd); + const startedAt = Date.now(); telemetry.capture("cli_auth_logout"); - const removed = await removeToken(cwd); - if (removed) { - console.log("Logged out."); - } else { - console.log("Not logged in."); + let success = false; + try { + const removed = await removeToken(cwd); + if (!args.json) { + console.log(removed ? "Logged out." : "Not logged in."); + } + success = true; + } finally { + telemetry.capture("cli_auth_logout_completed", { + success, + durationMs: Date.now() - startedAt, + }); } }, }); @@ -80,6 +162,12 @@ export const authCommand = defineCommand({ alias: "d", description: "Working directory", }, + json: { + type: "boolean", + description: + "Accepted on the status path for forward-compat; today the status output is plain text and emits no error envelope (no error paths)", + default: false, + }, }, subCommands: { login: loginCommand, @@ -94,26 +182,38 @@ export const authCommand = defineCommand({ const cwd = resolve(args.dir ?? process.cwd()); const telemetry = await getTelemetry(cwd); + const startedAt = Date.now(); telemetry.capture("cli_auth_status"); - const token = await getToken(cwd); - if (!token) { - console.log("Not logged in."); - console.log("Run `taskless auth login` to authenticate."); - return; - } + let success = false; + try { + const token = await getToken(cwd); + if (!token) { + console.log("Not logged in."); + console.log("Run `taskless auth login` to authenticate."); + success = true; + return; + } - const whoami = await fetchWhoami(token); - if (!whoami) { - console.log("Logged in, but unable to verify identity."); - console.log( - "Your token may be invalid or expired. Run `taskless auth login` to re-authenticate." - ); - return; - } + const whoami = await fetchWhoami(token); + if (!whoami) { + console.log("Logged in, but unable to verify identity."); + console.log( + "Your token may be invalid or expired. Run `taskless auth login` to re-authenticate." + ); + success = true; + return; + } - const orgs = whoami.orgs.map((o) => o.name); - const orgSuffix = orgs.length > 0 ? ` (${orgs.join(", ")})` : ""; - console.log(`Logged in as ${whoami.user}${orgSuffix}.`); + const orgs = whoami.orgs.map((o) => o.name); + const orgSuffix = orgs.length > 0 ? ` (${orgs.join(", ")})` : ""; + console.log(`Logged in as ${whoami.user}${orgSuffix}.`); + success = true; + } finally { + telemetry.capture("cli_auth_status_completed", { + success, + durationMs: Date.now() - startedAt, + }); + } }, }); diff --git a/packages/cli/src/commands/check.ts b/packages/cli/src/commands/check.ts index e4fd447d..bab9e7cb 100644 --- a/packages/cli/src/commands/check.ts +++ b/packages/cli/src/commands/check.ts @@ -5,12 +5,9 @@ import { defineCommand } from "citty"; import { runAstGrepScan } from "../rules/scan"; import { formatText } from "../util/format"; import { generateSgConfig } from "../filesystem/sgconfig"; -import { printSchema } from "../util/schema-output"; import { getTelemetry } from "../telemetry"; -import { - outputSchema as checkOutputSchema, - errorSchema as checkErrorSchema, -} from "../schemas/check"; +import { outputSchema as checkOutputSchema } from "../schemas/check"; +import { makeErrorEnvelope } from "../types/errors"; async function pathExists(absolutePath: string): Promise { try { @@ -102,104 +99,104 @@ export const checkCommand = defineCommand({ description: "Output as JSON", default: false, }, - schema: { + anonymous: { type: "boolean", - description: "Print input/output/error JSON Schemas and exit", + description: "Accepted for compatibility; check has no auth dependency", default: false, }, }, async run({ args, rawArgs }) { - // --schema short-circuits: print schemas and exit - if (args.schema) { - printSchema({ - output: checkOutputSchema, - error: checkErrorSchema, - }); - return; - } - const cwd = resolve(args.dir ?? process.cwd()); const telemetry = await getTelemetry(cwd); + const startedAt = Date.now(); telemetry.capture("cli_check"); - const positionalPaths = extractPositionalPaths(rawArgs); - const hadExplicitPaths = positionalPaths.length > 0; - const existingPaths = hadExplicitPaths - ? await filterExistingPaths(cwd, positionalPaths) - : []; + let success = false; + try { + const positionalPaths = extractPositionalPaths(rawArgs); + const hadExplicitPaths = positionalPaths.length > 0; + const existingPaths = hadExplicitPaths + ? await filterExistingPaths(cwd, positionalPaths) + : []; - // If the user passed paths but none exist (e.g. all-deleted diff), - // exit cleanly with empty results rather than falling back to a full scan. - if (hadExplicitPaths && existingPaths.length === 0) { - if (args.json) { - console.log( - JSON.stringify( - checkOutputSchema.parse({ success: true, results: [] }) - ) - ); + // If the user passed paths but none exist (e.g. all-deleted diff), + // exit cleanly with empty results rather than falling back to a full scan. + if (hadExplicitPaths && existingPaths.length === 0) { + if (args.json) { + console.log( + JSON.stringify( + checkOutputSchema.parse({ success: true, results: [] }) + ) + ); + } + success = true; + return; } - return; - } - // Check for rule files - const rulesDirectory = join(cwd, ".taskless", "rules"); - let ruleFiles: string[] = []; - try { - const entries = await readdir(rulesDirectory); - ruleFiles = entries.filter((f) => f.endsWith(".yml")); - } catch { - // .taskless/ or rules/ directory doesn't exist - } + // Check for rule files + const rulesDirectory = join(cwd, ".taskless", "rules"); + let ruleFiles: string[] = []; + try { + const entries = await readdir(rulesDirectory); + ruleFiles = entries.filter((f) => f.endsWith(".yml")); + } catch { + // .taskless/ or rules/ directory doesn't exist + } - if (ruleFiles.length === 0) { - if (args.json) { - console.log( - JSON.stringify( - checkOutputSchema.parse({ success: true, results: [] }) - ) - ); - } else { - console.log( - "No rules configured. Create one with `taskless rules create`." - ); + if (ruleFiles.length === 0) { + if (args.json) { + console.log( + JSON.stringify( + checkOutputSchema.parse({ success: true, results: [] }) + ) + ); + } else { + console.log( + "No rules configured. Create one with `taskless rule create`." + ); + } + success = true; + return; } - return; - } - // Generate ephemeral sgconfig.yml and run scanner - try { - await generateSgConfig(cwd); - const { results } = await runAstGrepScan(cwd, existingPaths); - const hasErrors = results.some((r) => r.severity === "error"); + // Generate ephemeral sgconfig.yml and run scanner + try { + await generateSgConfig(cwd); + const { results } = await runAstGrepScan(cwd, existingPaths); + const hasErrors = results.some((r) => r.severity === "error"); - // Format output - if (args.json) { - const output = checkOutputSchema.parse({ - success: !hasErrors, - results, - }); - console.log(JSON.stringify(output)); - } else { - console.log(formatText(results)); - } + // Format output + if (args.json) { + const output = checkOutputSchema.parse({ + success: !hasErrors, + results, + }); + console.log(JSON.stringify(output)); + } else { + console.log(formatText(results)); + } - // Exit code: 1 if any errors, 0 otherwise - if (hasErrors) { + // Exit code: 1 if any errors, 0 otherwise + if (hasErrors) { + process.exitCode = 1; + } + success = !hasErrors; + } catch (error) { + const message = `Error: ${error instanceof Error ? error.message : String(error)}`; + if (args.json) { + console.log( + JSON.stringify(makeErrorEnvelope("SCAN_FAILED", message)) + ); + } else { + console.error(message); + } process.exitCode = 1; } - } catch (error) { - const message = `Error: ${error instanceof Error ? error.message : String(error)}`; - if (args.json) { - const output = checkErrorSchema.parse({ - success: false, - error: message, - results: [], - }); - console.log(JSON.stringify(output)); - } else { - console.error(message); - } - process.exitCode = 1; + } finally { + telemetry.capture("cli_check_completed", { + success, + durationMs: Date.now() - startedAt, + }); } }, }); diff --git a/packages/cli/src/commands/help.ts b/packages/cli/src/commands/help.ts index 2b2f189c..4d55e00c 100644 --- a/packages/cli/src/commands/help.ts +++ b/packages/cli/src/commands/help.ts @@ -6,32 +6,69 @@ import { type Resolvable, type SubCommandsDef, } from "citty"; +import { z } from "zod"; import { getTelemetry } from "../telemetry"; +import { inputSchema as ruleCreateInputSchema } from "../schemas/rules-create"; +import { inputSchema as ruleImproveInputSchema } from "../schemas/rules-improve"; -// Help text files embedded at build time via Vite import.meta.glob +// Help text files embedded at build time via Vite import.meta.glob. +// Filename convention: .txt for the canonical recipe and +// .anonymous.txt for the local-only variant (when the flow +// genuinely differs). const helpFiles: Record = import.meta.glob("../help/*.txt", { query: "?raw", import: "default", eager: true, }); -// Build a lookup map: "check" → content, "auth-login" → content, etc. -function buildHelpMap(): Map { - const map = new Map(); +// Build two lookup maps: +// - helpMap: "rule-create" → canonical recipe text +// - anonymousMap: "rule-create" → anonymous variant text (if exists) +function buildHelpMaps(): { + helpMap: Map; + anonymousMap: Map; +} { + const helpMap = new Map(); + const anonymousMap = new Map(); for (const [path, content] of Object.entries(helpFiles)) { const filename = path .split("/") .pop() ?.replace(/\.txt$/, ""); - if (filename) { - map.set(filename, content); + if (!filename) continue; + if (filename.endsWith(".anonymous")) { + const topic = filename.slice(0, -".anonymous".length); + anonymousMap.set(topic, content); + } else { + helpMap.set(filename, content); } } - return map; + return { helpMap, anonymousMap }; } -const helpMap = buildHelpMap(); +const { helpMap, anonymousMap } = buildHelpMaps(); + +// Topic → Zod input schema. When a recipe contains the {{INPUT_SCHEMA}} +// placeholder, the help command substitutes the JSON Schema rendered +// from this Zod source. +const TOPIC_INPUT_SCHEMAS: Record = { + "rule-create": ruleCreateInputSchema, + "rule-improve": ruleImproveInputSchema, +}; + +function renderRecipe(content: string, topic: string): string { + let out = content; + out = out.replaceAll("{{CLI_VERSION}}", __VERSION__); + if (out.includes("{{INPUT_SCHEMA}}")) { + const schema = TOPIC_INPUT_SCHEMAS[topic]; + const rendered = schema + ? JSON.stringify(z.toJSONSchema(schema), null, 2) + : "(no input schema for this topic)"; + out = out.replaceAll("{{INPUT_SCHEMA}}", rendered); + } + return out; +} async function unwrap(resolvable: Resolvable): Promise { if (typeof resolvable === "function") { @@ -61,10 +98,16 @@ export function createHelpCommand(subCommands: SubCommandsDef) { description: "Working directory", default: process.cwd(), }, + anonymous: { + type: "boolean", + description: + "Return the local-only recipe variant when the topic has one", + default: false, + }, }, async run({ args, rawArgs }) { // Extract positional args from rawArgs, skipping flags and their values. - // --dir/-d take a value; --json/--schema are boolean and do not. + // --dir/-d take a value; --json/--anonymous are boolean and do not. const valueFlagSet = new Set(["--dir", "-d"]); const positionals: string[] = []; for (let index = 0; index < rawArgs.length; index++) { @@ -78,25 +121,20 @@ export function createHelpCommand(subCommands: SubCommandsDef) { const cwd = resolve(args.dir); const telemetry = await getTelemetry(cwd); - if (positionals.length === 0) { - telemetry.capture("cli_help"); - } else { - const commandEvents: Record = { - auth: "cli_help_auth", - check: "cli_help_check", - info: "cli_help_info", - init: "cli_help_init", - rules: "cli_help_rule", - }; - const first = positionals[0] as string; - const event = commandEvents[first] ?? "cli_help"; - telemetry.capture(event, { topic: positionals.join(" ") }); - } if (positionals.length === 0) { - // No args: show command index + // help_index: agent fetched the topic list + telemetry.capture("help_index"); + console.log("Taskless CLI\n"); - console.log("Commands:"); + console.log( + "For agents: this command returns recipes for an AI coding agent to follow." + ); + console.log( + "For humans: run `npx @taskless/cli` (no args) to install or update Taskless," + ); + console.log("then ask your coding agent to do the work.\n"); + console.log("Topics:"); const entries: Array<[string, string]> = []; for (const [name, cmd] of Object.entries(subCommands)) { @@ -111,18 +149,35 @@ export function createHelpCommand(subCommands: SubCommandsDef) { } console.log( - "\nRun `taskless help ` for details on a specific command." + "\nAppend `--anonymous` to any rule/check command to skip the Taskless API" + ); + console.log("and use local-only behavior."); + console.log( + "\nRun `taskless help ` for the full recipe (e.g. `taskless help rule create`)." ); return; } // Join positional args to form the lookup key const key = positionals.join("-"); - const content = helpMap.get(key); + + // Anonymous variant lookup: prefer .anonymous.txt when + // --anonymous is set, fall back to the canonical recipe. + const content = args.anonymous + ? (anonymousMap.get(key) ?? helpMap.get(key)) + : helpMap.get(key); if (content) { - console.log(content.trimEnd()); + // help_: agent fetched a specific recipe (intent signal) + const topicEvent = `help_${key.replaceAll("-", "_")}`; + telemetry.capture(topicEvent, { + topic: positionals.join(" "), + anonymous: args.anonymous, + }); + console.log(renderRecipe(content, key).trimEnd()); } else { + // help_unknown: agent asked for a topic that does not exist + telemetry.capture("help_unknown", { topic: positionals.join(" ") }); console.error(`Unknown command: ${positionals.join(" ")}`); console.error("Run `taskless help` for available commands."); process.exitCode = 1; diff --git a/packages/cli/src/commands/info.ts b/packages/cli/src/commands/info.ts index 3d73cbd8..5ebfafb7 100644 --- a/packages/cli/src/commands/info.ts +++ b/packages/cli/src/commands/info.ts @@ -4,12 +4,9 @@ import { defineCommand } from "citty"; import { checkStaleness } from "../install/install"; import { getToken } from "../auth/token"; import { fetchWhoami } from "../auth/whoami"; -import { printSchema } from "../util/schema-output"; -import { - outputSchema as infoOutputSchema, - errorSchema as infoErrorSchema, -} from "../schemas/info"; +import { outputSchema as infoOutputSchema } from "../schemas/info"; import { getTelemetry } from "../telemetry"; +import { makeErrorEnvelope } from "../types/errors"; export const infoCommand = defineCommand({ meta: { @@ -27,103 +24,108 @@ export const infoCommand = defineCommand({ description: "Output as JSON", default: false, }, - schema: { + anonymous: { type: "boolean", - description: "Print output/error JSON Schemas and exit", + description: "Skip the API/auth probe and report local state only", default: false, }, }, async run({ args }) { - if (args.schema) { - printSchema({ - output: infoOutputSchema, - error: infoErrorSchema, - }); - return; - } - const cwd = resolve(args.dir ?? process.cwd()); const telemetry = await getTelemetry(cwd); + const startedAt = Date.now(); telemetry.capture("cli_info"); - const [tools, token] = await Promise.all([ - checkStaleness(cwd), - getToken(cwd), - ]); + let success = false; + try { + const [tools, token] = await Promise.all([ + checkStaleness(cwd), + args.anonymous ? Promise.resolve() : getToken(cwd), + ]); - let auth: { user: string; email: string; orgs: string[] } | undefined; - if (token) { - const whoami = await fetchWhoami(token); - if (whoami) { - auth = { - user: whoami.user, - email: whoami.email, - orgs: whoami.orgs.map((o) => o.name), - }; + let auth: { user: string; email: string; orgs: string[] } | undefined; + if (!args.anonymous && token) { + const whoami = await fetchWhoami(token); + if (whoami) { + auth = { + user: whoami.user, + email: whoami.email, + orgs: whoami.orgs.map((o) => o.name), + }; + } } - } - const result = { - success: true as const, - version: __VERSION__, - tools, - loggedIn: token !== undefined, - auth, - }; + const result = { + success: true as const, + version: __VERSION__, + tools, + loggedIn: token !== undefined, + auth, + }; - if (args.json) { - const parsed = infoOutputSchema.safeParse(result); - if (!parsed.success) { - console.error( - JSON.stringify({ - success: false, - error: "Internal schema validation failed", - }) - ); - process.exitCode = 1; + if (args.json) { + const parsed = infoOutputSchema.safeParse(result); + if (!parsed.success) { + console.log( + JSON.stringify( + makeErrorEnvelope( + "INTERNAL_ERROR", + "Internal schema validation failed" + ) + ) + ); + process.exitCode = 1; + return; + } + console.log(JSON.stringify(parsed.data)); + success = true; return; } - console.log(JSON.stringify(parsed.data)); - return; - } - // Human-readable output - console.log(`Taskless CLI v${__VERSION__}\n`); + // Human-readable output + console.log(`Taskless CLI v${__VERSION__}\n`); - if (tools.length === 0) { - console.log("Tools: none detected"); - } else { - console.log("Tools:"); - for (const tool of tools) { - const total = tool.skills.length; - const upToDate = tool.skills.filter((s) => s.current).length; - const stale = total - upToDate; + if (tools.length === 0) { + console.log("Tools: none detected"); + } else { + console.log("Tools:"); + for (const tool of tools) { + const total = tool.skills.length; + const upToDate = tool.skills.filter((s) => s.current).length; + const stale = total - upToDate; - if (stale === 0) { - console.log( - ` ${tool.name}: ${String(total)} skills (all up to date)` - ); - } else { - console.log( - ` ${tool.name}: ${String(total)} skills (${String(stale)} outdated)` - ); - for (const skill of tool.skills) { - if (!skill.current) { - console.log( - ` - ${skill.name}: ${skill.installedVersion ?? "missing"} → ${skill.currentVersion}` - ); + if (stale === 0) { + console.log( + ` ${tool.name}: ${String(total)} skills (all up to date)` + ); + } else { + console.log( + ` ${tool.name}: ${String(total)} skills (${String(stale)} outdated)` + ); + for (const skill of tool.skills) { + if (!skill.current) { + console.log( + ` - ${skill.name}: ${skill.installedVersion ?? "missing"} → ${skill.currentVersion}` + ); + } } } } } - } - console.log(""); - if (auth) { - const orgs = auth.orgs.length > 0 ? ` (${auth.orgs.join(", ")})` : ""; - console.log(`Auth: logged in as ${auth.user}${orgs}`); - } else { - console.log("Auth: not logged in"); + console.log(""); + if (auth) { + const orgs = auth.orgs.length > 0 ? ` (${auth.orgs.join(", ")})` : ""; + console.log(`Auth: logged in as ${auth.user}${orgs}`); + } else { + console.log("Auth: not logged in"); + } + success = true; + } finally { + telemetry.capture("cli_info_completed", { + success, + durationMs: Date.now() - startedAt, + }); } }, }); diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 7e63b128..28ff04dd 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -41,6 +41,11 @@ export const initCommand = defineCommand({ "Install every mandatory skill to every detected tool without prompting", default: false, }, + anonymous: { + type: "boolean", + description: "Accepted for compatibility; init has no auth dependency", + default: false, + }, }, async run({ args }) { const cwd = resolve(args.dir ?? process.cwd()); @@ -76,6 +81,44 @@ export const initCommand = defineCommand({ }, }); +export const updateCommand = defineCommand({ + meta: { + name: "update", + description: + "Update Taskless skills in detected tools (non-interactive install)", + }, + args: { + dir: { + type: "string", + alias: "d", + description: "Working directory", + }, + anonymous: { + type: "boolean", + description: "Accepted for compatibility; update has no auth dependency", + default: false, + }, + }, + async run({ args }) { + const cwd = resolve(args.dir ?? process.cwd()); + const telemetry = await getTelemetry(cwd); + const startedAt = Date.now(); + telemetry.capture("cli_update"); + + let success = false; + try { + await runNonInteractive(cwd); + success = true; + } finally { + telemetry.capture("cli_update_completed", { + locations: await detectedLocationDirectories(cwd), + success, + durationMs: Date.now() - startedAt, + }); + } + }, +}); + async function runNonInteractive(cwd: string): Promise { await ensureTasklessDirectory(cwd); @@ -129,10 +172,24 @@ async function runNonInteractive(cwd: string): Promise { value: entry.command, })) ); + const removedSkillsByTarget = groupValuesByTarget( + result.removedSkills.map((entry) => ({ + target: entry.target, + value: entry.skill, + })) + ); + const removedCommandsByTarget = groupValuesByTarget( + result.removedCommands.map((entry) => ({ + target: entry.target, + value: entry.command, + })) + ); for (const { tool } of planTargets) { const targetSkills = skillsByTarget.get(tool.installDir) ?? []; const targetCommands = commandsByTarget.get(tool.installDir) ?? []; + const removedSkills = removedSkillsByTarget.get(tool.installDir) ?? []; + const removedCommands = removedCommandsByTarget.get(tool.installDir) ?? []; console.log( `${tool.name}: installed ${String(targetSkills.length)} skill(s)` ); @@ -144,6 +201,22 @@ async function runNonInteractive(cwd: string): Promise { ` + ${String(targetCommands.length)} command(s) in ${tool.installDir}/${tool.commands.path}/` ); } + if (removedSkills.length > 0) { + console.log( + ` removed ${String(removedSkills.length)} obsolete skill(s):` + ); + for (const name of removedSkills) { + console.log(` - ${name}`); + } + } + if (removedCommands.length > 0) { + console.log( + ` removed ${String(removedCommands.length)} obsolete command(s):` + ); + for (const name of removedCommands) { + console.log(` - ${name}`); + } + } } } diff --git a/packages/cli/src/commands/rules.ts b/packages/cli/src/commands/rules.ts index 0b088b16..a9d65065 100644 --- a/packages/cli/src/commands/rules.ts +++ b/packages/cli/src/commands/rules.ts @@ -5,7 +5,7 @@ import { defineCommand } from "citty"; import { ZodError } from "zod"; import { resolveIdentity } from "../auth/identity"; -import { verifyRule, getSchemaPayload } from "../rules/verify"; +import { verifyRule } from "../rules/verify"; import { submitRule, pollRuleStatus, iterateRule } from "../api/rules"; import { writeRuleFile, @@ -14,28 +14,19 @@ import { readRuleMetaFile, deleteRuleFiles, } from "../rules/files"; -import { printSchema } from "../util/schema-output"; import { inputSchema as createInputSchema, outputSchema as createOutputSchema, - errorSchema as createErrorSchema, } from "../schemas/rules-create"; import { inputSchema as improveInputSchema, outputSchema as improveOutputSchema, - errorSchema as improveErrorSchema, } from "../schemas/rules-improve"; -import { - outputSchema as metaOutputSchema, - errorSchema as metaErrorSchema, -} from "../schemas/rules-meta"; -import { - schemaOutputSchema as verifySchemaOutputSchema, - verifyOutputSchema, - verifyErrorSchema, -} from "../schemas/rules-verify"; +import { outputSchema as metaOutputSchema } from "../schemas/rules-meta"; +import { verifyOutputSchema } from "../schemas/rules-verify"; import { getTelemetry } from "../telemetry"; import { CliError } from "../util/cli-error"; +import { type CliErrorCode, makeErrorEnvelope } from "../types/errors"; /** Format today's date as YYYYMMDD */ function getTimestamp(): string { @@ -65,38 +56,31 @@ const createCommand = defineCommand({ description: "Output as JSON", default: false, }, - schema: { - type: "boolean", - description: "Print input/output/error JSON Schemas and exit", - default: false, - }, from: { type: "string", description: "Path to a JSON file containing the rule request (required). Example: --from .taskless/.tmp-rule-request.json", }, + anonymous: { + type: "boolean", + description: + "Direct the agent to use the local-only recipe (no API call)", + default: false, + }, }, async run({ args }) { - // --schema short-circuits: print schemas and exit - if (args.schema) { - printSchema({ - input: createInputSchema, - output: createOutputSchema, - error: createErrorSchema, - }); - return; - } - const cwd = resolve(args.dir ?? process.cwd()); const telemetry = await getTelemetry(cwd); + const startedAt = Date.now(); telemetry.capture("cli_rule_create"); /** Emit an error and exit, respecting --json mode */ - function fail(message: string): never { + function fail( + message: string, + code: CliErrorCode = "INTERNAL_ERROR" + ): never { if (args.json) { - console.log( - JSON.stringify(createErrorSchema.parse({ error: message })) - ); + console.log(JSON.stringify(makeErrorEnvelope(code, message))); } else { console.error(`Error: ${message}`); } @@ -104,147 +88,195 @@ const createCommand = defineCommand({ throw new CliError(message); } - // 1. Read and validate --from file - if (!args.from) { - fail( - "--from is required. Provide a path to a JSON file.\n Example: taskless rules create --from request.json" - ); - } - - const filePath = resolve(cwd, args.from); - let fileContent: string; - try { - fileContent = await readFile(filePath, "utf8"); - } catch { - fail(`Could not read file "${args.from}".`); - } - - let rawJson: unknown; - try { - rawJson = JSON.parse(fileContent) as unknown; - } catch { - fail(`"${args.from}" is not valid JSON.`); + if (args.anonymous) { + // Anonymous rule creation runs in the agent, not the CLI. Point the + // agent at the local-only recipe and exit cleanly. + const message = + "Anonymous rule generation runs in the agent. Run `taskless help rule create --anonymous` to fetch the local-only recipe."; + if (args.json) { + console.log( + JSON.stringify(makeErrorEnvelope("INVALID_INPUT", message)) + ); + } else { + console.error(message); + } + process.exitCode = 1; + telemetry.capture("cli_rule_create_completed", { + success: false, + durationMs: Date.now() - startedAt, + }); + return; } - let request: ReturnType; + let success = false; try { - request = createInputSchema.parse(rawJson); - } catch (error) { - if (error instanceof ZodError) { + // 1. Read and validate --from file + if (!args.from) { fail( - `Invalid input: ${error.issues.map((issue) => issue.message).join(", ")}` + "--from is required. Provide a path to a JSON file.\n Example: taskless rule create --from request.json", + "INVALID_INPUT" ); } - fail(error instanceof Error ? error.message : String(error)); - } - // 2. Resolve identity (orgId from JWT, repositoryUrl from git remote) - let identity; - try { - identity = await resolveIdentity(cwd); - } catch (error) { - fail(error instanceof Error ? error.message : String(error)); - } + const filePath = resolve(cwd, args.from); + let fileContent: string; + try { + fileContent = await readFile(filePath, "utf8"); + } catch { + fail(`Could not read file "${args.from}".`, "INVALID_INPUT"); + } - // 3. Submit rule to API - let ruleId: string; - try { - const response = await submitRule(identity.token, { - orgId: identity.orgId, - repositoryUrl: identity.repositoryUrl, - prompt: request.prompt, - successCases: request.successCases, - failureCases: request.failureCases, - }); - ruleId = response.ruleId; - } catch (error) { - fail(error instanceof Error ? error.message : String(error)); - } + let rawJson: unknown; + try { + rawJson = JSON.parse(fileContent) as unknown; + } catch { + fail(`"${args.from}" is not valid JSON.`, "INVALID_INPUT"); + } - // 4. Poll for results - console.error(`Rule submitted (${ruleId}). Waiting for generation...`); + let request: ReturnType; + try { + request = createInputSchema.parse(rawJson); + } catch (error) { + if (error instanceof ZodError) { + fail( + `Invalid input: ${error.issues.map((issue) => issue.message).join(", ")}`, + "INVALID_INPUT" + ); + } + fail( + error instanceof Error ? error.message : String(error), + "INVALID_INPUT" + ); + } - while (true) { - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)); + // 2. Resolve identity (orgId from JWT, repositoryUrl from git remote) + let identity; + try { + identity = await resolveIdentity(cwd); + } catch (error) { + // resolveIdentity throws on missing auth or missing git remote; + // surface the original message but pick a best-guess code. + const message = error instanceof Error ? error.message : String(error); + const code: CliErrorCode = /git remote|origin/i.test(message) + ? "NO_GITHUB_REMOTE" + : "AUTH_REQUIRED"; + fail(message, code); + } - let status; + // 3. Submit rule to API + let ruleId: string; try { - status = await pollRuleStatus(identity.token, ruleId); + const response = await submitRule(identity.token, { + orgId: identity.orgId, + repositoryUrl: identity.repositoryUrl, + prompt: request.prompt, + successCases: request.successCases, + failureCases: request.failureCases, + }); + ruleId = response.ruleId; } catch (error) { fail( - `Polling failed: ${error instanceof Error ? error.message : String(error)}` + error instanceof Error ? error.message : String(error), + "NETWORK_ERROR" ); } - switch (status.status) { - case "accepted": { - console.error("Status: accepted — waiting for processing..."); - break; - } - case "building": { - console.error("Status: building — generating rules..."); - break; - } - case "failed": { - fail(`Rule generation failed: ${status.error}`); - break; + // 4. Poll for results + console.error(`Rule submitted (${ruleId}). Waiting for generation...`); + + while (true) { + await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)); + + let status; + try { + status = await pollRuleStatus(identity.token, ruleId); + } catch (error) { + fail( + `Polling failed: ${error instanceof Error ? error.message : String(error)}`, + "NETWORK_ERROR" + ); } - case "generated": { - // 6. Write files - const timestamp = getTimestamp(); - const writtenFiles: string[] = []; - const rules = status.rules ?? []; - - for (const rule of rules) { - const ruleFile = await writeRuleFile(cwd, rule); - writtenFiles.push(ruleFile); - - if (rule.tests) { - const testFile = await writeRuleTestFile(cwd, rule, timestamp); - writtenFiles.push(testFile); - } - } - if (status.meta) { - const metaFiles = await writeRuleMetaFiles(cwd, status.meta); - writtenFiles.push(...metaFiles); + switch (status.status) { + case "accepted": { + console.error("Status: accepted — waiting for processing..."); + break; + } + case "building": { + console.error("Status: building — generating rules..."); + break; + } + case "failed": { + fail( + `Rule generation failed: ${status.error}`, + "RULE_GENERATION_FAILED" + ); + break; } + case "generated": { + // 6. Write files + const timestamp = getTimestamp(); + const writtenFiles: string[] = []; + const rules = status.rules ?? []; + + for (const rule of rules) { + const ruleFile = await writeRuleFile(cwd, rule); + writtenFiles.push(ruleFile); + + if (rule.tests) { + const testFile = await writeRuleTestFile(cwd, rule, timestamp); + writtenFiles.push(testFile); + } + } + + if (status.meta) { + const metaFiles = await writeRuleMetaFiles(cwd, status.meta); + writtenFiles.push(...metaFiles); + } - // 7. Output results - if (args.json) { - const output = createOutputSchema.parse({ - success: true, - ruleId, - rules: rules.map((r) => r.id), - files: writtenFiles, - }); - console.log(JSON.stringify(output)); - } else { - console.log(`Generated ${String(rules.length)} rule(s):\n`); - for (const filePath of writtenFiles) { - console.log(` ${filePath}`); + // 7. Output results + if (args.json) { + const output = createOutputSchema.parse({ + success: true, + ruleId, + rules: rules.map((r) => r.id), + files: writtenFiles, + }); + console.log(JSON.stringify(output)); + } else { + console.log(`Generated ${String(rules.length)} rule(s):\n`); + for (const filePath of writtenFiles) { + console.log(` ${filePath}`); + } } + success = true; + return; } - return; - } - case "pr": - case "merged": - case "closed": { - // Terminal states beyond generation — treat as done without files - if (args.json) { - const output = createOutputSchema.parse({ - success: true, - ruleId, - rules: [], - files: [], - }); - console.log(JSON.stringify(output)); - } else { - console.log(`Rule ${ruleId} is in state "${status.status}".`); + case "pr": + case "merged": + case "closed": { + // Terminal states beyond generation — treat as done without files + if (args.json) { + const output = createOutputSchema.parse({ + success: true, + ruleId, + rules: [], + files: [], + }); + console.log(JSON.stringify(output)); + } else { + console.log(`Rule ${ruleId} is in state "${status.status}".`); + } + success = true; + return; } - return; } } + } finally { + telemetry.capture("cli_rule_create_completed", { + success, + durationMs: Date.now() - startedAt, + }); } }, }); @@ -266,38 +298,31 @@ const improveCommand = defineCommand({ description: "Output as JSON", default: false, }, - schema: { - type: "boolean", - description: "Print input/output/error JSON Schemas and exit", - default: false, - }, from: { type: "string", description: "Path to a JSON file containing { ruleId, guidance, references? }. Example: --from .taskless/.tmp-iterate-request.json", }, + anonymous: { + type: "boolean", + description: + "Direct the agent to use the local-only recipe (no API call)", + default: false, + }, }, async run({ args }) { - // --schema short-circuits: print schemas and exit - if (args.schema) { - printSchema({ - input: improveInputSchema, - output: improveOutputSchema, - error: improveErrorSchema, - }); - return; - } - const cwd = resolve(args.dir ?? process.cwd()); const telemetry = await getTelemetry(cwd); + const startedAt = Date.now(); telemetry.capture("cli_rule_improve"); /** Emit an error and exit, respecting --json mode */ - function fail(message: string): never { + function fail( + message: string, + code: CliErrorCode = "INTERNAL_ERROR" + ): never { if (args.json) { - console.log( - JSON.stringify(improveErrorSchema.parse({ error: message })) - ); + console.log(JSON.stringify(makeErrorEnvelope(code, message))); } else { console.error(`Error: ${message}`); } @@ -305,146 +330,192 @@ const improveCommand = defineCommand({ throw new CliError(message); } - // 1. Read and validate --from file - if (!args.from) { - fail( - "--from is required. Provide a path to a JSON file.\n Example: taskless rules improve --from request.json" - ); - } - - const filePath = resolve(cwd, args.from); - let fileContent: string; - try { - fileContent = await readFile(filePath, "utf8"); - } catch { - fail(`Could not read file "${args.from}".`); - } - - let rawJson: unknown; - try { - rawJson = JSON.parse(fileContent) as unknown; - } catch { - fail(`"${args.from}" is not valid JSON.`); + if (args.anonymous) { + const message = + "Anonymous rule improvement runs in the agent. Run `taskless help rule improve --anonymous` to fetch the local-only recipe."; + if (args.json) { + console.log( + JSON.stringify(makeErrorEnvelope("INVALID_INPUT", message)) + ); + } else { + console.error(message); + } + process.exitCode = 1; + telemetry.capture("cli_rule_improve_completed", { + success: false, + durationMs: Date.now() - startedAt, + }); + return; } - let request: ReturnType; + let success = false; try { - request = improveInputSchema.parse(rawJson); - } catch (error) { - if (error instanceof ZodError) { + // 1. Read and validate --from file + if (!args.from) { fail( - `Invalid input: ${error.issues.map((issue) => issue.message).join(", ")}` + "--from is required. Provide a path to a JSON file.\n Example: taskless rule improve --from request.json", + "INVALID_INPUT" ); } - fail(error instanceof Error ? error.message : String(error)); - } - // 2. Resolve identity (orgId from JWT, repositoryUrl from git remote) - let identity; - try { - identity = await resolveIdentity(cwd); - } catch (error) { - fail(error instanceof Error ? error.message : String(error)); - } + const filePath = resolve(cwd, args.from); + let fileContent: string; + try { + fileContent = await readFile(filePath, "utf8"); + } catch { + fail(`Could not read file "${args.from}".`, "INVALID_INPUT"); + } - // 3. Submit iterate request to API - let requestId: string; - try { - const response = await iterateRule(identity.token, request.ruleId, { - orgId: identity.orgId, - guidance: request.guidance, - references: request.references, - }); - requestId = response.requestId; - } catch (error) { - fail(error instanceof Error ? error.message : String(error)); - } + let rawJson: unknown; + try { + rawJson = JSON.parse(fileContent) as unknown; + } catch { + fail(`"${args.from}" is not valid JSON.`, "INVALID_INPUT"); + } - // 4. Poll for results using the requestId - console.error( - `Iterate request submitted (${requestId}). Waiting for generation...` - ); + let request: ReturnType; + try { + request = improveInputSchema.parse(rawJson); + } catch (error) { + if (error instanceof ZodError) { + fail( + `Invalid input: ${error.issues.map((issue) => issue.message).join(", ")}`, + "INVALID_INPUT" + ); + } + fail( + error instanceof Error ? error.message : String(error), + "INVALID_INPUT" + ); + } - while (true) { - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)); + // 2. Resolve identity (orgId from JWT, repositoryUrl from git remote) + let identity; + try { + identity = await resolveIdentity(cwd); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + const code: CliErrorCode = /git remote|origin/i.test(message) + ? "NO_GITHUB_REMOTE" + : "AUTH_REQUIRED"; + fail(message, code); + } - let status; + // 3. Submit iterate request to API + let requestId: string; try { - status = await pollRuleStatus(identity.token, requestId); + const response = await iterateRule(identity.token, request.ruleId, { + orgId: identity.orgId, + guidance: request.guidance, + references: request.references, + }); + requestId = response.requestId; } catch (error) { fail( - `Polling failed: ${error instanceof Error ? error.message : String(error)}` + error instanceof Error ? error.message : String(error), + "NETWORK_ERROR" ); } - switch (status.status) { - case "accepted": { - console.error("Status: accepted — waiting for processing..."); - break; - } - case "building": { - console.error("Status: building — generating rules..."); - break; - } - case "failed": { - fail(`Rule iteration failed: ${status.error}`); - break; + // 4. Poll for results using the requestId + console.error( + `Iterate request submitted (${requestId}). Waiting for generation...` + ); + + while (true) { + await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)); + + let status; + try { + status = await pollRuleStatus(identity.token, requestId); + } catch (error) { + fail( + `Polling failed: ${error instanceof Error ? error.message : String(error)}`, + "NETWORK_ERROR" + ); } - case "generated": { - // 6. Write files (overwrites existing rule files) - const timestamp = getTimestamp(); - const writtenFiles: string[] = []; - const rules = status.rules ?? []; - - for (const rule of rules) { - const ruleFile = await writeRuleFile(cwd, rule); - writtenFiles.push(ruleFile); - - if (rule.tests) { - const testFile = await writeRuleTestFile(cwd, rule, timestamp); - writtenFiles.push(testFile); - } - } - if (status.meta) { - const metaFiles = await writeRuleMetaFiles(cwd, status.meta); - writtenFiles.push(...metaFiles); + switch (status.status) { + case "accepted": { + console.error("Status: accepted — waiting for processing..."); + break; + } + case "building": { + console.error("Status: building — generating rules..."); + break; + } + case "failed": { + fail( + `Rule iteration failed: ${status.error}`, + "RULE_GENERATION_FAILED" + ); + break; } + case "generated": { + // 6. Write files (overwrites existing rule files) + const timestamp = getTimestamp(); + const writtenFiles: string[] = []; + const rules = status.rules ?? []; + + for (const rule of rules) { + const ruleFile = await writeRuleFile(cwd, rule); + writtenFiles.push(ruleFile); + + if (rule.tests) { + const testFile = await writeRuleTestFile(cwd, rule, timestamp); + writtenFiles.push(testFile); + } + } + + if (status.meta) { + const metaFiles = await writeRuleMetaFiles(cwd, status.meta); + writtenFiles.push(...metaFiles); + } - // 7. Output results - if (args.json) { - const output = improveOutputSchema.parse({ - success: true, - requestId, - rules: rules.map((r) => r.id), - files: writtenFiles, - }); - console.log(JSON.stringify(output)); - } else { - console.log(`Updated ${String(rules.length)} rule(s):\n`); - for (const filePath of writtenFiles) { - console.log(` ${filePath}`); + // 7. Output results + if (args.json) { + const output = improveOutputSchema.parse({ + success: true, + requestId, + rules: rules.map((r) => r.id), + files: writtenFiles, + }); + console.log(JSON.stringify(output)); + } else { + console.log(`Updated ${String(rules.length)} rule(s):\n`); + for (const filePath of writtenFiles) { + console.log(` ${filePath}`); + } } + success = true; + return; } - return; - } - case "pr": - case "merged": - case "closed": { - if (args.json) { - const output = improveOutputSchema.parse({ - success: true, - requestId, - rules: [], - files: [], - }); - console.log(JSON.stringify(output)); - } else { - console.log(`Request ${requestId} is in state "${status.status}".`); + case "pr": + case "merged": + case "closed": { + if (args.json) { + const output = improveOutputSchema.parse({ + success: true, + requestId, + rules: [], + files: [], + }); + console.log(JSON.stringify(output)); + } else { + console.log( + `Request ${requestId} is in state "${status.status}".` + ); + } + success = true; + return; } - return; } } + } finally { + telemetry.capture("cli_rule_improve_completed", { + success, + durationMs: Date.now() - startedAt, + }); } }, }); @@ -465,9 +536,9 @@ const metaCommand = defineCommand({ description: "Output as JSON", default: false, }, - schema: { + anonymous: { type: "boolean", - description: "Print output/error JSON Schemas and exit", + description: "Accepted for compatibility; meta is purely local", default: false, }, id: { @@ -477,21 +548,17 @@ const metaCommand = defineCommand({ }, }, async run({ args }) { - if (args.schema) { - printSchema({ - output: metaOutputSchema, - error: metaErrorSchema, - }); - return; - } - const cwd = resolve(args.dir ?? process.cwd()); const telemetry = await getTelemetry(cwd); + const startedAt = Date.now(); telemetry.capture("cli_rule_meta"); - function fail(message: string): never { + function fail( + message: string, + code: CliErrorCode = "INTERNAL_ERROR" + ): never { if (args.json) { - console.log(JSON.stringify(metaErrorSchema.parse({ error: message }))); + console.log(JSON.stringify(makeErrorEnvelope(code, message))); } else { console.error(`Error: ${message}`); } @@ -499,31 +566,42 @@ const metaCommand = defineCommand({ throw new CliError(message); } - const meta = await readRuleMetaFile(cwd, args.id); - if (!meta) { - fail( - `No metadata found for rule "${args.id}". Expected .taskless/rule-metadata/${args.id}.yml` - ); - } + let success = false; + try { + const meta = await readRuleMetaFile(cwd, args.id); + if (!meta) { + fail( + `No metadata found for rule "${args.id}". Expected .taskless/rule-metadata/${args.id}.yml`, + "RULE_NOT_FOUND" + ); + } - if (args.json) { - let output; - try { - output = metaOutputSchema.parse({ id: args.id, ...meta }); - } catch (error) { - if (error instanceof ZodError) { - fail( - `Invalid metadata for rule "${args.id}": ${error.issues.map((issue) => issue.message).join(", ")}` - ); + if (args.json) { + let output; + try { + output = metaOutputSchema.parse({ id: args.id, ...meta }); + } catch (error) { + if (error instanceof ZodError) { + fail( + `Invalid metadata for rule "${args.id}": ${error.issues.map((issue) => issue.message).join(", ")}`, + "INVALID_INPUT" + ); + } + fail(error instanceof Error ? error.message : String(error)); + } + console.log(JSON.stringify(output)); + } else { + console.log(`Metadata for rule "${args.id}":\n`); + for (const [key, value] of Object.entries(meta)) { + console.log(` ${key}: ${String(value)}`); } - fail(error instanceof Error ? error.message : String(error)); - } - console.log(JSON.stringify(output)); - } else { - console.log(`Metadata for rule "${args.id}":\n`); - for (const [key, value] of Object.entries(meta)) { - console.log(` ${key}: ${String(value)}`); } + success = true; + } finally { + telemetry.capture("cli_rule_meta_completed", { + success, + durationMs: Date.now() - startedAt, + }); } }, }); @@ -539,6 +617,17 @@ const deleteCommand = defineCommand({ alias: "d", description: "Working directory", }, + anonymous: { + type: "boolean", + description: "Accepted for compatibility; delete is purely local", + default: false, + }, + json: { + type: "boolean", + description: + "On error, write the standardized { ok:false, code, message } envelope to stdout instead of human text on stderr", + default: false, + }, id: { type: "positional", description: "Rule ID to delete", @@ -548,17 +637,34 @@ const deleteCommand = defineCommand({ async run({ args }) { const cwd = resolve(args.dir ?? process.cwd()); const telemetry = await getTelemetry(cwd); + const startedAt = Date.now(); telemetry.capture("cli_rule_delete"); const id = args.id; - const deleted = await deleteRuleFiles(cwd, id); - if (deleted) { - console.log(`Deleted rule "${id}" and associated test files.`); - } else { - console.error( - `Error: Rule "${id}" not found in .taskless/rules/${id}.yml` - ); - process.exitCode = 1; + let success = false; + try { + const deleted = await deleteRuleFiles(cwd, id); + if (deleted) { + if (!args.json) { + console.log(`Deleted rule "${id}" and associated test files.`); + } + success = true; + } else { + const message = `Rule "${id}" not found in .taskless/rules/${id}.yml`; + if (args.json) { + console.log( + JSON.stringify(makeErrorEnvelope("RULE_NOT_FOUND", message)) + ); + } else { + console.error(`Error: ${message}`); + } + process.exitCode = 1; + } + } finally { + telemetry.capture("cli_rule_delete_completed", { + success, + durationMs: Date.now() - startedAt, + }); } }, }); @@ -579,10 +685,9 @@ const verifyCommand = defineCommand({ description: "Output as JSON", default: false, }, - schema: { + anonymous: { type: "boolean", - description: - "Dump combined ast-grep schema, Taskless requirements, and examples", + description: "Accepted for compatibility; verify is purely local", default: false, }, id: { @@ -592,85 +697,81 @@ const verifyCommand = defineCommand({ }, }, async run({ args }) { - // --schema mode: dump schema payload and exit - if (args.schema) { - const payload = verifySchemaOutputSchema.parse(getSchemaPayload()); - if (args.json) { - console.log(JSON.stringify(payload)); - } else { - console.log(JSON.stringify(payload, null, 2)); - } - return; - } - const cwd = resolve(args.dir ?? process.cwd()); const telemetry = await getTelemetry(cwd); + const startedAt = Date.now(); telemetry.capture("cli_rule_verify"); - if (!args.id) { + let success = false; + try { + if (!args.id) { + if (args.json) { + console.log( + JSON.stringify( + makeErrorEnvelope("INVALID_INPUT", "Rule ID is required.") + ) + ); + } else { + console.error( + "Error: Rule ID is required.\n Usage: taskless rule verify " + ); + } + process.exitCode = 1; + return; + } + + const result = await verifyRule(cwd, args.id); + if (args.json) { - console.log( - JSON.stringify( - verifyErrorSchema.parse({ - success: false, - error: "Rule ID is required.", - }) - ) - ); + console.log(JSON.stringify(verifyOutputSchema.parse(result))); } else { - console.error( - "Error: Rule ID is required.\n Usage: taskless rules verify " - ); - } - process.exitCode = 1; - return; - } + console.log(`Verifying rule: ${result.ruleId}\n`); - const result = await verifyRule(cwd, args.id); + // Layer 1 + console.log( + `Schema: ${result.schema.valid ? "✓ valid" : "✗ invalid"}` + ); + for (const error of result.schema.errors) { + console.log(` - ${error}`); + } - if (args.json) { - console.log(JSON.stringify(verifyOutputSchema.parse(result))); - } else { - console.log(`Verifying rule: ${result.ruleId}\n`); + // Layer 2 + console.log( + `Requirements: ${result.requirements.valid ? "✓ valid" : "✗ invalid"}` + ); + for (const error of result.requirements.errors) { + console.log(` - ${error}`); + } - // Layer 1 - console.log( - `Schema: ${result.schema.valid ? "✓ valid" : "✗ invalid"}` - ); - for (const error of result.schema.errors) { - console.log(` - ${error}`); - } + // Layer 3 + console.log( + `Tests: ${result.tests.valid ? "✓ passed" : "✗ failed"} (${String(result.tests.passed)} passed, ${String(result.tests.failed)} failed)` + ); + for (const error of result.tests.errors) { + console.log(` - ${error}`); + } - // Layer 2 - console.log( - `Requirements: ${result.requirements.valid ? "✓ valid" : "✗ invalid"}` - ); - for (const error of result.requirements.errors) { - console.log(` - ${error}`); + console.log( + `\nResult: ${result.success ? "✓ All checks passed" : "✗ Verification failed"}` + ); } - // Layer 3 - console.log( - `Tests: ${result.tests.valid ? "✓ passed" : "✗ failed"} (${String(result.tests.passed)} passed, ${String(result.tests.failed)} failed)` - ); - for (const error of result.tests.errors) { - console.log(` - ${error}`); + if (!result.success) { + process.exitCode = 1; } - - console.log( - `\nResult: ${result.success ? "✓ All checks passed" : "✗ Verification failed"}` - ); - } - - if (!result.success) { - process.exitCode = 1; + success = result.success; + } finally { + telemetry.capture("cli_rule_verify_completed", { + success, + durationMs: Date.now() - startedAt, + }); } }, }); -export const rulesCommand = defineCommand({ +export const ruleCommand = defineCommand({ meta: { - name: "rules", + name: "rule", description: "Manage Taskless rules", }, subCommands: { diff --git a/packages/cli/src/help/auth-login.txt b/packages/cli/src/help/auth-login.txt deleted file mode 100644 index 60969e86..00000000 --- a/packages/cli/src/help/auth-login.txt +++ /dev/null @@ -1,17 +0,0 @@ -Authenticate with taskless.io - -Starts a device authorization flow. The CLI displays a URL and a one-time -code. Open the URL in a browser, enter the code, and authorize access. -Credentials are saved locally once authorization completes. - -Usage: - taskless auth login - -Credential Storage: - Tokens are saved to ~/.config/taskless/auth.json - -Environment Variable: - Set TASKLESS_TOKEN to skip the device flow entirely. - -Examples: - taskless auth login diff --git a/packages/cli/src/help/auth-logout.txt b/packages/cli/src/help/auth-logout.txt deleted file mode 100644 index c34db930..00000000 --- a/packages/cli/src/help/auth-logout.txt +++ /dev/null @@ -1,13 +0,0 @@ -Remove saved authentication - -Deletes the locally saved authentication token. If using the -TASKLESS_TOKEN environment variable, unset it separately. - -Usage: - taskless auth logout - -Credential Storage: - Removes ~/.config/taskless/auth.json - -Examples: - taskless auth logout diff --git a/packages/cli/src/help/auth.txt b/packages/cli/src/help/auth.txt index 4a957d87..9bb7dce7 100644 --- a/packages/cli/src/help/auth.txt +++ b/packages/cli/src/help/auth.txt @@ -1,7 +1,77 @@ -Manage authentication with taskless.io +# Topic: auth (CLI v{{CLI_VERSION}} / topic v1) -Commands: - login Authenticate via device flow - logout Remove saved credentials +## Goal +Manage Taskless authentication. Three branches: +- **Login** — start the device-code flow and wait for the user to + approve in their browser. +- **Logout** — remove the saved token. +- **Status** — check whether a token is present and whose identity + it represents. -Run `taskless help auth ` for details on a specific command. +## Preconditions +- `.taskless/` directory exists. +- For login: the user has a browser to approve the device code. + +## Steps + +Pick the branch matching the user's intent. + +### Login + +1. Run: + ``` + npx @taskless/cli auth login + ``` +2. The CLI prints a URL and a device code. Tell the user to open the + URL and enter the code. +3. The CLI polls until the token is approved. On success, the token + is written to `.taskless/.env.local.json` and a confirmation is + printed. +4. Report success. Suggest `taskless info` to verify identity. + +The `--anonymous` flag is rejected on `auth login` — it errors with +"auth commands cannot be anonymous". Don't pass it. + +### Logout + +1. Run: + ``` + npx @taskless/cli auth logout + ``` +2. The CLI removes the saved token (or reports "Not logged in" if + none was present). +3. Report the outcome. + +### Status (no subcommand) + +1. Run: + ``` + npx @taskless/cli auth + ``` +2. Output is one of: + - "Not logged in." (with hint to run `auth login`) + - "Logged in as ()." + - "Logged in, but unable to verify identity." (token expired or + revoked — suggest re-login) +3. Report to the user. + +## Errors + +`auth login` and `auth logout` accept `--json`. On error, the +standardized `{ ok: false, code, message }` envelope is written to +stdout (and human text on stderr is suppressed). On success in +`--json` mode, the commands exit 0 silently — no success envelope is +emitted. The status path (`taskless auth` with no subcommand) accepts +`--json` for forward-compat but currently has no error paths to +report. + +| code | meaning | fix | +|------------------|-------------------------------------------------------------------------------------------|------------------------------| +| `INVALID_INPUT` | `--anonymous` passed to `auth login` (rejected: auth commands cannot be anonymous) | Don't pass `--anonymous` | +| `NETWORK_ERROR` | Device flow / token endpoint unreachable, or the device code expired before approval | Check connectivity; retry | +| `AUTH_REQUIRED` | The user denied the authorization request in their browser | Re-run `taskless auth login` | + +## See Also + +- `taskless help info` — see auth state and skill versions +- `taskless help rule create` — first action that requires auth diff --git a/packages/cli/src/help/check.txt b/packages/cli/src/help/check.txt index 053d0fa4..f396ea4d 100644 --- a/packages/cli/src/help/check.txt +++ b/packages/cli/src/help/check.txt @@ -1,39 +1,88 @@ -Run Taskless rules against your codebase - -Scans your project using ast-grep rules defined in .taskless/rules/. By -default, scans the entire project. You can also pass specific files or -directories as positional arguments to scan only those paths — useful for -CI workflows that check only changed files. - -Prerequisites: - Rules must exist in .taskless/rules/. Create one with `taskless rules create`. - -Usage: - taskless check [options] [paths...] - -Options: - -d, --dir Set working directory (default: current directory) - --json Output results as JSON - -Paths: - Optional positional arguments. When zero paths are passed, the entire - project is scanned. When one or more paths are passed, only those files - and directories are scanned. Paths are resolved relative to the working - directory. Paths that do not exist on disk (e.g. files deleted in a diff) - are silently filtered so you can pipe raw git output directly. - -Output: - Text (default): filepath:line:col severity message - JSON (--json): { "success": true, "results": [...] } - -Exit Codes: - 0 All checks passed, no rules found, or all supplied paths missing - 1 Errors detected or configuration issue - -Examples: - taskless check - taskless check -d ./my-project - taskless check --json - taskless check src/foo.ts src/bar.ts - taskless check $(git diff --name-only main...HEAD) - taskless check $(git diff --cached --name-only) +# Topic: check (CLI v{{CLI_VERSION}} / topic v1) + +## Goal +Run all rules in `.taskless/rules/` against the codebase and report +matches. Used standalone (full project scan), in CI (diff-only scan), +or after rule create/improve to validate. + +## Preconditions +- `.taskless/` directory exists. +- At least one rule exists in `.taskless/rules/`. (If none exist, + the CLI exits 0 with a friendly message suggesting + `taskless rule create`.) +- No auth required. + +## Steps + +1. **Decide scope.** Default is a full project scan. If the user + specified files (or you're in a CI context with a known diff), + pass them as positional arguments. + +2. **Invoke the CLI.** Either: + ``` + npx @taskless/cli check --json + ``` + or, scoped to specific paths: + ``` + npx @taskless/cli check --json src/foo.ts src/bar.ts + ``` + or, against a git diff: + ``` + npx @taskless/cli check --json $(git diff --name-only main...HEAD) + ``` + Paths that don't exist on disk are silently filtered, so you can + pipe raw `git diff` output directly without pre-filtering. + +3. **Parse the JSON output.** Shape: + ```json + { + "success": false, + "results": [ + { + "source": "ast-grep", + "ruleId": "no-eval", + "severity": "error", + "message": "Avoid eval()", + "note": null, + "file": "src/foo.ts", + "range": { + "start": { "line": 12, "column": 4 }, + "end": { "line": 12, "column": 14 } + }, + "matchedText": "eval(input)", + "fix": null + } + ] + } + ``` + +4. **Report findings to the user.** Group by `file`. Show `severity`, + `message`, and `ruleId` for each finding; the `range.start` is the + useful line/column to surface. The `success` field reflects only + error-severity findings: `success: false` means at least one + `severity: "error"` finding exists (exit code 1); `success: true` + with a non-empty `results` array means there are only + warning/info/hint findings (exit code 0); `success: true` with an + empty `results` array means the codebase is clean. Findings are + never reported via the `{ ok: false, code, message }` envelope — + the envelope only appears when the scan itself fails (e.g. + `SCAN_FAILED`) and the normal results payload is absent. + +## Exit codes + +- `0` — All checks passed, no rules configured, or all supplied + paths missing +- `1` — Errors detected or scan failed + +## Errors + +When `--json` is set, failures emit `{ ok: false, code, message }`: + +| code | meaning | fix | +|----------------|------------------------------------|----------------------------------| +| `SCAN_FAILED` | ast-grep scan errored | Report; check rule YAML validity | + +## See Also + +- `taskless help rule create` — add a rule if none exist +- `taskless help ci` — wire `check` into a CI pipeline diff --git a/packages/cli/src/help/ci.txt b/packages/cli/src/help/ci.txt new file mode 100644 index 00000000..b3086bdc --- /dev/null +++ b/packages/cli/src/help/ci.txt @@ -0,0 +1,196 @@ +# Topic: ci (CLI v{{CLI_VERSION}} / topic v1) + +## Goal +Wire `taskless check` into the user's existing CI so rules run +automatically on pushes and pull requests. Integrate with what they +already have — never replace or edit their main pipeline. + +This recipe teaches two patterns (full scan and diff scan) that +translate to any CI system. Common systems are listed as hints; if +you recognize one not on the list, apply the same patterns. + +## Preconditions +- `.taskless/` directory exists and contains at least one rule. (If + no rules exist, instruct the user to fetch + `taskless help rule create` first — wiring CI with zero rules + produces an always-green check that gives false confidence.) +- A local `taskless check` succeeds (or fails with real findings the + user is OK with seeing in CI's first run). +- No auth required for CI — `check` is unauthenticated. + +## Steps + +### 1. Discover the user's CI system + +Scan the repo root for CI config files. Hints (not exhaustive): + +| File / directory | CI system | +|-------------------------------------------|---------------------| +| `.github/workflows/*.yml` | GitHub Actions | +| `.gitlab-ci.yml` | GitLab CI | +| `.circleci/config.yml` | CircleCI | +| `Jenkinsfile` | Jenkins | +| `azure-pipelines.yml` | Azure Pipelines | +| `bitbucket-pipelines.yml` | Bitbucket Pipelines | +| `.buildkite/` | Buildkite | +| `.drone.yml` | Drone | +| `.travis.yml` | Travis CI | + +Sum up what you found and confirm with the user. If zero match, ask +which CI they use. If multiple match, ask which should run Taskless. + +### 2. Agree on the scan pattern + +- **Full scan** — `taskless check`. Scans everything. Best for runs + on the main/default branch. +- **Diff scan** — `taskless check $(git diff --name-only ...)`. + Faster for PR builds. Per-CI diff target var: + + | CI | Target branch variable | + |---------------------|----------------------------------------------| + | GitHub Actions | `github.base_ref` | + | GitLab CI | `CI_MERGE_REQUEST_TARGET_BRANCH_NAME` | + | CircleCI | `CIRCLE_BRANCH` (fetch main and diff against)| + | Jenkins | `env.CHANGE_TARGET` | + | Azure Pipelines | `System.PullRequest.TargetBranch` | + | Bitbucket Pipelines | `BITBUCKET_PR_DESTINATION_BRANCH` | + + `taskless check` silently filters paths that don't exist, so raw + `git diff --name-only` output can pipe in directly. + +**Recommended default:** diff scan on PRs, full scan on pushes to +main. + +### 3. Verify locally first + +Run `npx @taskless/cli check`: +- Clean pass → proceed. +- "No rules configured" → stop. Fetch `taskless help rule create`. +- Findings → tell the user CI will fail; ask whether to fix, + suppress, or proceed knowing the first CI run will be red. + +### 4. Generate the config + +Rules: +1. Write a NEW standalone file. Never modify the user's existing + CI config. +2. Prefer the CI's native include mechanism. Tell the user the one + line they need to add to their main config. +3. If the file you'd write already exists, ask before overwriting. + +Canonical paths: + +| CI | File path | +|---------------------|----------------------------------------------| +| GitHub Actions | `.github/workflows/taskless.yml` (standalone, no include needed) | +| GitLab CI | `.taskless/ci/gitlab.yml` (user adds `include`) | +| CircleCI | `.taskless/ci/circleci-job.yml` (no include — user copies job) | +| Jenkins | `.taskless/ci/taskless.Jenkinsfile` (user `load()`s) | +| Azure Pipelines | `.taskless/ci/azure-taskless.yml` (user references via `template:`) | +| Bitbucket Pipelines | `.taskless/ci/bitbucket-pipelines.yml` (user merges manually) | +| Other | `.taskless/ci/.` + clear wiring instructions | + +### 5. GitHub Actions reference template + +The reference template — translate the same shape (checkout with +full history, set up Node, conditional check) for other CIs. + +Substitute `{{PACKAGE_MANAGER_DLX}}` with `npx @taskless/cli`, +`pnpm dlx @taskless/cli`, `yarn dlx @taskless/cli`, or +`bunx @taskless/cli` based on `pnpm-lock.yaml`/`yarn.lock`/`bun.lockb`. + +```yaml +name: Taskless + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Taskless check + run: | + if [ "${{ '{{ github.event_name }}' }}" = "pull_request" ]; then + git fetch origin "${{ '{{ github.base_ref }}' }}" --depth=1 + FILES=$(git diff --name-only "origin/${{ '{{ github.base_ref }}' }}...HEAD") + if [ -z "$FILES" ]; then + echo "No changed files." + exit 0 + fi + {{PACKAGE_MANAGER_DLX}} check $FILES + else + {{PACKAGE_MANAGER_DLX}} check + fi +``` + +Simplifications: +- Full-scan only → drop the `if`, just run `{{PACKAGE_MANAGER_DLX}} check`. +- Diff-scan only → remove the `push:` trigger. + +### 6. Translate to other CIs + +The six universal steps: +1. Set up Node (v20 default; match the user's existing CI version). +2. Fetch with full depth (or enough to reach the target branch). +3. Fetch the target branch. +4. Compute changed files with `git diff --name-only "origin/...HEAD"`. +5. Exit early if the diff is empty. +6. Call `{{PACKAGE_MANAGER_DLX}} check $FILES` for PR builds, or + `{{PACKAGE_MANAGER_DLX}} check` for main-branch builds. + +YAML for GitHub/GitLab/Azure/Bitbucket; Groovy for Jenkins; +different structure for CircleCI. The six steps stay the same. + +### 7. Authentication in CI + +`taskless check` does NOT require authentication. The generated CI +config works out of the box with no secrets. Only mention auth if +the user explicitly asks to run authenticated commands (e.g. +`rule create`/`rule improve`) in CI — uncommon. + +### 8. Package manager caveats + +- **pnpm**: `pnpm dlx` works but has slow cold starts. If the user's + pipeline already sets up pnpm, suggest adding `@taskless/cli` as a + dev dep and calling it via `pnpm taskless check`. +- **Yarn v1 (classic)**: doesn't support `yarn dlx`. Use `npx`. +- **Bun**: `bunx` works. + +### 9. Report back + +Show: +1. The path written and a 10–15 line excerpt. +2. For CIs needing manual wiring, the exact `include:` / reference + line for their main config. +3. `git status` so they can review before committing. +4. A note that the first CI run exercises rules — if existing + matches exist, CI will fail until fixed or suppressed. + +## Errors + +- **No rules** → fetch `taskless help rule create`. Don't write CI + config. +- **Unrecognized CI** → produce a generic `.taskless/ci/check.sh` + script implementing the six universal steps. Be upfront it's a + starting point. +- **Target CI file exists, user declines overwrite** → stop. Show + what you would have written; let them reconcile manually. + +## See Also + +- `taskless help check` — the command being wired into CI +- `taskless help rule create` — required if no rules exist yet diff --git a/packages/cli/src/help/info.txt b/packages/cli/src/help/info.txt index 8bef55d4..84c43f03 100644 --- a/packages/cli/src/help/info.txt +++ b/packages/cli/src/help/info.txt @@ -1,21 +1,67 @@ -Show Taskless CLI information +# Topic: info (CLI v{{CLI_VERSION}} / topic v1) -Outputs a JSON object with the CLI version, installed tool/skill status, -and authentication state. +## Goal +Report local Taskless state: CLI version, installed skill versions +per detected tool, and (unless `--anonymous`) the user's auth state. +Used as a health check, for staleness detection, and to confirm +which version of the CLI/skills the agent is talking to. -Usage: - taskless info [options] +## Preconditions +- None. Works in any directory; doesn't require `.taskless/`. -Options: - -d, --dir Set working directory (default: current directory) +## Steps -Output: - A JSON object with the following fields: - version CLI version string - tools Array of detected tools with installed skill versions - loggedIn Whether a valid auth token is available - auth User details when logged in (user, email, orgs) +1. **Invoke the CLI** with JSON output: + ``` + npx @taskless/cli info --json + ``` + For an offline/local-only state report (no auth probe), pass + `--anonymous`: + ``` + npx @taskless/cli info --json --anonymous + ``` -Examples: - taskless info - taskless info -d ./my-project +2. **Parse the response.** Shape: + ```json + { + "success": true, + "version": "0.7.0", + "tools": [ + { + "name": "Claude Code", + "skills": [ + { "name": "taskless", "installedVersion": "0.7.0", + "currentVersion": "0.7.0", "current": true } + ] + } + ], + "loggedIn": true, + "auth": { "user": "...", "email": "...", "orgs": ["..."] } + } + ``` + +3. **Report to the user.** Summarize: + - CLI version + - For each tool: number of installed skills, count out-of-date + - Auth: logged in as (orgs) OR not logged in + +4. **Suggest reinit on staleness.** If any skill has `current: false`, + suggest `npx @taskless/cli` to reinstall and pull the latest + bundle. + +## Errors + +When `--json` is set, failures emit `{ ok: false, code, message }`: + +| code | meaning | fix | +|-------------------|----------------------------------|---------------------------| +| `INTERNAL_ERROR` | Internal schema validation | Report; likely a CLI bug | + +(Network errors during the auth probe are silently swallowed — +`info` falls back to reporting `loggedIn: false` rather than +failing.) + +## See Also + +- `taskless help auth` — log in / log out / status detail +- `taskless help check` — run rules against the codebase diff --git a/packages/cli/src/help/init.txt b/packages/cli/src/help/init.txt index 5b04dc4f..301fbbbc 100644 --- a/packages/cli/src/help/init.txt +++ b/packages/cli/src/help/init.txt @@ -1,35 +1,50 @@ -Install or update Taskless skills - -Launches an interactive wizard that detects supported coding agent tools in -your project, lets you pick which install locations to write to, choose any -optional skills (e.g. taskless-ci), and optionally log in to taskless.io. - -Running `taskless` with no subcommand in a TTY also launches this wizard. -In non-interactive contexts (pipes, CI) the wizard is skipped automatically. - -Supported install locations: - Claude Code .claude/ directory or CLAUDE.md file - OpenCode .opencode/ directory, opencode.jsonc, or opencode.json - Cursor .cursor/ directory or .cursorrules file - Fallback .agents/ (used when no other tool is detected) - -Usage: - taskless init [options] - -Options: - -d, --dir Set working directory (default: current directory) - --no-interactive Install every mandatory skill to every detected tool - without prompting. Skips optional skills and auth. - Used by CI and scripted installs. - -Behavior: - - Interactive (default): prompts for locations, optional skills, and auth. - Shows a diff against the previous install state before writing anything. - Ctrl-C at any step aborts cleanly with no filesystem changes. - - --no-interactive: installs mandatory skills to every detected tool, or to - .agents/skills/ as a fallback when no tools are detected. No prompts. - -Examples: - taskless init - taskless init -d ./my-project - taskless init --no-interactive +# Topic: init (CLI v{{CLI_VERSION}} / topic v1) + +## Goal +Install or update the Taskless skill into the user's coding-agent +tools (Claude Code, OpenCode, Cursor, etc.). The user runs this +themselves — the agent's role is mostly to point the user at the +right command when they need to install or upgrade. + +## Preconditions +- None at the user level. The command works in any directory and + bootstraps `.taskless/` on first run. +- For interactive mode: a TTY (running from a terminal). + +## Steps + +The user should run: +``` +npx @taskless/cli +``` +(no subcommand). In a TTY this launches the interactive wizard. In +non-TTY contexts it prints the topic index instead. + +For scripted installs (CI, Dockerfiles): +``` +npx @taskless/cli init --no-interactive +``` + +The wizard will: +1. Detect installed tools (Claude Code, OpenCode, Cursor) and let + the user pick install locations. +2. Show the auth tradeoff and offer to log in (skippable). +3. Show a diff against the previous install state before writing. +4. Write the consolidated `taskless` skill (and `tskl` command for + Claude Code) to each selected location. +5. Update `.taskless/taskless.json` with the install manifest. + +If the user is on v0.6 or earlier, the wizard removes the obsolete +per-task skills (taskless-check, taskless-create-rule, etc.) and the +old slash commands as part of the install. The summary shows what +was removed. + +## Errors + +- Wizard cancelled (Ctrl-C) → no filesystem writes. Re-run when ready. +- No tools detected → falls back to `.agents/skills/`. + +## See Also + +- `taskless help info` — verify what's installed and check staleness +- `taskless help auth` — authenticate after installing diff --git a/packages/cli/src/help/rule-create.anonymous.txt b/packages/cli/src/help/rule-create.anonymous.txt new file mode 100644 index 00000000..ec50f3c9 --- /dev/null +++ b/packages/cli/src/help/rule-create.anonymous.txt @@ -0,0 +1,112 @@ +# Topic: rule create (anonymous) (CLI v{{CLI_VERSION}} / topic v1) + +## Goal +Create a new ast-grep rule **locally** without contacting the Taskless +API. You (the agent) derive the rule yourself using the ast-grep +schema as a guide, write the rule and test files, then validate them +using `rule verify` in a feedback loop. + +## 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.** Before writing a rule, consult + the ast-grep rule reference at https://ast-grep.github.io/guide/rule-config.html + for valid fields, operators (`pattern`, `kind`, `regex`, + `any`/`all`/`has`/`inside`/`not`), and meta-variable syntax. The + Taskless-required fields you'll add on top are listed in step 5. + +2. **Gather the rule description.** Even if the user already provided + one, ask clarifying questions: + - What specific code pattern should be flagged? (concrete examples) + - What language is it in? + - Are there exceptions or edge cases where the pattern is OK? + - Can they show valid and invalid examples? + +3. **Check for similar existing rules.** Scan `.taskless/rules/`. If + anything overlaps, point it out and ask whether the user wants to + improve an existing rule via `taskless help rule improve --anonymous`. + +4. **Search the codebase for real instances.** Show the user what you + found and confirm any exclusions. + +5. **Derive the rule.** Write a YAML file at `.taskless/rules/.yml` + with at minimum: + - `id`: kebab-case identifier (e.g. `no-eval`, `prefer-const`) + - `language`: target language + - `severity`: `error`, `warning`, `info`, or `hint` + - `message`: concise single-line explanation + - `rule`: the ast-grep rule object + + Optional but useful: `note` (multi-line guidance, supports markdown), + `fix` (auto-fix pattern), `ignores` (file patterns to skip). + +6. **Write test cases.** Create `.taskless/rule-tests/-YYYYMMDD-test.yml` + with `id`, `valid: [...]`, and `invalid: [...]` arrays. Include at + least 2 valid and 2 invalid cases. Use real patterns from the + codebase where possible. + +7. **Run the verify feedback loop.** Run: + ``` + npx @taskless/cli rule verify --json + ``` + - If `success: true`: the rule passes. Report success to the user. + - If `success: false`: read the per-layer errors (`schema`, + `requirements`, `tests`). Fix the rule or tests. Re-run verify. + Repeat up to 3 times. If still failing after 3 iterations, report + to the user with the latest errors. + + Common fixes: + - Schema errors → check field types against the ast-grep schema. + - Missing required fields → add what the requirements list demands. + - Regex without kind → add a `kind` field alongside any `regex`. + - Test failures → adjust pattern or test cases so valid cases pass + and invalid cases trigger. + +8. **Report results.** Show the rule file path, test file path, and a + one-line summary of what the rule detects. Suggest fetching + `taskless help check` to validate against the broader codebase. + +## Important Notes + +- Do NOT write files to `.taskless/rule-metadata/`. Anonymous rules + have no metadata sidecar (the API path uses metadata for + iteration; anonymous rules iterate directly via file edits). +- Do NOT make any HTTP requests to taskless.io. +- The verify feedback loop is the quality gate — always run it before + reporting success. + +## ast-grep schema + +This recipe does not embed the full ast-grep schema. Read it from the +upstream docs at https://ast-grep.github.io/guide/rule-config.html +before authoring a rule. The Taskless-specific required fields layered +on top of the ast-grep rule are listed in step 5. + +## Errors + +`rule verify --json` has two output shapes depending on whether the +`` argument is supplied: + +- When `` is missing, the command emits the standardized + `{ ok:false, code:"INVALID_INPUT", message }` envelope on stdout + and exits non-zero. +- When `` is supplied, the command emits the layered result + `{ success, schema, requirements, tests, ... }` on stdout (success + or failure exit code depending on the layers). + +| where | code / shape | meaning | fix | +|---------------------|-----------------|------------------------------------------|--------------------------------------------| +| envelope (no ``)| `INVALID_INPUT` | Rule ID missing | Pass a valid rule ID positionally | +| layered: schema | (schema layer) | YAML didn't match ast-grep schema | Fix the rule structure | +| layered: req | (req layer) | Missing Taskless-required field | Add `id`/`language`/`severity`/etc. | +| layered: tests | (tests layer) | A test case didn't behave as expected | Fix the rule pattern OR the test case | + +## See Also + +- `taskless help rule create` — API-backed flow (auth required) +- `taskless help rule improve --anonymous` — iterate locally +- `taskless help check` — validate the new rule against the codebase diff --git a/packages/cli/src/help/rule-create.txt b/packages/cli/src/help/rule-create.txt new file mode 100644 index 00000000..9a9bba6e --- /dev/null +++ b/packages/cli/src/help/rule-create.txt @@ -0,0 +1,100 @@ +# Topic: rule create (CLI v{{CLI_VERSION}} / topic v1) + +## Goal +Generate a new ast-grep rule from a description and write the rule and +its tests to the user's `.taskless/` directory. The CLI calls the +Taskless API to do the generation; the agent's job is to enrich the +user's description before submitting and to report the result. + +If the user wants the local-only flow (no API call), fetch +`taskless help rule create --anonymous` instead. + +## Preconditions +- User is logged in (`taskless info --json` reports `loggedIn: true`). + If not, fetch `taskless help auth` first. +- Repository has a GitHub origin remote. +- `.taskless/` directory exists (otherwise the user has not run + `npx @taskless/cli` to install Taskless). + +## Steps + +1. **Confirm auth.** Run `npx @taskless/cli info --json` and check the + `loggedIn` field. If false, fetch `taskless help auth` and follow the + login recipe before continuing. If the user explicitly wants + anonymous mode, fetch `taskless help rule create --anonymous` instead. + +2. **Gather the rule description.** Even if the user already provided + one, ask clarifying questions: + - What specific code pattern should be flagged? (concrete examples) + - In what language? + - Are there contexts where the pattern is acceptable? + +3. **Check for similar existing rules.** Scan `.taskless/rules/` for + rule files. Read each rule's `message`, `note`, and `rule` fields. + If any overlap with the user's request, show the user and ask: + "It looks like you already have a rule that covers something + similar. Would you like to improve the existing rule instead?" + If yes, fetch `taskless help rule improve` instead. + +4. **Enrich the request.** + - Search the codebase for real instances of the pattern. Show the + user what you found. + - Ask for additional success and failure cases. + - Read `.gitignore`, ESLint configs, and `tsconfig.json` `exclude` + for default exclusion patterns. Present them as defaults. + - Ask about additional exclusions (e.g. `.d.ts` files, tests). + - Infer the primary language from the codebase. Confirm with the + user. Include it in the `prompt` field. + +5. **Confirm the enriched request.** Before submitting, summarize: + - The full prompt (including language and exclusion notes) + - The success case(s) + - The failure case(s) + +6. **Write the JSON payload.** Build a JSON object matching the input + schema below and write it to `.taskless/.tmp-rule-request.json`. + +7. **Invoke the CLI.** Run: + ``` + npx @taskless/cli rule create --from .taskless/.tmp-rule-request.json --json + ``` + This may take 30–60 seconds while the API generates the rule. + +8. **Clean up.** Delete `.taskless/.tmp-rule-request.json` regardless + of success or failure. + +9. **Report results.** The CLI writes the generated rule to + `.taskless/rules/.yml`, tests to + `.taskless/rule-tests/-YYYYMMDD-test.yml` (timestamped per + generation), and metadata to `.taskless/rule-metadata/.yml`. + Show the user the file paths. Suggest fetching `taskless help check` + to validate. + +## Input schema + +The `--from` JSON file conforms to: + +```json +{{INPUT_SCHEMA}} +``` + +Each example in `successCases` and `failureCases` is a separate +string. Multi-line code goes in a single string with literal newlines. + +## Errors + +When `--json` is set, failures emit `{ ok: false, code, message }`: + +| code | meaning | fix | +|--------------------------|------------------------------------|---------------------------------------------| +| `AUTH_REQUIRED` | not logged in | fetch `taskless help auth` | +| `NO_GITHUB_REMOTE` | no GitHub origin remote | tell the user; we cannot proceed | +| `INVALID_INPUT` | `--from` JSON failed validation | re-read the input schema, fix, retry | +| `NETWORK_ERROR` | API submit/poll failed | report and suggest retry | +| `RULE_GENERATION_FAILED` | API returned a generation failure | report the message; suggest enriching prompt | + +## See Also + +- `taskless help rule create --anonymous` — local-only flow (no API) +- `taskless help rule improve` — iterate on an existing rule +- `taskless help check` — validate the new rule against the codebase diff --git a/packages/cli/src/help/rule-delete.txt b/packages/cli/src/help/rule-delete.txt new file mode 100644 index 00000000..d2acef9d --- /dev/null +++ b/packages/cli/src/help/rule-delete.txt @@ -0,0 +1,48 @@ +# Topic: rule delete (CLI v{{CLI_VERSION}} / topic v1) + +## Goal +Remove a rule and its associated test files from `.taskless/`. Does +not contact the Taskless API; purely a local filesystem operation. + +## Preconditions +- `.taskless/` directory exists. +- The target rule file exists at `.taskless/rules/.yml`. +- No auth required. + +## Steps + +1. **Identify the rule.** If the user named one, use it. Otherwise, + list `.taskless/rules/` and ask which one. Confirm the user's + intent — deletion is destructive. + +2. **Invoke the CLI.** Run: + ``` + npx @taskless/cli rule delete + ``` + + The CLI removes: + - `.taskless/rules/.yml` + - All matching `.taskless/rule-tests/-*.yml` files + - `.taskless/rule-metadata/.yml` (if present) + +3. **Report results.** Show the user what was deleted. If the rule + had metadata (was API-generated), note that the rule cannot be + re-iterated via the API path after deletion. + +## Errors + +When `--json` is set, failures emit `{ ok: false, code, message }` on +stdout; on success the command exits 0 silently (no envelope). + +| code | meaning | fix | +|------------------|--------------------------|--------------------------------------| +| `RULE_NOT_FOUND` | Rule file does not exist | Confirm the ID; list rules first | + +The rule ID is required as a positional argument; citty rejects a +missing ID before the command body runs, so `INVALID_INPUT` is not +emitted from this command. + +## See Also + +- `taskless help rule create` — make a new rule +- `taskless help check` — run remaining rules to confirm nothing broke diff --git a/packages/cli/src/help/rule-improve.anonymous.txt b/packages/cli/src/help/rule-improve.anonymous.txt new file mode 100644 index 00000000..ddaa34c6 --- /dev/null +++ b/packages/cli/src/help/rule-improve.anonymous.txt @@ -0,0 +1,88 @@ +# Topic: rule improve (anonymous) (CLI v{{CLI_VERSION}} / topic v1) + +## Goal +Iterate on an existing ast-grep rule **locally** without contacting +the Taskless API. You (the agent) edit the rule YAML directly, then +validate with `rule verify` in a feedback loop. + +## Preconditions +- `.taskless/` directory exists and contains the target rule. +- The agent can read/write files and run shell commands. +- No auth required. + +## Steps + +1. **Identify the target rule.** If the user named one, use it. + Otherwise, list `.taskless/rules/` and ask which one. Read the + rule file and any test file (`.taskless/rule-tests/-*.yml`). + +2. **Gather improvement guidance.** Ask: + - Are there false positives (cases currently flagged but + shouldn't be)? Get concrete examples. + - Are there false negatives (cases passing but should be + caught)? Get concrete examples. + - Should the message, severity, or note change? + +3. **Plan the change.** Decide whether to: + - Tighten the `rule` (add `inside`, `not`, more specific `kind`) + - Loosen the `rule` (relax constraints, add `any` alternatives) + - Add `ignores` patterns for files/paths to skip + - Adjust `message`/`severity`/`note` + Summarize the planned change before editing. + +4. **Edit the rule file.** Make the change directly in + `.taskless/rules/.yml`. Preserve the existing `id`, + `language`, and `severity` unless the user explicitly asked to + change them. + +5. **Update test cases.** Add the new false-positive examples to the + `valid:` list and the new false-negative examples to the + `invalid:` list in `.taskless/rule-tests/-*.yml`. + +6. **Run the verify feedback loop.** Run: + ``` + npx @taskless/cli rule verify --json + ``` + - If `success: true`: report success. + - If `success: false`: read the per-layer errors, fix, re-run. + Repeat up to 3 times. After 3 failed attempts, report to the + user with the latest errors and ask for guidance. + + Common fixes: + - New tests fail → the rule still doesn't catch the case; + refine the pattern. + - Old tests broke → the new rule is too aggressive; add an + `ignores` clause or constrain `kind`. + - Schema errors → the YAML structure drifted; check fields. + +7. **Report results.** Show: + - The updated rule file path + - The updated test file path + - A diff-style summary of what changed + Suggest fetching `taskless help check` to validate against the + broader codebase. + +## Important Notes + +- Rules created in anonymous mode have no metadata sidecar — that's + fine. This recipe doesn't need or write metadata. +- If the rule was originally created via the API path (has metadata), + you can still use this anonymous recipe to iterate locally. The + metadata sidecar is preserved untouched. +- Do NOT make any HTTP requests to taskless.io. + +## Errors + +The verify primitive returns structured errors per layer: + +| layer | what failure means | fix | +|----------------|----------------------------------------------|-------------------------------------------| +| `schema` | YAML doesn't match ast-grep schema | Fix rule structure | +| `requirements` | Missing Taskless-required field | Add `id`/`language`/`severity`/etc. | +| `tests` | A test case behaved unexpectedly | Fix the rule pattern OR the test case | + +## See Also + +- `taskless help rule improve` — API-backed flow (auth required) +- `taskless help rule create --anonymous` — make a new rule locally +- `taskless help check` — validate the updated rule diff --git a/packages/cli/src/help/rule-improve.txt b/packages/cli/src/help/rule-improve.txt new file mode 100644 index 00000000..216f6f8c --- /dev/null +++ b/packages/cli/src/help/rule-improve.txt @@ -0,0 +1,103 @@ +# Topic: rule improve (CLI v{{CLI_VERSION}} / topic v1) + +## Goal +Iterate on an existing Taskless rule. The CLI submits the user's +guidance to the Taskless API iterate endpoint, which returns an +updated rule that overwrites the original on disk. The agent's job +is to gather the right ruleId + guidance + supporting references and +to report the result. + +If the user wants the local-only flow (no API call), fetch +`taskless help rule improve --anonymous` instead. + +## Preconditions +- User is logged in. +- Repository has a GitHub origin remote. +- The target rule exists in `.taskless/rules/.yml`. +- The rule has metadata at `.taskless/rule-metadata/.yml` (the + `ticketId` is required by the iterate endpoint). If metadata is + missing, the rule was created in anonymous mode and cannot be + iterated via the API path — fetch the anonymous variant instead. + +## Steps + +1. **Confirm auth.** Run `npx @taskless/cli info --json` and check + `loggedIn`. If false, fetch `taskless help auth`. + +2. **Identify the rule to improve.** If the user named one, use it. + Otherwise, list rules in `.taskless/rules/` and ask which one. + Read the existing rule file so you can summarize what it does. + +3. **Fetch the rule's metadata.** Run: + ``` + npx @taskless/cli rule meta --json + ``` + This returns the `ticketId` needed for the iterate request. If the + metadata is missing, the rule cannot be iterated via API — fetch + `taskless help rule improve --anonymous` instead. + +4. **Gather improvement guidance.** Ask the user what should change: + - Are there false positives we need to exclude? + - Are there missed cases we need to catch? + - Is the message confusing or misleading? + - Should the severity change? + +5. **Collect supporting references.** Ask the user for any code + examples that should be: + - **Not flagged** (currently flagged but shouldn't be) — false + positive references. + - **Flagged** (currently passing but should be caught) — false + negative references. + + Each reference is `{ filename: string, content: string }`. Multiple + references are an array. + +6. **Confirm the request.** Summarize the rule, the guidance, and + the references before submitting. + +7. **Write the JSON payload.** Build a JSON object matching the input + schema below and write it to `.taskless/.tmp-improve-request.json`. + +8. **Invoke the CLI.** Run: + ``` + npx @taskless/cli rule improve --from .taskless/.tmp-improve-request.json --json + ``` + This may take 30–60 seconds while the API generates the update. + +9. **Clean up.** Delete `.taskless/.tmp-improve-request.json` + regardless of success or failure. + +10. **Report results.** The CLI overwrites the rule file (and its + test file) with the updated version. Show the file paths and a + summary of what changed. Suggest fetching `taskless help check` + to validate. + +## Input schema + +The `--from` JSON file conforms to: + +```json +{{INPUT_SCHEMA}} +``` + +`ruleId` is the original rule's ticket ID (returned by +`taskless rule meta --json`), not the YAML file name. + +## Errors + +When `--json` is set, failures emit `{ ok: false, code, message }`: + +| code | meaning | fix | +|--------------------------|----------------------------------------|----------------------------------------------| +| `AUTH_REQUIRED` | not logged in | fetch `taskless help auth` | +| `NO_GITHUB_REMOTE` | no GitHub origin remote | tell the user; we cannot proceed | +| `INVALID_INPUT` | `--from` JSON failed validation | re-read input schema, fix, retry | +| `RULE_NOT_FOUND` | metadata missing for the given rule | use anonymous variant or recreate via create | +| `NETWORK_ERROR` | API submit/poll failed | report and suggest retry | +| `RULE_GENERATION_FAILED` | API returned a generation failure | report; suggest enriching guidance/references | + +## See Also + +- `taskless help rule improve --anonymous` — local-only flow +- `taskless help rule create` — make a new rule from scratch +- `taskless help check` — validate the updated rule diff --git a/packages/cli/src/help/rule-meta.txt b/packages/cli/src/help/rule-meta.txt new file mode 100644 index 00000000..3dfe7b3c --- /dev/null +++ b/packages/cli/src/help/rule-meta.txt @@ -0,0 +1,30 @@ +# Topic: rule meta (CLI v{{CLI_VERSION}} / topic v1) + +## Goal +Read sidecar metadata for an API-generated rule. Used internally by +the `rule improve` recipe to fetch the `ticketId` needed for iteration. + +## Preconditions +- The rule was created via the API path (anonymous-mode rules have + no metadata sidecar). +- `.taskless/rule-metadata/.yml` exists. + +## Steps + +``` +npx @taskless/cli rule meta --json +``` + +Returns the metadata fields: `ticketId`, `generatedAt`, schema +version, etc. + +## Errors + +| code | meaning | fix | +|------------------|--------------------------------------|------------------------------------| +| `RULE_NOT_FOUND` | metadata sidecar missing | Use anonymous improve flow instead | +| `INVALID_INPUT` | metadata file malformed | File is corrupted; re-create rule | + +## See Also + +- `taskless help rule improve` — the primary consumer of this command diff --git a/packages/cli/src/help/rule-verify.txt b/packages/cli/src/help/rule-verify.txt new file mode 100644 index 00000000..47db3642 --- /dev/null +++ b/packages/cli/src/help/rule-verify.txt @@ -0,0 +1,58 @@ +# Topic: rule verify (CLI v{{CLI_VERSION}} / topic v1) + +## Goal +Validate a rule against the ast-grep schema and run its test cases. +Primary consumer is the anonymous create/improve flow (the verify +feedback loop), but agents can also call it directly to lint a rule. + +## Preconditions +- `.taskless/rules/.yml` exists. +- Optionally `.taskless/rule-tests/-*.yml` exists (test cases run + if present). +- No auth required. + +## Steps + +``` +npx @taskless/cli rule verify --json +``` + +Three layers of validation run in order: + +1. **Schema** — YAML conforms to the ast-grep rule schema. +2. **Requirements** — Taskless-required fields present (`id`, + `language`, `severity`, `message`, `rule`); `regex` always + accompanied by `kind`. +3. **Tests** — `sg test` runs the test file. Each `valid` case must + not match; each `invalid` case must match. + +Output (JSON): +```json +{ + "ruleId": "no-eval", + "success": false, + "schema": { "valid": true, "errors": [] }, + "requirements": { "valid": false, "errors": ["missing severity"] }, + "tests": { "valid": true, "passed": 4, "failed": 0, "errors": [] } +} +``` + +When iterating in a feedback loop (anonymous create/improve), read +the per-layer errors, fix the rule or tests, re-run. Cap at 3 +attempts and report to the user if still failing. + +## Exit codes + +- `0` — All three layers passed. +- `1` — Any layer failed (or `--json` missing required ID). + +## Errors + +| code | meaning | fix | +|------------------|----------------------|----------------------------------| +| `INVALID_INPUT` | Rule ID not provided | Pass the rule ID positionally | + +## See Also + +- `taskless help rule create --anonymous` — primary consumer (loop) +- `taskless help rule improve --anonymous` — primary consumer (loop) diff --git a/packages/cli/src/help/rule.txt b/packages/cli/src/help/rule.txt new file mode 100644 index 00000000..f1a23409 --- /dev/null +++ b/packages/cli/src/help/rule.txt @@ -0,0 +1,22 @@ +# Topic: rule (CLI v{{CLI_VERSION}} / topic v1) + +## Goal +Umbrella for rule operations. Use the specific subcommand for the +action you want. + +## Subcommands + +| Subcommand | Recipe | +|---------------------|----------------------------------------------| +| Create a rule | `taskless help rule create` | +| Improve a rule | `taskless help rule improve` | +| Delete a rule | `taskless help rule delete` | +| Verify a rule | `taskless help rule verify` (agent-internal) | +| Read rule metadata | `taskless help rule meta` (agent-internal) | + +For local-only flows on create/improve, append `--anonymous` to the +help fetch. + +## See Also + +- `taskless help check` — run all configured rules diff --git a/packages/cli/src/help/rules-create.txt b/packages/cli/src/help/rules-create.txt deleted file mode 100644 index 08ab7cff..00000000 --- a/packages/cli/src/help/rules-create.txt +++ /dev/null @@ -1,29 +0,0 @@ -Create a new rule from a JSON file - -Reads a JSON file describing the desired rule, submits it to the Taskless -API for generation, and writes the resulting rule and test files to -.taskless/rules/ and .taskless/rule-tests/. - -Prerequisites: - Run `npx @taskless/cli@latest auth login` to authenticate first. - Your repository must have a GitHub origin remote. - -Usage: - taskless rules create --from [options] - -Options: - --from Path to a JSON file containing the rule request (required) - -d, --dir Set working directory (default: current directory) - --json Output results as JSON - -JSON File Fields: - prompt (required) Description of the pattern to detect - successCases (optional) Array of example code strings that should pass - failureCases (optional) Array of example code strings that should fail - -Output: - Lists generated rule and test file paths. - -Examples: - taskless rules create --from request.json - taskless rules create --from .taskless/.tmp-rule-request.json --json diff --git a/packages/cli/src/help/rules-delete.txt b/packages/cli/src/help/rules-delete.txt deleted file mode 100644 index ec122092..00000000 --- a/packages/cli/src/help/rules-delete.txt +++ /dev/null @@ -1,20 +0,0 @@ -Delete a rule and its test files - -Removes a rule YAML file and any associated test files from the project. - -Usage: - taskless rules delete [options] - -Options: - -d, --dir Set working directory (default: current directory) - -Arguments: - rule-id The rule identifier (matches the filename without extension) - -Exit Codes: - 0 Rule deleted successfully - 1 Rule not found - -Examples: - taskless rules delete no-console-log - taskless rules delete detect-innerhtml -d ./my-project diff --git a/packages/cli/src/help/rules-improve.txt b/packages/cli/src/help/rules-improve.txt deleted file mode 100644 index 39272953..00000000 --- a/packages/cli/src/help/rules-improve.txt +++ /dev/null @@ -1,29 +0,0 @@ -Improve an existing rule with guidance - -Reads a JSON file containing the rule ID and improvement guidance, submits -it to the Taskless API, and writes the updated rule and test files to -.taskless/rules/ and .taskless/rule-tests/. - -Prerequisites: - Run `npx @taskless/cli@latest auth login` to authenticate first. - Your repository must have a GitHub origin remote. - -Usage: - taskless rules improve --from [options] - -Options: - --from Path to a JSON file containing the improve request (required) - -d, --dir Set working directory (default: current directory) - --json Output results as JSON - -JSON File Fields: - ruleId (required) The rule request ID to improve - guidance (required) Description of what should change - references (optional) Array of { filename, content } objects providing context - -Output: - Lists updated rule and test file paths. - -Examples: - taskless rules improve --from request.json - taskless rules improve --from .taskless/.tmp-improve-request.json --json diff --git a/packages/cli/src/help/rules-meta.txt b/packages/cli/src/help/rules-meta.txt deleted file mode 100644 index fc590507..00000000 --- a/packages/cli/src/help/rules-meta.txt +++ /dev/null @@ -1,20 +0,0 @@ -Show sidecar metadata for a rule - -Reads the metadata file for a generated rule from .taskless/rule-metadata/ -and displays its contents. Metadata includes the ticket ID used for -iterating on the rule, generation timestamp, and schema version. - -Usage: - taskless rules meta [options] - -Options: - -d, --dir Set working directory (default: current directory) - --json Output results as JSON - --schema Print output/error JSON Schemas and exit - -Output: - Displays the rule's sidecar metadata fields (ticketId, generatedAt, etc.). - -Examples: - taskless rules meta no-console-log - taskless rules meta no-console-log --json diff --git a/packages/cli/src/help/rules-verify.txt b/packages/cli/src/help/rules-verify.txt deleted file mode 100644 index ee704daf..00000000 --- a/packages/cli/src/help/rules-verify.txt +++ /dev/null @@ -1,30 +0,0 @@ -Validate a rule against the ast-grep schema and run tests - -Performs three layers of validation: - 1. Schema validation against the official ast-grep rule schema - 2. Taskless requirement checks (required fields, regex-requires-kind) - 3. Test execution via sg test (if test file exists) - -Usage: - taskless rules verify [options] - taskless rules verify --schema [options] - -Arguments: - Rule ID to verify (matches .taskless/rules/.yml) - -Options: - -d, --dir Set working directory (default: current directory) - --json Output as JSON - --schema Dump combined ast-grep schema, Taskless requirements, - and annotated examples for agent consumption - -Schema Mode: - When --schema is used, no rule ID is required. Outputs a JSON payload - containing the official ast-grep rule JSON schema, Taskless-specific - requirements, and curated examples. Useful for agents learning to - write ast-grep rules. - -Examples: - taskless rules verify no-eval - taskless rules verify no-eval --json - taskless rules verify --schema --json diff --git a/packages/cli/src/help/rules.txt b/packages/cli/src/help/rules.txt deleted file mode 100644 index ce8f55db..00000000 --- a/packages/cli/src/help/rules.txt +++ /dev/null @@ -1,8 +0,0 @@ -Manage Taskless rules - -Commands: - create Create a new rule from a description - improve Improve an existing rule with guidance - delete Delete a rule and its test files - -Run `taskless help rules ` for details on a specific command. diff --git a/packages/cli/src/help/update.txt b/packages/cli/src/help/update.txt new file mode 100644 index 00000000..8571c23e --- /dev/null +++ b/packages/cli/src/help/update.txt @@ -0,0 +1,46 @@ +# Topic: update (CLI v{{CLI_VERSION}} / topic v1) + +## Goal +Update Taskless skills in the user's coding-agent tools to the +latest bundled version. Non-interactive — no wizard, no prompts. +Installs to all detected tool locations using the same logic as +`taskless init --no-interactive`, but exposed as its own subcommand +so the agent can run it directly without explaining flags. + +This is the right command when the user has Taskless already +installed and just wants to refresh to a new version (e.g. after +running `npx @taskless/cli@latest`). + +## Preconditions +- None at the user level. Works in any directory. +- For tool detection: at least one of `.claude/`, `.opencode/`, + `.cursor/`, or a related marker file exists. Otherwise installs + to `.agents/skills/` as a fallback. + +## Steps + +``` +npx @taskless/cli update +``` + +The CLI: +1. Detects installed tools (Claude Code, OpenCode, Cursor, etc.) +2. Reads the previous install state from `.taskless/taskless.json` +3. Computes the diff (skills/commands to add, remove) +4. Writes the consolidated `taskless` skill (and `tskl` command for + tools that support commands) to each detected location +5. Removes obsolete files from prior versions +6. Updates the install manifest +7. Prints a summary including what was added and removed + +## Errors + +`update` is non-interactive. On success it exits 0 and prints a +human-readable summary of skills/commands added, removed, or kept in +sync (same surface as `init --no-interactive`). On failure it exits +non-zero with the error message on stderr. + +## See Also + +- `taskless help init` — interactive variant (wizard with prompts) +- `taskless help info` — verify what's installed and check staleness diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 8f1af162..475f8b7d 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -2,21 +2,24 @@ import { defineCommand, runCommand, showUsage } from "citty"; import { authCommand } from "./commands/auth"; import { checkCommand } from "./commands/check"; -import { initCommand } from "./commands/init"; +import { initCommand, updateCommand } from "./commands/init"; import { infoCommand } from "./commands/info"; import { createHelpCommand } from "./commands/help"; -import { rulesCommand } from "./commands/rules"; +import { ruleCommand } from "./commands/rules"; import { shutdownTelemetry } from "./telemetry"; import { CliError } from "./util/cli-error"; const subCommands = { init: initCommand, + update: updateCommand, info: infoCommand, check: checkCommand, auth: authCommand, - rules: rulesCommand, + rule: ruleCommand, }; +const helpCommand = createHelpCommand(subCommands); + const main = defineCommand({ meta: { name: "taskless", @@ -34,15 +37,10 @@ const main = defineCommand({ description: "Output as JSON", default: false, }, - schema: { - type: "boolean", - description: "Print input/output/error JSON Schemas and exit", - default: false, - }, }, subCommands: { ...subCommands, - help: createHelpCommand(subCommands), + help: helpCommand, }, async run({ rawArgs, cmd }) { // citty always calls the parent's run handler, even after a subcommand. @@ -60,7 +58,7 @@ const main = defineCommand({ } // Only delegate to `init` when the only flags present are ones init - // also understands (`-d` / `--dir`). Help/version/schema/json flags and + // also understands (`-d` / `--dir`). Help/version/json flags and // any unknown flags should fall through to citty's default help instead // of silently launching the wizard. const onlyInitFlags = rawArgs.every((argument, index) => { @@ -76,15 +74,23 @@ const main = defineCommand({ return; } - // TTY → run the interactive wizard. Non-TTY → show help as before so - // scripted invocations that pipe `taskless` keep working. Forward the - // full rawArgs so `-d ` is preserved end-to-end. + // TTY → run the interactive wizard. Non-TTY → print a short preamble + // explaining the context and then delegate to `help` so agents and + // pipes see the topic index. if (process.stdout.isTTY === true && process.stdin.isTTY === true) { await runCommand(initCommand, { rawArgs }); return; } - await showUsage(cmd); + console.error( + "Taskless CLI — non-interactive context detected.\n" + + " For interactive install, run from a terminal.\n" + + " For scripted install, run `taskless init --no-interactive`.\n" + + " For agent recipes, run `taskless help` (no args) for the topic index.\n" + ); + // Forward the parent's rawArgs (e.g. `-d `) so the help command + // doesn't mis-parse them as positional topic names. + await runCommand(helpCommand, { rawArgs: ["help", ...rawArgs] }); }, }); diff --git a/packages/cli/src/install/catalog.ts b/packages/cli/src/install/catalog.ts index fdbc0bc7..6054e23f 100644 --- a/packages/cli/src/install/catalog.ts +++ b/packages/cli/src/install/catalog.ts @@ -4,16 +4,7 @@ export interface SkillDescriptor { } export const SKILL_CATALOG: readonly SkillDescriptor[] = [ - { name: "taskless-check", optional: false }, - { name: "taskless-ci", optional: true }, - { name: "taskless-create-rule", optional: false }, - { name: "taskless-create-rule-anonymous", optional: false }, - { name: "taskless-delete-rule", optional: false }, - { name: "taskless-improve-rule", optional: false }, - { name: "taskless-improve-rule-anonymous", optional: false }, - { name: "taskless-info", optional: false }, - { name: "taskless-login", optional: false }, - { name: "taskless-logout", optional: false }, + { name: "taskless", optional: false }, ]; export function getMandatorySkillNames(): string[] { diff --git a/packages/cli/src/types/errors.ts b/packages/cli/src/types/errors.ts new file mode 100644 index 00000000..ba357b5b --- /dev/null +++ b/packages/cli/src/types/errors.ts @@ -0,0 +1,38 @@ +/** + * Stable error codes emitted by CLI commands when --json is set. + * Recipes reference these codes by name in their `## Errors` section, + * so renaming a code is a breaking change for the agent contract. + * + * Add new codes by extending the union; do not rename existing codes + * without a major version bump. + */ +export type CliErrorCode = + | "AUTH_REQUIRED" + | "NO_GITHUB_REMOTE" + | "RULE_GENERATION_FAILED" + | "RULE_NOT_FOUND" + | "INVALID_INPUT" + | "NETWORK_ERROR" + | "SCAN_FAILED" + | "INTERNAL_ERROR"; + +/** + * Standardized JSON error envelope written to stdout when an action + * command exits with an error AND `--json` was set. + */ +export interface CliErrorEnvelope { + ok: false; + code: CliErrorCode; + message: string; +} + +export function makeErrorEnvelope( + code: CliErrorCode, + message: string +): CliErrorEnvelope { + return { ok: false, code, message }; +} + +export function writeJsonError(code: CliErrorCode, message: string): void { + console.log(JSON.stringify(makeErrorEnvelope(code, message))); +} diff --git a/packages/cli/src/util/schema-output.ts b/packages/cli/src/util/schema-output.ts deleted file mode 100644 index 2bb96c85..00000000 --- a/packages/cli/src/util/schema-output.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { z } from "zod"; -import type { z as zType } from "zod"; - -/** - * Print the --schema output for a command. - * Outputs three labeled blocks: Input Schema, Output Schema, Error Schema. - */ -export function printSchema(schemas: { - input?: zType.ZodType; - output: zType.ZodType; - error: zType.ZodType; -}): void { - console.log("Input Schema:"); - if (schemas.input) { - console.log(JSON.stringify(z.toJSONSchema(schemas.input), null, 2)); - } else { - console.log("This command does not accept JSON input."); - } - - console.log(""); - console.log("Output Schema:"); - console.log(JSON.stringify(z.toJSONSchema(schemas.output), null, 2)); - - console.log(""); - console.log("Error Schema:"); - console.log(JSON.stringify(z.toJSONSchema(schemas.error), null, 2)); -} diff --git a/packages/cli/src/wizard/index.ts b/packages/cli/src/wizard/index.ts index c8570974..7064a451 100644 --- a/packages/cli/src/wizard/index.ts +++ b/packages/cli/src/wizard/index.ts @@ -1,13 +1,11 @@ import { intro, outro, cancel, log } from "@clack/prompts"; import { ensureTasklessDirectory } from "../filesystem/directory"; -import { SKILL_CATALOG } from "../install/catalog"; import { applyInstallPlan, AGENTS_FALLBACK, getEmbeddedCommands, getEmbeddedSkills, - type EmbeddedSkill, type EmbeddedCommand, type InstallPlanTarget, type ToolDescriptor, @@ -18,7 +16,6 @@ import { getTelemetry } from "../telemetry"; import { WizardCancelled } from "./ask"; import { getCliVersion, renderIntro } from "./intro"; import { promptLocations } from "./steps/locations"; -import { promptOptionalSkills } from "./steps/optional-skills"; import { promptAuth } from "./steps/auth"; import { renderSummaryAndConfirm } from "./steps/summary"; @@ -69,21 +66,6 @@ const TOOL_BY_INSTALL_DIR: Record = { ".agents": AGENTS_FALLBACK, }; -function resolveSkillsForSelection( - embeddedSkills: EmbeddedSkill[], - optionalSelection: string[] -): EmbeddedSkill[] { - const optionalSet = new Set(optionalSelection); - const result: EmbeddedSkill[] = []; - for (const descriptor of SKILL_CATALOG) { - const include = !descriptor.optional || optionalSet.has(descriptor.name); - if (!include) continue; - const embedded = embeddedSkills.find((s) => s.name === descriptor.name); - if (embedded) result.push(embedded); - } - return result; -} - export async function runWizard( options: RunWizardOptions ): Promise { @@ -92,7 +74,8 @@ export async function runWizard( let cancelledStep: string | undefined; let locations: string[] = []; - let optionalSkills: string[] = []; + // Optional skills no longer exist post-consolidation — always empty. + const optionalSkills: string[] = []; let authPromptShown = false; let authCompleted = false; @@ -101,17 +84,14 @@ export async function runWizard( try { locations = await promptLocations(options.cwd); - optionalSkills = await promptOptionalSkills(); const authResult = await promptAuth(options.cwd); authPromptShown = authResult.prompted; authCompleted = authResult.loggedIn; const embeddedSkills = getEmbeddedSkills(); const embeddedCommands = getEmbeddedCommands(); - const selectedSkills = resolveSkillsForSelection( - embeddedSkills, - optionalSkills - ); + // Catalog has one entry now (`taskless`); install all embedded skills. + const selectedSkills = embeddedSkills; const planTargets: InstallPlanTarget[] = locations.map((directory) => { const tool = TOOL_BY_INSTALL_DIR[directory]; diff --git a/packages/cli/src/wizard/steps/optional-skills.ts b/packages/cli/src/wizard/steps/optional-skills.ts deleted file mode 100644 index 4ccb1a7b..00000000 --- a/packages/cli/src/wizard/steps/optional-skills.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { multiselect } from "@clack/prompts"; - -import { getOptionalSkillNames } from "../../install/catalog"; -import { ask } from "../ask"; - -export async function promptOptionalSkills(): Promise { - const options = getOptionalSkillNames(); - - if (options.length === 0) { - return []; - } - - const selected = await ask("optionalSkills", () => - multiselect({ - message: "Any optional skills to include?", - options: options.map((name) => ({ - value: name, - label: name, - hint: describe(name), - })), - initialValues: [], - required: false, - }) - ); - - return selected; -} - -function describe(name: string): string | undefined { - switch (name) { - case "taskless-ci": { - return "Integrate Taskless with your CI pipeline"; - } - default: { - return undefined; - } - } -} diff --git a/packages/cli/test/anonymous-flag.test.ts b/packages/cli/test/anonymous-flag.test.ts new file mode 100644 index 00000000..c763d4f5 --- /dev/null +++ b/packages/cli/test/anonymous-flag.test.ts @@ -0,0 +1,190 @@ +import { execFile } from "node:child_process"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { promisify } from "node:util"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +const execFileAsync = promisify(execFile); +const binPath = resolve(import.meta.dirname, "../dist/index.js"); + +async function runCli( + args: string[] +): Promise<{ stdout: string; stderr: string; exitCode: number }> { + try { + const { stdout, stderr } = await execFileAsync("node", [binPath, ...args]); + return { stdout, stderr, exitCode: 0 }; + } catch (error) { + const execError = error as { + stdout: string; + stderr: string; + code: number; + }; + return { + stdout: execError.stdout ?? "", + stderr: execError.stderr ?? "", + exitCode: execError.code, + }; + } +} + +describe("--anonymous flag (per-command behavior matrix)", () => { + let cwd: string; + + beforeEach(async () => { + cwd = await mkdtemp(join(tmpdir(), "taskless-anon-")); + }); + + afterEach(async () => { + await rm(cwd, { recursive: true, force: true }); + }); + + describe("info --anonymous", () => { + it("skips the API/auth probe and reports loggedIn: false", async () => { + const result = await runCli(["info", "--anonymous", "--json", "-d", cwd]); + expect(result.exitCode).toBe(0); + const parsed = JSON.parse(result.stdout) as { + loggedIn: boolean; + auth?: unknown; + }; + // Even if a token were present, --anonymous suppresses the lookup. + expect(parsed.loggedIn).toBe(false); + expect(parsed.auth).toBeUndefined(); + }); + }); + + describe("auth login --anonymous", () => { + it("rejects with exit 1 and 'auth commands cannot be anonymous'", async () => { + const result = await runCli(["auth", "login", "--anonymous", "-d", cwd]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("auth commands cannot be anonymous"); + }); + }); + + describe("auth logout --anonymous", () => { + it("accepts the flag as no-op (same behavior as plain logout)", async () => { + const result = await runCli(["auth", "logout", "--anonymous", "-d", cwd]); + expect(result.exitCode).toBe(0); + // Either "Logged out." or "Not logged in." depending on initial state; + // both are success. + expect(result.stdout).toMatch(/Logged out|Not logged in/); + }); + }); + + describe("check --anonymous", () => { + it("accepts the flag as no-op (same behavior as plain check)", async () => { + // No .taskless/ directory → friendly "no rules" message, exit 0 + const result = await runCli(["check", "--anonymous", "-d", cwd]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("No rules configured"); + }); + }); + + describe("rule create --anonymous", () => { + it("exits with a pointer to the local-only recipe (does not run generation)", async () => { + // No --from needed; the --anonymous branch short-circuits before + // file validation. + const result = await runCli(["rule", "create", "--anonymous", "-d", cwd]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("taskless help rule create --anonymous"); + }); + + it("with --json, emits the standardized envelope", async () => { + const result = await runCli([ + "rule", + "create", + "--anonymous", + "--json", + "-d", + cwd, + ]); + expect(result.exitCode).not.toBe(0); + const parsed = JSON.parse(result.stdout) as { ok: boolean; code: string }; + expect(parsed.ok).toBe(false); + expect(parsed.code).toBe("INVALID_INPUT"); + }); + }); + + describe("rule improve --anonymous", () => { + it("exits with a pointer to the local-only recipe", async () => { + const result = await runCli([ + "rule", + "improve", + "--anonymous", + "-d", + cwd, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("taskless help rule improve --anonymous"); + }); + }); + + describe("rule delete --anonymous", () => { + it("accepts the flag as no-op (same behavior as plain delete)", async () => { + // Rule doesn't exist → exit 1 with "not found", same as without --anonymous + const result = await runCli([ + "rule", + "delete", + "nonexistent", + "--anonymous", + "-d", + cwd, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("not found"); + }); + }); + + describe("rule verify --anonymous", () => { + it("accepts the flag as no-op", async () => { + // No rule ID → INVALID_INPUT regardless of --anonymous + const result = await runCli([ + "rule", + "verify", + "--anonymous", + "--json", + "-d", + cwd, + ]); + expect(result.exitCode).not.toBe(0); + const parsed = JSON.parse(result.stdout) as { code: string }; + expect(parsed.code).toBe("INVALID_INPUT"); + }); + }); + + describe("rule meta --anonymous", () => { + it("accepts the flag as no-op", async () => { + // Rule doesn't exist → RULE_NOT_FOUND regardless of --anonymous + await mkdir(join(cwd, ".taskless"), { recursive: true }); + await writeFile( + join(cwd, ".taskless", "taskless.json"), + JSON.stringify({ version: 2, install: {} }) + ); + const result = await runCli([ + "rule", + "meta", + "nonexistent", + "--anonymous", + "--json", + "-d", + cwd, + ]); + expect(result.exitCode).not.toBe(0); + const parsed = JSON.parse(result.stdout) as { code: string }; + expect(parsed.code).toBe("RULE_NOT_FOUND"); + }); + }); + + describe("init --anonymous", () => { + it("accepts the flag as no-op", async () => { + const result = await runCli([ + "init", + "--no-interactive", + "--anonymous", + "-d", + cwd, + ]); + expect(result.exitCode).toBe(0); + }); + }); +}); diff --git a/packages/cli/test/apply-install-plan.test.ts b/packages/cli/test/apply-install-plan.test.ts index 17b911ed..5c7bf8f8 100644 --- a/packages/cli/test/apply-install-plan.test.ts +++ b/packages/cli/test/apply-install-plan.test.ts @@ -58,76 +58,68 @@ afterEach(async () => { describe("applyInstallPlan", () => { it("writes selected skills to the target and records state", async () => { const skills = getEmbeddedSkills(); - const mandatoryCheck = skills.find((s) => s.name === "taskless-check")!; - const ci = skills.find((s) => s.name === "taskless-ci")!; + const taskless = skills.find((s) => s.name === "taskless")!; const result = await applyInstallPlan( cwd, { - targets: [ - { tool: TEST_TOOL, skills: [mandatoryCheck, ci], commands: [] }, - ], + targets: [{ tool: TEST_TOOL, skills: [taskless], commands: [] }], }, { cliVersion: "0.5.4" } ); - expect(result.writtenSkills).toHaveLength(2); + expect(result.writtenSkills).toHaveLength(1); expect(result.removedSkills).toHaveLength(0); - const checkContent = await readFile( - join(cwd, ".claude", "skills", "taskless-check", "SKILL.md"), + const skillContent = await readFile( + join(cwd, ".claude", "skills", "taskless", "SKILL.md"), "utf8" ); - expect(checkContent).toContain("taskless-check"); + expect(skillContent).toContain("taskless"); const state = await readInstallState(cwd); expect(state.cliVersion).toBe("0.5.4"); - expect(state.targets[".claude"]?.skills).toEqual([ - "taskless-check", - "taskless-ci", - ]); + expect(state.targets[".claude"]?.skills).toEqual(["taskless"]); }); - it("surgically removes skills on re-run that dropped one", async () => { + it("surgically removes obsolete skills recorded in the previous state", async () => { const skills = getEmbeddedSkills(); - const mandatoryCheck = skills.find((s) => s.name === "taskless-check")!; - const ci = skills.find((s) => s.name === "taskless-ci")!; - - await applyInstallPlan( - cwd, - { - targets: [ - { tool: TEST_TOOL, skills: [mandatoryCheck, ci], commands: [] }, - ], + const taskless = skills.find((s) => s.name === "taskless")!; + + // Seed manifest with a stale skill name (e.g. left over from a prior + // CLI version) AND a real one. We don't need the file on disk — the + // diff drives removals from the manifest, not the filesystem. + const { writeInstallState } = await import("../src/install/state"); + await writeInstallState(cwd, { + installedAt: "2026-04-01T00:00:00.000Z", + cliVersion: "0.5.4", + targets: { + ".claude": { + skills: ["taskless", "taskless-removed-fixture"], + commands: [], + }, }, - { cliVersion: "0.5.4" } - ); - expect( - await exists(join(cwd, ".claude", "skills", "taskless-ci", "SKILL.md")) - ).toBe(true); + }); const second = await applyInstallPlan( cwd, { - targets: [{ tool: TEST_TOOL, skills: [mandatoryCheck], commands: [] }], + targets: [{ tool: TEST_TOOL, skills: [taskless], commands: [] }], }, { cliVersion: "0.5.4" } ); expect(second.removedSkills).toEqual([ - { target: ".claude", skill: "taskless-ci" }, + { target: ".claude", skill: "taskless-removed-fixture" }, ]); - expect(await exists(join(cwd, ".claude", "skills", "taskless-ci"))).toBe( - false - ); expect( - await exists(join(cwd, ".claude", "skills", "taskless-check", "SKILL.md")) + await exists(join(cwd, ".claude", "skills", "taskless", "SKILL.md")) ).toBe(true); }); it("does not touch unknown files in the skills directory", async () => { const skills = getEmbeddedSkills(); - const mandatoryCheck = skills.find((s) => s.name === "taskless-check")!; + const taskless = skills.find((s) => s.name === "taskless")!; // User-owned file that the CLI must never delete const userOwned = join(cwd, ".claude", "skills", "user-tool", "SKILL.md"); @@ -139,7 +131,7 @@ describe("applyInstallPlan", () => { await applyInstallPlan( cwd, { - targets: [{ tool: TEST_TOOL, skills: [mandatoryCheck], commands: [] }], + targets: [{ tool: TEST_TOOL, skills: [taskless], commands: [] }], }, { cliVersion: "0.5.4" } ); @@ -149,10 +141,10 @@ describe("applyInstallPlan", () => { it("zero-diff re-run produces no removals", async () => { const skills = getEmbeddedSkills(); - const mandatoryCheck = skills.find((s) => s.name === "taskless-check")!; + const taskless = skills.find((s) => s.name === "taskless")!; const plan = { - targets: [{ tool: TEST_TOOL, skills: [mandatoryCheck], commands: [] }], + targets: [{ tool: TEST_TOOL, skills: [taskless], commands: [] }], }; await applyInstallPlan(cwd, plan, { cliVersion: "0.5.4" }); const second = await applyInstallPlan(cwd, plan, { cliVersion: "0.5.4" }); @@ -160,4 +152,98 @@ describe("applyInstallPlan", () => { expect(second.removedSkills).toHaveLength(0); expect(second.writtenSkills).toHaveLength(1); }); + + it("v0.6 → v0.7 migration removes 10 old skills + 6 old commands and writes the consolidated skill", async () => { + const skills = getEmbeddedSkills(); + const taskless = skills.find((s) => s.name === "taskless")!; + + // Seed manifest exactly as v0.6 would have left it. + const v6Skills = [ + "taskless-check", + "taskless-ci", + "taskless-create-rule", + "taskless-create-rule-anonymous", + "taskless-delete-rule", + "taskless-improve-rule", + "taskless-improve-rule-anonymous", + "taskless-info", + "taskless-login", + "taskless-logout", + ]; + const v6Commands = [ + "check.md", + "improve.md", + "info.md", + "login.md", + "logout.md", + "rule.md", + ]; + + // Create the actual files on disk so we can assert they're deleted. + const claudeSkills = join(cwd, ".claude", "skills"); + for (const name of v6Skills) { + await mkdir(join(claudeSkills, name), { recursive: true }); + await writeFile( + join(claudeSkills, name, "SKILL.md"), + "# stale v0.6 skill", + "utf8" + ); + } + const claudeCommands = join(cwd, ".claude", "commands", "tskl"); + await mkdir(claudeCommands, { recursive: true }); + for (const name of v6Commands) { + await writeFile(join(claudeCommands, name), "stale v0.6 command", "utf8"); + } + + // Record those installs in the manifest so the diff sees them as + // existing. + const { writeInstallState } = await import("../src/install/state"); + await writeInstallState(cwd, { + installedAt: "2026-04-17T00:00:00.000Z", + cliVersion: "0.6.0", + targets: { + ".claude": { skills: v6Skills, commands: v6Commands }, + }, + }); + + // Now run the v0.7 install plan: one skill (taskless), one command + // (tskl.md). The install should delete the 10 + 6 obsolete files and + // write the new ones. + const result = await applyInstallPlan( + cwd, + { + targets: [ + { + tool: TEST_TOOL, + skills: [taskless], + commands: [{ filename: "tskl.md", content: "# new command" }], + }, + ], + }, + { cliVersion: "0.7.0" } + ); + + expect(result.removedSkills).toHaveLength(10); + expect(result.removedCommands).toHaveLength(6); + expect(result.writtenSkills).toHaveLength(1); + expect(result.writtenCommands).toHaveLength(1); + + // Old files gone + for (const name of v6Skills) { + expect(await exists(join(claudeSkills, name))).toBe(false); + } + for (const name of v6Commands) { + expect(await exists(join(claudeCommands, name))).toBe(false); + } + + // New files present + expect(await exists(join(claudeSkills, "taskless", "SKILL.md"))).toBe(true); + expect(await exists(join(claudeCommands, "tskl.md"))).toBe(true); + + // Manifest reflects the new layout + const state = await readInstallState(cwd); + expect(state.cliVersion).toBe("0.7.0"); + expect(state.targets[".claude"]?.skills).toEqual(["taskless"]); + expect(state.targets[".claude"]?.commands).toEqual(["tskl.md"]); + }); }); diff --git a/packages/cli/test/cli.test.ts b/packages/cli/test/cli.test.ts index cd1383cd..0a252bae 100644 --- a/packages/cli/test/cli.test.ts +++ b/packages/cli/test/cli.test.ts @@ -77,19 +77,13 @@ describe("cli", () => { expect(stdout).toContain("Claude Code"); const skillContent = await readFile( - join( - temporaryDirectory, - ".claude", - "skills", - "taskless-info", - "SKILL.md" - ), + join(temporaryDirectory, ".claude", "skills", "taskless", "SKILL.md"), "utf8" ); - expect(skillContent).toContain("name: taskless-info"); + expect(skillContent).toContain("name: taskless"); const commandContent = await readFile( - join(temporaryDirectory, ".claude", "commands", "tskl", "info.md"), + join(temporaryDirectory, ".claude", "commands", "tskl", "tskl.md"), "utf8" ); expect(commandContent).toContain("Taskless"); diff --git a/packages/cli/test/error-envelope.test.ts b/packages/cli/test/error-envelope.test.ts new file mode 100644 index 00000000..488eed06 --- /dev/null +++ b/packages/cli/test/error-envelope.test.ts @@ -0,0 +1,233 @@ +import { execFile } from "node:child_process"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { promisify } from "node:util"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +const execFileAsync = promisify(execFile); +const binPath = resolve(import.meta.dirname, "../dist/index.js"); + +interface ErrorEnvelope { + ok: false; + code: string; + message: string; +} + +async function runCli( + args: string[] +): Promise<{ stdout: string; stderr: string; exitCode: number }> { + try { + const { stdout, stderr } = await execFileAsync("node", [binPath, ...args]); + return { stdout, stderr, exitCode: 0 }; + } catch (error) { + const execError = error as { + stdout: string; + stderr: string; + code: number; + }; + return { + stdout: execError.stdout ?? "", + stderr: execError.stderr ?? "", + exitCode: execError.code, + }; + } +} + +function parseEnvelope(stdout: string): ErrorEnvelope { + // The envelope is the last JSON line in stdout. (Some commands also + // print progress to stderr, so we ignore that.) + const lines = stdout.split("\n").filter((l) => l.trim().startsWith("{")); + expect(lines.length).toBeGreaterThan(0); + const last = lines.at(-1)!; + return JSON.parse(last) as ErrorEnvelope; +} + +describe("standardized error envelope (--json)", () => { + let cwd: string; + + beforeEach(async () => { + cwd = await mkdtemp(join(tmpdir(), "taskless-errors-")); + await mkdir(join(cwd, ".taskless"), { recursive: true }); + await writeFile( + join(cwd, ".taskless", "taskless.json"), + JSON.stringify({ + version: "2026-03-03", + orgId: 123, + repositoryUrl: "https://github.com/test/test", + }) + ); + }); + + afterEach(async () => { + await rm(cwd, { recursive: true, force: true }); + }); + + describe("rule create", () => { + it("emits INVALID_INPUT when --from is missing", async () => { + const result = await runCli(["rule", "create", "--json", "-d", cwd]); + expect(result.exitCode).not.toBe(0); + const env = parseEnvelope(result.stdout); + expect(env.ok).toBe(false); + expect(env.code).toBe("INVALID_INPUT"); + expect(env.message).toContain("--from"); + }); + + it("emits INVALID_INPUT when --from file does not exist", async () => { + const result = await runCli([ + "rule", + "create", + "--from", + "nonexistent.json", + "--json", + "-d", + cwd, + ]); + expect(result.exitCode).not.toBe(0); + const env = parseEnvelope(result.stdout); + expect(env.code).toBe("INVALID_INPUT"); + }); + + it("emits INVALID_INPUT when --from file is not valid JSON", async () => { + const badFile = join(cwd, "bad.json"); + await writeFile(badFile, "not json at all"); + const result = await runCli([ + "rule", + "create", + "--from", + badFile, + "--json", + "-d", + cwd, + ]); + expect(result.exitCode).not.toBe(0); + const env = parseEnvelope(result.stdout); + expect(env.code).toBe("INVALID_INPUT"); + }); + + it("emits INVALID_INPUT when --from file is missing the prompt field", async () => { + const file = join(cwd, "no-prompt.json"); + await writeFile(file, JSON.stringify({ language: "typescript" })); + const result = await runCli([ + "rule", + "create", + "--from", + file, + "--json", + "-d", + cwd, + ]); + expect(result.exitCode).not.toBe(0); + const env = parseEnvelope(result.stdout); + expect(env.code).toBe("INVALID_INPUT"); + }); + }); + + describe("rule improve", () => { + it("emits INVALID_INPUT when --from is missing", async () => { + const result = await runCli(["rule", "improve", "--json", "-d", cwd]); + expect(result.exitCode).not.toBe(0); + const env = parseEnvelope(result.stdout); + expect(env.code).toBe("INVALID_INPUT"); + }); + }); + + describe("rule meta", () => { + it("emits RULE_NOT_FOUND when metadata sidecar is missing", async () => { + const result = await runCli([ + "rule", + "meta", + "nonexistent-rule", + "--json", + "-d", + cwd, + ]); + expect(result.exitCode).not.toBe(0); + const env = parseEnvelope(result.stdout); + expect(env.code).toBe("RULE_NOT_FOUND"); + expect(env.message).toContain("nonexistent-rule"); + }); + }); + + describe("rule verify", () => { + it("emits INVALID_INPUT when no rule ID is provided", async () => { + const result = await runCli(["rule", "verify", "--json", "-d", cwd]); + expect(result.exitCode).not.toBe(0); + const env = parseEnvelope(result.stdout); + expect(env.code).toBe("INVALID_INPUT"); + expect(env.message).toContain("Rule ID is required"); + }); + }); + + describe("rule delete", () => { + it("emits RULE_NOT_FOUND when the rule file does not exist", async () => { + const result = await runCli([ + "rule", + "delete", + "nonexistent-rule", + "--json", + "-d", + cwd, + ]); + expect(result.exitCode).not.toBe(0); + const env = parseEnvelope(result.stdout); + expect(env.code).toBe("RULE_NOT_FOUND"); + expect(env.message).toContain("nonexistent-rule"); + }); + + it("is silent on stdout when a real rule is deleted in --json mode", async () => { + const rulesDirectory = join(cwd, ".taskless", "rules"); + await mkdir(rulesDirectory, { recursive: true }); + await writeFile( + join(rulesDirectory, "doomed.yml"), + "id: doomed\nlanguage: typescript\nseverity: error\nmessage: ''\nrule: { pattern: 'eval($X)' }\n" + ); + const result = await runCli([ + "rule", + "delete", + "doomed", + "--json", + "-d", + cwd, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout.trim()).toBe(""); + }); + }); + + describe("auth login", () => { + it("emits INVALID_INPUT when --anonymous is set", async () => { + const result = await runCli([ + "auth", + "login", + "--anonymous", + "--json", + "-d", + cwd, + ]); + expect(result.exitCode).not.toBe(0); + const env = parseEnvelope(result.stdout); + expect(env.code).toBe("INVALID_INPUT"); + expect(env.message).toContain("anonymous"); + }); + }); + + describe("auth logout", () => { + it("is silent on stdout in --json mode and exits 0", async () => { + const result = await runCli(["auth", "logout", "--json", "-d", cwd]); + expect(result.exitCode).toBe(0); + expect(result.stdout.trim()).toBe(""); + }); + }); + + describe("envelope shape", () => { + it("envelope has exactly the documented fields", async () => { + const result = await runCli(["rule", "create", "--json", "-d", cwd]); + const env = parseEnvelope(result.stdout); + expect(Object.keys(env).toSorted()).toEqual(["code", "message", "ok"]); + expect(env.ok).toBe(false); + expect(typeof env.code).toBe("string"); + expect(typeof env.message).toBe("string"); + }); + }); +}); diff --git a/packages/cli/test/help-extensions.test.ts b/packages/cli/test/help-extensions.test.ts new file mode 100644 index 00000000..099826db --- /dev/null +++ b/packages/cli/test/help-extensions.test.ts @@ -0,0 +1,167 @@ +import { execFile } from "node:child_process"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { promisify } from "node:util"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +const execFileAsync = promisify(execFile); +const binPath = resolve(import.meta.dirname, "../dist/index.js"); + +async function runCli( + args: string[] +): Promise<{ stdout: string; stderr: string; exitCode: number }> { + try { + const { stdout, stderr } = await execFileAsync("node", [binPath, ...args]); + return { stdout, stderr, exitCode: 0 }; + } catch (error) { + const execError = error as { + stdout: string; + stderr: string; + code: number; + }; + return { + stdout: execError.stdout ?? "", + stderr: execError.stderr ?? "", + exitCode: execError.code, + }; + } +} + +describe("taskless help (no args)", () => { + let cwd: string; + + beforeEach(async () => { + cwd = await mkdtemp(join(tmpdir(), "taskless-help-")); + }); + + afterEach(async () => { + await rm(cwd, { recursive: true, force: true }); + }); + + it("prints the human slug", async () => { + const result = await runCli(["help", "-d", cwd]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("For agents:"); + expect(result.stdout).toContain("For humans:"); + }); + + it("prints the topic table including all expected topics", async () => { + const result = await runCli(["help", "-d", cwd]); + expect(result.stdout).toContain("Topics:"); + for (const topic of ["init", "info", "check", "auth", "rule"]) { + expect(result.stdout).toContain(topic); + } + }); + + it("mentions the --anonymous flag", async () => { + const result = await runCli(["help", "-d", cwd]); + expect(result.stdout).toContain("--anonymous"); + }); +}); + +describe("taskless help ", () => { + let cwd: string; + + beforeEach(async () => { + cwd = await mkdtemp(join(tmpdir(), "taskless-help-topic-")); + }); + + afterEach(async () => { + await rm(cwd, { recursive: true, force: true }); + }); + + it("returns the canonical recipe for a known topic", async () => { + const result = await runCli(["help", "rule", "create", "-d", cwd]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("# Topic: rule create"); + expect(result.stdout).toContain("## Goal"); + expect(result.stdout).toContain("## Steps"); + }); + + it("interpolates {{CLI_VERSION}} in the recipe header", async () => { + const result = await runCli(["help", "rule", "create", "-d", cwd]); + // Should contain a version pattern, not the literal placeholder + expect(result.stdout).not.toContain("{{CLI_VERSION}}"); + expect(result.stdout).toMatch(/CLI v\d+\.\d+\.\d+/); + }); + + it("interpolates {{INPUT_SCHEMA}} for topics with a Zod input", async () => { + const result = await runCli(["help", "rule", "create", "-d", cwd]); + expect(result.stdout).not.toContain("{{INPUT_SCHEMA}}"); + // Embedded schema includes the JSON Schema $schema URI + expect(result.stdout).toContain('"$schema"'); + expect(result.stdout).toContain('"prompt"'); + expect(result.stdout).toContain('"successCases"'); + }); + + it("exits 1 for an unknown topic", async () => { + const result = await runCli(["help", "totally-unknown", "-d", cwd]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Unknown command"); + }); +}); + +describe("taskless help --anonymous (variant lookup)", () => { + let cwd: string; + + beforeEach(async () => { + cwd = await mkdtemp(join(tmpdir(), "taskless-help-anon-")); + }); + + afterEach(async () => { + await rm(cwd, { recursive: true, force: true }); + }); + + it("returns the .anonymous variant when one exists (rule create)", async () => { + const result = await runCli([ + "help", + "rule", + "create", + "--anonymous", + "-d", + cwd, + ]); + expect(result.exitCode).toBe(0); + // The anonymous recipe declares "(anonymous)" in its header + expect(result.stdout).toContain("# Topic: rule create (anonymous)"); + }); + + it("returns the .anonymous variant for rule improve", async () => { + const result = await runCli([ + "help", + "rule", + "improve", + "--anonymous", + "-d", + cwd, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("# Topic: rule improve (anonymous)"); + }); + + it("falls back to the canonical recipe when no variant exists (check)", async () => { + const canonical = await runCli(["help", "check", "-d", cwd]); + const anonymous = await runCli(["help", "check", "--anonymous", "-d", cwd]); + expect(anonymous.exitCode).toBe(0); + // Same body — falls back to check.txt since no check.anonymous.txt + expect(anonymous.stdout).toBe(canonical.stdout); + }); + + it("returns the canonical recipe when --anonymous is omitted", async () => { + const result = await runCli(["help", "rule", "create", "-d", cwd]); + expect(result.stdout).toContain("# Topic: rule create"); + expect(result.stdout).not.toContain("(anonymous)"); + }); +}); + +describe("bare taskless (non-TTY) routes to help index", () => { + it("prints the non-interactive preamble + topic index", async () => { + // execFile gives no TTY, which triggers the routing. No flags so + // citty doesn't try to forward them to the help subcommand. + const result = await runCli([]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toContain("non-interactive context detected"); + expect(result.stdout).toContain("Topics:"); + }); +}); diff --git a/packages/cli/test/init-no-interactive.test.ts b/packages/cli/test/init-no-interactive.test.ts index 9aa0c307..622596b5 100644 --- a/packages/cli/test/init-no-interactive.test.ts +++ b/packages/cli/test/init-no-interactive.test.ts @@ -28,7 +28,7 @@ describe("taskless init --no-interactive", () => { await rm(cwd, { recursive: true, force: true }); }); - it("installs mandatory skills to detected tools without any prompt", async () => { + it("installs the consolidated skill to detected tools without any prompt", async () => { await mkdir(join(cwd, ".claude"), { recursive: true }); const { stdout } = await execFileAsync("node", [ @@ -41,28 +41,11 @@ describe("taskless init --no-interactive", () => { expect(stdout).toContain("Claude Code: installed"); - // Mandatory skill present expect( - await exists(join(cwd, ".claude", "skills", "taskless-check", "SKILL.md")) + await exists(join(cwd, ".claude", "skills", "taskless", "SKILL.md")) ).toBe(true); }); - it("does NOT install optional skills (taskless-ci) in --no-interactive", async () => { - await mkdir(join(cwd, ".claude"), { recursive: true }); - - await execFileAsync("node", [ - binPath, - "init", - "--no-interactive", - "-d", - cwd, - ]); - - expect( - await exists(join(cwd, ".claude", "skills", "taskless-ci", "SKILL.md")) - ).toBe(false); - }); - it("falls back to .agents/ when no tools are detected", async () => { const { stdout } = await execFileAsync("node", [ binPath, @@ -74,7 +57,7 @@ describe("taskless init --no-interactive", () => { expect(stdout).toContain("No tools detected. Using fallback: .agents/"); expect( - await exists(join(cwd, ".agents", "skills", "taskless-check", "SKILL.md")) + await exists(join(cwd, ".agents", "skills", "taskless", "SKILL.md")) ).toBe(true); }); @@ -111,6 +94,36 @@ describe("taskless init --no-interactive", () => { expect(stdout).toContain("Claude Code: installed"); }); + it("`taskless update` runs the same non-interactive install path", async () => { + await mkdir(join(cwd, ".claude"), { recursive: true }); + + const { stdout } = await execFileAsync("node", [ + binPath, + "update", + "-d", + cwd, + ]); + + expect(stdout).toContain("Claude Code: installed"); + expect( + await exists(join(cwd, ".claude", "skills", "taskless", "SKILL.md")) + ).toBe(true); + }); + + it("`taskless update` falls back to .agents/ when no tools are detected", async () => { + const { stdout } = await execFileAsync("node", [ + binPath, + "update", + "-d", + cwd, + ]); + + expect(stdout).toContain("No tools detected. Using fallback: .agents/"); + expect( + await exists(join(cwd, ".agents", "skills", "taskless", "SKILL.md")) + ).toBe(true); + }); + it("writes taskless.json with install state recorded", async () => { await mkdir(join(cwd, ".claude"), { recursive: true }); diff --git a/packages/cli/test/rules-from.test.ts b/packages/cli/test/rule-from.test.ts similarity index 97% rename from packages/cli/test/rules-from.test.ts rename to packages/cli/test/rule-from.test.ts index a6e599dd..88d2e750 100644 --- a/packages/cli/test/rules-from.test.ts +++ b/packages/cli/test/rule-from.test.ts @@ -33,7 +33,7 @@ describe("rules create --from", () => { try { await execFileAsync("node", [ binPath, - "rules", + "rule", "create", "-d", temporaryDirectory, @@ -49,7 +49,7 @@ describe("rules create --from", () => { try { await execFileAsync("node", [ binPath, - "rules", + "rule", "create", "--from", "nonexistent.json", @@ -70,7 +70,7 @@ describe("rules create --from", () => { try { await execFileAsync("node", [ binPath, - "rules", + "rule", "create", "--from", badFile, @@ -91,7 +91,7 @@ describe("rules create --from", () => { try { await execFileAsync("node", [ binPath, - "rules", + "rule", "create", "--from", noPromptFile, diff --git a/packages/cli/test/schema.test.ts b/packages/cli/test/schema.test.ts deleted file mode 100644 index 1f22dd12..00000000 --- a/packages/cli/test/schema.test.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { execFile } from "node:child_process"; -import { mkdtemp, rm } from "node:fs/promises"; -import { resolve, join } from "node:path"; -import { tmpdir } from "node:os"; -import { promisify } from "node:util"; -import { describe, expect, it, beforeEach, afterEach } from "vitest"; - -const execFileAsync = promisify(execFile); -const binPath = resolve(import.meta.dirname, "../dist/index.js"); - -/** Run the CLI and capture output, allowing non-zero exit codes */ -async function runCli( - args: string[] -): Promise<{ stdout: string; stderr: string; exitCode: number }> { - try { - const { stdout, stderr } = await execFileAsync("node", [binPath, ...args]); - return { stdout, stderr, exitCode: 0 }; - } catch (error) { - const execError = error as { - stdout: string; - stderr: string; - code: number; - }; - return { - stdout: execError.stdout ?? "", - stderr: execError.stderr ?? "", - exitCode: execError.code, - }; - } -} - -/** - * Parse the three schema blocks from --schema output. - * Returns { input, output, error } where each is either a parsed JSON object or a string message. - */ -function parseSchemaOutput(stdout: string): { - input: Record | string; - output: Record; - error: Record; -} { - const sections = stdout.split(/\n\n/); - const result: Record = {}; - - for (const section of sections) { - const lines = section.trim(); - if (lines.startsWith("Input Schema:")) { - const body = lines.replace("Input Schema:", "").trim(); - try { - result.input = JSON.parse(body) as Record; - } catch { - result.input = body; - } - } else if (lines.startsWith("Output Schema:")) { - const body = lines.replace("Output Schema:", "").trim(); - result.output = JSON.parse(body) as Record; - } else if (lines.startsWith("Error Schema:")) { - const body = lines.replace("Error Schema:", "").trim(); - result.error = JSON.parse(body) as Record; - } - } - - return result as { - input: Record | string; - output: Record; - error: Record; - }; -} - -describe("--schema flag", () => { - let temporaryDirectory: string; - - beforeEach(async () => { - temporaryDirectory = await mkdtemp(join(tmpdir(), "taskless-schema-")); - }); - - afterEach(async () => { - await rm(temporaryDirectory, { recursive: true, force: true }); - }); - - describe("rules create --schema", () => { - it("exits 0 and prints three schema blocks", async () => { - const { stdout, exitCode } = await runCli([ - "rules", - "create", - "--schema", - ]); - expect(exitCode).toBe(0); - expect(stdout).toContain("Input Schema:"); - expect(stdout).toContain("Output Schema:"); - expect(stdout).toContain("Error Schema:"); - }); - - it("has a valid input schema with prompt as required", async () => { - const { stdout } = await runCli(["rules", "create", "--schema"]); - const schemas = parseSchemaOutput(stdout); - - expect(typeof schemas.input).toBe("object"); - const input = schemas.input as Record; - expect(input.type).toBe("object"); - - const properties = input.properties as Record; - expect(properties).toHaveProperty("prompt"); - expect(properties).toHaveProperty("successCases"); - expect(properties).toHaveProperty("failureCases"); - - const required = input.required as string[]; - expect(required).toContain("prompt"); - }); - - it("has a valid output schema with success, ruleId, rules, files", async () => { - const { stdout } = await runCli(["rules", "create", "--schema"]); - const schemas = parseSchemaOutput(stdout); - - const output = schemas.output; - expect(output.type).toBe("object"); - - const properties = output.properties as Record; - expect(properties).toHaveProperty("success"); - expect(properties).toHaveProperty("ruleId"); - expect(properties).toHaveProperty("rules"); - expect(properties).toHaveProperty("files"); - }); - - it("has a valid error schema with error field", async () => { - const { stdout } = await runCli(["rules", "create", "--schema"]); - const schemas = parseSchemaOutput(stdout); - - const error = schemas.error; - expect(error.type).toBe("object"); - - const properties = error.properties as Record; - expect(properties).toHaveProperty("error"); - }); - - it("does not require auth or config", async () => { - // Run in a directory with no .taskless/ — should still work - const { exitCode } = await runCli([ - "rules", - "create", - "--schema", - "-d", - temporaryDirectory, - ]); - expect(exitCode).toBe(0); - }); - - it("ignores --from when --schema is passed", async () => { - const { exitCode, stdout } = await runCli([ - "rules", - "create", - "--schema", - "--from", - "nonexistent.json", - ]); - expect(exitCode).toBe(0); - expect(stdout).toContain("Input Schema:"); - }); - }); - - describe("rules improve --schema", () => { - it("exits 0 and prints three schema blocks", async () => { - const { stdout, exitCode } = await runCli([ - "rules", - "improve", - "--schema", - ]); - expect(exitCode).toBe(0); - expect(stdout).toContain("Input Schema:"); - expect(stdout).toContain("Output Schema:"); - expect(stdout).toContain("Error Schema:"); - }); - - it("has input schema requiring ruleId and guidance", async () => { - const { stdout } = await runCli(["rules", "improve", "--schema"]); - const schemas = parseSchemaOutput(stdout); - - const input = schemas.input as Record; - const properties = input.properties as Record; - expect(properties).toHaveProperty("ruleId"); - expect(properties).toHaveProperty("guidance"); - expect(properties).toHaveProperty("references"); - - const required = input.required as string[]; - expect(required).toContain("ruleId"); - expect(required).toContain("guidance"); - }); - }); - - describe("rules meta --schema", () => { - it("exits 0 and prints schema blocks", async () => { - const { stdout, exitCode } = await runCli([ - "rules", - "meta", - "dummy-id", - "--schema", - ]); - expect(exitCode).toBe(0); - expect(stdout).toContain("Input Schema:"); - expect(stdout).toContain("Output Schema:"); - expect(stdout).toContain("Error Schema:"); - }); - - it("has no JSON input schema", async () => { - const { stdout } = await runCli([ - "rules", - "meta", - "dummy-id", - "--schema", - ]); - const schemas = parseSchemaOutput(stdout); - - expect(typeof schemas.input).toBe("string"); - expect(schemas.input).toContain("does not accept JSON input"); - }); - - it("has output schema with id, ticketId, generatedAt, schemaVersion", async () => { - const { stdout } = await runCli([ - "rules", - "meta", - "dummy-id", - "--schema", - ]); - const schemas = parseSchemaOutput(stdout); - - const output = schemas.output; - expect(output.type).toBe("object"); - - const properties = output.properties as Record; - expect(properties).toHaveProperty("id"); - expect(properties).toHaveProperty("ticketId"); - expect(properties).toHaveProperty("generatedAt"); - expect(properties).toHaveProperty("schemaVersion"); - }); - - it("has a valid error schema with error field", async () => { - const { stdout } = await runCli([ - "rules", - "meta", - "dummy-id", - "--schema", - ]); - const schemas = parseSchemaOutput(stdout); - - const error = schemas.error; - expect(error.type).toBe("object"); - - const properties = error.properties as Record; - expect(properties).toHaveProperty("error"); - }); - - it("does not require auth or config", async () => { - const { exitCode } = await runCli([ - "rules", - "meta", - "dummy-id", - "--schema", - "-d", - temporaryDirectory, - ]); - expect(exitCode).toBe(0); - }); - }); - - describe("check --schema", () => { - it("exits 0 and prints three schema blocks", async () => { - const { stdout, exitCode } = await runCli(["check", "--schema"]); - expect(exitCode).toBe(0); - expect(stdout).toContain("Input Schema:"); - expect(stdout).toContain("Output Schema:"); - expect(stdout).toContain("Error Schema:"); - }); - - it("has no JSON input schema", async () => { - const { stdout } = await runCli(["check", "--schema"]); - const schemas = parseSchemaOutput(stdout); - - expect(typeof schemas.input).toBe("string"); - expect(schemas.input).toContain("does not accept JSON input"); - }); - - it("has output schema with success and results", async () => { - const { stdout } = await runCli(["check", "--schema"]); - const schemas = parseSchemaOutput(stdout); - - const output = schemas.output; - expect(output.type).toBe("object"); - - const properties = output.properties as Record; - expect(properties).toHaveProperty("success"); - expect(properties).toHaveProperty("results"); - }); - - it("does not require .taskless/ directory", async () => { - const { exitCode } = await runCli([ - "check", - "--schema", - "-d", - temporaryDirectory, - ]); - expect(exitCode).toBe(0); - }); - }); -}); diff --git a/packages/cli/test/verify.test.ts b/packages/cli/test/verify.test.ts index 0a29c43e..fcddff85 100644 --- a/packages/cli/test/verify.test.ts +++ b/packages/cli/test/verify.test.ts @@ -1,17 +1,11 @@ import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; -import { resolve } from "node:path"; import { describe, expect, it, beforeEach, afterEach } from "vitest"; import { stringify } from "yaml"; import { verifyRule, getSchemaPayload } from "../src/rules/verify"; -const execFileAsync = promisify(execFile); -const CLI_PATH = resolve(import.meta.dirname, "..", "dist", "index.js"); - describe("verifyRule", () => { let temporaryDirectory: string; @@ -350,19 +344,3 @@ describe("getSchemaPayload", () => { ).toBe(true); }); }); - -describe("rules verify CLI", () => { - it("--schema --json outputs valid JSON with expected keys", async () => { - const { stdout } = await execFileAsync("node", [ - CLI_PATH, - "rules", - "verify", - "--schema", - "--json", - ]); - const payload = JSON.parse(stdout) as Record; - expect(payload).toHaveProperty("astGrepSchema"); - expect(payload).toHaveProperty("tasklessRequirements"); - expect(payload).toHaveProperty("examples"); - }); -}); diff --git a/packages/cli/test/wizard-integration.test.ts b/packages/cli/test/wizard-integration.test.ts index 73877d75..41b1c2aa 100644 --- a/packages/cli/test/wizard-integration.test.ts +++ b/packages/cli/test/wizard-integration.test.ts @@ -15,7 +15,6 @@ const fakeCancelSymbol = Symbol("cancel"); // Clack mock responses are set per-test via these mutable refs. const clackResponses: { locations?: string[] | symbol; - optionalSkills?: string[] | symbol; auth?: boolean | symbol; summary?: boolean | symbol; } = {}; @@ -32,12 +31,7 @@ vi.mock("@clack/prompts", () => ({ }, note: () => {}, isCancel: (value: unknown) => value === fakeCancelSymbol, - multiselect: vi.fn(({ message }: { message: string }) => { - if (message.toLowerCase().includes("install")) { - return Promise.resolve(clackResponses.locations); - } - return Promise.resolve(clackResponses.optionalSkills); - }), + multiselect: vi.fn(() => Promise.resolve(clackResponses.locations)), confirm: vi.fn(({ message }: { message: string }) => { if (message.toLowerCase().includes("log in")) { return Promise.resolve(clackResponses.auth); @@ -62,7 +56,6 @@ beforeEach(async () => { await mkdir(join(cwd, ".claude"), { recursive: true }); captureSpy.mockClear(); clackResponses.locations = undefined; - clackResponses.optionalSkills = undefined; clackResponses.auth = undefined; clackResponses.summary = undefined; vi.stubEnv("TASKLESS_TOKEN", "stub-token"); @@ -75,9 +68,8 @@ afterEach(async () => { }); describe("runWizard end-to-end", () => { - it("installs selected location + optional skill and records manifest", async () => { + it("installs all bundled skills to selected location and records manifest", async () => { clackResponses.locations = [".claude"]; - clackResponses.optionalSkills = ["taskless-ci"]; clackResponses.summary = true; const { runWizard } = await import("../src/wizard"); @@ -85,13 +77,10 @@ describe("runWizard end-to-end", () => { expect(result.status).toBe("completed"); expect(result.locations).toEqual([".claude"]); - expect(result.optionalSkills).toEqual(["taskless-ci"]); + expect(result.optionalSkills).toEqual([]); expect( - await exists(join(cwd, ".claude", "skills", "taskless-check", "SKILL.md")) - ).toBe(true); - expect( - await exists(join(cwd, ".claude", "skills", "taskless-ci", "SKILL.md")) + await exists(join(cwd, ".claude", "skills", "taskless", "SKILL.md")) ).toBe(true); const manifest = JSON.parse( @@ -100,45 +89,33 @@ describe("runWizard end-to-end", () => { version: number; install: { targets: Record }; }; - expect(manifest.install.targets[".claude"]?.skills).toContain( - "taskless-check" - ); - expect(manifest.install.targets[".claude"]?.skills).toContain( - "taskless-ci" - ); + expect(manifest.install.targets[".claude"]?.skills).toContain("taskless"); expect(captureSpy).toHaveBeenCalledWith( "cli_init_completed", expect.objectContaining({ locations: [".claude"], - optionalSkills: ["taskless-ci"], + optionalSkills: [], nonInteractive: false, }) ); }); - it("surgically removes a previously-installed optional skill on re-run", async () => { + it("re-running with the same location is idempotent", async () => { clackResponses.locations = [".claude"]; - clackResponses.optionalSkills = ["taskless-ci"]; clackResponses.summary = true; const { runWizard } = await import("../src/wizard"); await runWizard({ cwd }); expect( - await exists(join(cwd, ".claude", "skills", "taskless-ci", "SKILL.md")) + await exists(join(cwd, ".claude", "skills", "taskless", "SKILL.md")) ).toBe(true); - // Re-run: drop taskless-ci - clackResponses.optionalSkills = []; - clackResponses.summary = true; + // Re-run with the same selection — no diff, should complete cleanly. await runWizard({ cwd }); - - expect(await exists(join(cwd, ".claude", "skills", "taskless-ci"))).toBe( - false - ); - expect(await exists(join(cwd, ".claude", "skills", "taskless-check"))).toBe( - true - ); + expect( + await exists(join(cwd, ".claude", "skills", "taskless", "SKILL.md")) + ).toBe(true); }); it("cancelling at locations step writes nothing and emits cli_init_cancelled", async () => { @@ -150,7 +127,7 @@ describe("runWizard end-to-end", () => { expect(result.status).toBe("cancelled"); expect(result.cancelledStep).toBe("locations"); - expect(await exists(join(cwd, ".claude", "skills", "taskless-check"))).toBe( + expect(await exists(join(cwd, ".claude", "skills", "taskless"))).toBe( false ); expect(await exists(join(cwd, ".taskless", "taskless.json"))).toBe(false); @@ -163,57 +140,30 @@ describe("runWizard end-to-end", () => { it("cancelling the summary confirm writes nothing", async () => { clackResponses.locations = [".claude"]; - clackResponses.optionalSkills = ["taskless-ci"]; - // Simulate previous install of taskless-ci so the re-run has removals - // that trigger the summary confirm. clackResponses.summary = false; - const { runWizard, applyInstallPlan } = await import("../src/wizard").then( - async () => { - const wizard = await import("../src/wizard"); - const install = await import("../src/install/install"); - return { - runWizard: wizard.runWizard, - applyInstallPlan: install.applyInstallPlan, - }; - } - ); - - // Seed an earlier install so the diff has removals on the next wizard run + // Seed install state with a stale skill name that is no longer in the + // bundle so the next wizard run computes a removal and shows the + // summary confirm. We don't actually need the file on disk — the diff + // computation reads the manifest, not the filesystem. const { ensureTasklessDirectory } = await import("../src/filesystem/directory"); await ensureTasklessDirectory(cwd); - const { getEmbeddedSkills } = await import("../src/install/install"); - const skills = getEmbeddedSkills(); - await applyInstallPlan( - cwd, - { - targets: [ - { - tool: { - name: "Claude Code", - detect: [{ type: "directory", path: ".claude" }], - installDir: ".claude", - skills: { path: "skills" }, - commands: { path: "commands/tskl" }, - }, - skills: skills.filter((s) => s.name === "taskless-ci"), - commands: [], - }, - ], + const { writeInstallState } = await import("../src/install/state"); + await writeInstallState(cwd, { + installedAt: "2026-05-10T00:00:00.000Z", + cliVersion: "0.5.4", + targets: { + ".claude": { + skills: ["taskless-removed-fixture-skill"], + commands: [], + }, }, - { cliVersion: "0.5.4" } - ); + }); - // Now run wizard dropping taskless-ci → removal → summary confirms=false - clackResponses.optionalSkills = []; + const { runWizard } = await import("../src/wizard"); const result = await runWizard({ cwd }); expect(result.status).toBe("cancelled"); expect(result.cancelledStep).toBe("summary"); - - // taskless-ci should still be present (no write happened) - expect( - await exists(join(cwd, ".claude", "skills", "taskless-ci", "SKILL.md")) - ).toBe(true); }); }); diff --git a/scripts/generate-commands.ts b/scripts/generate-commands.ts deleted file mode 100644 index 7e4249af..00000000 --- a/scripts/generate-commands.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { - readFileSync, - writeFileSync, - mkdirSync, - readdirSync, - rmSync, -} from "node:fs"; -import { join, resolve } from "node:path"; -import { parse, stringify } from "yaml"; - -const ROOT = resolve(import.meta.dirname, ".."); -const SKILLS_DIR = join(ROOT, "skills"); -const COMMANDS_DIR = join(ROOT, "commands"); -const PREFIX = "taskless-"; - -interface Frontmatter { - data: Record; - content: string; -} - -const FRONTMATTER_REGEX = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/; - -function parseFrontmatter(source: string): Frontmatter { - const match = FRONTMATTER_REGEX.exec(source); - if (!match) return { data: {}, content: source }; - return { - data: (parse(match[1] ?? "") ?? {}) as Record, - content: match[2] ?? "", - }; -} - -function stringifyFrontmatter( - body: string, - data: Record -): string { - return `---\n${stringify(data, { lineWidth: 0 })}---\n${body}`; -} - -function titleCase(value: string): string { - return value - .split("-") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" "); -} - -// Read all skill directories -const skillDirectories = readdirSync(SKILLS_DIR, { withFileTypes: true }) - .filter((d) => d.isDirectory() && d.name.startsWith(PREFIX)) - .map((d) => d.name); - -if (skillDirectories.length === 0) { - console.log("No skills found."); - throw new Error("No skills found matching prefix"); -} - -// Clean commands directory (remove generated subdirectories) -rmSync(join(COMMANDS_DIR, "tskl"), { recursive: true, force: true }); - -let generated = 0; - -for (const directory of skillDirectories) { - const skillPath = join(SKILLS_DIR, directory, "SKILL.md"); - - let raw: string; - try { - raw = readFileSync(skillPath, "utf8"); - } catch { - console.warn(`Warning: Could not read ${skillPath}, skipping.`); - continue; - } - - const parsed = parseFrontmatter(raw); - const data = parsed.data as { - name?: string; - description?: string; - metadata?: Record; - }; - - const commandName = data.metadata?.commandName; - - // Skip skills without a command (commandName is "-" or missing) - if (!commandName || commandName === "-") { - console.log(` ${directory}: skipped (no command)`); - continue; - } - - // commandName is "namespace:name" (e.g. "taskless:info") → commands/taskless/info.md - const parts = commandName.split(":"); - const displayName = parts.at(-1) ?? commandName; - const commandPath = join( - COMMANDS_DIR, - ...parts.slice(0, -1), - `${displayName}.md` - ); - - // Build command frontmatter - const commandData: Record = { - name: `Taskless: ${titleCase(displayName)}`, - description: data.description ?? "", - category: "Taskless", - tags: ["taskless"], - }; - - if (data.metadata) { - commandData.metadata = data.metadata; - } - - const commandContent = stringifyFrontmatter(parsed.content, commandData); - mkdirSync(join(COMMANDS_DIR, ...parts.slice(0, -1)), { recursive: true }); - writeFileSync(commandPath, commandContent, "utf8"); - generated++; - console.log(` ${commandPath}`); -} - -console.log(`\nGenerated ${String(generated)} command(s).`); diff --git a/skills/taskless-check/SKILL.md b/skills/taskless-check/SKILL.md deleted file mode 100644 index 0988ea94..00000000 --- a/skills/taskless-check/SKILL.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: taskless-check -description: Checks a repository using the Taskless rules via the CLI. Use when the user wants to run a check, test rules, or validate code against taskless rules. Trigger on "check my code", "run taskless check", "test my rules", or "validate with taskless". -metadata: - author: taskless - version: 0.6.0 - commandName: tskl:check -compatibility: Designed for Agents implementing the Agent Skills specification. ---- - -# Taskless Check - -When this skill is invoked, perform a check of the codebase using the Taskless CLI and report the results. - -## Instructions - -**Package manager:** All commands below use `npx` as the default. If the project uses a different package manager (check for `pnpm-lock.yaml`, `yarn.lock`, or `bun.lockb`), prefer its equivalent: `pnpm dlx`, `yarn dlx` (Yarn Berry/2+ only), or `bunx`. - -1. **Read current command documentation.** Run `npx @taskless/cli@latest help check` and read the output. Use this to understand the command's options, output format, and exit codes. - -2. **Invoke the CLI with JSON output.** Run `npx @taskless/cli@latest check --json` and capture stdout. - -3. **Parse the response.** Parse the JSON output with `JSON.parse()`. Use the fields described in the help output to determine success or failure and report any issues found to the user. - -4. **Handle errors.** If the command exits with a non-zero code or the output is not valid JSON, report the error and suggest running `npx @taskless/cli@latest init` if configuration is missing. diff --git a/skills/taskless-ci/SKILL.md b/skills/taskless-ci/SKILL.md deleted file mode 100644 index c4ff165a..00000000 --- a/skills/taskless-ci/SKILL.md +++ /dev/null @@ -1,186 +0,0 @@ ---- -name: taskless-ci -description: Integrates Taskless into a developer's CI environment. Use when the user wants to set up Taskless in their CI pipeline, wire up automated rule checks on pull requests, or scaffold CI configuration for Taskless. Trigger on "set up CI", "add taskless to CI", "taskless in GitHub Actions", "run taskless on PRs", or "wire up CI for taskless". -metadata: - author: taskless - version: 0.6.0 - commandName: "-" -compatibility: Designed for Agents implementing the Agent Skills specification. ---- - -# Taskless CI - -When this skill is invoked, help the user wire `taskless check` into their existing CI system so Taskless rules run automatically on pushes and pull requests. - -Your goal is to **integrate with what the user already has** — not replace it. Discover the CI they use, agree on an approach, generate a minimal standalone config. Their existing pipelines stay untouched. - -This skill teaches two patterns (**full scan** and **diff scan**) that translate to any CI system. Common systems are listed below as hints, but if you recognize a CI system not on that list, apply the same patterns to it — the shape of the job is the same everywhere. - -## Instructions - -**Package manager:** All commands below use `npx` as the default. If the project uses a different package manager (check for `pnpm-lock.yaml`, `yarn.lock`, or `bun.lockb`), prefer its equivalent in generated config: `pnpm dlx`, `yarn dlx` (Yarn Berry/2+ only), or `bunx`. - -### 1. Discover the user's CI system - -Scan the repo root for CI config files. Common hints (not exhaustive — other CI systems use similar file/dir conventions): - -- `.github/workflows/*.yml` — **GitHub Actions** -- `.gitlab-ci.yml` — **GitLab CI** -- `.circleci/config.yml` — **CircleCI** -- `Jenkinsfile` — **Jenkins** -- `azure-pipelines.yml` or `.azure-pipelines.yml` — **Azure Pipelines** -- `bitbucket-pipelines.yml` — **Bitbucket Pipelines** -- `.buildkite/` — **Buildkite** -- `.drone.yml` — **Drone** -- `.travis.yml` — **Travis CI** -- Any other config file in the root that obviously belongs to a CI system you recognize - -**Sum up what you found briefly** (e.g., "I see GitHub Actions in `.github/workflows/ci.yml`") and confirm with the user. If zero signals match, ask which CI they use. If multiple match, ask which one should run Taskless — one is usually enough. - -### 2. Agree on the scan pattern - -There are two building blocks. The generated config uses one, the other, or both: - -- **Full scan** — `taskless check`. Scans the entire repo. Simplest. Best for small-to-medium codebases and for runs on the main/default branch (catches anything a diff might miss). -- **Diff scan** — `taskless check `. Scans only files in the current diff. Faster on large codebases. Best for pull request runs where feedback speed matters. - -Compute the diff using whatever the target CI system exposes. Every CI provides a way: - -- **GitHub Actions**: `github.base_ref` for PRs, `origin/$GITHUB_BASE_REF...HEAD` as the diff target -- **GitLab CI**: `CI_MERGE_REQUEST_TARGET_BRANCH_NAME` for MRs -- **CircleCI**: `CIRCLE_BRANCH` + a fetched `main` (CircleCI doesn't expose a base ref directly; fetch main and diff against it) -- **Jenkins**: `env.CHANGE_TARGET` for PR builds (multibranch pipeline) -- **Azure Pipelines**: `System.PullRequest.TargetBranch` for PR builds -- **Bitbucket Pipelines**: `BITBUCKET_PR_DESTINATION_BRANCH` for PR builds -- Any other system: look up the user's CI docs for "base branch" / "target branch" — every CI exposes this - -Because `taskless check` silently ignores paths that don't exist, you can pipe raw `git diff --name-only` output straight into it without filtering deleted files. - -**Recommended default** (offer this unless the user has a reason to override): diff scan on PRs, full scan on pushes to the main/default branch. Catches regressions on main while keeping PR feedback fast. - -### 3. Verify `taskless check` works locally before writing CI - -**Don't skip this.** A green local check is the cheapest signal the CI config will work. Run: - -``` -npx @taskless/cli@latest check -``` - -- Clean pass → proceed. -- `"No rules configured"` → stop and invoke `taskless-create-rule` (or ask the user to create rules first). Wiring CI with no rules produces a CI check that's always green and gives false confidence — a footgun. -- Non-zero with actual matches → tell the user CI will fail on these. Ask if they want to fix, suppress, or set up CI knowing the first run will be red. - -### 4. Read current CLI help - -``` -npx @taskless/cli@latest help check -``` - -Use this to confirm current flags and usage before embedding them in a config file that will stick around. - -### 5. Generate the config - -**Rules for generation:** - -1. **Write a new, standalone file. Never modify an existing CI file the user owns.** If they regret Taskless, removing one file is easier than unwinding edits to their main pipeline. -2. **Prefer the CI system's native include/import mechanism.** If the CI system supports one, write a standalone snippet and tell the user the single line they need to add to their main config. If it doesn't (Bitbucket, some uses of CircleCI), write the snippet to `.taskless/ci/` and give the user explicit instructions on where to paste it. -3. **Before writing**, check if the target file already exists. If it does, **ask before overwriting**. Never silently replace their work. - -**Canonical path for each CI system:** - -- GitHub Actions → `.github/workflows/taskless.yml` (standalone workflow, no include needed) -- GitLab CI → `.taskless/ci/gitlab.yml` (user adds `include: { local: '.taskless/ci/gitlab.yml' }` to their root `.gitlab-ci.yml`) -- CircleCI → `.taskless/ci/circleci-job.yml` (CircleCI doesn't support include; user copies the job + workflow entry into their `.circleci/config.yml`) -- Jenkins → `.taskless/ci/taskless.Jenkinsfile` (user loads via `load()` or copies the `stage` into their Jenkinsfile) -- Azure Pipelines → `.taskless/ci/azure-taskless.yml` (user references via `template:` under a job's `steps:`) -- Bitbucket Pipelines → `.taskless/ci/bitbucket-pipelines.yml` (Bitbucket doesn't support include; user merges into their root `bitbucket-pipelines.yml`) -- Any other CI system: use the same idea — a standalone file under `.taskless/ci/` with whatever `.` extension is idiomatic, plus clear instructions for wiring it up. - -### 6. Config templates (two patterns, everything else is translation) - -The template below is **GitHub Actions** — the most common and self-contained case. It demonstrates both the full-scan and diff-scan blocks. For any other CI system, translate the same structure: checkout with full history, set up Node, run the check command conditionally based on whether the build is for a PR or a push. - -Substitute `{{PACKAGE_MANAGER_DLX}}` with the user's package manager invocation: `npx @taskless/cli@latest`, `pnpm dlx @taskless/cli@latest`, `yarn dlx @taskless/cli@latest`, or `bunx @taskless/cli@latest`. - -**`.github/workflows/taskless.yml`**: - -```yaml -name: Taskless - -on: - push: - branches: [main] - pull_request: - -permissions: - contents: read - -jobs: - check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Taskless check - run: | - if [ "${{ github.event_name }}" = "pull_request" ]; then - git fetch origin "${{ github.base_ref }}" --depth=1 - FILES=$(git diff --name-only "origin/${{ github.base_ref }}...HEAD") - if [ -z "$FILES" ]; then - echo "No changed files." - exit 0 - fi - {{PACKAGE_MANAGER_DLX}} check $FILES - else - {{PACKAGE_MANAGER_DLX}} check - fi -``` - -**Simplifications depending on user choice:** - -- User picked full-scan only → drop the `if`, just run `{{PACKAGE_MANAGER_DLX}} check`. -- User picked diff-scan only → remove the `push:` trigger, keep only the PR branch. - -**For other CI systems**, emit the equivalent: - -1. Set up Node.js (v20 is a safe default; match their existing CI if they use a specific version). -2. Fetch with full depth (or enough depth to reach the target branch — most CIs default to a shallow fetch). -3. Fetch the target branch. -4. Compute changed files with `git diff --name-only "origin/...HEAD"`. -5. Exit early if the diff is empty. -6. Call `{{PACKAGE_MANAGER_DLX}} check $FILES` for PR builds, or `{{PACKAGE_MANAGER_DLX}} check` for main-branch builds. - -The exact syntax varies — YAML for GitHub/GitLab/Azure/Bitbucket, Groovy for Jenkins, different job/workflow structure for CircleCI. But the six steps above are universal. If you recognize the user's CI system, emit idiomatic config for it; don't try to retrofit GitHub Actions syntax where it doesn't belong. - -### 7. Authentication in CI - -`taskless check` does **not** require authentication — it only reads rule files from `.taskless/rules/`. The generated CI config works out of the box with no secrets or environment variables. - -Only mention auth if the user explicitly wants to run authenticated commands in CI (e.g., `rules create`, `rules improve`). That's uncommon; for a check-only integration, skip it. - -### 8. Package manager caveats - -- **pnpm**: CI runners using `pnpm dlx` work but have slower cold starts than `npx`. If the user's main pipeline already installs pnpm (e.g., via `pnpm/action-setup@v4` in GitHub Actions), they may prefer adding `@taskless/cli` as a project dependency and calling it via `pnpm taskless check`. -- **Yarn v1 (classic)**: does not support `yarn dlx`. Use `npx` instead — Yarn v1 ships with npm, so `npx` is available. -- **bun**: `bunx` works fine. - -### 9. Report back to the user - -After writing, show: - -1. The path written and a short excerpt (first 10–15 lines). -2. For CI systems that need manual wiring (anything not GitHub Actions): the exact `include:` / reference line they need to add to their main config. -3. `git status` so they can see the new file before committing. -4. A brief note that the first CI run will exercise the rules — if there are existing red matches, CI will fail until fixed or suppressed. - -### 10. Handle errors - -- **No rules yet** (`taskless check` says "No rules configured"): invoke `taskless-create-rule`, do NOT write CI config. -- **User's CI system isn't one you recognize**: still help — extract the scan pattern (checkout → fetch base → diff → invoke), and produce a generic shell script under `.taskless/ci/check.sh` that they can wire into whatever system they use. Be upfront that you're providing a starting point, not a finished config. -- **Target CI file already exists** and the user declines overwriting: stop. Tell them what you would have written and let them reconcile manually. diff --git a/skills/taskless-create-rule-anonymous/SKILL.md b/skills/taskless-create-rule-anonymous/SKILL.md deleted file mode 100644 index 8d2cf064..00000000 --- a/skills/taskless-create-rule-anonymous/SKILL.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -name: taskless-create-rule-anonymous -description: Creates a new Taskless rule locally without API access. Uses the agent to derive ast-grep rules from the schema and validates them with the verify feedback loop. -metadata: - author: taskless - version: 0.6.0 - commandName: "-" -compatibility: Designed for Agents implementing the Agent Skills specification. ---- - -# Taskless Rule Create (Anonymous) - -This skill creates ast-grep rules locally without requiring Taskless authentication. You will derive the rule yourself using the ast-grep schema as a guide, then validate it using the verify command. - -## Instructions - -**Package manager:** All commands below use `npx` as the default. If the project uses a different package manager (check for `pnpm-lock.yaml`, `yarn.lock`, or `bun.lockb`), prefer its equivalent: `pnpm dlx`, `yarn dlx` (Yarn Berry/2+ only), or `bunx`. - -1. **Learn the ast-grep rule format.** Run `npx @taskless/cli@latest rules verify --schema --json` and read the output. This gives you: - - `astGrepSchema`: The official ast-grep rule JSON Schema — the full reference for what fields are valid. - - `tasklessRequirements`: Fields Taskless requires beyond ast-grep defaults (`id`, `language`, `severity`, `message`, `rule`) and additional rules (e.g., `regex` requires `kind`). - - `examples`: Annotated rule examples showing common patterns (simple match, regex with kind, composite rules). - - Study the examples carefully — they show how `pattern`, `kind`, `regex`, `any`, `all`, `has`, `inside`, and other combinators work. - -2. **Gather the rule description.** Even if the user provided a description with their command, you MUST ask clarifying questions before proceeding. Do NOT skip to rule generation. Ask the user: - - What specific code pattern should be flagged? Get concrete examples. - - What language is it in? - - Are there exceptions or edge cases where the pattern is acceptable? - - Can they show examples of code that should and shouldn't trigger? - - Wait for the user's responses before moving to step 3. - -3. **Check for existing similar rules.** Scan `.taskless/rules/` for existing rule files. If any overlap with the user's request, point it out and ask if they want to improve an existing rule instead (via `taskless-improve-rule-anonymous`). - -4. **Search the codebase for real examples.** Proactively scan for instances of the pattern. Show the user what you found: - - "I found N instances of this pattern. Should any of these be excluded?" - - Highlight variations that might need separate handling. - -5. **Derive the rule.** Using the ast-grep schema, examples, and the user's input, write the rule as a YAML file. The rule MUST include: - - `id`: A kebab-case identifier (e.g., `no-eval`, `prefer-const`) - - `language`: The target language (e.g., `typescript`, `javascript`, `python`) - - `severity`: One of `error`, `warning`, `info`, or `hint` - - `message`: A concise single-line explanation of why the rule fires - - `rule`: The ast-grep rule object (using `pattern`, `kind`, `regex`, `any`, `all`, etc.) - - Optional but recommended: - - `note`: Additional guidance or suggested fixes (supports markdown) - - `fix`: Auto-fix pattern if applicable - - Write the rule to `.taskless/rules/.yml`. - -6. **Write test cases.** Create a test file at `.taskless/rule-tests/--test.yml` with: - - `id`: Same as the rule ID - - `valid`: An array of code snippets that should NOT trigger the rule - - `invalid`: An array of code snippets that SHOULD trigger the rule - - Include at least 2 valid and 2 invalid cases. Use real patterns from the codebase where possible. - -7. **Verify the rule.** Run `npx @taskless/cli@latest rules verify --json` and check the result: - - If `success` is `true`: the rule passes all checks. Report success to the user. - - If `success` is `false`: read the error details from each layer (`schema`, `requirements`, `tests`) and fix the issues. Then re-run verify. Repeat until it passes or you've made 3 attempts. - - Common fixes: - - Schema errors: Check field types and structure against the ast-grep schema. - - Missing required fields: Add any fields listed in the Taskless requirements. - - Regex without kind: Add a `kind` field alongside any `regex` field. - - Test failures: Adjust the rule pattern or test cases so valid cases pass and invalid cases are caught. - -8. **Report results.** Once verified, show the user: - - The rule file path - - The test file path - - A summary of what the rule detects - - Suggest running `taskless-check` to see the rule in action - -## Important Notes - -- Do NOT write files to `.taskless/rule-metadata/`. Anonymous rules have no metadata sidecar. -- Do NOT make any API calls to taskless.io. -- The verify feedback loop is your quality gate — always run it before reporting success. diff --git a/skills/taskless-create-rule/SKILL.md b/skills/taskless-create-rule/SKILL.md deleted file mode 100644 index cfc2d69f..00000000 --- a/skills/taskless-create-rule/SKILL.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -name: taskless-create-rule -description: Creates a new Taskless rule from a description. Use when the user wants to create a rule, add a lint rule, define a code pattern to detect, or generate an ast-grep rule. Trigger on "create a rule", "add a taskless rule", "new rule for", or "detect this pattern". -metadata: - author: taskless - version: 0.6.0 - commandName: tskl:rule -compatibility: Designed for Agents implementing the Agent Skills specification. ---- - -# Taskless Rule Create - -When this skill is invoked, work with the user to build a comprehensive rule request and generate a rule. - -Your goal is to produce the best possible rule by enriching the user's initial description with concrete examples, edge cases, and exclusions — not just pass their request through verbatim. - -## Instructions - -**Package manager:** All commands below use `npx` as the default. If the project uses a different package manager (check for `pnpm-lock.yaml`, `yarn.lock`, or `bun.lockb`), prefer its equivalent: `pnpm dlx`, `yarn dlx` (Yarn Berry/2+ only), or `bunx`. - -1. **Check authentication status.** Run `npx @taskless/cli@latest info --json` and parse the JSON output. Check the `loggedIn` field: - - If `loggedIn` is `true`: continue with step 2 below (API-backed flow). - - If `loggedIn` is `false`: **stop here** and invoke the `taskless-create-rule-anonymous` skill instead. Pass along any context the user has already provided about the rule they want to create. - -2. **Read current command documentation.** Run `npx @taskless/cli@latest help rules create` and read the output. Use this to understand the command's `--from` JSON fields, options, and examples. - -3. **Gather the rule description.** Even if the user provided a description with their command, you MUST ask clarifying questions before proceeding. Do NOT skip to rule generation. Ask what specific code pattern should be flagged, with concrete examples. This becomes the `prompt` field (required). - -4. **Check for existing similar rules.** Once you have the user's description, scan `.taskless/rules/` for existing rule files. Read each rule's `message`, `note`, and `rule` fields to understand what patterns are already covered. If any existing rule appears to overlap with the user's request: - - Show the user the similar rule(s) and explain the overlap. - - Ask: "It looks like you already have a rule that covers something similar. Would you like to **improve the existing rule** instead of creating a new one?" - - If the user wants to improve, stop this skill and invoke the `taskless-improve-rule` skill (command name `tskl:improve`) with context about which rule to iterate on. - - If the user confirms they want a separate new rule, proceed with creation. - -5. **Enrich the request.** After receiving the initial description, actively work with the user to strengthen the rule. Do all of the following: - - a. **Search the codebase for real examples.** Proactively scan the codebase for instances of the pattern they want to detect. Show them what you found and ask: - - "I found N instances of this pattern in your codebase. Should I include some as examples in the rule request?" - - If you find variations of the pattern, highlight them as potential edge cases. - - b. **Ask for success and failure cases.** Even if the user provided examples, ask if there are other cases to consider: - - "Are there edge cases or variations of this pattern that should also be caught?" - - "Can you show me an example of the _correct_ way to write this code?" - - Use any examples the user provided in their description as a starting point, but look for more. - - c. **Collect default ignores from the project.** Before asking the user about exclusions, check the project for existing ignore patterns that inform what files the rule should skip. Look at: - - `.gitignore` — files already excluded from version control (e.g., `node_modules/`, `dist/`, build artifacts) - - Linter configs (e.g., `eslint.config.js`, `.eslintignore`) — files or directories already excluded from linting - - `tsconfig.json` `exclude` field — files excluded from type checking - - Any other relevant config that signals "these files are not authored source code" - - Use these to build a baseline set of ignores. Present them to the user as defaults that will be included in the rule prompt. - - d. **Ask about additional exclusions.** Beyond the defaults, ask the user if there are files, directories, or contexts where the pattern is acceptable: - - "Are there any files or directories where this pattern should be allowed? (e.g., `.d.ts` files, test files, generated code)" - - Incorporate both the default ignores and user-specified exclusions into the `prompt` field so the rule generator understands the boundaries. - - e. **Infer the language.** Detect the primary language from the codebase or the user's examples. Confirm your assumption with the user. Include the target language in the `prompt` field (e.g., "Detect X in TypeScript files") so the rule generator knows what language to target. - -6. **Confirm the enriched request.** Before submitting, present a summary of what you'll send to the API: - - The full prompt (including language and any exclusion notes) - - The success case(s) - - The failure case(s) - - Ask the user to confirm or adjust before proceeding. - -7. **Write the JSON payload to a file.** Build a JSON object with the gathered fields. Write the JSON to `.taskless/.tmp-rule-request.json`. - - **Multiple examples:** The `successCases` and `failureCases` fields are arrays of strings. Each example is a separate array element: - - ```json - { - "prompt": "...", - "failureCases": [ - "/// \nexport class MyWorker { ... }", - "/// \nconst x = import.meta.env.FOO;" - ], - "successCases": [ - "import type { DurableObjectState } from 'cloudflare:workers';\nexport class MyWorker { ... }", - "// .d.ts files are exempt — triple-slash is idiomatic there\n/// " - ] - } - ``` - -8. **Invoke the CLI.** Run `npx @taskless/cli@latest rules create --from .taskless/.tmp-rule-request.json --json`. The command may take 30-60 seconds as it polls the API. - -9. **Clean up.** After the command completes (success or failure), delete the `.taskless/.tmp-rule-request.json` file. - -10. **Report the results.** When the CLI completes, show the generated file paths and suggest running `taskless-check` to test the new rule. The CLI also writes sidecar metadata to `.taskless/rule-metadata/.yml` containing the `ticketId` used for future iterations. You can retrieve this with `npx @taskless/cli@latest rules meta --json`. - -11. **Handle errors.** If the CLI fails: - - **Authentication required**: Suggest the `taskless-login` skill. - - **Missing organization info**: Suggest running `npx @taskless/cli@latest auth login` to re-authenticate. - - **API errors**: Report the error message and suggest trying again. diff --git a/skills/taskless-delete-rule/SKILL.md b/skills/taskless-delete-rule/SKILL.md deleted file mode 100644 index d97cf6e9..00000000 --- a/skills/taskless-delete-rule/SKILL.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: taskless-delete-rule -description: Deletes a Taskless rule and its test files. Use when the user wants to remove a rule, delete a lint rule, or clean up an unwanted rule. Trigger on "delete rule", "remove taskless rule", "delete this rule", or "remove rule". -metadata: - author: taskless - version: 0.6.0 - commandName: "-" -compatibility: Designed for Agents implementing the Agent Skills specification. ---- - -# Taskless Rule Delete - -When this skill is invoked, help the user identify which rule to delete, confirm the selection, and invoke the CLI to remove it. - -## Instructions - -**Package manager:** All commands below use `npx` as the default. If the project uses a different package manager (check for `pnpm-lock.yaml`, `yarn.lock`, or `bun.lockb`), prefer its equivalent: `pnpm dlx`, `yarn dlx` (Yarn Berry/2+ only), or `bunx`. - -1. **Read current command documentation.** Run `npx @taskless/cli@latest help rules delete` and read the output. Use this to understand the command's arguments, options, and exit codes. - -2. **List available rules.** Scan the `.taskless/rules/` directory for `.yml` files. Present the rule IDs (filenames without the `.yml` extension) to the user. - - If no rules are found, inform the user: - - ``` - No rules found in .taskless/rules/. There are no rules to delete. - ``` - -3. **Identify the target rule.** If the user already specified a rule (e.g., "delete the console-log rule"), match it to an available rule ID. If the match is ambiguous or unclear, ask the user to clarify which rule they mean. - -4. **Confirm before deleting.** Show the user the rule ID and ask for confirmation before proceeding. - -5. **Invoke the CLI.** Run the delete command using the syntax shown in the help output (e.g., `npx @taskless/cli@latest rules delete `). - -6. **Report the result.** Confirm which files were deleted. - -7. **Handle errors.** If the CLI reports the rule was not found, inform the user and suggest checking the rule ID. diff --git a/skills/taskless-improve-rule-anonymous/SKILL.md b/skills/taskless-improve-rule-anonymous/SKILL.md deleted file mode 100644 index 2576aa93..00000000 --- a/skills/taskless-improve-rule-anonymous/SKILL.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -name: taskless-improve-rule-anonymous -description: Improves existing Taskless rules locally without API access. Uses the agent to modify ast-grep rules and validates changes with the verify feedback loop. -metadata: - author: taskless - version: 0.6.0 - commandName: "-" -compatibility: Designed for Agents implementing the Agent Skills specification. ---- - -# Taskless Improve Rule (Anonymous) - -This skill improves existing ast-grep rules locally without requiring Taskless authentication. You will modify the rule yourself using the ast-grep schema as a guide, then validate changes using the verify command. - -## Instructions - -**Package manager:** All commands below use `npx` as the default. If the project uses a different package manager (check for `pnpm-lock.yaml`, `yarn.lock`, or `bun.lockb`), prefer its equivalent: `pnpm dlx`, `yarn dlx` (Yarn Berry/2+ only), or `bunx`. - -1. **Learn the ast-grep rule format.** Run `npx @taskless/cli@latest rules verify --schema --json` and read the output. Study the `astGrepSchema`, `tasklessRequirements`, and `examples` to understand valid rule structure and patterns. - -2. **Inventory existing rules.** If the user has named a specific rule, go directly to it. Otherwise, scan `.taskless/rules/` for `.yml` files and present a summary: - - Rule ID (filename without `.yml`) - - Language it targets - - What pattern it detects (from `message`, `note`, or `rule` fields) - - Any associated test files in `.taskless/rule-tests/` - -3. **Read the rule and its tests.** Read the full content of `.taskless/rules/.yml` and any matching test file in `.taskless/rule-tests/`. Understand the current rule logic before making changes. - -4. **Understand the improvement request.** Ask the user what they want to improve: - - What is the rule doing wrong? (false positives, false negatives, wrong fix, missing edge cases) - - Can they show an example of the incorrect behavior? - - What would the correct behavior look like? - -5. **Search for evidence in the codebase.** Scan for instances where the rule fires (or fails to fire): - - "I found N places where this rule triggers. Are any of these false positives?" - - "I found N places where this pattern exists but the rule misses it. Should it catch these?" - -6. **Decide on approach.** Based on the user's feedback, choose one of three strategies: - - ### Option A — Iterate on the existing rule (most common) - - Use when the rule is fundamentally correct but needs refinement (false positives, missed edge cases, fix adjustments). - - Modify `.taskless/rules/.yml` in place - - Write a new test file at `.taskless/rule-tests/--test.yml` with updated cases - - ### Option B — Replace the rule - - Use when the rule is fundamentally wrong and needs a different approach. - - Invoke the `taskless-create-rule-anonymous` skill to create the replacement - - Delete the old rule: `npx @taskless/cli@latest rules delete ` - - ### Option C — Expand with additional rules - - Use when the user's need has grown beyond a single rule. - - Invoke `taskless-create-rule-anonymous` for each new rule - - Optionally delete superseded rules - - **Present your chosen approach to the user and get confirmation before proceeding.** - -7. **For Option A — modify the rule.** Using the ast-grep schema and the user's feedback: - - Edit the rule YAML to address the issues - - Ensure all Taskless-required fields remain present (`id`, `language`, `severity`, `message`, `rule`) - - Write updated test cases that exercise the improved behavior - - Include test cases for the specific issues the user reported - -8. **Verify the changes.** Run `npx @taskless/cli@latest rules verify --json` and check the result: - - If `success` is `true`: report success to the user. - - If `success` is `false`: read the error details, fix the issues, and re-run verify. Repeat until it passes or you've made 3 attempts. - -9. **Report results.** Show the user: - - What changed in the rule - - The updated file paths - - Suggest running `taskless-check` to test the changes - -## Important Notes - -- Do NOT write files to `.taskless/rule-metadata/`. Anonymous rules have no metadata sidecar. -- Do NOT make any API calls to taskless.io. -- The verify feedback loop is your quality gate — always run it before reporting success. -- When delegating to `taskless-create-rule-anonymous` (Options B/C), that skill handles its own verify loop. diff --git a/skills/taskless-improve-rule/SKILL.md b/skills/taskless-improve-rule/SKILL.md deleted file mode 100644 index 459b497d..00000000 --- a/skills/taskless-improve-rule/SKILL.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -name: taskless-improve-rule -description: Improves existing Taskless rules by iterating with guidance. Use when the user wants to refine, fix, or improve existing rules. Trigger on "improve rule", "fix my rule", "iterate on rule", "refine taskless rule", or "my rule isn't working". -metadata: - author: taskless - version: 0.6.0 - commandName: tskl:improve -compatibility: Designed for Agents implementing the Agent Skills specification. ---- - -# Taskless Improve - -When this skill is invoked, help the user improve an existing Taskless rule by determining the best approach and executing it. - -This is a decision-making skill. You must evaluate the situation and choose the right strategy — not every improvement is a simple iteration. - -## Instructions - -**Package manager:** All commands below use `npx` as the default. If the project uses a different package manager (check for `pnpm-lock.yaml`, `yarn.lock`, or `bun.lockb`), prefer its equivalent: `pnpm dlx`, `yarn dlx` (Yarn Berry/2+ only), or `bunx`. - -1. **Check authentication status.** Run `npx @taskless/cli@latest info --json` and parse the JSON output. Check the `loggedIn` field: - - If `loggedIn` is `true`: continue with step 2 below (API-backed flow). - - If `loggedIn` is `false`: **stop here** and invoke the `taskless-improve-rule-anonymous` skill instead. Pass along any context the user has already provided about which rule to improve and what changes they want. - -2. **Read current command documentation.** Run `npx @taskless/cli@latest help rules improve` and read the output. Use this to understand the improve command's `--from` JSON fields, options, and examples. - -3. **Inventory existing rules.** If the user has already named a specific rule, skip to that rule directly. Otherwise, scan the `.taskless/rules/` directory for `.yml` files and present a summary. For each rule, note: - - The rule ID (filename without `.yml`) - - The language it targets - - The pattern it detects (from the `message`, `note`, or `rule` fields) - - Any associated test files in `.taskless/rule-tests/` - - Once a rule is selected, check for its sidecar metadata by running `npx @taskless/cli@latest rules meta --json`. If metadata exists, note the `ticketId` — this is required for the iterate API. - -4. **Understand the improvement request.** Ask the user what they want to improve. Gather specifics: - - Which rule(s) are problematic? - - What is the rule doing wrong? (false positives, false negatives, wrong fix, missing edge cases, etc.) - - Can they show an example of the incorrect behavior? - - What would the correct behavior look like? - -5. **Search for evidence in the codebase.** Proactively scan the codebase for instances where the rule is triggering (or failing to trigger). Show the user what you found: - - "I found N places where this rule fires. Are any of these false positives?" - - "I found N places where this pattern exists but the rule doesn't catch it. Should it?" - -6. **Decide on approach.** Based on the user's feedback and your analysis, determine the best strategy: - - ### Option A — Iterate on a single rule (most common) - - Use this when the user wants to refine an existing rule that is fundamentally correct but needs adjustment. Examples: - - The rule has false positives that need to be excluded - - The rule misses certain variations of the pattern - - The fix suggestion is incorrect or incomplete - - The rule needs to handle edge cases better - - ### Option B — Replace an existing rule - - Use this when the rule is fundamentally wrong and needs a completely different approach. Examples: - - The rule's pattern matching strategy is incorrect (e.g., using string matching when AST matching is better suited to the task) - - The rule targets the wrong language construct entirely - - The user's requirements have changed significantly from the original rule - - For this approach: create a new rule (via the rule create flow) and then delete the old one. - - ### Option C — Create additional rules - - Use this when the user's need has expanded beyond what a single rule can cover. Examples: - - The user wants to detect the same pattern in multiple languages - - The pattern has distinct variants that are better handled by separate rules - - The user wants related but distinct checks - - For this approach: create new rules and optionally remove old ones that are being superseded. - - **Present your chosen approach to the user and get confirmation before proceeding.** - -7. **Execute the chosen approach.** - - ### For Option A (iterate): - - a. **Build the JSON payload.** Create a JSON object with: - - `ruleId`: The ticket ID from the rule's sidecar metadata. Retrieve it by running `npx @taskless/cli@latest rules meta --json` and reading the `ticketId` field. If no metadata file exists (rule was created before metadata support), fall back to using the rule filename as the identifier. Providing the ticket ID allows the API to understand the existing rule's logic and how to adjust it based on your guidance. - - `guidance`: A clear, specific description of what should change. Include: - - What the rule is doing wrong - - What it should do instead - - Specific examples of false positives/negatives - - Any exclusions or edge cases to handle - - `references` (optional): Include the current rule file and test file contents so the API has full context. Each reference is `{ "filename": "", "content": "" }`. - - Example payload (note: `ruleId` is the `ticketId` UUID from `rules meta --json`, not the rule filename): - - ```json - { - "ruleId": "d4f8e2a1-7b3c-4e9f-a5d6-1c2b3e4f5a6b", - "guidance": "The rule currently flags console.log statements inside catch blocks, but these are intentional error logging. Exclude console.log/console.error/console.warn calls that appear inside catch blocks. Also exclude any console calls in files under src/scripts/ as those are CLI tools where console output is expected.", - "references": [ - { - "filename": "rules/no-console-log.yml", - "content": "id: no-console-log\nlanguage: typescript\n..." - }, - { - "filename": "rule-tests/no-console-log-20260328-test.yml", - "content": "id: no-console-log\n..." - } - ] - } - ``` - - b. **Write the JSON to a temp file.** Write to `.taskless/.tmp-improve-request.json`. - - c. **Invoke the CLI.** Run `npx @taskless/cli@latest rules improve --from .taskless/.tmp-improve-request.json --json`. The command may take 30-60 seconds as it polls the API. - - d. **Clean up.** After the command completes (success or failure), delete `.taskless/.tmp-improve-request.json`. - - e. **Report results.** Show the updated file paths and suggest running `taskless-check` to test the changes. The CLI also updates the sidecar metadata in `.taskless/rule-metadata/`. - - ### For Option B (replace): - - a. Note the old rule ID for deletion. - b. Invoke the `taskless-create-rule` skill (command name `tskl:rule`) to create the replacement rule. This ensures the full enrichment workflow (examples, exclusions, confirmation) is followed. - c. After the new rule is generated, delete the old rule: `npx @taskless/cli@latest rules delete `. - d. Report results. - - ### For Option C (expand): - - a. For each new rule needed, invoke the `taskless-create-rule` skill (command name `tskl:rule`). - b. If any old rules are being superseded, delete them after the new rules are created: `npx @taskless/cli@latest rules delete `. - c. Report all changes. - -8. **Suggest testing.** After any approach, suggest running `taskless-check` to test the updated rules against the codebase. - -9. **Handle errors.** If the CLI fails: - - **Authentication required**: Suggest the `taskless-login` skill. - - **Missing organization info**: Suggest running `npx @taskless/cli@latest auth login` to re-authenticate. - - **Rule not found**: The ruleId may be incorrect. Check the rule's metadata or suggest creating a new rule instead. - - **API errors**: Report the error message and suggest trying again. diff --git a/skills/taskless-info/SKILL.md b/skills/taskless-info/SKILL.md deleted file mode 100644 index a4323d53..00000000 --- a/skills/taskless-info/SKILL.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -name: taskless-info -description: Confirms that the Taskless skills plugin is installed and working. Use when the user wants to verify their Taskless setup, check plugin status, test the connection, or run a health check. Trigger on "is taskless working", "check taskless", "taskless status", or "taskless info". -metadata: - author: taskless - version: 0.6.0 - commandName: tskl:info -compatibility: Designed for Agents implementing the Agent Skills specification. ---- - -# Taskless Info - -When this skill is invoked, verify that the Taskless CLI is reachable and report its version. - -## Instructions - -**Package manager:** All commands below use `npx` as the default. If the project uses a different package manager (check for `pnpm-lock.yaml`, `yarn.lock`, or `bun.lockb`), prefer its equivalent: `pnpm dlx`, `yarn dlx` (Yarn Berry/2+ only), or `bunx`. - -1. **Read current command documentation.** Run `npx @taskless/cli@latest help info` and read the output. Use this to understand the command's output format and available options. - -2. **Invoke the CLI.** Run `npx @taskless/cli@latest info` and capture stdout. - -3. **Parse the response.** The CLI outputs JSON to stdout. Parse it with `JSON.parse()` and extract the fields described in the help output. Key fields to report: - - `version`: The version of the Taskless CLI. - - `tools`: An array of coding agent tools with their installed skills and versions. - - `loggedIn`: Indicates if the user is logged into Taskless. - -4. **Report the result.** Display a confirmation message with the version: - - ``` - Taskless skills plugin is installed and working. - CLI version: - - Tools: - - - - : Installed version , Current version , Up to date: - ... - ``` - -5. **Handle errors.** If the command fails (non-zero exit code) or the output is not valid JSON: - - Report that the Taskless CLI could not be reached. - - Suggest checking network connectivity and that npm/pnpm is available. - - Show the raw error output if available. - -6. **Report if Upgrade is Required** If any installed skill is not current, include a note that an upgrade is recommended. Offer to run `npx @taskless/cli@latest init` for them to reinitialize with the latest skills. - -## Example Output - -``` -Taskless skills plugin is installed and working. -CLI version: 0.0.1 -``` diff --git a/skills/taskless-login/SKILL.md b/skills/taskless-login/SKILL.md deleted file mode 100644 index 937d0539..00000000 --- a/skills/taskless-login/SKILL.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: taskless-login -description: Explains how to authenticate with Taskless. Use when the user wants to log in, authenticate, connect their account, or set up credentials. Trigger on "taskless login", "authenticate taskless", "taskless auth", or "connect to taskless". -metadata: - author: taskless - version: 0.6.0 - commandName: tskl:login -compatibility: Designed for Agents implementing the Agent Skills specification. ---- - -# Taskless Login - -When this skill is invoked, explain the authentication process and provide the CLI command the user needs to run. - -**Important:** Do NOT attempt to run the login command. The device flow requires interactive terminal input (displaying a URL and polling for browser-based authorization) that cannot be performed by an agent. - -## Instructions - -**Package manager:** All commands below use `npx` as the default. If the project uses a different package manager (check for `pnpm-lock.yaml`, `yarn.lock`, or `bun.lockb`), prefer its equivalent: `pnpm dlx`, `yarn dlx` (Yarn Berry/2+ only), or `bunx`. - -1. **Read current command documentation.** Run `npx @taskless/cli@latest help auth login` and read the output. Use this to understand the login flow, credential storage, and alternatives. - -2. **Present the login command and explain the process.** Using the information from the help output, display the command the user should run in their terminal and explain what will happen (device flow, credential storage, environment variable alternative). diff --git a/skills/taskless-logout/SKILL.md b/skills/taskless-logout/SKILL.md deleted file mode 100644 index 74b5102e..00000000 --- a/skills/taskless-logout/SKILL.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: taskless-logout -description: Explains how to remove saved Taskless authentication. Use when the user wants to log out, disconnect, remove credentials, or clear their Taskless session. Trigger on "taskless logout", "disconnect taskless", or "remove taskless auth". -metadata: - author: taskless - version: 0.6.0 - commandName: tskl:logout -compatibility: Designed for Agents implementing the Agent Skills specification. ---- - -# Taskless Logout - -When this skill is invoked, explain how to remove saved authentication and provide the CLI command. - -**Important:** Do NOT attempt to run the logout command. Provide the command for the user to run in their terminal. - -## Instructions - -**Package manager:** All commands below use `npx` as the default. If the project uses a different package manager (check for `pnpm-lock.yaml`, `yarn.lock`, or `bun.lockb`), prefer its equivalent: `pnpm dlx`, `yarn dlx` (Yarn Berry/2+ only), or `bunx`. - -1. **Read current command documentation.** Run `npx @taskless/cli@latest help auth logout` and read the output. Use this to understand what the command does, credential storage location, and any caveats. - -2. **Present the logout command and explain what it does.** Using the information from the help output, display the command the user should run and explain the effects (credential removal, environment variable note). diff --git a/skills/taskless/SKILL.md b/skills/taskless/SKILL.md new file mode 100644 index 00000000..fd773dfa --- /dev/null +++ b/skills/taskless/SKILL.md @@ -0,0 +1,67 @@ +--- +name: taskless +description: | + Use for any Taskless task. Trigger when the user mentions Taskless by name, + or when their request involves the .taskless/ directory or files in it + (rules, rule-tests, rule-metadata). + + Specifically: + - "create/add/write a taskless rule for X" + - "improve/fix/iterate on this taskless rule" + - "delete/remove this taskless rule" + - "run taskless", "taskless check", "validate against taskless rules" + - "taskless login/logout/status", "is taskless connected" + - "add taskless to CI", "wire taskless into github actions" + + Do NOT trigger on generic ESLint, linting, or rule requests that don't + reference Taskless or .taskless/ files. +metadata: + author: taskless + version: 0.6.0 + commandName: tskl +compatibility: Designed for Agents implementing the Agent Skills specification. +--- + +# Taskless + +You do NOT have the steps for any Taskless action in your context. The current +canonical recipes live behind `npx @taskless/cli help `. Always fetch +the recipe first; do not improvise from prior knowledge — recipes change with +each CLI version. + +## First step: confirm Taskless is installed here + +If the working directory does not contain a `.taskless/` directory, ask the +user to confirm they meant Taskless (vs. ESLint or another tool). If they +confirm, offer to run `npx @taskless/cli` to install. Otherwise, stop. + +## Topics + +| User wants | Topic | +| -------------------------- | ------------------------------------- | +| First-time install | tell user to run `npx @taskless/cli` | +| Update existing install | `npx @taskless/cli update` | +| Create a new rule | `npx @taskless/cli help rule create` | +| Improve an existing rule | `npx @taskless/cli help rule improve` | +| Delete a rule | `npx @taskless/cli help rule delete` | +| Check code against rules | `npx @taskless/cli help check` | +| Log in, log out, or status | `npx @taskless/cli help auth` | +| Wire into CI | `npx @taskless/cli help ci` | + +If the user's intent is ambiguous between two topics, run +`npx @taskless/cli help` (no args) to see the disambiguation table, or ask +the user. + +## --anonymous + +Any rule/check command accepts `--anonymous` to skip the Taskless API and +use local-only behavior. When the user is offline OR explicitly asks for +anonymous mode, fetch the recipe with +`npx @taskless/cli help --anonymous`, which returns the local-only +flow (when one exists for that topic). + +## First-run latency + +The first invocation of `npx @taskless/cli` on a machine pays an npm +cold-fetch (~5–15 seconds). This is normal — do not report it as a timeout +or failure. Subsequent invocations are cached and fast.