Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/recipe-command-per-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@taskless/cli": patch
---

Every `taskless agent <topic>` recipe now opens with the rule the installed skill already states: run each Taskless command as its own shell call, with nothing chained to it, and write files with your file tools. Agents copy a recipe's own examples, so two that chained a command were rewritten: `check` now runs `git diff --name-only` separately instead of substituting it, and `create-vale-rule` measures one rule with `check --json --rule <id>` instead of piping into `jq`. Every topic version is bumped, so a cached recipe is refetched.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-17
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## Why

0.13.1 taught the installed skill to run each Taskless command as its own shell call, with nothing chained around it. That rule lives only in `skills/taskless/SKILL.md`. An agent deep in a recipe such as `onboard` or `report` is reading that recipe, and copies the recipe's own command examples, so chaining (`S=... && mkdir -p $S && npx @taskless/cli@...`, `echo ======` separators) creeps back in, and with it the auto-mode refusals and the zsh drop that 0.13.1 fixed. Two recipes still show a chained example themselves: `check` substitutes `$(git diff ...)` into the command line, and `create-vale-rule` pipes `check --json` into `jq`.

## What Changes

- Every standalone recipe carries the same short paragraph directly beneath its `# Topic:` header: run each Taskless command as its own shell call, start the line with the invocation, chain nothing to it, read output from the call's result, and write files with the agent's file tools.
- The paragraph is rendered from one `%(ONE_COMMAND_PER_CALL)s` placeholder, built from the same invocation `%(TASKLESS_CLI)s` resolves to, so the wording cannot drift between recipes.
- `check` tells the agent to run `git diff --name-only` as its own call and pass the paths it printed, instead of substituting it into the command.
- `create-vale-rule` measures one rule with `check --json --rule <id>` (which shipped for taskless/cli#379) instead of piping the whole-project result into `jq`.
- Every changed topic's version bumps, so an agent holding a cached recipe refetches.

## Capabilities

### New Capabilities

None.

### Modified Capabilities

- `cli-agent`: every recipe states the one-command-per-call rule beneath its header.

## Impact

- `packages/cli/src/prompts/recipes.ts`: the `ONE_COMMAND_PER_CALL` variable.
- `packages/cli/src/agent/*.md`: the placeholder in all 26 standalone recipes (not the `feedback-invite` fragment, which is appended under a recipe that already carries it), topic version bumps, and the two rewritten examples.
- Tests: a `prompts.test.ts` case asserting every topic's rendering carries the paragraph; topic-version assertions in `onboard.test.ts` and `report.test.ts`.
- Delivery shape: **single PR**. One placeholder and its use across the recipes, small enough to review together.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## ADDED Requirements

### Requirement: Every recipe states the one-command-per-call rule

Every recipe file at `packages/cli/src/agent/<topic>.md`, except `feedback-invite.md`, SHALL carry the `%(ONE_COMMAND_PER_CALL)s` placeholder as its own paragraph directly beneath the `# Topic:` header line and its closing blank line, before `## Goal`. The renderer SHALL substitute one shared paragraph, identical for every topic except for the rendered invocation, which SHALL be the same value `%(TASKLESS_CLI)s` resolves to. The paragraph SHALL tell the agent to run each Taskless command as its own shell call, starting the line with the invocation and chaining nothing to it (no variable assignment, `cd`, `mkdir`, `&&`, `;`, pipe, redirect, or command substitution), to read output from the call's result, and to write any file a recipe asks for with its file tools.

When rendered with `mechanics: false`, the paragraph SHALL name no invocation and SHALL say "the Taskless invocation" in its place, because a caller with no CLI receives no invocations. The paragraph SHALL sit outside the header block, so `header: false` SHALL NOT remove it. `feedback-invite.md` is exempt because it is never served alone: the survey gate appends it, without its header, beneath a recipe that already carries the paragraph. A recipe's own command examples SHALL follow the rule: no example the agent is told to run SHALL chain anything to a Taskless command. Text the agent writes into a file, such as a CI job or a git hook, is not a command the agent runs and is exempt.

#### Scenario: Every topic renders the paragraph

- **WHEN** any topic other than `feedback-invite` is rendered
- **THEN** the text SHALL contain the one-command-per-call paragraph with the resolved invocation

#### Scenario: The paragraph survives header suppression

- **WHEN** a recipe is rendered with `header: false`
- **THEN** the text SHALL still contain the one-command-per-call paragraph

#### Scenario: A caller with no CLI gets the rule without an invocation

- **WHEN** a recipe is rendered with `mechanics: false`
- **THEN** the paragraph SHALL say "the Taskless invocation" and SHALL NOT contain an invocation or the `<taskless-cli>` marker

#### Scenario: A diff-scoped check is not a command substitution

- **WHEN** the `check` recipe describes scanning the files a branch changed
- **THEN** it SHALL tell the agent to run `git diff --name-only` as its own call and pass the paths it printed
- **AND** SHALL NOT show `$(git diff ...)` inside a Taskless command line
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## 1. Shared paragraph

- [x] 1.1 Add the `ONE_COMMAND_PER_CALL` variable to `buildVariables()` in `src/prompts/recipes.ts`, rendered from the resolved invocation
- [x] 1.2 Place `%(ONE_COMMAND_PER_CALL)s` beneath the header of all 26 standalone recipes (not the `feedback-invite` fragment, which is appended under a recipe that already carries it) and bump those topic versions
- [x] 1.3 Rewrite the chained examples in `check.md` and `create-vale-rule.md`
- [x] 1.4 Add a `prompts.test.ts` case for every topic, with and without the header; update the topic-version assertions

## 2. Release

- [x] 2.1 Add a `patch` changeset; run `pnpm typecheck`, `pnpm lint`, and the full test suite
- [x] 2.2 Run the pre-archive scenario check from CLAUDE.md, then archive the change
27 changes: 27 additions & 0 deletions openspec/specs/cli-agent/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,3 +432,30 @@ The topic is built into the `agent` command, not a recipe file. It SHALL NOT car

- **WHEN** `taskless agent ok` is run
- **THEN** PostHog SHALL receive a `cli_agent` event whose `topic` property is `ok`

### Requirement: Every recipe states the one-command-per-call rule

Every recipe file at `packages/cli/src/agent/<topic>.md`, except `feedback-invite.md`, SHALL carry the `%(ONE_COMMAND_PER_CALL)s` placeholder as its own paragraph directly beneath the `# Topic:` header line and its closing blank line, before `## Goal`. The renderer SHALL substitute one shared paragraph, identical for every topic except for the rendered invocation, which SHALL be the same value `%(TASKLESS_CLI)s` resolves to. The paragraph SHALL tell the agent to run each Taskless command as its own shell call, starting the line with the invocation and chaining nothing to it (no variable assignment, `cd`, `mkdir`, `&&`, `;`, pipe, redirect, or command substitution), to read output from the call's result, and to write any file a recipe asks for with its file tools.

When rendered with `mechanics: false`, the paragraph SHALL name no invocation and SHALL say "the Taskless invocation" in its place, because a caller with no CLI receives no invocations. The paragraph SHALL sit outside the header block, so `header: false` SHALL NOT remove it. `feedback-invite.md` is exempt because it is never served alone: the survey gate appends it, without its header, beneath a recipe that already carries the paragraph. A recipe's own command examples SHALL follow the rule: no example the agent is told to run SHALL chain anything to a Taskless command. Text the agent writes into a file, such as a CI job or a git hook, is not a command the agent runs and is exempt.

#### Scenario: Every topic renders the paragraph

- **WHEN** any topic other than `feedback-invite` is rendered
- **THEN** the text SHALL contain the one-command-per-call paragraph with the resolved invocation

#### Scenario: The paragraph survives header suppression

- **WHEN** a recipe is rendered with `header: false`
- **THEN** the text SHALL still contain the one-command-per-call paragraph

#### Scenario: A caller with no CLI gets the rule without an invocation

- **WHEN** a recipe is rendered with `mechanics: false`
- **THEN** the paragraph SHALL say "the Taskless invocation" and SHALL NOT contain an invocation or the `<taskless-cli>` marker

#### Scenario: A diff-scoped check is not a command substitution

- **WHEN** the `check` recipe describes scanning the files a branch changed
- **THEN** it SHALL tell the agent to run `git diff --name-only` as its own call and pass the paths it printed
- **AND** SHALL NOT show `$(git diff ...)` inside a Taskless command line
4 changes: 3 additions & 1 deletion packages/cli/src/agent/auth.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: auth (CLI v%(CLI_VERSION)s / topic v2)
# Topic: auth (CLI v%(CLI_VERSION)s / topic v3)

%(ONE_COMMAND_PER_CALL)s

## Goal
Manage Taskless authentication. Three branches:
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/bug-report.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: bug-report (CLI v%(CLI_VERSION)s / topic v1)
# Topic: bug-report (CLI v%(CLI_VERSION)s / topic v2)

%(ONE_COMMAND_PER_CALL)s

## You are here
This is `bug-report`. It helps you report a bug in Taskless to the
Expand Down
13 changes: 8 additions & 5 deletions packages/cli/src/agent/check.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: check (CLI v%(CLI_VERSION)s / topic v8)
# Topic: check (CLI v%(CLI_VERSION)s / topic v9)

%(ONE_COMMAND_PER_CALL)s

## Goal
Run the applicable rules against the codebase and report matches. Two
Expand Down Expand Up @@ -205,12 +207,13 @@ delete the rules to make `check` pass, and do not suggest
```
%(TASKLESS_CLI)s check --json src/foo.ts src/bar.ts
```
or, against a git diff:
or, against a git diff: run `git diff --name-only main...HEAD` as
its own call, then pass the paths it printed:
```
%(TASKLESS_CLI)s check --json $(git diff --name-only main...HEAD)
%(TASKLESS_CLI)s check --json src/foo.ts src/deleted.ts
```
Paths that don't exist on disk are skipped, so you can pipe raw
`git diff` output directly without pre-filtering. The skipped paths
Paths that don't exist on disk are skipped, so you can pass every
path `git diff` printed without pre-filtering. The skipped paths
are named in a notice (`notices` under `--json`), so a mistyped
path is visible rather than reading as clean.

Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/ci.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: ci (CLI v%(CLI_VERSION)s / topic v5)
# Topic: ci (CLI v%(CLI_VERSION)s / topic v6)

%(ONE_COMMAND_PER_CALL)s

## Goal
Wire `%(TASKLESS_CLI)s check` into the user's existing CI so rules run
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/create-legacy-rule.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: create-legacy-rule (CLI v%(CLI_VERSION)s / topic v1)
# Topic: create-legacy-rule (CLI v%(CLI_VERSION)s / topic v2)

%(ONE_COMMAND_PER_CALL)s

## You are here
This is `create-legacy-rule`. It helps you write a rule for a linter the
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/create-remote-rule.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: create-remote-rule (CLI v%(CLI_VERSION)s / topic v7)
# Topic: create-remote-rule (CLI v%(CLI_VERSION)s / topic v8)

%(ONE_COMMAND_PER_CALL)s

## You are here
This is `create-remote-rule`. It helps you have the Taskless service
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/create-runtime-rule.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: create-runtime-rule (CLI v%(CLI_VERSION)s / topic v5)
# Topic: create-runtime-rule (CLI v%(CLI_VERSION)s / topic v6)

%(ONE_COMMAND_PER_CALL)s

## You are here
This is `create-runtime-rule`. It helps you write a runtime rule: a
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/create-sg-rule.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: create-sg-rule (CLI v%(CLI_VERSION)s / topic v10)
# Topic: create-sg-rule (CLI v%(CLI_VERSION)s / topic v11)

%(ONE_COMMAND_PER_CALL)s

## You are here
This is `create-sg-rule`. It helps you write an ast-grep rule: a check
Expand Down
23 changes: 12 additions & 11 deletions packages/cli/src/agent/create-vale-rule.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: create-vale-rule (CLI v%(CLI_VERSION)s / topic v17)
# Topic: create-vale-rule (CLI v%(CLI_VERSION)s / topic v18)

%(ONE_COMMAND_PER_CALL)s

## You are here
This is `create-vale-rule`. It helps you write a Vale rule: a check over
Expand Down Expand Up @@ -945,20 +947,19 @@ it.
# of the four were written by the assistant, never by the human author.
```

There is no single command that runs one Vale rule over the project
and counts (taskless/cli#379 tracks one). `verify` isolates the rule
but sees only its fixtures. The workable way is a whole-project
`check` filtered afterwards by rule id:
`verify` isolates the rule but sees only its fixtures. To measure it
over the whole project, run `check` with only that rule:

```
%(TASKLESS_CLI)s check --json | jq '[.results[] | select(.ruleId == "<id>")] | length'
%(TASKLESS_CLI)s check --json --rule <id>
```

A finding carries no record of which alternative matched, so to
count one branch, run the rule with that branch as its only `raw`
entry, note the number, then fold it back into the alternation. Drop
`| length` to read the hits themselves, which is what tells you
whether a hit is a false positive or a real one nobody had noticed.
Count the entries in `results`. A finding carries no record of which
alternative matched, so to count one branch, run the rule with that
branch as its only `raw` entry, note the number, then fold it back
into the alternation. Read the hits themselves too: that is what
tells you whether a hit is a false positive or a real one nobody had
noticed.

6. **Validate, then verify.** Two commands, both taking the rule's
directory as their argument, both run from the project root:
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/delete-rule.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: delete-rule (CLI v%(CLI_VERSION)s / topic v4)
# Topic: delete-rule (CLI v%(CLI_VERSION)s / topic v5)

%(ONE_COMMAND_PER_CALL)s

## Goal
Remove a rule and its associated test files from `.taskless/`. Does not
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/detect.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: detect (CLI v%(CLI_VERSION)s / topic v2)
# Topic: detect (CLI v%(CLI_VERSION)s / topic v3)

%(ONE_COMMAND_PER_CALL)s

## Goal
Scan the working directory for the linters it configures, the
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/feedback.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: feedback (CLI v%(CLI_VERSION)s / topic v1)
# Topic: feedback (CLI v%(CLI_VERSION)s / topic v2)

%(ONE_COMMAND_PER_CALL)s

## You are here
This is `feedback`. It helps you send the Taskless team feedback the user
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/hooks.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: hooks (CLI v%(CLI_VERSION)s / topic v2)
# Topic: hooks (CLI v%(CLI_VERSION)s / topic v3)

%(ONE_COMMAND_PER_CALL)s

## Goal
Run `check` on the staged files before every commit, in the hook tool
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/improve-rule.anonymous.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: improve-rule (anonymous) (CLI v%(CLI_VERSION)s / topic v4)
# Topic: improve-rule (anonymous) (CLI v%(CLI_VERSION)s / topic v5)

%(ONE_COMMAND_PER_CALL)s

## Goal
Iterate on an existing ast-grep rule **locally** without contacting
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/improve-rule.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: improve-rule (CLI v%(CLI_VERSION)s / topic v9)
# Topic: improve-rule (CLI v%(CLI_VERSION)s / topic v10)

%(ONE_COMMAND_PER_CALL)s

## Goal
Iterate on an existing Taskless rule. The CLI submits the user's
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/info.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: info (CLI v%(CLI_VERSION)s / topic v2)
# Topic: info (CLI v%(CLI_VERSION)s / topic v3)

%(ONE_COMMAND_PER_CALL)s

## Goal
Report local Taskless state: CLI version, installed skill versions
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/init.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: init (CLI v%(CLI_VERSION)s / topic v9)
# Topic: init (CLI v%(CLI_VERSION)s / topic v10)

%(ONE_COMMAND_PER_CALL)s

## Goal
Install or update the Taskless skill in this project, and migrate the
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/onboard.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: onboard (CLI v%(CLI_VERSION)s / topic v8)
# Topic: onboard (CLI v%(CLI_VERSION)s / topic v9)

%(ONE_COMMAND_PER_CALL)s

## Goal
Take a user who has just installed Taskless from zero rules to a
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/recover-rule.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: recover-rule (CLI v%(CLI_VERSION)s / topic v3)
# Topic: recover-rule (CLI v%(CLI_VERSION)s / topic v4)

%(ONE_COMMAND_PER_CALL)s

## Goal
Put an issued rule back the way Taskless issued it, after `check`
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/report.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: report (CLI v%(CLI_VERSION)s / topic v6)
# Topic: report (CLI v%(CLI_VERSION)s / topic v7)

%(ONE_COMMAND_PER_CALL)s

## Goal
Publish a Taskless report: an unlisted page on tskl.es that shows what
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/route.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: route (CLI v%(CLI_VERSION)s / topic v6)
# Topic: route (CLI v%(CLI_VERSION)s / topic v7)

%(ONE_COMMAND_PER_CALL)s

## Goal
Turn "write me a rule that…" into one command to run. This is the front
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/rule-feedback.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: rule-feedback (CLI v%(CLI_VERSION)s / topic v5)
# Topic: rule-feedback (CLI v%(CLI_VERSION)s / topic v6)

%(ONE_COMMAND_PER_CALL)s

## You are here
This is `rule-feedback`. It helps you turn what a user just said about
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/rule-meta.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: rule-meta (CLI v%(CLI_VERSION)s / topic v4)
# Topic: rule-meta (CLI v%(CLI_VERSION)s / topic v5)

%(ONE_COMMAND_PER_CALL)s

## Goal
Report what `%(TASKLESS_CLI)s rule meta` does today, so no recipe and no
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/rule.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: rule (CLI v%(CLI_VERSION)s / topic v4)
# Topic: rule (CLI v%(CLI_VERSION)s / topic v5)

%(ONE_COMMAND_PER_CALL)s

## Goal
Umbrella for rule operations. Fetch the topic for the action you want.
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/update.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: update (CLI v%(CLI_VERSION)s / topic v16)
# Topic: update (CLI v%(CLI_VERSION)s / topic v17)

%(ONE_COMMAND_PER_CALL)s

## You are here
This is `update`. It tells you what an upgrade changed for the rules
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/agent/validate-rule.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Topic: validate-rule (CLI v%(CLI_VERSION)s / topic v6)
# Topic: validate-rule (CLI v%(CLI_VERSION)s / topic v7)

%(ONE_COMMAND_PER_CALL)s

## Goal
Check that a rule is well-formed (`validate`), then run its tests
Expand Down
Loading
Loading