From 99e03f4f47df3d4c2b1a61180891438ca159a190 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Wed, 12 Aug 2026 21:52:11 -0700 Subject: [PATCH 1/7] docs(openspec): propose the agent command and the Vale authoring path `add-vale-rule-engine` shipped a chooser without a destination. `engine-selection` teaches an agent to conclude a rule belongs to `vale`, and then there is nowhere to go: `static.txt` is ast-grep authoring, and its only mention of Vale tells the reader to confirm `sg` was right. The `runtime` answer dead-ends the same way. The exclusion was recorded as a deliberate non-goal at the time, which was defensible while nothing chose Vale -- the chooser is what makes the gap reachable. The same work exposed that the surface is shaped for a human. `help` names the command after a human's reason for typing it, and longform addressing makes an agent know both the words and their order. Single hyphenated tokens read as literal strings to copy rather than phrases to paraphrase, which is the failure this surface cannot afford. Proposes: `help` -> `agent`, flattened single-token topics, `static` -> `create-sg-rule` and `existing` -> `create-legacy-rule`, new `create-vale-rule` and `create-runtime-rule`, `route` merged with `engine-selection` into one front door, and a section-less scaffold. Two decisions carry the most weight. Merging `route` and `engine-selection` removes a second fetch and a handoff to answer one question, but it deletes a topic the platform generator consumes -- so the criterion distributes to the destinations, each `create-*-rule` stating the evidence that makes its engine right. That is what keeps it exportable, since a consumer with no `route` step and no `detect --json` could never use a chooser anyway. And the section-less scaffold ships only paired with surfacing Vale's stderr on a zero-exit run: with no section to copy, the likely first edit is a rule assignment at top level, which Vale ignores and reports on stderr, which we currently discard -- reintroducing the silent disable one level up. Pre-1.0, every backwards-incompatible item here is a MINOR bump. Delivery shape: single PR stacked on #100. A half-renamed command surface is not shippable, since recipes cross-reference each other by literal command string. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3 --- .../.openspec.yaml | 2 + .../design.md | 103 ++++++++++++++++++ .../proposal.md | 49 +++++++++ .../specs/cli-agent-authoring/spec.md | 63 +++++++++++ .../specs/cli-help/spec.md | 60 ++++++++++ .../specs/cli-knowledge-prompts/spec.md | 39 +++++++ .../specs/cli-rule-routing/spec.md | 88 +++++++++++++++ .../specs/cli-vale-rule-engine/spec.md | 39 +++++++ .../agent-command-and-vale-authoring/tasks.md | 45 ++++++++ 9 files changed, 488 insertions(+) create mode 100644 openspec/changes/agent-command-and-vale-authoring/.openspec.yaml create mode 100644 openspec/changes/agent-command-and-vale-authoring/design.md create mode 100644 openspec/changes/agent-command-and-vale-authoring/proposal.md create mode 100644 openspec/changes/agent-command-and-vale-authoring/specs/cli-agent-authoring/spec.md create mode 100644 openspec/changes/agent-command-and-vale-authoring/specs/cli-help/spec.md create mode 100644 openspec/changes/agent-command-and-vale-authoring/specs/cli-knowledge-prompts/spec.md create mode 100644 openspec/changes/agent-command-and-vale-authoring/specs/cli-rule-routing/spec.md create mode 100644 openspec/changes/agent-command-and-vale-authoring/specs/cli-vale-rule-engine/spec.md create mode 100644 openspec/changes/agent-command-and-vale-authoring/tasks.md diff --git a/openspec/changes/agent-command-and-vale-authoring/.openspec.yaml b/openspec/changes/agent-command-and-vale-authoring/.openspec.yaml new file mode 100644 index 0000000..b6b2d1f --- /dev/null +++ b/openspec/changes/agent-command-and-vale-authoring/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-13 diff --git a/openspec/changes/agent-command-and-vale-authoring/design.md b/openspec/changes/agent-command-and-vale-authoring/design.md new file mode 100644 index 0000000..1b06336 --- /dev/null +++ b/openspec/changes/agent-command-and-vale-authoring/design.md @@ -0,0 +1,103 @@ +## Context + +The CLI's `help/*.txt` recipes are the knowledge surface agents read. They are addressed longform (`positionals.join("-")` in `commands/help.ts`), exported in part through `@taskless/cli/prompts` for the platform generator, and cross-referenced from each other by literal command string — ~306 occurrences of `taskless help` across 77 files. + +`add-vale-rule-engine` added `engine-selection`, which decides between `sg`, `vale`, and `runtime`. Two of those three answers have no authoring procedure. That change recorded Vale authoring as an explicit non-goal, which was defensible when nothing chose Vale; the chooser makes it reachable. + +Separately, the scaffolded `.vale.ini` opens an unscoped `[*]`. A whole-project check under it lints build output and, until #100, `.taskless/` itself. + +## Goals / Non-Goals + +**Goals:** + +- Every answer `route` can produce leads to a procedure that exists. +- One fetch from request to destination, returning a command an agent can run verbatim. +- A topic vocabulary that reads as literal tokens rather than paraphrasable phrases. +- A scaffold that lints nothing until someone scopes it deliberately. + +**Non-Goals:** + +- **A `.vale.ini` writer.** The agent authors the section, exactly as it authors `sgconfig.yml` rule entries today. Construction moves to the downstream generator, consistent with `add-vale-rule-engine/design.md:107`. +- Rule _generation_ for Vale via the service. `remote` remains the service path; this change gives the local path a destination. +- Renaming the `cli-help` capability file. The command renames; the spec keeps its name, so this change does not also move spec files (see D5). +- Restricting Vale's feature set, or deciding build-output exclusion (tracked separately in #101). + +## Decisions + +### D1 — `route` and `engine-selection` merge into one topic + +There is one decision, made once. `route` absorbs the engine reasoning and returns one of the four `create-*-rule` topics. `engine-selection` ceases to exist as a separate topic. + +This reverses the scoping `engine-selection` asserts today — that route decides destination, the topic decides engine, and locally the two compose. The separation is clean on paper and expensive in practice: it costs an agent two fetches and a correct handoff between them to answer one question, and the handoff is where an agent drops context. Worse, the two decisions are not independent in the direction the split assumes; "author this locally" and "which engine can express it" are answered from the same evidence, so splitting them means reading the same signals twice. + +**Where the reasoning goes for consumers outside the CLI.** The platform generator consumes `engine-selection` through `TOPICS` and has no `route` step, so merging cannot simply delete what it reads. Exporting `route` instead is wrong — it is built on `taskless detect --json` and local authoring, neither of which a Worker can do. + +The reasoning therefore **distributes to the destinations**: each `create-*-rule` recipe states the evidence that makes its engine the right one, and `route` applies those same tests to dispatch. A consumer choosing between `create-sg-rule` and `create-vale-rule` reads the criterion at the destination rather than fetching a chooser first. This keeps the exported surface honest — every exported topic is one a service-side consumer can actually act on — and it removes the class of drift where a chooser and its destinations disagree about when each applies. + +_Alternative rejected:_ keep `engine-selection` as a third exported topic that `route` also applies. Two statements of the same criterion, guaranteed to drift, and it preserves the second fetch for exactly the consumer we were trying to simplify. + +_Alternative rejected:_ export `route`. It is local-only by construction. + +### D2 — Verb-noun names, single token, no aliases + +`create-sg-rule`, `create-vale-rule`, `create-runtime-rule`, `create-legacy-rule`. Not everything needs the noun — `route` stays `route`. + +Hyphenated single tokens are the point rather than a side effect. A multi-word phrase invites an agent to paraphrase or reorder; a hyphenated token reads as a literal string to copy. This is the same reason the resolution stops joining positionals: with one token there is no order to get wrong. + +`static` → `create-sg-rule` also removes a leak. "Static" is a trust tier, and `engine-selection` is explicit that tier and engine are different axes; naming the ast-grep authoring topic after the tier taught the confusion the other topic exists to correct. + +_Alternative rejected:_ `static-sg` / `static-vale`. Preserves the tier leak and does not match what `route` decides. + +### D3 — Break `TOPICS`, no deprecation window + +`TOPICS` becomes `["create-sg-rule", "create-vale-rule", "create-runtime-rule"]`. `engine-selection` leaves the export because it stops existing (D1); the criterion it carried is now stated by the destinations themselves. + +The package is pre-1.0, so this ships **MINOR**. That is what the leading zero means, and it holds for every backwards-incompatible item in this change. An alias would have to be carried by the type union (`PromptTopic`), the `PROMPTS` map, and the disjointness test, and would be dead the moment the generator updates. + +The real exposure is not the rename but the **deploy skew**: the generator is a separate deploy consuming a published package, so it breaks on upgrade rather than at our build time. Mitigated by the changeset, not by code. + +_Alternative rejected:_ export both names for one release. Doubles the exported surface to protect a single known consumer that we control. + +### D4 — Section-less scaffold, and stderr notices as its precondition + +The scaffolded `.vale.ini` carries `StylesPath` and `MinAlertLevel` and no section. Measured: Vale runs clean and reports `{}`. + +This is only safe **with** stderr surfacing, and the two ship together. With no section to copy, the natural first edit is `rules. = YES` at top level, which Vale reports as `W101 '' isn't a core option; Vale is ignoring it` — on stderr, with exit 0 and valid `{}` on stdout. `runVale` reads stderr only on a non-zero exit, so today that diagnostic is discarded and the user gets: rule authored, `verify` passes, `check` silent. That is the exact failure the Vale work exists to eliminate, and shipping the scaffold change alone would reintroduce it one level up. + +_Alternative rejected:_ scaffold `[*.md]`. A user's first rule works immediately, but the default silently decides scope for them, and markdown is a guess about what a repo's prose is. + +### D5 — The `cli-help` capability keeps its name + +The command becomes `agent`; the spec file stays `openspec/specs/cli-help/spec.md`. + +Renaming a capability means moving a spec directory and rewriting every cross-reference to it in the same change that already renames a command and four topics. The capability's subject — the CLI's agent-knowledge surface — is unchanged; only its command name moves. Worth doing later on its own, and worth not doing here. + +## Risks / Trade-offs + +- **Generator breaks on CLI upgrade, not at build time** → `TOPICS` is consumed across a deploy boundary. The changeset must name the rename explicitly, and the generator's update is a coordinated follow-up rather than an assumption. +- **306 mechanical edits invite a missed one** → a stale `taskless help X` in a recipe is invisible until an agent runs it and gets nothing. Mitigated by an assertion that no shipped recipe contains the string `taskless help`, which is cheap and total. +- **Absorbing engine choice into `route` makes `route` longer** → it now carries the reasoning that justified a separate topic. If it grows past being readable in one pass, the split was load-bearing after all and should come back as a fetch. +- **A section-less scaffold means a fresh project's first Vale rule does nothing until scoped** → intended, and the reason `create-vale-rule` must teach the section rather than assume it. The stderr notice is what makes the failure legible instead of silent. +- **Telemetry vocabulary changes** → `cli_help` topic values change wholesale; anything keyed on `static` goes quiet rather than erroring. Worth naming before it is diagnosed as a traffic drop. + +## Migration Plan + +No user data or on-disk state migrates. Existing projects keep whatever `.vale.ini` they have — the scaffold change affects new projects only, and `0004` is unreleased, so no project has the old scaffold in the field. + +The rename is a hard cutover in one PR: recipes cross-reference each other by literal command, so a partial rename produces recipes pointing at commands that do not exist. + +### D6 — There is no `create-remote-rule`; `create-runtime-rule` covers the gated path + +`route` names four destinations, all engine-shaped. "Remote" is not a fifth engine — it describes who generates, which is a different question, and the one place it bites is the same place `create-runtime-rule` already has to speak: the user is logged out. + +So `create-runtime-rule` owns the gated story end to end — why executing code requires login, reconciliation, and signing, and what to do when the user has none of them. A logged-out user meets one topic explaining one gate, rather than being routed to a topic about remoteness that then explains authentication. + +**What this leaves unresolved, deliberately.** `remote.txt` and `rule-create.txt` today serve a real and different flow: the service generating an _ast-grep_ rule when local authoring cannot. That is escalation, not a destination — `route` is already specified as biased local with the service as last resort — so it survives as a fallback inside `route` rather than as a peer of the four. Whether those two recipes keep their names, merge, or fold into `create-sg-rule`'s failure path is not settled here. + +_Alternative rejected:_ a fifth `create-remote-rule` destination. Puts a non-engine on an engine-shaped list, and splits the logged-out explanation across two topics. + +## Open Questions + +- `remote.txt` and `rule-create.txt` are no longer named by `route`, but still implement service generation. Do they remain fetchable topics, fold into `create-sg-rule`'s escalation path, or merge with each other? Not settled by D6, and worth deciding before the sweep in task group 3 rewrites their cross-references. +- Should `create-runtime-rule` explain the login requirement itself, or defer to `auth`? Stated as the former; worth confirming it does not duplicate `auth`. +- Does the distributed engine criterion (D1) belong in each recipe's Preconditions or as a named section? Affects whether a consumer can extract it mechanically. diff --git a/openspec/changes/agent-command-and-vale-authoring/proposal.md b/openspec/changes/agent-command-and-vale-authoring/proposal.md new file mode 100644 index 0000000..b6e209e --- /dev/null +++ b/openspec/changes/agent-command-and-vale-authoring/proposal.md @@ -0,0 +1,49 @@ +## Why + +`add-vale-rule-engine` shipped a chooser without a destination. `engine-selection` teaches an agent to decide a rule belongs to `vale`, and then there is nowhere to go: `static.txt` is 76 lines of ast-grep authoring, and its only mention of Vale is a See Also line telling the reader to _confirm_ `sg` was right. An agent that follows the procedure correctly and lands on `vale` dead-ends, and so does one that lands on `runtime`. That was recorded as a deliberate non-goal at the time (`add-vale-rule-engine/design.md:16` excludes "generating Vale rules and authoring the committed `.vale.ini`"), but the chooser is what makes the gap reachable, and it is now shipped. + +The same work exposed that the surface an agent reads is shaped for a human. `taskless help` names the command after a human's reason for typing it; agents are not asking for help, they are fetching a procedure. And the surface is addressed longform — `taskless help rule create` resolves by joining positionals — so an agent must know both the words and their order. Hyphenated single tokens read as literal strings an agent copies rather than a phrase it might paraphrase, which is the failure this surface cannot afford. + +## What Changes + +- **BREAKING** — `taskless help ` becomes `taskless agent `. The command is named for who reads it. +- **BREAKING** — topic addressing flattens to a single token. `taskless help rule create` becomes `taskless agent create-rule`; the `positionals.join("-")` resolution is removed rather than generalized. +- **BREAKING** — authoring topics are renamed to verb-noun, matching what `route` decides: + - `static` → `create-sg-rule` + - `existing` → `create-legacy-rule` + - new `create-vale-rule` + - new `create-runtime-rule` +- **BREAKING** — `TOPICS` in `@taskless/cli/prompts` renames with them. `["static", "engine-selection"]` becomes `["create-sg-rule", "create-vale-rule", "create-runtime-rule"]`. No alias is kept. Pre-1.0, a backwards-incompatible change is a **MINOR** bump. +- `route` becomes the single front door, returning a concrete next command rather than a category. Its decision set is the four `create-*-rule` topics; service generation stays an escalation for when local authoring cannot express the rule, not a peer destination. +- **BREAKING** — `engine-selection` merges into `route` and stops existing as a topic. Its criterion distributes: `route` applies it to dispatch, and each `create-*-rule` recipe states the evidence that makes its own engine right. That is what keeps it exportable — a consumer outside the CLI has no `route` step and cannot run `taskless detect --json`, so a chooser topic was unusable to it anyway. +- `create-vale-rule` covers what no topic covers today: authoring a Vale style file under `vale/rules/`, scoping it with a `.vale.ini` section, and writing `pass/`/`fail` fixtures. Consistent with `create-sg-rule`, the agent writes these files; no CLI writer is introduced. +- The scaffolded `.vale.ini` ships **no section**, so a fresh project lints nothing until a user scopes something deliberately. `create-vale-rule` teaches writing that first section. +- Vale's stderr diagnostics on a successful run surface as notices. This is required by the change above, not incidental: with no section to copy, the likely first mistake is a rule assignment at top level, which Vale reports as `W101 ... is ignoring it` on stderr and which today is discarded — reproducing the silent-disable class the Vale work exists to eliminate. + +## Capabilities + +### New Capabilities + +- `cli-agent-authoring`: the four `create-*-rule` procedures — what each engine's authored artifacts are, where they live, and what makes one complete. Covers the Vale authoring path that has no home today. + +### Modified Capabilities + +- `cli-help`: the command renames to `agent` and topic addressing flattens to a single token. Longform resolution is removed. +- `cli-rule-routing`: `route` dispatches to a concrete `create-*-rule` topic rather than a category, absorbing the engine decision it previously deferred to `engine-selection`. +- `cli-knowledge-prompts`: `TOPICS` renames, gains the Vale and runtime authoring topics, and loses `engine-selection`; pre-1.0 breaking changes are restated as MINOR. +- `cli-vale-rule-engine`: the scaffolded config carries no section, and Vale's stderr diagnostics on a zero-exit run become notices. + +## Impact + +- **`packages/cli/src/commands/help.ts`** — renamed, positional-join resolution removed. +- **`packages/cli/src/help/*.txt`** — two renames, two new files, and ~306 cross-references across 77 files that name `taskless help`. +- **`packages/cli/src/prompts/index.ts`** — `TOPICS`/`INTERNAL_TOPICS` membership and the `PromptTopic` union. +- **`@taskless/cli/prompts`** — published, typed export. The platform generator consumes `TOPICS` and deploys separately from the CLI, so it breaks on upgrade rather than at build time. Flagged in the changeset. +- **`packages/cli/src/filesystem/migrations/0004-vale-engine.ts`** — `VALE_CONFIG_CONTENT` drops its `[*]` section. +- **`packages/cli/src/rules/vale/run.ts`** — stderr captured on a zero-exit run and returned as a notice. +- **`skills/taskless/SKILL.md`** — the one skill naming `taskless help`. +- **Telemetry** — `cli_help` events carry a `topic` whose vocabulary changes; dashboards keyed on `static` go quiet. + +## Delivery Shape + +**Single PR**, stacked on #100. The rename is mechanical but total: a half-renamed command surface is not a shippable intermediate state, and splitting the topic renames from the command rename would leave cross-references pointing at commands that do not exist yet. Reviewable because the diff is overwhelmingly one substitution repeated, with four files of genuinely new prose. diff --git a/openspec/changes/agent-command-and-vale-authoring/specs/cli-agent-authoring/spec.md b/openspec/changes/agent-command-and-vale-authoring/specs/cli-agent-authoring/spec.md new file mode 100644 index 0000000..1aad0d1 --- /dev/null +++ b/openspec/changes/agent-command-and-vale-authoring/specs/cli-agent-authoring/spec.md @@ -0,0 +1,63 @@ +## ADDED Requirements + +### Requirement: Every engine a rule can be routed to has an authoring recipe + +The CLI SHALL provide an authoring recipe for each engine `route` can name: `create-sg-rule`, `create-vale-rule`, and `create-runtime-rule`, alongside `create-legacy-rule` for a linter the repository already uses. + +A decision procedure that can produce an answer with no destination is incomplete. Engine selection can conclude `vale` or `runtime`, and before this change neither had a procedure, so an agent that reasoned correctly arrived nowhere. + +#### Scenario: Each engine choice reaches a procedure + +- **WHEN** engine selection concludes `sg`, `vale`, or `runtime` +- **THEN** a recipe exists that authors a rule for that engine + +#### Scenario: A legacy destination exists for repositories with their own linter + +- **WHEN** the repository already runs a linter that can express the rule +- **THEN** `create-legacy-rule` SHALL author it in that tool's own dialect + +### Requirement: The Vale authoring recipe covers rule, scope, and fixtures + +The `create-vale-rule` recipe SHALL instruct the agent to produce three artifacts, and SHALL state that a rule is incomplete without all three: + +1. A Vale style file under `.taskless/vale/rules/.yml`. +2. A section in the committed `.taskless/vale/.vale.ini` scoping which files the rule applies to, enabling it as `rules. = YES`. +3. `pass/` and `fail/` fixture documents under `.taskless/vale/rule-tests//`. + +The recipe SHALL state that the scaffolded config carries no section, so the first rule authored in a project also authors the first scope. + +#### Scenario: Authoring produces all three artifacts + +- **WHEN** the agent follows `create-vale-rule` +- **THEN** it writes the style file, a scoping section enabling the rule, and both fixture buckets + +#### Scenario: The recipe teaches the first section + +- **WHEN** a project's `.vale.ini` has no section yet +- **THEN** the recipe SHALL direct the agent to add one scoped to the files the rule is about, rather than assuming a section exists + +#### Scenario: An unscoped rule is not silently accepted + +- **WHEN** the agent enables a rule without placing it inside a section +- **THEN** the recipe SHALL identify this as incomplete, because Vale ignores a rule assignment outside a section + +### Requirement: Authoring recipes write files rather than invoking a writer + +The `create-*-rule` recipes SHALL instruct the agent to write the rule, its configuration, and its fixtures directly. The CLI SHALL NOT provide a command that generates a Vale style file or edits `.vale.ini` on the agent's behalf. + +This matches how ast-grep rules are authored today: the agent writes the rule and its config entry, and construction belongs to the downstream generator rather than to the CLI. + +#### Scenario: No CLI writer for Vale configuration + +- **WHEN** an agent authors a Vale rule +- **THEN** it edits `.vale.ini` itself +- **AND** the CLI SHALL NOT offer a subcommand that performs that edit + +### Requirement: The runtime authoring recipe states why it is gated + +The `create-runtime-rule` recipe SHALL explain that runtime rules execute code and therefore require login, reconciliation, and signing, and SHALL state this as a property of executing code rather than as a property of the engine's capability. + +#### Scenario: The gate is explained where it is encountered + +- **WHEN** an agent follows `create-runtime-rule` +- **THEN** the recipe SHALL state why the runtime tier is gated when the static tiers are not diff --git a/openspec/changes/agent-command-and-vale-authoring/specs/cli-help/spec.md b/openspec/changes/agent-command-and-vale-authoring/specs/cli-help/spec.md new file mode 100644 index 0000000..72a1d57 --- /dev/null +++ b/openspec/changes/agent-command-and-vale-authoring/specs/cli-help/spec.md @@ -0,0 +1,60 @@ +## MODIFIED Requirements + +### Requirement: Help subcommand displays rich help text for commands + +The CLI SHALL support an `agent` subcommand that accepts at most one positional argument identifying a topic AND an optional `--anonymous` boolean flag. Topics SHALL be addressed by a single token; the subcommand SHALL NOT join multiple positionals into a topic key. When a topic is provided, the 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 agent` for the topic index. + +When no positional argument is provided, the subcommand SHALL print a topic index containing a one-paragraph human slug followed by a topic disambiguation table mapping topic names to their summaries. + +The subcommand is named for its reader. It serves agents fetching a procedure, not humans asking for help, and single-token addressing exists so a topic name is a literal string an agent copies rather than a phrase it can reorder or paraphrase. + +#### Scenario: Agent subcommand for a topic returns the recipe + +- **WHEN** a user runs `taskless agent check` +- **THEN** the CLI SHALL print the contents of `check.txt` to stdout + +#### Scenario: Multi-word topic paths are not resolved + +- **WHEN** a user runs `taskless agent rule create` +- **THEN** the CLI SHALL NOT look up `rule-create.txt` by joining the positionals +- **AND** it SHALL exit non-zero rather than guessing a topic + +#### Scenario: Formerly nested topics are addressed by one token + +- **WHEN** a user runs `taskless agent create-rule` +- **THEN** the CLI SHALL look up `create-rule.txt` and print its contents + +#### Scenario: The former command name is gone + +- **WHEN** a user runs `taskless help check` +- **THEN** the CLI SHALL NOT print recipe text for `check` + +### Requirement: Routing recipes reference engine selection + +The `route` recipe SHALL apply the engine-selection reasoning directly and name a concrete authoring topic, rather than referring the reader onward to select an engine. The `create-sg-rule` and `create-vale-rule` recipes SHALL reference `engine-selection` so an agent that arrives at one directly can confirm the engine is right for the rule in hand. + +#### Scenario: Route names a destination without a second fetch + +- **WHEN** an agent follows `route` +- **THEN** the recipe SHALL name one `create-*-rule` topic or `remote` +- **AND** it SHALL NOT require fetching `engine-selection` first to do so + +#### Scenario: Authoring recipes cross-reference engine selection + +- **WHEN** an agent reads `create-sg-rule` or `create-vale-rule` +- **THEN** the recipe SHALL reference `taskless agent engine-selection` as the check that the engine matches the rule + +## ADDED Requirements + +### Requirement: Shipped recipes name only commands that exist + +No embedded recipe SHALL contain the string `taskless help`. Recipes cross-reference each other by literal command string, so a stale reference is invisible until an agent runs it and receives nothing. + +#### Scenario: No recipe references the removed command + +- **WHEN** the embedded recipe set is inspected +- **THEN** no recipe SHALL contain `taskless help` diff --git a/openspec/changes/agent-command-and-vale-authoring/specs/cli-knowledge-prompts/spec.md b/openspec/changes/agent-command-and-vale-authoring/specs/cli-knowledge-prompts/spec.md new file mode 100644 index 0000000..fce97a1 --- /dev/null +++ b/openspec/changes/agent-command-and-vale-authoring/specs/cli-knowledge-prompts/spec.md @@ -0,0 +1,39 @@ +## MODIFIED Requirements + +### Requirement: Topic names and accessor shape are stable public API + +The set of `PromptTopic` names, the `getPrompt`/`PROMPTS` shape, and the existing fields of `PromptOptions` SHALL be treated as public API; recipe _text_ MAY change freely. + +The package is pre-1.0, so a backwards-incompatible change to that surface SHALL be released as a **MINOR** bump. This is what the leading zero means, and it applies to renaming a topic, removing one, or changing the accessor signature. + +What the requirement actually protects is not the version number but the notice. `TOPICS` is consumed across a deploy boundary, so a downstream consumer breaks when it upgrades rather than when this package builds, and the version alone cannot warn anyone. A breaking change SHALL therefore name the removed or renamed topics explicitly in its changeset. + +#### Scenario: Renaming or removing a topic + +- **WHEN** a topic is removed or renamed, or the accessor signature changes +- **THEN** it SHALL be released as a MINOR bump +- **AND** the changeset SHALL name the removed or renamed topics +- **AND** a recipe text edit SHALL require neither + +#### Scenario: Adding an option + +- **WHEN** a new optional field is added to `PromptOptions` +- **THEN** it SHALL NOT require more than a PATCH bump, since existing call sites keep their behavior + +## ADDED Requirements + +### Requirement: Exported topics cover both static-tier authoring paths + +`TOPICS` SHALL export the authoring recipe for each static-tier engine — `create-sg-rule` and `create-vale-rule` — alongside `engine-selection`. + +A consumer that can decide a rule belongs to `vale` must be able to reach the procedure for authoring one. Exporting the chooser without the Vale destination reproduces, for the platform generator, the dead end this change removes from the CLI. + +#### Scenario: Both authoring paths are reachable from the export + +- **WHEN** a consumer imports `TOPICS` +- **THEN** it SHALL contain `create-sg-rule`, `create-vale-rule`, and `engine-selection` + +#### Scenario: The exported set follows the rename + +- **WHEN** a consumer imports `TOPICS` +- **THEN** it SHALL NOT contain `static`, which no longer names a recipe diff --git a/openspec/changes/agent-command-and-vale-authoring/specs/cli-rule-routing/spec.md b/openspec/changes/agent-command-and-vale-authoring/specs/cli-rule-routing/spec.md new file mode 100644 index 0000000..0b18fd9 --- /dev/null +++ b/openspec/changes/agent-command-and-vale-authoring/specs/cli-rule-routing/spec.md @@ -0,0 +1,88 @@ +## MODIFIED Requirements + +### Requirement: Route is the local authoring classifier + +The CLI SHALL provide a `route` help recipe that instructs the agent to classify a rule-authoring request into one of four destinations — `create-legacy-rule`, `create-sg-rule`, `create-vale-rule`, or `create-runtime-rule` — using `taskless detect --json` signals plus the user's intent. The `route` recipe SHALL be biased to stay local, treating service generation as an escalation available when local authoring cannot express the rule rather than as a peer destination. + +`route` SHALL decide the engine as part of this classification rather than deferring it to a separate topic. There is one decision, made from one reading of the evidence: whether a rule is expressible locally and which engine can express it are answered from the same signals, so splitting them costs a second fetch and a handoff without adding information. + +Each destination SHALL be a topic an agent can fetch by name, so classifying produces a command to run rather than a category to interpret. + +#### Scenario: Route fetches detection before classifying + +- **WHEN** the agent fetches the `route` recipe to author a rule +- **THEN** the recipe SHALL direct the agent to run `taskless detect --json` and + use its signals as input to the classification + +#### Scenario: Route classifies into one of four destinations + +- **WHEN** the agent follows `route` +- **THEN** it SHALL select exactly one of `create-legacy-rule`, `create-sg-rule`, `create-vale-rule`, or `create-runtime-rule` +- **AND** it SHALL fetch the corresponding recipe to perform the authoring + +#### Scenario: Every destination resolves to a recipe + +- **WHEN** any destination `route` can name is fetched +- **THEN** a recipe of that exact name SHALL exist + +#### Scenario: The engine is decided without a second fetch + +- **WHEN** the agent follows `route` +- **THEN** it SHALL arrive at an engine-specific recipe without fetching a separate engine-selection topic + +### Requirement: Static recipe authors a verified local ast-grep rule + +The CLI SHALL provide a `create-sg-rule` help recipe that instructs the agent to author a +local ast-grep rule on-device, without calling the Taskless service, and to +verify it against the user's success and failure cases before reporting success. +The recipe SHALL produce the canonical on-disk rule shape and paths used by remote +generation so that `check`, `improve`, and `verify` see a single dialect. + +The recipe SHALL be named for the artifact it produces rather than for a trust tier. "Static" describes when a rule runs, which is a different axis from which engine enforces it, and naming the ast-grep authoring path after the tier taught the conflation that engine selection exists to correct. + +#### Scenario: Local authoring without the service + +- **WHEN** the agent follows `create-sg-rule` +- **THEN** it SHALL write the rule on-device without requiring login or the + Taskless API + +### Requirement: Trust tier is not an engine-selection input + +Engine reasoning SHALL NOT treat login, reconciliation, or signing as inputs to the engine choice: `sg` and `vale` are both static-tier, and only `runtime` carries those concerns, so trust tier is a distinct axis from which engine can express a rule. + +#### Scenario: Trust tier is not an engine-selection input + +- **WHEN** the reasoning distinguishes `sg` from `vale` +- **THEN** it does so on the prose-versus-structure axis, not on any auth, reconcile, or signing property, since both are static-tier + +## ADDED Requirements + +### Requirement: Engine reasoning lives in route and in each destination + +The engine criterion SHALL be stated in `route`, which applies it to dispatch, and in each `create-*-rule` recipe, which states the evidence that makes its own engine the right one. It SHALL NOT be stated in a separate chooser topic. + +Distributing it this way is what lets the reasoning stay exportable. A consumer outside the CLI has no `route` step and cannot run `taskless detect --json`, so a chooser topic is unusable to it; a destination that states its own criterion is usable by anything that can read one recipe. It also removes the drift class where a chooser and its destinations disagree about when each applies. + +#### Scenario: A destination states when it applies + +- **WHEN** an agent or consumer reads `create-sg-rule` or `create-vale-rule` +- **THEN** the recipe SHALL state the evidence that makes that engine the right one for a rule + +#### Scenario: No separate chooser topic exists + +- **WHEN** the embedded recipe set is inspected +- **THEN** there SHALL be no topic whose only purpose is selecting among engines + +## REMOVED Requirements + +### Requirement: Engine selection is a separate axis from authoring destination + +**Reason**: The separation cost an agent two fetches and a handoff to answer one question. Whether a rule is expressible locally and which engine can express it are answered from the same evidence, so reading it twice added a failure point without adding information. + +**Migration**: The engine criterion moves into `route` and into each `create-*-rule` recipe (see "Engine reasoning lives in route and in each destination"). The one part of this requirement that was not about the split — that trust tier is a distinct axis — is retained as its own requirement above. Consumers that fetched `engine-selection` read the destination recipes instead, which is the surface `TOPICS` now exports. + +### Requirement: An engine-selection topic states which engine can enforce a rule + +**Reason**: The topic it required no longer exists as a separate recipe. + +**Migration**: Its content — the three engine definitions, evidence-before-answer, and the boundary cases — moves into `route` and the `create-*-rule` recipes. The requirements that constrained the reasoning itself ("Available code context outranks the phrasing of the request", "Ambiguity resolves to an engine known to be available") remain in force and now bind `route` and the destination recipes rather than a standalone topic. diff --git a/openspec/changes/agent-command-and-vale-authoring/specs/cli-vale-rule-engine/spec.md b/openspec/changes/agent-command-and-vale-authoring/specs/cli-vale-rule-engine/spec.md new file mode 100644 index 0000000..9181652 --- /dev/null +++ b/openspec/changes/agent-command-and-vale-authoring/specs/cli-vale-rule-engine/spec.md @@ -0,0 +1,39 @@ +## ADDED Requirements + +### Requirement: The scaffolded Vale config carries no section + +The `.vale.ini` written when a project is scaffolded SHALL contain `StylesPath` and `MinAlertLevel` and no section. A project therefore lints nothing with Vale until someone scopes something deliberately. + +An unscoped `[*]` applies every enabled rule to every file the walk reaches, which makes the default the most aggressive scope available rather than the narrowest. Scope is the author's decision, and the scaffold SHALL NOT make it on their behalf. + +#### Scenario: A freshly scaffolded project reports nothing + +- **WHEN** `check` runs against a scaffolded project with a rule file present and no section added +- **THEN** Vale SHALL report no findings +- **AND** the run SHALL NOT be reported as an engine failure + +#### Scenario: Scope is added by the author + +- **WHEN** an author scopes a rule by adding a section +- **THEN** only files matching that section SHALL be subject to it + +### Requirement: Vale diagnostics on a successful run are surfaced as notices + +When Vale exits zero and writes to stderr, the CLI SHALL surface that output as a notice on the check result. A notice SHALL NOT affect the exit code. + +This is a precondition of the section-less scaffold rather than an independent improvement. With no section to copy, the likely first edit is a rule assignment at the top level of the file, which Vale reports as ignoring — on stderr, with a zero exit and a well-formed empty result. Discarding that output leaves the author with a rule that verifies, runs, and reports nothing, which is the silent-disable failure this engine's design exists to prevent. + +#### Scenario: An ignored rule assignment reaches the user + +- **WHEN** `.vale.ini` enables a rule outside any section and `check` runs +- **THEN** the CLI SHALL surface Vale's diagnostic that the assignment was ignored + +#### Scenario: A diagnostic does not fail the check + +- **WHEN** Vale exits zero, writes a diagnostic to stderr, and reports no findings +- **THEN** the check SHALL exit zero + +#### Scenario: Silence stays silent + +- **WHEN** Vale exits zero and writes nothing to stderr +- **THEN** the CLI SHALL add no notice diff --git a/openspec/changes/agent-command-and-vale-authoring/tasks.md b/openspec/changes/agent-command-and-vale-authoring/tasks.md new file mode 100644 index 0000000..eac50fc --- /dev/null +++ b/openspec/changes/agent-command-and-vale-authoring/tasks.md @@ -0,0 +1,45 @@ +# Tasks + +## 1. Rename the command + +- [ ] 1.1 Rename `packages/cli/src/commands/help.ts` to `agent.ts` and the exported command to `agent`. Register it in `src/index.ts` +- [ ] 1.2 Remove the positional-join resolution (`positionals.join("-")`). Accept at most one positional; more than one is an error rather than a joined key. The unknown-topic message points at `taskless agent` +- [ ] 1.3 Keep the telemetry event name `cli_help` or rename it deliberately — decide once and record it, since dashboards key on it. If renamed, note it in the changeset alongside the `TOPICS` break +- [ ] 1.4 Update `help-extensions.test.ts`, `help-routing-telemetry.test.ts`, `anonymous-flag.test.ts`, `onboard.test.ts`, and `cli.test.ts` to invoke `agent` + +## 2. Rename and add the authoring topics + +- [ ] 2.1 `git mv` `help/static.txt` → `help/create-sg-rule.txt`; retitle its header and rewrite its Goal to name the artifact rather than the tier +- [ ] 2.2 `git mv` `help/existing.txt` → `help/create-legacy-rule.txt`; retitle and update its header +- [ ] 2.3 Flatten the `rule-*` topics to verb-noun single tokens (`rule-create` → `create-rule`, `rule-improve` → `improve-rule`, `rule-delete` → `delete-rule`, `rule-verify` → `verify-rule`), including their `.anonymous` variants. Decide `rule-meta` and `rule` deliberately — they are not creation verbs and may keep their names +- [ ] 2.4 Author `help/create-vale-rule.txt`: the three artifacts (style file, `.vale.ini` section, `pass/`/`fail` fixtures), that the scaffold ships section-less so the first rule writes the first scope, and that a rule enabled outside a section is ignored by Vale. State the evidence that makes `vale` the right engine for a rule, since no chooser topic states it any more. Cross-reference `verify-rule` +- [ ] 2.5 Author `help/create-runtime-rule.txt`: what a runtime rule is, where its `check.ts` lives, and why executing code requires login, reconciliation, and signing when the static tiers do not +- [ ] 2.6 Rewrite `help/route.txt` to classify into the four `create-*-rule` destinations, applying the engine reasoning inline rather than deferring to a second fetch. Service generation is an escalation when local authoring cannot express the rule, not a fifth destination. It must name a command the agent can run verbatim +- [ ] 2.7 Merge `help/engine-selection.txt` into `help/route.txt` and delete it. Its three engine definitions, evidence-before-answer procedure, and boundary cases move into `route`; the per-engine criterion is restated in each `create-*-rule` recipe so a consumer reading one destination knows when it applies +- [ ] 2.8 Re-home the engine-reasoning requirements that survive the merge — "Available code context outranks the phrasing of the request" and "Ambiguity resolves to an engine known to be available" now bind `route` and the destinations. Update `help-extensions.test.ts`, which asserts against the standalone topic + +## 3. Sweep the cross-references + +- [ ] 3.1 Replace every `taskless help ` occurrence with `taskless agent ` across `src/help/*.txt`, `src/**/*.ts`, `skills/taskless/SKILL.md`, `README.md`, and `packages/cli/README.md` (~306 occurrences, 77 files). Leave `CHANGELOG.md` alone — it is a historical record +- [ ] 3.2 Update every reference to a renamed topic (`static`, `existing`, `rule create`, …) to its new single-token name +- [ ] 3.3 Add a test asserting no shipped recipe contains the string `taskless help`, and that every topic named in a recipe's See Also resolves to an embedded file. A stale cross-reference is otherwise invisible until an agent runs it + +## 4. Update the export surface + +- [ ] 4.1 `TOPICS` becomes `["create-sg-rule", "create-vale-rule", "create-runtime-rule"]` and no longer exports `engine-selection`; move the renamed authoring topics through `INTERNAL_TOPICS` as their membership requires, keeping the two lists disjoint and jointly exhaustive over the recipe files +- [ ] 4.2 Update `prompts.test.ts` — the membership test compares against the files on disk, so it fails until the rename is complete in both places +- [ ] 4.3 Write the changeset as **MINOR** — pre-1.0, backwards-incompatible is MINOR — naming the removed topic names explicitly and stating that `@taskless/cli/prompts` consumers break on upgrade rather than at build time + +## 5. Scaffold and diagnostics (ships together) + +- [ ] 5.1 `VALE_CONFIG_CONTENT` in `0004-vale-engine.ts` drops its `[*]` section, leaving `StylesPath` and `MinAlertLevel` +- [ ] 5.2 `runVale` captures stderr on a zero-exit run and returns it as a notice on the `ok` outcome; `runValeEngine` forwards it to `DispatchResult.notices`. A notice must not touch the exit code +- [ ] 5.3 Test that a rule enabled outside a section produces a notice containing Vale's `W101` text and exits zero — this is the pairing that keeps 5.1 from reintroducing a silent disable +- [ ] 5.4 Extend the mixed-engine integration test: a scaffolded project with a rule file and no section reports nothing and does not fail; adding a section makes the same rule fire + +## 6. Verify + +- [ ] 6.1 `pnpm typecheck`, `pnpm lint`, `pnpm --filter @taskless/cli build`, `pnpm --filter @taskless/cli test` +- [ ] 6.2 Run `taskless agent` with no argument, with each renamed topic, and with a removed name, confirming the index lists the new vocabulary and a removed name exits non-zero +- [ ] 6.3 `pnpm openspec validate --all --strict` (note: `cli-rules` and `cli-update-engine` fail on `main` already and are unrelated) +- [ ] 6.4 Archive the change From 6f1f5880470df148671b10c4b1485607b3184f0a Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Wed, 12 Aug 2026 22:20:04 -0700 Subject: [PATCH 2/7] docs(openspec): route on login state, and merge remote into create-remote-rule Settles the two open questions. `route` reads login state early, because it determines which destinations exist and discovering it late means classifying against the wrong set. It does not follow that the agent should open by asking "do you want remote generation?" -- at that point neither it nor the user knows whether the rule is a two-line pattern or something local authoring cannot express, so the question costs a turn and cannot be answered well. Service generation is offered where it is genuinely a choice: locally expressible AND logged in. Not logged in, or not locally expressible, are not choices and are not posed as one. That narrows the biased-local requirement rather than reversing it. The bias survives for the case it was written about -- local authoring that works is not abandoned for the service -- while a logged-in user stops being steered away from a path they already pay for. No recipe delegates to another. A logged-in runtime request routes straight to `create-remote-rule`; `create-runtime-rule` becomes the logged-out path and owns the gate explanation. Routing runtime through a topic that forwards would reintroduce the second fetch D1 removes, and split one explanation across two files. `remote.txt` and `rule-create.txt` merge into `create-remote-rule` -- a content merge, not a rename, since split across a boundary statement and a procedure an agent fetches one only to learn it needs the other. Adds a verification step worth more than prose review: rehearse each recipe against a subagent with no prior context, ask what steps it would take, and check that account against what the recipe intends. A recipe that reads correctly to its author and produces the wrong plan is exactly what reading it again cannot catch. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3 --- .../design.md | 37 +++++++++++++++---- .../proposal.md | 4 +- .../specs/cli-agent-authoring/spec.md | 24 +++++++++++- .../specs/cli-rule-routing/spec.md | 24 ++++++++++-- .../agent-command-and-vale-authoring/tasks.md | 8 ++-- 5 files changed, 80 insertions(+), 17 deletions(-) diff --git a/openspec/changes/agent-command-and-vale-authoring/design.md b/openspec/changes/agent-command-and-vale-authoring/design.md index 1b06336..9ccb615 100644 --- a/openspec/changes/agent-command-and-vale-authoring/design.md +++ b/openspec/changes/agent-command-and-vale-authoring/design.md @@ -18,7 +18,7 @@ Separately, the scaffolded `.vale.ini` opens an unscoped `[*]`. A whole-project **Non-Goals:** - **A `.vale.ini` writer.** The agent authors the section, exactly as it authors `sgconfig.yml` rule entries today. Construction moves to the downstream generator, consistent with `add-vale-rule-engine/design.md:107`. -- Rule _generation_ for Vale via the service. `remote` remains the service path; this change gives the local path a destination. +- Rule _generation_ for Vale via the service. `create-remote-rule` dispatches to the service as it does today; this change gives the local paths destinations and renames the remote one. - Renaming the `cli-help` capability file. The command renames; the spec keeps its name, so this change does not also move spec files (see D5). - Restricting Vale's feature set, or deciding build-output exclusion (tracked separately in #101). @@ -26,7 +26,7 @@ Separately, the scaffolded `.vale.ini` opens an unscoped `[*]`. A whole-project ### D1 — `route` and `engine-selection` merge into one topic -There is one decision, made once. `route` absorbs the engine reasoning and returns one of the four `create-*-rule` topics. `engine-selection` ceases to exist as a separate topic. +There is one decision, made once. `route` absorbs the engine reasoning and returns one of the five `create-*-rule` topics. `engine-selection` ceases to exist as a separate topic. This reverses the scoping `engine-selection` asserts today — that route decides destination, the topic decides engine, and locally the two compose. The separation is clean on paper and expensive in practice: it costs an agent two fetches and a correct handoff between them to answer one question, and the handoff is where an agent drops context. Worse, the two decisions are not independent in the direction the split assumes; "author this locally" and "which engine can express it" are answered from the same evidence, so splitting them means reading the same signals twice. @@ -40,7 +40,7 @@ _Alternative rejected:_ export `route`. It is local-only by construction. ### D2 — Verb-noun names, single token, no aliases -`create-sg-rule`, `create-vale-rule`, `create-runtime-rule`, `create-legacy-rule`. Not everything needs the noun — `route` stays `route`. +`create-sg-rule`, `create-vale-rule`, `create-runtime-rule`, `create-legacy-rule`, `create-remote-rule`. Not everything needs the noun — `route` stays `route`. Hyphenated single tokens are the point rather than a side effect. A multi-word phrase invites an agent to paraphrase or reorder; a hyphenated token reads as a literal string to copy. This is the same reason the resolution stops joining positionals: with one token there is no order to get wrong. @@ -86,18 +86,39 @@ No user data or on-disk state migrates. Existing projects keep whatever `.vale.i The rename is a hard cutover in one PR: recipes cross-reference each other by literal command, so a partial rename produces recipes pointing at commands that do not exist. -### D6 — There is no `create-remote-rule`; `create-runtime-rule` covers the gated path +### D6 — The logged-out gate is explained once, by `create-runtime-rule` -`route` names four destinations, all engine-shaped. "Remote" is not a fifth engine — it describes who generates, which is a different question, and the one place it bites is the same place `create-runtime-rule` already has to speak: the user is logged out. +"Remote" describes who generates rather than which engine, so the two are not peers in kind — but the place that matters is the same place `create-runtime-rule` has to speak anyway: the user is logged out. -So `create-runtime-rule` owns the gated story end to end — why executing code requires login, reconciliation, and signing, and what to do when the user has none of them. A logged-out user meets one topic explaining one gate, rather than being routed to a topic about remoteness that then explains authentication. +`create-runtime-rule` therefore owns the gated story — why executing code requires login, reconciliation, and signing, and how to get there. A logged-out user meets one topic explaining one gate, rather than being handed between a topic about remoteness and one about authentication. **What this leaves unresolved, deliberately.** `remote.txt` and `rule-create.txt` today serve a real and different flow: the service generating an _ast-grep_ rule when local authoring cannot. That is escalation, not a destination — `route` is already specified as biased local with the service as last resort — so it survives as a fallback inside `route` rather than as a peer of the four. Whether those two recipes keep their names, merge, or fold into `create-sg-rule`'s failure path is not settled here. _Alternative rejected:_ a fifth `create-remote-rule` destination. Puts a non-engine on an engine-shaped list, and splits the logged-out explanation across two topics. +### D7 — Login state is read early; remote is offered only where it is a choice + +`route` reads login state near the top, before dispatching. It changes which destinations exist, so discovering it late means classifying against a set that may be wrong. + +It does **not** follow that the agent should open by asking "do you want remote generation?". At that point it does not know whether the rule is a two-line `sg` pattern or something local authoring cannot express, and neither does the user — the question costs a turn and cannot be answered well. Remote is offered when it is genuinely a choice: the rule is locally expressible **and** the user is logged in. Not logged in, or not locally expressible, are not choices and are not posed as one. + +This narrows the existing "biased to stay local" requirement rather than reversing it. The bias survives for the case it was written about — local authoring that _works_ is not abandoned for the service — while a logged-in user stops being steered away from a path they have already paid for. + +**No topic delegates to another.** A logged-in runtime request routes to `create-remote-rule` directly; `create-runtime-rule` is the logged-out path. Routing runtime through a topic that then forwards would reintroduce the second fetch D1 exists to remove, and would split the login explanation across two files. + +_Alternative rejected:_ offer remote unconditionally as step one. Reverses the local bias outright, and asks a question before the information exists to answer it. + +_Alternative rejected:_ `create-runtime-rule` checks login and forwards. Two fetches, and the reader meets the gate explanation only on one branch. + +### D8 — `remote` and `rule-create` merge into `create-remote-rule` + +`remote.txt` states the client-side boundary; `rule-create.txt` is the procedure that enriches a description, calls the API, and reports. Split across two topics, an agent fetches one to learn it needs the other. + +This is a content merge, not a rename: both texts have material that survives, and the result has to read as one procedure rather than two concatenated. + +_Alternative rejected:_ keep `remote` as a boundary statement and `create-remote-rule` as the procedure. Preserves the second fetch under new names. + ## Open Questions -- `remote.txt` and `rule-create.txt` are no longer named by `route`, but still implement service generation. Do they remain fetchable topics, fold into `create-sg-rule`'s escalation path, or merge with each other? Not settled by D6, and worth deciding before the sweep in task group 3 rewrites their cross-references. -- Should `create-runtime-rule` explain the login requirement itself, or defer to `auth`? Stated as the former; worth confirming it does not duplicate `auth`. +- Does `create-runtime-rule` duplicate `auth`? It now owns the login explanation for the runtime tier specifically; worth confirming the two do not drift. - Does the distributed engine criterion (D1) belong in each recipe's Preconditions or as a named section? Affects whether a consumer can extract it mechanically. diff --git a/openspec/changes/agent-command-and-vale-authoring/proposal.md b/openspec/changes/agent-command-and-vale-authoring/proposal.md index b6e209e..f64115b 100644 --- a/openspec/changes/agent-command-and-vale-authoring/proposal.md +++ b/openspec/changes/agent-command-and-vale-authoring/proposal.md @@ -13,9 +13,11 @@ The same work exposed that the surface an agent reads is shaped for a human. `ta - `existing` → `create-legacy-rule` - new `create-vale-rule` - new `create-runtime-rule` + - `remote` + `rule-create` merge into `create-remote-rule` - **BREAKING** — `TOPICS` in `@taskless/cli/prompts` renames with them. `["static", "engine-selection"]` becomes `["create-sg-rule", "create-vale-rule", "create-runtime-rule"]`. No alias is kept. Pre-1.0, a backwards-incompatible change is a **MINOR** bump. -- `route` becomes the single front door, returning a concrete next command rather than a category. Its decision set is the four `create-*-rule` topics; service generation stays an escalation for when local authoring cannot express the rule, not a peer destination. +- `route` becomes the single front door, returning a concrete next command rather than a category. Its decision set is the five `create-*-rule` topics. It reads login state early, because that changes which destinations exist, and offers service generation **only where it is a real choice** — when the rule is locally expressible and the user is logged in. A user who is not logged in, or a rule local authoring cannot express, is not being offered anything. - **BREAKING** — `engine-selection` merges into `route` and stops existing as a topic. Its criterion distributes: `route` applies it to dispatch, and each `create-*-rule` recipe states the evidence that makes its own engine right. That is what keeps it exportable — a consumer outside the CLI has no `route` step and cannot run `taskless detect --json`, so a chooser topic was unusable to it anyway. +- `create-runtime-rule` becomes the logged-**out** path: what a runtime rule is, why executing code requires login, reconciliation, and signing, and how to get there. A logged-in runtime request goes straight to `create-remote-rule` from `route`, so no topic delegates to another. - `create-vale-rule` covers what no topic covers today: authoring a Vale style file under `vale/rules/`, scoping it with a `.vale.ini` section, and writing `pass/`/`fail` fixtures. Consistent with `create-sg-rule`, the agent writes these files; no CLI writer is introduced. - The scaffolded `.vale.ini` ships **no section**, so a fresh project lints nothing until a user scopes something deliberately. `create-vale-rule` teaches writing that first section. - Vale's stderr diagnostics on a successful run surface as notices. This is required by the change above, not incidental: with no section to copy, the likely first mistake is a rule assignment at top level, which Vale reports as `W101 ... is ignoring it` on stderr and which today is discarded — reproducing the silent-disable class the Vale work exists to eliminate. diff --git a/openspec/changes/agent-command-and-vale-authoring/specs/cli-agent-authoring/spec.md b/openspec/changes/agent-command-and-vale-authoring/specs/cli-agent-authoring/spec.md index 1aad0d1..7a2f382 100644 --- a/openspec/changes/agent-command-and-vale-authoring/specs/cli-agent-authoring/spec.md +++ b/openspec/changes/agent-command-and-vale-authoring/specs/cli-agent-authoring/spec.md @@ -53,11 +53,31 @@ This matches how ast-grep rules are authored today: the agent writes the rule an - **THEN** it edits `.vale.ini` itself - **AND** the CLI SHALL NOT offer a subcommand that performs that edit -### Requirement: The runtime authoring recipe states why it is gated +### Requirement: The runtime authoring recipe is the logged-out path -The `create-runtime-rule` recipe SHALL explain that runtime rules execute code and therefore require login, reconciliation, and signing, and SHALL state this as a property of executing code rather than as a property of the engine's capability. +The `create-runtime-rule` recipe SHALL explain that runtime rules execute code and therefore require login, reconciliation, and signing, SHALL state this as a property of executing code rather than of the engine's capability, and SHALL say how to obtain access. + +It SHALL NOT forward the agent to another authoring recipe. A logged-in runtime request is routed to `create-remote-rule` by `route`, so this recipe is reached only when the gate is closed and exists to explain that one gate once. #### Scenario: The gate is explained where it is encountered - **WHEN** an agent follows `create-runtime-rule` - **THEN** the recipe SHALL state why the runtime tier is gated when the static tiers are not +- **AND** it SHALL state how to obtain access + +#### Scenario: The recipe does not delegate + +- **WHEN** an agent follows `create-runtime-rule` +- **THEN** it SHALL NOT be directed to fetch another authoring recipe to proceed + +### Requirement: Service generation is one recipe + +The CLI SHALL provide a single `create-remote-rule` recipe covering both the client-side boundary of service generation and the procedure itself — enriching the user's description, dispatching to the Taskless service, and reporting the result. + +Split across a boundary statement and a procedure, an agent fetches one only to learn it needs the other, which is the second fetch this change exists to remove. + +#### Scenario: One fetch reaches the whole procedure + +- **WHEN** an agent follows `create-remote-rule` +- **THEN** the recipe SHALL carry both the boundary and the dispatch procedure +- **AND** it SHALL NOT require fetching a second topic to complete the request diff --git a/openspec/changes/agent-command-and-vale-authoring/specs/cli-rule-routing/spec.md b/openspec/changes/agent-command-and-vale-authoring/specs/cli-rule-routing/spec.md index 0b18fd9..62cf0ce 100644 --- a/openspec/changes/agent-command-and-vale-authoring/specs/cli-rule-routing/spec.md +++ b/openspec/changes/agent-command-and-vale-authoring/specs/cli-rule-routing/spec.md @@ -2,7 +2,7 @@ ### Requirement: Route is the local authoring classifier -The CLI SHALL provide a `route` help recipe that instructs the agent to classify a rule-authoring request into one of four destinations — `create-legacy-rule`, `create-sg-rule`, `create-vale-rule`, or `create-runtime-rule` — using `taskless detect --json` signals plus the user's intent. The `route` recipe SHALL be biased to stay local, treating service generation as an escalation available when local authoring cannot express the rule rather than as a peer destination. +The CLI SHALL provide a `route` help recipe that instructs the agent to classify a rule-authoring request into one of five destinations — `create-legacy-rule`, `create-sg-rule`, `create-vale-rule`, `create-runtime-rule`, or `create-remote-rule` — using `taskless detect --json` signals plus the user's intent. The `route` recipe SHALL read the user's login state before dispatching, since it determines which destinations are reachable. It SHALL remain biased to stay local: local authoring that works SHALL NOT be abandoned for the service. `route` SHALL decide the engine as part of this classification rather than deferring it to a separate topic. There is one decision, made from one reading of the evidence: whether a rule is expressible locally and which engine can express it are answered from the same signals, so splitting them costs a second fetch and a handoff without adding information. @@ -14,10 +14,10 @@ Each destination SHALL be a topic an agent can fetch by name, so classifying pro - **THEN** the recipe SHALL direct the agent to run `taskless detect --json` and use its signals as input to the classification -#### Scenario: Route classifies into one of four destinations +#### Scenario: Route classifies into one of five destinations - **WHEN** the agent follows `route` -- **THEN** it SHALL select exactly one of `create-legacy-rule`, `create-sg-rule`, `create-vale-rule`, or `create-runtime-rule` +- **THEN** it SHALL select exactly one of `create-legacy-rule`, `create-sg-rule`, `create-vale-rule`, `create-runtime-rule`, or `create-remote-rule` - **AND** it SHALL fetch the corresponding recipe to perform the authoring #### Scenario: Every destination resolves to a recipe @@ -25,6 +25,24 @@ Each destination SHALL be a topic an agent can fetch by name, so classifying pro - **WHEN** any destination `route` can name is fetched - **THEN** a recipe of that exact name SHALL exist +#### Scenario: Service generation is offered only where it is a choice + +- **WHEN** the rule is expressible locally AND the user is logged in +- **THEN** `route` MAY offer `create-remote-rule` as an alternative and ask the user +- **AND WHEN** the user is not logged in, or the rule is not expressible locally +- **THEN** `route` SHALL NOT pose service generation as a choice, because it is not one + +#### Scenario: A logged-in runtime request routes straight to the service + +- **WHEN** the rule requires the runtime engine AND the user is logged in +- **THEN** `route` SHALL name `create-remote-rule` +- **AND** no recipe SHALL forward the agent from one destination to another + +#### Scenario: A logged-out runtime request reaches the explanation + +- **WHEN** the rule requires the runtime engine AND the user is not logged in +- **THEN** `route` SHALL name `create-runtime-rule` + #### Scenario: The engine is decided without a second fetch - **WHEN** the agent follows `route` diff --git a/openspec/changes/agent-command-and-vale-authoring/tasks.md b/openspec/changes/agent-command-and-vale-authoring/tasks.md index eac50fc..9baffeb 100644 --- a/openspec/changes/agent-command-and-vale-authoring/tasks.md +++ b/openspec/changes/agent-command-and-vale-authoring/tasks.md @@ -13,8 +13,9 @@ - [ ] 2.2 `git mv` `help/existing.txt` → `help/create-legacy-rule.txt`; retitle and update its header - [ ] 2.3 Flatten the `rule-*` topics to verb-noun single tokens (`rule-create` → `create-rule`, `rule-improve` → `improve-rule`, `rule-delete` → `delete-rule`, `rule-verify` → `verify-rule`), including their `.anonymous` variants. Decide `rule-meta` and `rule` deliberately — they are not creation verbs and may keep their names - [ ] 2.4 Author `help/create-vale-rule.txt`: the three artifacts (style file, `.vale.ini` section, `pass/`/`fail` fixtures), that the scaffold ships section-less so the first rule writes the first scope, and that a rule enabled outside a section is ignored by Vale. State the evidence that makes `vale` the right engine for a rule, since no chooser topic states it any more. Cross-reference `verify-rule` -- [ ] 2.5 Author `help/create-runtime-rule.txt`: what a runtime rule is, where its `check.ts` lives, and why executing code requires login, reconciliation, and signing when the static tiers do not -- [ ] 2.6 Rewrite `help/route.txt` to classify into the four `create-*-rule` destinations, applying the engine reasoning inline rather than deferring to a second fetch. Service generation is an escalation when local authoring cannot express the rule, not a fifth destination. It must name a command the agent can run verbatim +- [ ] 2.5 Author `help/create-runtime-rule.txt` as the logged-**out** path: what a runtime rule is, where its `check.ts` lives, why executing code requires login, reconciliation, and signing when the static tiers do not, and how to obtain access. It must not forward to another recipe +- [ ] 2.5a Merge `help/remote.txt` and `help/rule-create.txt` into `help/create-remote-rule.txt` (plus the `.anonymous` variant). A content merge, not a rename: both texts have material that survives, and the result must read as one procedure rather than two concatenated +- [ ] 2.6 Rewrite `help/route.txt` to read login state early and classify into the five `create-*-rule` destinations, applying the engine reasoning inline rather than deferring to a second fetch. Offer `create-remote-rule` only where it is a genuine choice — locally expressible AND logged in. A logged-in runtime request routes straight to `create-remote-rule`; a logged-out one to `create-runtime-rule`. It must name a command the agent can run verbatim - [ ] 2.7 Merge `help/engine-selection.txt` into `help/route.txt` and delete it. Its three engine definitions, evidence-before-answer procedure, and boundary cases move into `route`; the per-engine criterion is restated in each `create-*-rule` recipe so a consumer reading one destination knows when it applies - [ ] 2.8 Re-home the engine-reasoning requirements that survive the merge — "Available code context outranks the phrasing of the request" and "Ambiguity resolves to an engine known to be available" now bind `route` and the destinations. Update `help-extensions.test.ts`, which asserts against the standalone topic @@ -40,6 +41,7 @@ ## 6. Verify - [ ] 6.1 `pnpm typecheck`, `pnpm lint`, `pnpm --filter @taskless/cli build`, `pnpm --filter @taskless/cli test` -- [ ] 6.2 Run `taskless agent` with no argument, with each renamed topic, and with a removed name, confirming the index lists the new vocabulary and a removed name exits non-zero +- [ ] 6.2 **Rehearse the recipes against a fresh agent.** For `route` and each `create-*-rule`, hand the text to a subagent with no prior context and ask it to describe the steps it would take and the commands it would run. Check its account against what the recipe intends — a recipe that reads correctly to its author and produces the wrong plan is the failure mode prose review cannot catch. Feed the corrections back into the text +- [ ] 6.2a Run `taskless agent` with no argument, with each renamed topic, and with a removed name, confirming the index lists the new vocabulary and a removed name exits non-zero - [ ] 6.3 `pnpm openspec validate --all --strict` (note: `cli-rules` and `cli-update-engine` fail on `main` already and are unrelated) - [ ] 6.4 Archive the change From d467e42b04cae4fc564a1f7f851c5ce08f1bbb50 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Wed, 12 Aug 2026 22:26:43 -0700 Subject: [PATCH 3/7] docs(openspec): state the engine criterion once, and defer login to auth Closes both open questions, and corrects an overclaim in D1 while doing it. D1 justified merging the chooser partly on the criterion distributing to the destinations, each stating the evidence for its own engine. That is the same duplication the merge exists to remove, one level down: five copies of one test, and the first edit to any of them diverges silently. The criterion now lives once, in `route`'s destination table, which is where the comparison is actually made. Destinations orient instead. Each opens with a fixed line -- what topic this is, what it helps you write, revisit routing if that is not what you need. Its job is self-correction: an agent that arrived wrongly, by guessing or by a user naming a topic directly, should find out in the first line where recovery is cheap, not after authoring the wrong artifact. Scope only, never the comparison. The cost is worth stating rather than hiding. A consumer reading only `create-vale-rule` gets its scope but not the boundary cases that settle hard calls. Enough to pick between destinations; not enough to adjudicate an ambiguous rule. If the generator needs the full test it consumes `route` or keeps its own classifier -- recorded as the one consumer this change leaves with less than it had, rather than papered over by copying prose into five files. `create-runtime-rule` points at `auth` rather than restating login. An extra CLI turn is cheap when each turn delivers something concrete, and a copy of the login procedure inside a rule-authoring recipe goes stale the first time login changes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3 --- .../design.md | 25 ++++++++++++++++--- .../specs/cli-agent-authoring/spec.md | 21 ++++++++++++++-- .../specs/cli-rule-routing/spec.md | 10 ++++---- .../agent-command-and-vale-authoring/tasks.md | 5 ++-- 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/openspec/changes/agent-command-and-vale-authoring/design.md b/openspec/changes/agent-command-and-vale-authoring/design.md index 9ccb615..ef2152e 100644 --- a/openspec/changes/agent-command-and-vale-authoring/design.md +++ b/openspec/changes/agent-command-and-vale-authoring/design.md @@ -32,10 +32,16 @@ This reverses the scoping `engine-selection` asserts today — that route decide **Where the reasoning goes for consumers outside the CLI.** The platform generator consumes `engine-selection` through `TOPICS` and has no `route` step, so merging cannot simply delete what it reads. Exporting `route` instead is wrong — it is built on `taskless detect --json` and local authoring, neither of which a Worker can do. -The reasoning therefore **distributes to the destinations**: each `create-*-rule` recipe states the evidence that makes its engine the right one, and `route` applies those same tests to dispatch. A consumer choosing between `create-sg-rule` and `create-vale-rule` reads the criterion at the destination rather than fetching a chooser first. This keeps the exported surface honest — every exported topic is one a service-side consumer can actually act on — and it removes the class of drift where a chooser and its destinations disagree about when each applies. +The criterion therefore lives **once, in `route`'s destination table**, which is where the comparison is actually made. Restating it in each destination would be the drift risk the merge was meant to remove, one level down. + +Each destination instead opens with a short orientation line naming what it is for and what to do if that is wrong — see D9. That is deliberately less than the full criterion: enough for a reader who arrived at the wrong recipe to notice and go back, not a second copy of the test. + +**What this costs the exported surface, stated plainly.** A consumer reading only `create-vale-rule` gets its scope ("prose and markup") but not the boundary cases that distinguish hard calls — prose-about-code, per-document versus cross-document. For picking between two destinations that is sufficient; for adjudicating a genuinely ambiguous rule it is not. If the generator needs the full test it will have to consume `route` or keep its own classifier, and that is a decision for the generator rather than something this change should pre-empt by duplicating prose. _Alternative rejected:_ keep `engine-selection` as a third exported topic that `route` also applies. Two statements of the same criterion, guaranteed to drift, and it preserves the second fetch for exactly the consumer we were trying to simplify. +_Alternative rejected:_ restate the full criterion in every destination. Five copies of one test, and the first edit to any of them is a divergence nobody notices. + _Alternative rejected:_ export `route`. It is local-only by construction. ### D2 — Verb-noun names, single token, no aliases @@ -118,7 +124,20 @@ This is a content merge, not a rename: both texts have material that survives, a _Alternative rejected:_ keep `remote` as a boundary statement and `create-remote-rule` as the procedure. Preserves the second fetch under new names. +### D9 — Destinations orient, they do not re-decide + +Each `create-*-rule` recipe opens with a fixed-shape line: what topic the reader is in, what kinds of rule it helps write, and an instruction to revisit the routing decision if that is not what they need. + +Its job is self-correction, not classification. An agent that arrived at the wrong recipe — because it guessed, because a user named a topic directly, or because `route` was wrong — should discover that in the first line rather than after authoring the wrong artifact. Recovery is cheap there and expensive later. + +Keeping it to orientation is what stops it becoming a second criterion. The comparison between engines happens in one place; a destination only has to answer "am I the right place", which needs its own scope and nothing about the others. + +### D10 — `create-runtime-rule` defers to `auth` + +It explains why the runtime tier is gated — executing code requires reconciliation and signing — and points at `auth` for obtaining access, rather than restating the login procedure. + +An extra CLI turn is not a cost worth avoiding when each turn delivers something concrete: `auth` is maintained as the authority on login, and a copy inside a rule-authoring recipe is a copy that goes stale the first time login changes. + ## Open Questions -- Does `create-runtime-rule` duplicate `auth`? It now owns the login explanation for the runtime tier specifically; worth confirming the two do not drift. -- Does the distributed engine criterion (D1) belong in each recipe's Preconditions or as a named section? Affects whether a consumer can extract it mechanically. +- If the platform generator needs the full engine criterion rather than per-destination scope, does it consume `route` or keep its own classifier? Out of scope here (D1), but it is the one consumer this change leaves with less than it had. diff --git a/openspec/changes/agent-command-and-vale-authoring/specs/cli-agent-authoring/spec.md b/openspec/changes/agent-command-and-vale-authoring/specs/cli-agent-authoring/spec.md index 7a2f382..1b6340f 100644 --- a/openspec/changes/agent-command-and-vale-authoring/specs/cli-agent-authoring/spec.md +++ b/openspec/changes/agent-command-and-vale-authoring/specs/cli-agent-authoring/spec.md @@ -55,7 +55,7 @@ This matches how ast-grep rules are authored today: the agent writes the rule an ### Requirement: The runtime authoring recipe is the logged-out path -The `create-runtime-rule` recipe SHALL explain that runtime rules execute code and therefore require login, reconciliation, and signing, SHALL state this as a property of executing code rather than of the engine's capability, and SHALL say how to obtain access. +The `create-runtime-rule` recipe SHALL explain that runtime rules execute code and therefore require login, reconciliation, and signing, and SHALL state this as a property of executing code rather than of the engine's capability. It SHALL point at `auth` for obtaining access rather than restating the login procedure, which `auth` owns. It SHALL NOT forward the agent to another authoring recipe. A logged-in runtime request is routed to `create-remote-rule` by `route`, so this recipe is reached only when the gate is closed and exists to explain that one gate once. @@ -63,7 +63,7 @@ It SHALL NOT forward the agent to another authoring recipe. A logged-in runtime - **WHEN** an agent follows `create-runtime-rule` - **THEN** the recipe SHALL state why the runtime tier is gated when the static tiers are not -- **AND** it SHALL state how to obtain access +- **AND** it SHALL refer the reader to `auth` rather than restating how to log in #### Scenario: The recipe does not delegate @@ -81,3 +81,20 @@ Split across a boundary statement and a procedure, an agent fetches one only to - **WHEN** an agent follows `create-remote-rule` - **THEN** the recipe SHALL carry both the boundary and the dispatch procedure - **AND** it SHALL NOT require fetching a second topic to complete the request + +### Requirement: Every authoring recipe opens by orienting the reader + +Each `create-*-rule` recipe SHALL open with a line naming the topic the reader is in, the kinds of rule it helps write, and an instruction to revisit the routing decision if that is not what they need. + +The line SHALL orient, not classify: it states this recipe's own scope and SHALL NOT restate the criterion distinguishing the engines from each other, which `route` holds in one place. An agent that arrived at the wrong recipe — by guessing, by a user naming a topic directly, or because `route` was wrong — should discover it in the first line, where recovery is cheap, rather than after authoring the wrong artifact. + +#### Scenario: A misrouted reader is told how to recover + +- **WHEN** an agent opens any `create-*-rule` recipe +- **THEN** the first lines SHALL name what that recipe helps write +- **AND** SHALL instruct the agent to revisit its routing decision if it needs a different kind of check + +#### Scenario: The orientation is not a second criterion + +- **WHEN** the orientation line is read +- **THEN** it SHALL describe only this recipe's scope, not the comparison between engines diff --git a/openspec/changes/agent-command-and-vale-authoring/specs/cli-rule-routing/spec.md b/openspec/changes/agent-command-and-vale-authoring/specs/cli-rule-routing/spec.md index 62cf0ce..85c90f6 100644 --- a/openspec/changes/agent-command-and-vale-authoring/specs/cli-rule-routing/spec.md +++ b/openspec/changes/agent-command-and-vale-authoring/specs/cli-rule-routing/spec.md @@ -77,14 +77,14 @@ Engine reasoning SHALL NOT treat login, reconciliation, or signing as inputs to ### Requirement: Engine reasoning lives in route and in each destination -The engine criterion SHALL be stated in `route`, which applies it to dispatch, and in each `create-*-rule` recipe, which states the evidence that makes its own engine the right one. It SHALL NOT be stated in a separate chooser topic. +The engine criterion SHALL be stated once, in `route`'s destination table, which is where the comparison between engines is made. It SHALL NOT be stated in a separate chooser topic, and SHALL NOT be restated in the destination recipes. -Distributing it this way is what lets the reasoning stay exportable. A consumer outside the CLI has no `route` step and cannot run `taskless detect --json`, so a chooser topic is unusable to it; a destination that states its own criterion is usable by anything that can read one recipe. It also removes the drift class where a chooser and its destinations disagree about when each applies. +One statement is the point. A criterion copied into each destination is five copies of one test, and the first edit to any of them is a divergence nobody notices — the drift this merge exists to remove, reappearing one level down. Destinations orient the reader to their own scope instead, which needs nothing about the other engines. -#### Scenario: A destination states when it applies +#### Scenario: The comparison lives in one place -- **WHEN** an agent or consumer reads `create-sg-rule` or `create-vale-rule` -- **THEN** the recipe SHALL state the evidence that makes that engine the right one for a rule +- **WHEN** the embedded recipe set is inspected +- **THEN** exactly one recipe SHALL state the criterion distinguishing the engines from each other #### Scenario: No separate chooser topic exists diff --git a/openspec/changes/agent-command-and-vale-authoring/tasks.md b/openspec/changes/agent-command-and-vale-authoring/tasks.md index 9baffeb..89c87f4 100644 --- a/openspec/changes/agent-command-and-vale-authoring/tasks.md +++ b/openspec/changes/agent-command-and-vale-authoring/tasks.md @@ -13,10 +13,11 @@ - [ ] 2.2 `git mv` `help/existing.txt` → `help/create-legacy-rule.txt`; retitle and update its header - [ ] 2.3 Flatten the `rule-*` topics to verb-noun single tokens (`rule-create` → `create-rule`, `rule-improve` → `improve-rule`, `rule-delete` → `delete-rule`, `rule-verify` → `verify-rule`), including their `.anonymous` variants. Decide `rule-meta` and `rule` deliberately — they are not creation verbs and may keep their names - [ ] 2.4 Author `help/create-vale-rule.txt`: the three artifacts (style file, `.vale.ini` section, `pass/`/`fail` fixtures), that the scaffold ships section-less so the first rule writes the first scope, and that a rule enabled outside a section is ignored by Vale. State the evidence that makes `vale` the right engine for a rule, since no chooser topic states it any more. Cross-reference `verify-rule` -- [ ] 2.5 Author `help/create-runtime-rule.txt` as the logged-**out** path: what a runtime rule is, where its `check.ts` lives, why executing code requires login, reconciliation, and signing when the static tiers do not, and how to obtain access. It must not forward to another recipe +- [ ] 2.5 Author `help/create-runtime-rule.txt` as the logged-**out** path: what a runtime rule is, where its `check.ts` lives, and why executing code requires login, reconciliation, and signing when the static tiers do not. Point at `auth` for obtaining access rather than restating it. It must not forward to another authoring recipe - [ ] 2.5a Merge `help/remote.txt` and `help/rule-create.txt` into `help/create-remote-rule.txt` (plus the `.anonymous` variant). A content merge, not a rename: both texts have material that survives, and the result must read as one procedure rather than two concatenated - [ ] 2.6 Rewrite `help/route.txt` to read login state early and classify into the five `create-*-rule` destinations, applying the engine reasoning inline rather than deferring to a second fetch. Offer `create-remote-rule` only where it is a genuine choice — locally expressible AND logged in. A logged-in runtime request routes straight to `create-remote-rule`; a logged-out one to `create-runtime-rule`. It must name a command the agent can run verbatim -- [ ] 2.7 Merge `help/engine-selection.txt` into `help/route.txt` and delete it. Its three engine definitions, evidence-before-answer procedure, and boundary cases move into `route`; the per-engine criterion is restated in each `create-*-rule` recipe so a consumer reading one destination knows when it applies +- [ ] 2.7 Merge `help/engine-selection.txt` into `help/route.txt` and delete it. Its three engine definitions, evidence-before-answer procedure, and boundary cases move into `route`'s destination table — stated once, not copied into the destinations +- [ ] 2.7a Give every `create-*-rule` recipe the same opening orientation line: what topic this is, what it helps you write, and revisit routing if that is not what you need. Fixed shape across all five so an agent recognises it; scope only, never the comparison between engines - [ ] 2.8 Re-home the engine-reasoning requirements that survive the merge — "Available code context outranks the phrasing of the request" and "Ambiguity resolves to an engine known to be available" now bind `route` and the destinations. Update `help-extensions.test.ts`, which asserts against the standalone topic ## 3. Sweep the cross-references From d736f46c3fe0ad82a00c0dbe744ce1896eb01a92 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Wed, 12 Aug 2026 22:27:24 -0700 Subject: [PATCH 4/7] docs(openspec): record why the prompts are exported, and soften the route claim D1 asserted that exporting `route` is wrong because it is built on local mechanics. That is too strong: `route` is expected to be exported in a later change. It is not exported *here* because its local mechanics need separating from its reasoning first, and doing that inside a change that already renames a command and five topics is how a rename becomes unreviewable. Recorded as deferred rather than rejected. That also closes the last open question. The gap this change leaves -- destinations carry scope, not the boundary cases -- closes when `route` is exported: the service holds the route prompt, needs no escalation path because it is the escalation, and supplies its own runtime prompt for its own agentic flow. Worth stating why the prompts are exported at all, since it changes what "enough" means. The goal is consistency between the local and remote paths, not transferring a capability the service lacks. The service can classify without us; what it must not do is classify differently. A rule routed to `vale` locally and `sg` server-side is one request answered two ways, and that is what the shared surface exists to prevent. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3 --- .../agent-command-and-vale-authoring/design.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openspec/changes/agent-command-and-vale-authoring/design.md b/openspec/changes/agent-command-and-vale-authoring/design.md index ef2152e..9e37534 100644 --- a/openspec/changes/agent-command-and-vale-authoring/design.md +++ b/openspec/changes/agent-command-and-vale-authoring/design.md @@ -30,19 +30,23 @@ There is one decision, made once. `route` absorbs the engine reasoning and retur This reverses the scoping `engine-selection` asserts today — that route decides destination, the topic decides engine, and locally the two compose. The separation is clean on paper and expensive in practice: it costs an agent two fetches and a correct handoff between them to answer one question, and the handoff is where an agent drops context. Worse, the two decisions are not independent in the direction the split assumes; "author this locally" and "which engine can express it" are answered from the same evidence, so splitting them means reading the same signals twice. -**Where the reasoning goes for consumers outside the CLI.** The platform generator consumes `engine-selection` through `TOPICS` and has no `route` step, so merging cannot simply delete what it reads. Exporting `route` instead is wrong — it is built on `taskless detect --json` and local authoring, neither of which a Worker can do. +**Where the reasoning goes for consumers outside the CLI.** The platform generator consumes `engine-selection` through `TOPICS`, so merging cannot simply delete what it reads. `route` is expected to be exported in a later change, at which point it carries the criterion to the service directly; it is not exported here because it still contains local mechanics (`taskless detect --json`, on-device authoring) that a Worker cannot run, and untangling those is its own piece of work. The criterion therefore lives **once, in `route`'s destination table**, which is where the comparison is actually made. Restating it in each destination would be the drift risk the merge was meant to remove, one level down. Each destination instead opens with a short orientation line naming what it is for and what to do if that is wrong — see D9. That is deliberately less than the full criterion: enough for a reader who arrived at the wrong recipe to notice and go back, not a second copy of the test. -**What this costs the exported surface, stated plainly.** A consumer reading only `create-vale-rule` gets its scope ("prose and markup") but not the boundary cases that distinguish hard calls — prose-about-code, per-document versus cross-document. For picking between two destinations that is sufficient; for adjudicating a genuinely ambiguous rule it is not. If the generator needs the full test it will have to consume `route` or keep its own classifier, and that is a decision for the generator rather than something this change should pre-empt by duplicating prose. +**What this costs the exported surface, and for how long.** A consumer reading only `create-vale-rule` gets its scope ("prose and markup") but not the boundary cases that settle hard calls — prose-about-code, per-document versus cross-document. Sufficient for picking between destinations; not for adjudicating a genuinely ambiguous rule. + +That gap closes when `route` is exported. The service will hold the route prompt, which states when each engine applies, and needs no escalation path of its own — it is the escalation. It can then supply its own runtime prompt for its own agentic flow. + +Worth being clear about why the prompts are exported at all, because it changes what "enough" means: the goal is **consistency between the local and remote paths**, not transferring a capability the service lacks. The service can classify without us. What it should not do is classify _differently_ — a rule routed to `vale` locally and to `sg` server-side is the same request answered two ways, and that is the failure the shared surface exists to prevent. _Alternative rejected:_ keep `engine-selection` as a third exported topic that `route` also applies. Two statements of the same criterion, guaranteed to drift, and it preserves the second fetch for exactly the consumer we were trying to simplify. _Alternative rejected:_ restate the full criterion in every destination. Five copies of one test, and the first edit to any of them is a divergence nobody notices. -_Alternative rejected:_ export `route`. It is local-only by construction. +_Deferred, not rejected:_ export `route`. Its local mechanics need separating from its reasoning first, and doing that inside a change that already renames a command and five topics is how a rename becomes unreviewable. ### D2 — Verb-noun names, single token, no aliases @@ -140,4 +144,4 @@ An extra CLI turn is not a cost worth avoiding when each turn delivers something ## Open Questions -- If the platform generator needs the full engine criterion rather than per-destination scope, does it consume `route` or keep its own classifier? Out of scope here (D1), but it is the one consumer this change leaves with less than it had. +- None outstanding. From 093aca1583d5a9f8b1e45325e015160ca29a66f1 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Wed, 12 Aug 2026 23:34:17 -0700 Subject: [PATCH 5/7] ref(cli): rename the `help` command to `agent`, one token per topic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The subcommand is named for its reader: it serves agents fetching a procedure, not humans asking for help. Topics are now addressed by exactly one token. Joining positionals made `taskless help rule create` resolve `rule-create.txt`, which invited an agent to reorder or paraphrase a topic name and still get a hit. A single hyphenated token is a literal string to copy, so extra positionals are an error rather than something to guess at. The `cli_help` telemetry event name is deliberately unchanged — dashboards key on it, and renaming it alongside the upcoming `TOPICS` break would take them dark for an unrelated reason. Recipe text still says `taskless help`; that sweep is a later task group. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3 --- .../agent-command-and-vale-authoring/tasks.md | 10 ++- .../cli/src/commands/{help.ts => agent.ts} | 51 +++++++---- packages/cli/src/commands/rules.ts | 4 +- packages/cli/src/index.ts | 14 +-- packages/cli/src/telemetry-run.ts | 4 +- packages/cli/test/anonymous-flag.test.ts | 4 +- packages/cli/test/cli-run.test.ts | 2 +- packages/cli/test/cli.test.ts | 2 +- packages/cli/test/help-extensions.test.ts | 87 +++++++++++-------- .../cli/test/help-routing-telemetry.test.ts | 10 +-- packages/cli/test/help-telemetry.test.ts | 26 ++++-- packages/cli/test/onboard.test.ts | 8 +- packages/cli/test/prompts.test.ts | 6 +- 13 files changed, 136 insertions(+), 92 deletions(-) rename packages/cli/src/commands/{help.ts => agent.ts} (71%) diff --git a/openspec/changes/agent-command-and-vale-authoring/tasks.md b/openspec/changes/agent-command-and-vale-authoring/tasks.md index 89c87f4..1b3ec3a 100644 --- a/openspec/changes/agent-command-and-vale-authoring/tasks.md +++ b/openspec/changes/agent-command-and-vale-authoring/tasks.md @@ -2,10 +2,12 @@ ## 1. Rename the command -- [ ] 1.1 Rename `packages/cli/src/commands/help.ts` to `agent.ts` and the exported command to `agent`. Register it in `src/index.ts` -- [ ] 1.2 Remove the positional-join resolution (`positionals.join("-")`). Accept at most one positional; more than one is an error rather than a joined key. The unknown-topic message points at `taskless agent` -- [ ] 1.3 Keep the telemetry event name `cli_help` or rename it deliberately — decide once and record it, since dashboards key on it. If renamed, note it in the changeset alongside the `TOPICS` break -- [ ] 1.4 Update `help-extensions.test.ts`, `help-routing-telemetry.test.ts`, `anonymous-flag.test.ts`, `onboard.test.ts`, and `cli.test.ts` to invoke `agent` +- [x] 1.1 Rename `packages/cli/src/commands/help.ts` to `agent.ts` and the exported command to `agent`. Register it in `src/index.ts` +- [x] 1.2 Remove the positional-join resolution (`positionals.join("-")`). Accept at most one positional; more than one is an error rather than a joined key. The unknown-topic message points at `taskless agent` +- [x] 1.3 Keep the telemetry event name `cli_help` or rename it deliberately — decide once and record it, since dashboards key on it. If renamed, note it in the changeset alongside the `TOPICS` break + - **Decision: keep `cli_help`.** Renaming it in the same change that breaks the `TOPICS` export would take the dashboards dark for a reason unrelated to this change, and agent-call volume needs to stay visible under the existing event. The event name is not part of any agent-facing contract, so it can be renamed later on its own. Recorded as a comment at the capture site in `agent.ts`; nothing to add to the changeset +- [x] 1.4 Update `help-extensions.test.ts`, `help-routing-telemetry.test.ts`, `anonymous-flag.test.ts`, `onboard.test.ts`, and `cli.test.ts` to invoke `agent` + - Also required, not listed: `help-telemetry.test.ts` (imports `createHelpCommand` directly), `prompts.test.ts` (spawns `binPath help ` for the parity test), and `cli-run.test.ts` (its `resolveCommandName` case named `help`) ## 2. Rename and add the authoring topics diff --git a/packages/cli/src/commands/help.ts b/packages/cli/src/commands/agent.ts similarity index 71% rename from packages/cli/src/commands/help.ts rename to packages/cli/src/commands/agent.ts index d89e4c9..e50647b 100644 --- a/packages/cli/src/commands/help.ts +++ b/packages/cli/src/commands/agent.ts @@ -10,8 +10,8 @@ import { import { getTelemetry } from "../telemetry"; import { getRecipe } from "../prompts/recipes"; -// Help-only recipe topics (no backing subcommand) that should still be -// discoverable from the `taskless help` index. The rule-authoring front +// Recipe-only topics (no backing subcommand) that should still be +// discoverable from the `taskless agent` index. The rule-authoring front // door (`route`) and its destinations live here so an agent can find them. const RECIPE_TOPICS: ReadonlyArray<[string, string]> = [ ["route", "Decide where to author a rule (existing/static/remote)"], @@ -36,11 +36,11 @@ async function resolveDescription( return meta?.description ?? ""; } -export function createHelpCommand(subCommands: SubCommandsDef) { +export function createAgentCommand(subCommands: SubCommandsDef) { return defineCommand({ meta: { - name: "help", - description: "Show help for a command", + name: "agent", + description: "Return a recipe for an AI coding agent to follow", }, args: { dir: { @@ -67,14 +67,19 @@ export function createHelpCommand(subCommands: SubCommandsDef) { if (!argument.includes("=") && valueFlagSet.has(argument)) index++; continue; } - if (argument !== "help") positionals.push(argument); + if (argument !== "agent") positionals.push(argument); } const cwd = resolve(args.dir); const telemetry = await getTelemetry(cwd); if (positionals.length === 0) { - // cli_help with the index marker: agent fetched the topic list + // cli_help with the index marker: agent fetched the topic list. + // The event name stays `cli_help` even though the command is now + // `agent`: dashboards key on it, it is not part of any agent-facing + // contract, and renaming it in the same change that breaks the + // `TOPICS` export would take those dashboards dark for a reason + // unrelated to this change. telemetry.capture("cli_help", { topic: "(index)" }); console.log("Taskless CLI\n"); @@ -89,7 +94,7 @@ export function createHelpCommand(subCommands: SubCommandsDef) { const entries: Array<[string, string]> = []; for (const [name, cmd] of Object.entries(subCommands)) { - if (name === "help") continue; + if (name === "agent") continue; const description = await resolveDescription(cmd); entries.push([name, description]); } @@ -114,30 +119,44 @@ export function createHelpCommand(subCommands: SubCommandsDef) { ); console.log("and use local-only behavior."); console.log( - "\nRun `taskless help ` for the full recipe (e.g. `taskless help rule create`)." + "\nRun `taskless agent ` for the full recipe (e.g. `taskless agent rule-create`)." ); return; } - // Join positional args to form the lookup key - const key = positionals.join("-"); + // Topics are addressed by exactly one token. Joining positionals into a + // key used to make `rule create` resolve `rule-create.txt`, which invited + // an agent to reorder or paraphrase a topic name and still get a hit. + // A single hyphenated token is a literal string to copy, so extra + // positionals are an error rather than something to guess at. + if (positionals.length > 1) { + telemetry.capture("cli_help", { topic: positionals.join(" ") }); + console.error(`Too many arguments: ${positionals.join(" ")}`); + console.error( + "A topic is a single token. Run `taskless agent` for the topic index." + ); + process.exitCode = 1; + return; + } + + const key = positionals[0]!; // Anonymous variant lookup: prefer .anonymous.txt when // --anonymous is set, fall back to the canonical recipe. The lookup and - // the render both live in the shared prompts module, so `help` and the + // the render both live in the shared prompts module, so `agent` and the // `@taskless/cli/prompts` export emit the same text. const recipe = getRecipe(key, { anonymous: args.anonymous }); if (recipe) { // cli_help: agent fetched a specific recipe (intent signal). The topic // is the served topic; filtering on it replaces the old per-topic events. - telemetry.capture("cli_help", { topic: positionals.join(" ") }); + telemetry.capture("cli_help", { topic: key }); console.log(recipe.trimEnd()); } else { // cli_help for an unknown topic — still the attempted topic string. - telemetry.capture("cli_help", { topic: positionals.join(" ") }); - console.error(`Unknown command: ${positionals.join(" ")}`); - console.error("Run `taskless help` for available commands."); + telemetry.capture("cli_help", { topic: key }); + console.error(`Unknown command: ${key}`); + console.error("Run `taskless agent` for available topics."); process.exitCode = 1; } }, diff --git a/packages/cli/src/commands/rules.ts b/packages/cli/src/commands/rules.ts index 17250bb..301930c 100644 --- a/packages/cli/src/commands/rules.ts +++ b/packages/cli/src/commands/rules.ts @@ -105,7 +105,7 @@ const createCommand = defineCommand({ // 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."; + "Anonymous rule generation runs in the agent. Run `taskless agent rule-create --anonymous` to fetch the local-only recipe."; if (args.json) { console.log( JSON.stringify(makeErrorEnvelope("INVALID_INPUT", message)) @@ -348,7 +348,7 @@ const improveCommand = defineCommand({ if (args.anonymous) { const message = - "Anonymous rule improvement runs in the agent. Run `taskless help rule improve --anonymous` to fetch the local-only recipe."; + "Anonymous rule improvement runs in the agent. Run `taskless agent rule-improve --anonymous` to fetch the local-only recipe."; if (args.json) { console.log( JSON.stringify(makeErrorEnvelope("INVALID_INPUT", message)) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 0ed1ba3..154bbe4 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,11 +1,11 @@ import { defineCommand, runCommand, showUsage } from "citty"; +import { createAgentCommand } from "./commands/agent"; import { authCommand } from "./commands/auth"; import { checkCommand } from "./commands/check"; import { detectCommand } from "./commands/detect"; import { initCommand, updateCommand } from "./commands/init"; import { infoCommand } from "./commands/info"; -import { createHelpCommand } from "./commands/help"; import { onboardCommand } from "./commands/onboard"; import { ruleCommand } from "./commands/rules"; import { @@ -27,7 +27,7 @@ const subCommands = { rule: ruleCommand, }; -const helpCommand = createHelpCommand(subCommands); +const agentCommand = createAgentCommand(subCommands); const main = defineCommand({ meta: { @@ -55,7 +55,7 @@ const main = defineCommand({ }, subCommands: { ...subCommands, - help: helpCommand, + agent: agentCommand, }, async run({ rawArgs, cmd }) { // citty always calls the parent's run handler, even after a subcommand. @@ -90,7 +90,7 @@ const main = defineCommand({ } // TTY → run the interactive wizard. Non-TTY → print a short preamble - // explaining the context and then delegate to `help` so agents and + // explaining the context and then delegate to `agent` so agents and // pipes see the topic index. if (process.stdout.isTTY === true && process.stdin.isTTY === true) { await runCommand(initCommand, { rawArgs }); @@ -101,11 +101,11 @@ const main = defineCommand({ "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" + " For agent recipes, run `taskless agent` (no args) for the topic index.\n" ); - // Forward the parent's rawArgs (e.g. `-d `) so the help command + // Forward the parent's rawArgs (e.g. `-d `) so the agent command // doesn't mis-parse them as positional topic names. - await runCommand(helpCommand, { rawArgs: ["help", ...rawArgs] }); + await runCommand(agentCommand, { rawArgs: ["agent", ...rawArgs] }); }, }); diff --git a/packages/cli/src/telemetry-run.ts b/packages/cli/src/telemetry-run.ts index 3421b92..26607bf 100644 --- a/packages/cli/src/telemetry-run.ts +++ b/packages/cli/src/telemetry-run.ts @@ -7,8 +7,8 @@ import { CLIError } from "./util/cli-error"; * Derive the cli_run `command` property from the raw argv. Flags (and the * value after `-d`/`--dir`) are skipped; the first positional is the command, * and `rule` keeps its subcommand (e.g. `rule create`) since that distinction - * is meaningful. `help`'s topic is recorded separately on cli_help, so the - * command for a help invocation is just `help`. + * is meaningful. `agent`'s topic is recorded separately on cli_help, so the + * command for an agent invocation is just `agent`. */ export function resolveCommandName(rawArguments: string[]): string { const valueFlags = new Set(["-d", "--dir"]); diff --git a/packages/cli/test/anonymous-flag.test.ts b/packages/cli/test/anonymous-flag.test.ts index c763d4f..52a20f6 100644 --- a/packages/cli/test/anonymous-flag.test.ts +++ b/packages/cli/test/anonymous-flag.test.ts @@ -86,7 +86,7 @@ describe("--anonymous flag (per-command behavior matrix)", () => { // 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"); + expect(result.stderr).toContain("taskless agent rule-create --anonymous"); }); it("with --json, emits the standardized envelope", async () => { @@ -115,7 +115,7 @@ describe("--anonymous flag (per-command behavior matrix)", () => { cwd, ]); expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("taskless help rule improve --anonymous"); + expect(result.stderr).toContain("taskless agent rule-improve --anonymous"); }); }); diff --git a/packages/cli/test/cli-run.test.ts b/packages/cli/test/cli-run.test.ts index 40aa46b..c6a1ebe 100644 --- a/packages/cli/test/cli-run.test.ts +++ b/packages/cli/test/cli-run.test.ts @@ -9,7 +9,7 @@ describe("resolveCommandName", () => { [["check", "--json"], "check"], [["rule", "create"], "rule create"], [["rule"], "rule"], - [["help", "route"], "help"], + [["agent", "route"], "agent"], [["-d", "/tmp", "check"], "check"], [["--dir", "/tmp", "info"], "info"], [[], "(default)"], diff --git a/packages/cli/test/cli.test.ts b/packages/cli/test/cli.test.ts index 9291589..444c382 100644 --- a/packages/cli/test/cli.test.ts +++ b/packages/cli/test/cli.test.ts @@ -34,7 +34,7 @@ describe("cli", () => { }); describe("no args", () => { - it("shows help text and exits with code 0", async () => { + it("shows the agent topic index and exits with code 0", async () => { const { stdout } = await execFileAsync("node", [binPath]); expect(stdout).toContain("taskless"); expect(stdout).toContain("info"); diff --git a/packages/cli/test/help-extensions.test.ts b/packages/cli/test/help-extensions.test.ts index eca96b9..b715528 100644 --- a/packages/cli/test/help-extensions.test.ts +++ b/packages/cli/test/help-extensions.test.ts @@ -28,11 +28,11 @@ async function runCli( } } -describe("taskless help (no args)", () => { +describe("taskless agent (no args)", () => { let cwd: string; beforeEach(async () => { - cwd = await mkdtemp(join(tmpdir(), "taskless-help-")); + cwd = await mkdtemp(join(tmpdir(), "taskless-agent-")); }); afterEach(async () => { @@ -40,14 +40,14 @@ describe("taskless help (no args)", () => { }); it("prints the human slug", async () => { - const result = await runCli(["help", "-d", cwd]); + const result = await runCli(["agent", "-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]); + const result = await runCli(["agent", "-d", cwd]); expect(result.stdout).toContain("Topics:"); for (const topic of ["init", "info", "check", "auth", "rule"]) { expect(result.stdout).toContain(topic); @@ -55,12 +55,12 @@ describe("taskless help (no args)", () => { }); it("mentions the --anonymous flag", async () => { - const result = await runCli(["help", "-d", cwd]); + const result = await runCli(["agent", "-d", cwd]); expect(result.stdout).toContain("--anonymous"); }); it("lists the routing recipe topics under Authoring recipes", async () => { - const result = await runCli(["help", "-d", cwd]); + const result = await runCli(["agent", "-d", cwd]); expect(result.stdout).toContain("Authoring recipes:"); for (const topic of [ "route", @@ -74,11 +74,11 @@ describe("taskless help (no args)", () => { }); }); -describe("taskless help ", () => { +describe("taskless agent ", () => { let cwd: string; beforeEach(async () => { - cwd = await mkdtemp(join(tmpdir(), "taskless-help-routing-")); + cwd = await mkdtemp(join(tmpdir(), "taskless-agent-routing-")); }); afterEach(async () => { @@ -88,7 +88,7 @@ describe("taskless help ", () => { it.each(["route", "existing", "static", "remote", "engine-selection"])( "resolves the %s recipe without an unknown-topic error", async (topic) => { - const result = await runCli(["help", topic, "-d", cwd]); + const result = await runCli(["agent", topic, "-d", cwd]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain(`# Topic: ${topic}`); expect(result.stdout).toContain("## Goal"); @@ -97,11 +97,11 @@ describe("taskless help ", () => { ); }); -describe("taskless help ", () => { +describe("taskless agent ", () => { let cwd: string; beforeEach(async () => { - cwd = await mkdtemp(join(tmpdir(), "taskless-help-topic-")); + cwd = await mkdtemp(join(tmpdir(), "taskless-agent-topic-")); }); afterEach(async () => { @@ -109,7 +109,7 @@ describe("taskless help ", () => { }); it("returns the canonical recipe for a known topic", async () => { - const result = await runCli(["help", "rule", "create", "-d", cwd]); + const result = await runCli(["agent", "rule-create", "-d", cwd]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("# Topic: rule create"); expect(result.stdout).toContain("## Goal"); @@ -117,7 +117,7 @@ describe("taskless help ", () => { }); it("interpolates %(CLI_VERSION)s in the recipe header", async () => { - const result = await runCli(["help", "rule", "create", "-d", cwd]); + const result = await runCli(["agent", "rule-create", "-d", cwd]); // Should contain a version pattern, not the literal placeholder. // Guard against both the legacy mustache syntax and the current // sprintf-js named-arg syntax leaking through. @@ -127,7 +127,7 @@ describe("taskless help ", () => { }); it("interpolates %(INPUT_SCHEMA)s for topics with a Zod input", async () => { - const result = await runCli(["help", "rule", "create", "-d", cwd]); + const result = await runCli(["agent", "rule-create", "-d", cwd]); expect(result.stdout).not.toContain("{{INPUT_SCHEMA}}"); expect(result.stdout).not.toContain("%(INPUT_SCHEMA)s"); // Embedded schema includes the JSON Schema $schema URI @@ -137,7 +137,7 @@ describe("taskless help ", () => { }); it("renders %(PACKAGE_MANAGER_DLX)s as the agent-fill marker", async () => { - const result = await runCli(["help", "ci", "-d", cwd]); + const result = await runCli(["agent", "ci", "-d", cwd]); expect(result.exitCode).toBe(0); // Sprintf substitutes the placeholder; the agent-fill marker should // appear in its rendered form, never as the @@ -148,17 +148,34 @@ describe("taskless help ", () => { }); it("exits 1 for an unknown topic", async () => { - const result = await runCli(["help", "totally-unknown", "-d", cwd]); + const result = await runCli(["agent", "totally-unknown", "-d", cwd]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Unknown command"); }); + + it("points an unknown topic at `taskless agent`, not the removed command", async () => { + const result = await runCli(["agent", "totally-unknown", "-d", cwd]); + expect(result.stderr).toContain("Run `taskless agent` for available"); + }); + + // A topic is one token. The old resolver joined positionals, so + // `rule create` and `create rule` both reached `rule-create.txt` — which + // invited an agent to paraphrase a topic name and still get a hit. Extra + // positionals are now an error rather than something to guess at. + it("rejects a multi-token topic instead of joining the positionals", async () => { + const result = await runCli(["agent", "rule", "create", "-d", cwd]); + expect(result.exitCode).not.toBe(0); + expect(result.stdout).not.toContain("# Topic:"); + expect(result.stderr).toContain("Too many arguments"); + expect(result.stderr).toContain("A topic is a single token"); + }); }); -describe("taskless help --anonymous (variant lookup)", () => { +describe("taskless agent --anonymous (variant lookup)", () => { let cwd: string; beforeEach(async () => { - cwd = await mkdtemp(join(tmpdir(), "taskless-help-anon-")); + cwd = await mkdtemp(join(tmpdir(), "taskless-agent-anon-")); }); afterEach(async () => { @@ -167,9 +184,8 @@ describe("taskless help --anonymous (variant lookup)", () => { it("returns the .anonymous variant when one exists (rule create)", async () => { const result = await runCli([ - "help", - "rule", - "create", + "agent", + "rule-create", "--anonymous", "-d", cwd, @@ -181,9 +197,8 @@ describe("taskless help --anonymous (variant lookup)", () => { it("returns the .anonymous variant for rule improve", async () => { const result = await runCli([ - "help", - "rule", - "improve", + "agent", + "rule-improve", "--anonymous", "-d", cwd, @@ -193,24 +208,24 @@ describe("taskless help --anonymous (variant lookup)", () => { }); 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]); + const canonical = await runCli(["agent", "check", "-d", cwd]); + const anonymous = await runCli(["agent", "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]); + const result = await runCli(["agent", "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", () => { +describe("bare taskless (non-TTY) routes to the agent 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. + // citty doesn't try to forward them to the agent subcommand. const result = await runCli([]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain("non-interactive context detected"); @@ -218,11 +233,11 @@ describe("bare taskless (non-TTY) routes to help index", () => { }); }); -describe("taskless help engine-selection", () => { +describe("taskless agent engine-selection", () => { let cwd: string; beforeEach(async () => { - cwd = await mkdtemp(join(tmpdir(), "taskless-help-engine-")); + cwd = await mkdtemp(join(tmpdir(), "taskless-agent-engine-")); }); afterEach(async () => { @@ -230,7 +245,7 @@ describe("taskless help engine-selection", () => { }); it("names all three engines and puts evidence before the answer", async () => { - const result = await runCli(["help", "engine-selection", "-d", cwd]); + const result = await runCli(["agent", "engine-selection", "-d", cwd]); expect(result.exitCode).toBe(0); for (const engine of ["`sg`", "`vale`", "`runtime`"]) { expect(result.stdout).toContain(engine); @@ -239,7 +254,7 @@ describe("taskless help engine-selection", () => { }); it("carries the three boundary cases", async () => { - const result = await runCli(["help", "engine-selection", "-d", cwd]); + const result = await runCli(["agent", "engine-selection", "-d", cwd]); // Each is a wrong answer someone actually reaches for. expect(result.stdout).toContain("Prose about code is still prose"); expect(result.stdout).toContain("one document at a time"); @@ -247,7 +262,7 @@ describe("taskless help engine-selection", () => { }); it("states the ambiguity default as a property, not as `sg`", async () => { - const result = await runCli(["help", "engine-selection", "-d", cwd]); + const result = await runCli(["agent", "engine-selection", "-d", cwd]); // D7: "choose an engine you know is available" stays correct on an // unsupported arch and server-side alike; naming `sg` outright would be // false on the host where `sg` is the missing one. @@ -256,13 +271,13 @@ describe("taskless help engine-selection", () => { }); it("stays out of the authoring-destination decision", async () => { - const result = await runCli(["help", "engine-selection", "-d", cwd]); + const result = await runCli(["agent", "engine-selection", "-d", cwd]); // Scope guard (3.4): it may point at `route`, never re-decide it. expect(result.stdout).toContain("is `route`, and it is a separate"); }); it("follows the recipe header and section convention", async () => { - const result = await runCli(["help", "engine-selection", "-d", cwd]); + const result = await runCli(["agent", "engine-selection", "-d", cwd]); expect(result.stdout).toContain("# Topic: engine-selection"); for (const section of [ "## Goal", diff --git a/packages/cli/test/help-routing-telemetry.test.ts b/packages/cli/test/help-routing-telemetry.test.ts index c3925b4..a391ae0 100644 --- a/packages/cli/test/help-routing-telemetry.test.ts +++ b/packages/cli/test/help-routing-telemetry.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -// Spy on the telemetry capture by mocking the telemetry module the help +// Spy on the telemetry capture by mocking the telemetry module the agent // command imports. The factory is invoked lazily at import time, so the // closure over `capture` resolves after initialization (same pattern as // telemetry.test.ts mocking posthog-node). @@ -15,7 +15,7 @@ vi.mock("../src/telemetry", () => ({ shutdownTelemetry: () => Promise.resolve(), })); -const { createHelpCommand } = await import("../src/commands/help"); +const { createAgentCommand } = await import("../src/commands/agent"); interface RunnableCommand { run: (context: { @@ -24,7 +24,7 @@ interface RunnableCommand { }) => Promise; } -describe("help routing topics emit cli_help intent telemetry", () => { +describe("agent routing topics emit cli_help intent telemetry", () => { let logSpy: ReturnType; beforeEach(() => { @@ -40,10 +40,10 @@ describe("help routing topics emit cli_help intent telemetry", () => { it.each(["route", "existing", "static", "remote", "engine-selection"])( "captures cli_help for %s", async (topic) => { - const command = createHelpCommand({}) as unknown as RunnableCommand; + const command = createAgentCommand({}) as unknown as RunnableCommand; await command.run({ args: { dir: process.cwd(), anonymous: false }, - rawArgs: ["help", topic], + rawArgs: ["agent", topic], }); expect(capture).toHaveBeenCalledWith( diff --git a/packages/cli/test/help-telemetry.test.ts b/packages/cli/test/help-telemetry.test.ts index 5e11937..b103c23 100644 --- a/packages/cli/test/help-telemetry.test.ts +++ b/packages/cli/test/help-telemetry.test.ts @@ -3,7 +3,7 @@ import { join, resolve } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -// Spy on telemetry by mocking the module the help command imports. The factory +// Spy on telemetry by mocking the module the agent command imports. The factory // is invoked lazily at import time (same pattern as telemetry.test.ts). const capture = vi.fn(); vi.mock("../src/telemetry", () => ({ @@ -13,7 +13,7 @@ vi.mock("../src/telemetry", () => ({ shutdownTelemetry: () => Promise.resolve(), })); -const { createHelpCommand } = await import("../src/commands/help"); +const { createAgentCommand } = await import("../src/commands/agent"); interface RunnableCommand { run: (context: { @@ -22,15 +22,15 @@ interface RunnableCommand { }) => Promise; } -async function runHelp(rawArguments: string[]): Promise { - const command = createHelpCommand({}) as unknown as RunnableCommand; +async function runAgent(rawArguments: string[]): Promise { + const command = createAgentCommand({}) as unknown as RunnableCommand; await command.run({ args: { dir: process.cwd(), anonymous: false }, rawArgs: rawArguments, }); } -describe("help emits cli_help { topic }", () => { +describe("agent emits cli_help { topic }", () => { let logSpy: ReturnType; let errorSpy: ReturnType; @@ -46,19 +46,27 @@ describe("help emits cli_help { topic }", () => { }); it("captures the served topic", async () => { - await runHelp(["help", "rule", "create"]); - expect(capture).toHaveBeenCalledWith("cli_help", { topic: "rule create" }); + await runAgent(["agent", "rule-create"]); + expect(capture).toHaveBeenCalledWith("cli_help", { topic: "rule-create" }); }); it("captures the index marker for no topic", async () => { - await runHelp(["help"]); + await runAgent(["agent"]); expect(capture).toHaveBeenCalledWith("cli_help", { topic: "(index)" }); }); it("captures the attempted topic for an unknown topic", async () => { - await runHelp(["help", "nope"]); + await runAgent(["agent", "nope"]); expect(capture).toHaveBeenCalledWith("cli_help", { topic: "nope" }); }); + + // The rejected invocation is still an intent signal — it says an agent + // reached for a topic by phrase rather than by token — so it is captured + // under the same event with the attempted words joined for readability. + it("captures the attempted words when too many positionals are given", async () => { + await runAgent(["agent", "rule", "create"]); + expect(capture).toHaveBeenCalledWith("cli_help", { topic: "rule create" }); + }); }); // Rather than asserting "no help_* event" inside every behavioral test above, diff --git a/packages/cli/test/onboard.test.ts b/packages/cli/test/onboard.test.ts index 298547d..c7afc25 100644 --- a/packages/cli/test/onboard.test.ts +++ b/packages/cli/test/onboard.test.ts @@ -182,7 +182,7 @@ describe("taskless onboard", () => { expect(stderr).toContain("--mark-complete"); }); - it("`taskless help onboard` matches the recipe printed by `taskless onboard --force`", async () => { + it("`taskless agent onboard` matches the recipe printed by `taskless onboard --force`", async () => { // Pre-mark onboarded so the `onboard` path also prints the recipe via // --force, ensuring we compare recipe-vs-recipe rather than gate-vs-recipe. await mkdir(join(cwd, ".taskless"), { recursive: true }); @@ -192,11 +192,11 @@ describe("taskless onboard", () => { "utf8" ); - const help = await runCli(["help", "onboard", "-d", cwd], cwd); + const agentRecipe = await runCli(["agent", "onboard", "-d", cwd], cwd); const onboard = await runCli(["onboard", "--force", "-d", cwd], cwd); - expect(help.exitCode).toBe(0); + expect(agentRecipe.exitCode).toBe(0); expect(onboard.exitCode).toBe(0); - expect(onboard.stdout.trim()).toBe(help.stdout.trim()); + expect(onboard.stdout.trim()).toBe(agentRecipe.stdout.trim()); }); }); diff --git a/packages/cli/test/prompts.test.ts b/packages/cli/test/prompts.test.ts index 8c35a76..f7de904 100644 --- a/packages/cli/test/prompts.test.ts +++ b/packages/cli/test/prompts.test.ts @@ -156,11 +156,11 @@ describe("typed accessor", () => { }); }); -describe("help command parity", () => { +describe("agent command parity", () => { it.each([...TOPICS])( - "matches `taskless help %s` byte for byte", + "matches `taskless agent %s` byte for byte", async (topic) => { - const { stdout } = await execFileAsync("node", [binPath, "help", topic]); + const { stdout } = await execFileAsync("node", [binPath, "agent", topic]); // The command trims trailing whitespace before printing; console.log then // adds the single newline that stdout carries. expect(stdout.trimEnd()).toBe(getPrompt(topic).trimEnd()); From 6ca8baffaa9391ce6e540ed5c195fbcfb4e9a87e Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Thu, 13 Aug 2026 01:10:27 -0700 Subject: [PATCH 6/7] docs(openspec): add a harness that executes the authoring recipes The recipes are the deliverable of this change, and the failure mode is one review cannot see: prose that reads correctly to its author and produces the wrong artifact. So execute them instead of reading them. A fresh subagent gets the recipe text, a sandbox path, and a rule intent in plain words -- and no repository access, because with it the agent finds the existing `no-simply.yml` and copies it, and the loop tests our fixtures rather than our writing. Then check the artifacts mechanically and, the part that actually decides it, whether `check` reports the finding and `verify` passes. `build:dev` is the right target rather than `dist/`: it bakes an absolute path into the recipe text, so the agent reads the command it will really run. Against `dist/` we would be testing a recipe no reader receives. Three intents, not one. Everything in this repo today is `existence`, so a recipe drafted from our own examples teaches token blocklists -- and Vale has eleven extension points, most prose rules being substitutions or capitalization. `create-sg-rule` runs through the same harness as a control: it documents a flow that already works, so a failure there means the harness is wrong rather than the recipe. Converged when an agent, given an intent the recipe never names, produces a rule that fires on its fail fixture and stays quiet on its pass fixture first try. The iteration log is kept, because it is the only evidence a reviewer can check without rerunning the loop. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3 --- .../agent-command-and-vale-authoring/tasks.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/openspec/changes/agent-command-and-vale-authoring/tasks.md b/openspec/changes/agent-command-and-vale-authoring/tasks.md index 1b3ec3a..2242056 100644 --- a/openspec/changes/agent-command-and-vale-authoring/tasks.md +++ b/openspec/changes/agent-command-and-vale-authoring/tasks.md @@ -22,6 +22,19 @@ - [ ] 2.7a Give every `create-*-rule` recipe the same opening orientation line: what topic this is, what it helps you write, and revisit routing if that is not what you need. Fixed shape across all five so an agent recognises it; scope only, never the comparison between engines - [ ] 2.8 Re-home the engine-reasoning requirements that survive the merge — "Available code context outranks the phrasing of the request" and "Ambiguity resolves to an engine known to be available" now bind `route` and the destinations. Update `help-extensions.test.ts`, which asserts against the standalone topic +## 2b. Prove the authoring recipes by executing them + +The recipes are the deliverable, and a recipe that reads well to its author while producing the wrong artifact is exactly what reviewing the prose cannot catch. Execute them instead. + +- [ ] 2b.1 `pnpm --filter @taskless/cli build:dev`. This target exists for this: `TASKLESS_BUILD_TARGET=dev` bakes `__TASKLESS_CLI__` as an **absolute path** to `dist-dev/index.js`, so recipe text carries a command that actually runs from any directory. Testing against `dist/` instead would exercise a recipe no reader ever receives, since theirs says `npx @taskless/cli` +- [ ] 2b.2 Build the harness: scaffold a throwaway project in a temp directory using the built CLI, so the sandbox is a real `taskless init` scaffold — section-less `.vale.ini`, empty `vale/rules/` — and not a hand-made approximation of one +- [ ] 2b.3 Hand a **fresh, non-forked** subagent only three things: the recipe text, the sandbox path, and a rule intent stated in plain words. It must NOT have repository access. With it, the agent finds the existing `no-simply.yml` and the mixed-engine fixture and copies them, and the loop tests our fixtures rather than our writing +- [ ] 2b.4 Check the artifacts mechanically: a style file at `.taskless/vale/rules/.yml` with valid `extends`/`message`/`level`; a **scoped section** in `.vale.ini` enabling `rules.`; fixtures in both `pass/` and `fail/`. Then the assertion that matters — `check` reports the finding, and `verify` passes +- [ ] 2b.5 Iterate across three intents that exercise different extension points — one `existence`, one `substitution` (prefer X over Y), one `capitalization` (headings, product names). Everything in this repo today is `existence`, so a recipe drafted from our own examples teaches token blocklists and nothing else. Vale has eleven extension points and most real prose rules are not blocklists +- [ ] 2b.6 Every failure is a defect in the prose, not in the agent. Fix the recipe and re-run with a fresh agent. Converged when an agent, given an intent the recipe never names, produces a rule that fires on its `fail` fixture and stays quiet on its `pass` fixture, first try, uncorrected +- [ ] 2b.7 Keep the iteration log — what failed, what changed, what finally held. It is the evidence the prose works, and the only part of this a reviewer can check without rerunning the loop +- [ ] 2b.8 Run the same harness over `create-sg-rule` as a control. It documents a flow that already works, so a failure there means the harness is wrong rather than the recipe + ## 3. Sweep the cross-references - [ ] 3.1 Replace every `taskless help ` occurrence with `taskless agent ` across `src/help/*.txt`, `src/**/*.ts`, `skills/taskless/SKILL.md`, `README.md`, and `packages/cli/README.md` (~306 occurrences, 77 files). Leave `CHANGELOG.md` alone — it is a historical record @@ -44,7 +57,7 @@ ## 6. Verify - [ ] 6.1 `pnpm typecheck`, `pnpm lint`, `pnpm --filter @taskless/cli build`, `pnpm --filter @taskless/cli test` -- [ ] 6.2 **Rehearse the recipes against a fresh agent.** For `route` and each `create-*-rule`, hand the text to a subagent with no prior context and ask it to describe the steps it would take and the commands it would run. Check its account against what the recipe intends — a recipe that reads correctly to its author and produces the wrong plan is the failure mode prose review cannot catch. Feed the corrections back into the text +- [ ] 6.2 **Rehearse `route` against a fresh agent.** Hand it the text with no prior context and a request to author a rule, then check which destination it names and why. Unlike the authoring recipes (2b), `route` produces a decision rather than artifacts, so the plan it describes is the only thing there is to check. Cover one case per destination, including a logged-out runtime request - [ ] 6.2a Run `taskless agent` with no argument, with each renamed topic, and with a removed name, confirming the index lists the new vocabulary and a removed name exits non-zero - [ ] 6.3 `pnpm openspec validate --all --strict` (note: `cli-rules` and `cli-update-engine` fail on `main` already and are unrelated) - [ ] 6.4 Archive the change From 1453142475d5028b6bcecf2519a24ca254bf10ed Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Thu, 13 Aug 2026 08:51:08 -0700 Subject: [PATCH 7/7] docs(openspec): add resume notes for picking this change back up Handoff for a context reset. `tasks.md` is the authority on what is done; this records what a fresh reader cannot recover from the artifacts -- which worktree the work lives in, the environment traps that each cost an hour last session, the Vale facts already measured so they are not re-derived, and the landmines group 2 walks into. Points at the two decisions most likely to be re-litigated by accident: the engine criterion is stated once in `route` rather than copied into the destinations, and the section-less scaffold ships paired with stderr surfacing or it reintroduces the silent disable the Vale work exists to remove. Not part of the artifact set; delete it when the change is archived. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3 --- .../resume.md | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 openspec/changes/agent-command-and-vale-authoring/resume.md diff --git a/openspec/changes/agent-command-and-vale-authoring/resume.md b/openspec/changes/agent-command-and-vale-authoring/resume.md new file mode 100644 index 0000000..c07e65f --- /dev/null +++ b/openspec/changes/agent-command-and-vale-authoring/resume.md @@ -0,0 +1,111 @@ +# Resume notes — `agent-command-and-vale-authoring` + +Handoff for picking this up after a context reset. Not part of the OpenSpec artifact set; +delete it when the change is archived. + +## Where you are + +- **PR #102** (draft), branch `openspec/agent-command-and-vale-authoring`, stacked on **#100**. +- **Work in the worktree** at `worktrees/impl-102`, not the main checkout. The main checkout + should be on `main`; a branch can only be checked out in one worktree at a time. +- Stack below you, all green and already reviewed: **#71 → #93 → #94 → #95 → #100**. + The user drives the merge-down; do not merge anything without being asked. +- **Group 1 of 6 is complete** (`093aca1`). Everything else is unstarted. + `tasks.md` is the authority — read it first, and trust its checkboxes over this file. + +## Read these before doing anything + +1. `tasks.md` — the plan, including group **2b** (the recipe test harness) +2. `design.md` — decisions D1–D10, each with its rejected alternatives. All open questions closed. +3. `specs/*/spec.md` — what the recipes and the scaffold must do +4. `proposal.md` — the why, and the delivery shape (single PR, stacked on #100) + +## Environment traps that will cost you an hour each + +- **`NODE_OPTIONS` is broken in this session's shell.** It carries a `--require` preload + pointing at a deleted temp file, so every `node`, `pnpm`, and `git commit` (husky) call + dies with `MODULE_NOT_FOUND`. Prefix everything: + `NODE_OPTIONS="--max-old-space-size=4096"`. The symptom is a build that looks like it + exits 0 through a pipe while `dist/` is never written, which then fails ~125 unrelated tests. +- **Run `pnpm --filter @taskless/cli build` before `pnpm --filter @taskless/cli test`.** + Many suites spawn the built CLI. A stale or missing `dist/` produces failures that read + exactly like real regressions — this already caused one false alarm on `main`. +- **`commit.gpgsign` is true locally.** Keep it. An earlier restack silently stripped + signatures from 34 commits because `git rebase` does not re-sign without it, and CI does + not catch unsigned commits. If you rebase, audit with `git log --format='%G?'` afterwards. +- **Do not use `git checkout ` to undo a probe** when the file also holds uncommitted + work — it discards both. This bit twice in the previous session. + +## Next up: group 2, and how to do it + +Group 2 is the authorship — the part that needs care. The user wants to see the prose before +it is finalised, but **only after 2b has been run against it**, so they review a tested +artifact rather than a draft. + +Order that works: + +1. Draft `create-vale-rule.txt`. Source of truth for Vale specifics is + `https://docs.vale.sh/llms-full.txt` (a full corpus for LLM consumption), or + `https://docs.vale.sh/topics/.md?ask=`. **`https://docs.vale.sh/styles` 404s.** + Known-good facts already measured this session: + - Common rule fields: `extends` (required), `message` (required), + `level` (`suggestion`|`warning`|`error`, default `suggestion`), `scope`, `link`. + - Eleven extension points: `existence`, `substitution`, `occurrence`, `repetition`, + `consistency`, `conditional`, `capitalization`, `metric`, `spelling`, `sequence`, `script`. + - `StylesPath = .` makes `rules/` the StyleName, so a rule at `vale/rules/no-simply.yml` + has the check id `rules.no-simply`. `StylesPath = rules` resolves nothing — measured. + - A rule assignment outside any `[section]` is **silently ignored**; Vale reports + `W101 ... is ignoring it` on **stderr** with exit 0 and a valid `{}` on stdout. +2. Model its shape on `create-sg-rule.txt` (the renamed `static.txt`). If the Vale recipe + diverges structurally, that is a signal you invented something rather than followed the + established pattern. +3. **Run group 2b against it** before showing the user anything. See `tasks.md` 2b.1–2b.8. + The load-bearing constraint: the test subagent gets the recipe text, a sandbox path, and a + rule intent — and **no repository access**. With repo access it finds the existing + `no-simply.yml` and the mixed-engine fixture and copies them, and you are testing fixtures + rather than prose. +4. Bring the user the recipe **plus the iteration log**. + +## Group 2 landmines already identified + +- **`help-extensions.test.ts` has a five-test `taskless agent engine-selection` block.** + Task 2.7 deletes that topic, so those tests must be retired as part of the merge, not after. +- **`RECIPE_TOPICS` in `agent.ts`** still lists `route/existing/static/remote/engine-selection`. + Both `help-extensions.test.ts` and `help-routing-telemetry.test.ts` assert against that exact + list. Groups 2 and 4 both touch it. +- **The `remote` + `rule-create` merge (2.5a) is a content merge, not a rename.** Both texts + have material that survives; the result must read as one procedure rather than two + concatenated. This is the piece most worth the user's eye. +- Group 1 deliberately left recipe `.txt` files untouched. **Do not start sweeping + cross-references during group 2** — that is 3.1, and mixing them makes both unreviewable. + +## Group 3 note, when you get there + +`rule.txt` documents a table of multi-token forms (`taskless help rule create`, +`... rule meta`). After group 1 these are **actively broken**, not merely stale — they hit the +new "Too many arguments" path. Lead group 3 with it. + +## Decisions you should not silently revisit + +All are argued in `design.md` with rejected alternatives. The two most likely to be +re-litigated by accident: + +- **D1** — `route` and `engine-selection` merge; the engine criterion is stated **once**, in + `route`'s destination table. Destinations carry a short orientation line (D9), never a second + copy of the criterion. +- **D4** — the section-less scaffold ships **paired** with surfacing Vale's stderr on a + zero-exit run. Shipping the scaffold alone reintroduces the silent-disable failure the whole + Vale stack exists to eliminate. They are one requirement, not two. + +Also settled: pre-1.0, every backwards-incompatible change here is a **MINOR** bump — never +MAJOR. The telemetry event stays `cli_help` (the user wants agent-call volume visible under the +existing event). + +## Outside this PR + +- **#99** — migrate subprocess handling to execa (inventory and sequencing already written up) +- **#101** — whether a whole-project Vale check should skip build output; `.taskless/` is + already excluded as of #100 +- Stale worktrees under `worktrees/` from earlier agents; `git worktree list` to review +- `openspec validate --all --strict` fails on `spec/cli-rules` and `spec/cli-update-engine` + on `main` already — pre-existing, unrelated, do not chase it