Skip to content

docs(openspec): propose the agent command and the Vale authoring path - #102

Draft
thecodedrift wants to merge 13 commits into
openspec/add-vale-rule-engine-5-integrationfrom
openspec/agent-command-and-vale-authoring
Draft

docs(openspec): propose the agent command and the Vale authoring path#102
thecodedrift wants to merge 13 commits into
openspec/add-vale-rule-engine-5-integrationfrom
openspec/agent-command-and-vale-authoring

Conversation

@thecodedrift

@thecodedrift thecodedrift commented Aug 13, 2026

Copy link
Copy Markdown
Member

Stack (root → tip):

Proposal only — no implementation. Draft so the archive gate stays quiet until the work is stacked beneath it.

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. runtime dead-ends the same way. That exclusion was a recorded non-goal at the time, defensible while nothing chose Vale; the chooser is what made it reachable.

What it proposes

From To
taskless help <topic> taskless agent <topic>
taskless help rule create taskless agent create-rule (single token, no positional join)
static create-sg-rule
existing create-legacy-rule
create-vale-rule, create-runtime-rule
route + engine-selection one route

Plus a section-less scaffolded .vale.ini, so a project lints nothing until someone scopes it deliberately.

The two decisions worth reviewing

D1 — route and engine-selection merge. One decision, made once 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 cost a second fetch and a handoff without adding information.

The consequence needed solving rather than hand-waving: the platform generator consumes engine-selection through TOPICS and has no route step, so merging can't just delete what it reads. Exporting route instead is wrong — it's built on taskless detect --json and local authoring, neither of which a Worker can do. So the criterion distributes to the destinations: each create-*-rule states the evidence that makes its engine right, and route applies those same tests. That keeps every exported topic one a service-side consumer can actually act on, and removes the drift class where a chooser and its destinations disagree.

D4 — the section-less scaffold ships paired with surfacing Vale's stderr. Measured: with no section to copy, the natural first edit is rules.<id> = YES at top level, which Vale reports as W101 ... is ignoring it — on stderr, exit 0, valid {} on stdout. runVale reads stderr only on a non-zero exit, so today that's discarded and the author gets a rule that verifies, runs, and reports nothing. Shipping the scaffold alone would reintroduce the exact silent-disable class this engine's design exists to prevent, one level up. They are one requirement in the spec, not two.

Also settled

  • No create-remote-rule. "Remote" names who generates, not an engine, and the one place it bites — the user is logged out — is where create-runtime-rule already has to speak. One topic, one gate explained once.
  • Every backwards-incompatible item here is a MINOR bump. Pre-1.0.
  • Found while writing it: cli-knowledge-prompts currently requires a topic rename be MAJOR. The delta corrects that to pre-1.0 semantics, keeping the obligation that actually matters — naming the rename in the changeset, since TOPICS crosses a deploy boundary and the generator breaks on upgrade rather than at our build time.

Open questions in the design

  • remote.txt / rule-create.txt still implement service generation but are no longer named by route. Keep, fold into create-sg-rule's escalation path, or merge? Worth deciding before the cross-reference sweep.
  • Does create-runtime-rule explain the login gate itself, or defer to auth?

Scope

~306 occurrences of taskless help across 77 files — but concentrated: only one skill file, the rest are recipe cross-references and tests. Delivery shape is a single PR stacked on #100, because recipes cross-reference each other by literal command string, so a half-renamed surface points at commands that don't exist.

pnpm openspec validate --strict passes.

Refs #100

`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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
thecodedrift and others added 12 commits August 12, 2026 22:20
…mote-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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
…oute 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
Group 2 of agent-command-and-vale-authoring: the recipes themselves.

`route` absorbs `engine-selection` (D1). One fetch now goes from a
rule-authoring request to a runnable command, with the engine criterion
stated once, in route's destination table, and the runtime row split on
login state (D7). Destinations open with a fixed orientation banner that
self-corrects a misrouted reader without restating the criterion (D9).

Topics flatten to verb-noun single tokens. `static` -> `create-sg-rule`
also drops a leak: "static" is a trust tier, not an engine, and naming
the ast-grep path after the tier taught the confusion the criterion
exists to correct. `remote` + `rule-create` merge into
`create-remote-rule` (D8); `rule-create.anonymous` is deleted rather
than renamed, since it duplicated `static.txt` and a "local variant of
the remote recipe" is the contradiction `route` resolves.

Two recipes are new. `create-vale-rule` teaches the three artifacts, and
above all the section: the scaffold ships section-less, so the first
rule in a project also writes the first scope, and an assignment outside
a section is ignored rather than rejected. `create-runtime-rule` is the
logged-out path, explaining the gate as a property of executing code and
deferring to `auth` (D6, D10).

Groups 5.1 and 5.2 land here rather than later, deliberately: they are
preconditions of testing the Vale recipe honestly. The scaffolded
.vale.ini drops its `[*]`, and runVale surfaces a zero-exit stderr as a
notice so the mistake that scaffold invites is legible instead of
silent. Measured end to end — a top-level `rules.<id> = YES` now prints
Vale's W101 and still exits 0.

Also corrects three spec deltas that still referenced `engine-selection`
as a live topic, and re-homes the two engine-reasoning requirements that
survive the merge onto `route`.

572 tests pass; typecheck and lint clean.
Run B (substitution) converged first try and independently confirmed
run A's three worst findings. Its most valuable addition: both `tokens`
and `swap` keys are Go RE2 patterns presented in the recipe as literal
strings, and a wrong guess there fails as a silent non-match — the exact
failure the recipe spends a section warning about.

Also records one finding rejected as a harness artifact, so a later
reader does not "fix" build:dev's absolute-path rewriting.
Round 1 of 2b: three sandboxed agents authored a real Vale rule from the
rendered recipe with no repository access. Two converged first try; the
third took a retry and found the two defects that mattered.

The field table was factually wrong. It said `%s` interpolates the
match; for `substitution` a single `%s` interpolates the *replacement*,
so "Use GitHub not %s" against "Github" rendered "Use GitHub not
GitHub". This is the worst kind of defect — it passes every check the
recipe tells you to run, and only a human reading the message sees it.

The extension-point table also pointed product names at
`capitalization`, which applies `match` to a whole scope and cannot
express "this word, wherever it appears". Measured, it flags entire
sentences. Product-name spelling is a `substitution`.

Beyond those: tokens and swap keys are Go RE2 patterns presented as
literals (no lookaround, implicit word boundaries, live metacharacters,
first-wins on overlap); `success` and the exit code do not answer the
question the fixtures ask, only `results[].ruleId` does; `check <path>`
is not scoped to the rule under test; MinAlertLevel was missing from the
debug ladder, which had no branch for the over-broad case at all; and
the pass bucket is near-misses, not correct prose.

Claims of CLI enforcement are removed rather than softened. Nothing
validates the fixture layout today, and saying otherwise taught a
reader to rely on a check that does not run.

One agent recommendation was rejected on measurement: `ignorecase: true`
does not flag text already equal to its replacement, so the proposed
warning would have been false.

572 tests pass. Round 2 is running against the revised text.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant