Skip to content

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

Merged
thecodedrift merged 41 commits into
openspec/add-vale-rule-engine-5-integrationfrom
openspec/agent-command-and-vale-authoring
Aug 19, 2026
Merged

docs(openspec): propose the agent command and the Vale authoring path#102
thecodedrift merged 41 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

Contains #103

Why

A rule is spread across locations today, and for Vale one of them is shared by every rule in the project. Five sandboxed harness runs against create-vale-rule (on #102) found silent failures in that shared .vale.ini and nowhere else — an assignment above the first matcher, a glob that missed the fixture's extension, three names that had to agree with nothing reporting when they didn't.

The same reasoning generalizes past Vale: sg and runtime rules are also split between a rules/ tree and a parallel rule-tests/ tree, so no engine has a single path meaning "this rule". Fixing Vale alone would leave three layouts instead of one.

What changes

One directory per rule, identical across engines:

.taskless/rules/sg/no-eval/
    no-eval.yml
    .tests/no-eval-20260101-test.yml

.taskless/rules/vale/no-simply/
    no-simply.yml
    .vale.ini            # this rule's matchers, excludes, tskl) metadata
    .tests/pass/ok.md
    .tests/fail/bad.md

.taskless/rules/runtime/unused-exports/
    check.ts
    captures/exported-symbol.yml
    .tests/…

check assembles the Vale and ast-grep configs from the per-rule tree and gitignores them. rule verify <id> is replaced by path-addressed verify <path> (required components) and test <path> (tests), with verify running as a prerequisite layer of test.

Adds example/ — a small real project so a reader can see an install rather than infer it from tests that build their own fixtures, with a check test so it can't drift.

The dot on .tests/ is load-bearing, and measured

Probe ast-grep 0.41.0 Vale 3.17.1
tests/ inside the rule dir failsmissing field 'language' fine
__tests__/ fails fine
.tests/ skipped by rule discovery fine, even containing a .yml
test runner reads it testDir: …/.tests works; snapshots land inside buckets lint when targeted

ruleDirs recurses and parses every .yml beneath as a rule, so a plain tests/ directory hard-fails the scan.

This is a dependency on undocumented behavior and D2 records it as one, with two mitigations: the failure is loud — a parse error naming the file, not a test silently reinterpreted as a rule — and a test pins it. The rejected alternative (materialize a rules-only tree for ast-grep, keeping a plain tests/) is written down as the fallback if the assumption breaks.

Other measured constraints

  • <id>/<id>.yml resolves as check <id>.<id> only under a StylesPath naming its parent; under StylesPath = . it resolves to nothing. This reverses the note in migration 0004 — correct for the flat layout, backwards for this one — so the task list rewrites that docstring rather than deleting it.
  • Vale rejects unknown keys in a style (E201), so scope cannot ride inside the style file. ast-grep can express scoping in the rule, which is why it gets no per-rule config — an empty file per rule is symmetry as decoration.

Delivery shape

Stacked, merging down on #102. 0005 layers on 0004; both are unreleased and both ship in this stack, so consumers run them as one upgrade and never observe the intermediate layout.

openspec validate --all --strict will flag the cli-agent-authoring delta until #102 archives — that capability is introduced there.

Spec-only. Implementation follows on approval.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3

Contains #106

Cleanup on top of the stack so pnpm openspec validate --all --strict is green and the gate in #105 can be turned on without landing red.

Two failures, and a third problem that wasn't failing

cli-update-engine — deleted. A tombstone: capability decommissioned, every requirement removed, file kept "for historical reference". --strict needs at least one requirement. specs/ describes what's true now, and the history is already in the 2026-03-30-remove-scaffold-dependency archive, including that change's own cli-update-engine delta. Nothing outside the archives referenced it.

cli-rules — 18 errors. Scenarios and SHALL keywords added to 15 requirements. The five specifying taskless rule verify are removed: this stack deleted that command in favour of the path-addressed verify and test, now owned by cli-rule-validation. A requirement stating the subcommand is gone replaces them. Two requirements named pre-layout paths (.taskless/rule-tests/<id>.yml, and deleting "rule and test files" rather than the rule directory); both repathed while being touched.

The one worth reading. A second ## inside a requirements section ends it, so every requirement below stops being parsed:

Spec In file Visible before --strict before
cli-rules 33 23 failing
infrastructure 20 1 passing
skills 7 1 passing

infrastructure and skills passed throughout, because the single requirement each still exposed was well-formed. 35 requirements were unread while the validator reported success. This is the same shape as the cli-help defect repaired earlier in the stack, where a lost code fence turned a documentation template into real headings.

The topical groupings are kept as bold lead-in lines instead of headings. Un-nesting infrastructure immediately exposed a real requirement with no SHALL, now fixed.

skills also still told agents to fetch recipes via npx @taskless/cli help <topic>.

Result

23 specs, 0 failures, with every requirement actually visible to the parser. 594 tests, lint and typecheck unchanged: this PR touches only openspec/specs/.

Note that --strict alone would not have caught the hidden-requirement class. Detecting it needs a separate check that every ### Requirement: sits under ## Requirements — noted on #105 for whoever wires the gate.

Refs #105

🤖 Generated with Claude Code

https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3

@thecodedrift

Copy link
Copy Markdown
Member Author

@claude /review

@claude

claude Bot commented Aug 14, 2026

Copy link
Copy Markdown

Claude finished @thecodedrift's task in 5m 55s —— View job


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

  • Gather context (PR diff vs. origin/openspec/add-vale-rule-engine-5-integration, OpenSpec proposal/spec/design/tasks, CLAUDE.md conventions)
  • Review for correctness (line-by-line diff scan, removed-behavior audit, cross-file tracing)
  • Review for consistency/architecture (engine dispatch vs. verify ownership)
  • Review for reuse/duplication
  • Review for efficiency
  • Review for simplification
  • Review against CLAUDE.md / STYLEGUIDE-CODE.md conventions
  • Post inline comments and summary

Overall assessment

Despite the PR title/body framing this as "proposal only — no implementation," the diff ships real behavior changes: helpagent rename, single-positional-token enforcement, the Vale ownership/verify path, and the stderr-notice plumbing. Reviewed accordingly.

The core design (D1 merging route/engine-selection, D4 pairing the section-less scaffold with stderr surfacing) is sound and the rationale in the PR body is well-argued. The implementation has a few concrete gaps, the most important being that the rename this PR performs isn't fully propagated through the help-recipe corpus, which undercuts the PR's own stated goal (agents navigating a coherent, self-consistent recipe graph).

Correctness — dead cross-references left after the helpagent rename (highest priority)

taskless help <topic> (the old command form) still appears in 14 help recipe files after this PR: rule.txt, delete-rule.txt, improve-rule.txt, improve-rule.anonymous.txt, verify-rule.txt, check.txt, ci.txt, detect.txt, info.txt, init.txt, onboard.txt, rule-meta.txt, update.txt, auth.txt (verified via grep against the current tree). None of these files are in the PR's changed-files list, so they weren't touched by the rename.

Since help is no longer a registered subcommand (index.ts now registers only agent) and agent.ts rejects multi-token topics ("Too many arguments"), following any of these recipes verbatim either silently no-ops (hasPositional guard exits 0 with no output) or hard-errors. This bites on paths agents are actively steered down:

  • onboard.txt (lines ~94, 120-122) — the very first action step of onboarding a fresh session.
  • ci.txt (lines ~15, 69, 210-221) — the zero-rules fallback in CI mode.
  • detect.txt (lines ~48-50, 62-64) — routing to existing/static, both renamed (create-legacy-rule/create-sg-rule) and never cross-referenced from here.
  • verify-rule.txt / improve-rule.txt — self-referential "See Also" escalation paths broken by this PR's own renames.

Given the PR explicitly scopes itself around "~306 occurrences... concentrated" and calls out a "cross-reference sweep" as an open question, this looks like exactly the kind of half-swept surface the PR itself warns about (static.txt/remote.txt-style dangling references) — worth resolving before this lands, or explicitly deferring with a tracked follow-up.

Correctness — ownership/verify inconsistencies

  • packages/cli/src/rules/owner.ts (inline comments posted):

    • rulefileOwners hardcodes a two-engine candidate list (sg, vale) instead of iterating ENGINES/ENGINE_LAYOUTS, silently omitting runtime. A runtime-owned rule id returns zero owners and falls through to the ast-grep verifier, which reports a bogus "missing rule file" under engine: "sg".
    • ruleFileLocation always reconstructs the canonical path from ENGINE_LAYOUTS, even though rulefileOwners matches against astGrepRuleFileCandidates, which also includes the legacy .taskless/rules/<id>.yml location. If the ambiguity is caused by a legacy-location file, the "exists for more than one engine" error names a path that doesn't exist and never mentions the real conflicting file. Since rulefileOwners already knows which candidate matched, returning {engine, path} pairs and dropping ruleFileLocation would fix this at the source rather than reconstructing (and risking a wrong reconstruction).
  • packages/cli/src/rules/vale/verify.ts (verifyValeRule, ok branch, ~line 256): reads outcome.results but never outcome.notice. This directly undercuts D4's own rationale — the whole point of surfacing Vale's stderr notice is to catch a section-less scaffold silently going quiet, but rule verify <id> --json on a Vale rule drops that notice while taskless check on the identical repo state prints it via dispatch.tscheck.ts. Same underlying condition, visible through one entry point and invisible through the other. Not covered by the new rule-verify-dispatch.test.ts.

  • dispatch.ts vs. owner.ts disagree on ambiguity: rulefileOwners (used by rule verify) treats a rule id existing under both sg and vale as a hard error. runAstGrepEngine/runValeEngine in dispatch.ts (used by check) never perform this cross-engine check — each just scans its own directory, so the same on-disk state that verify refuses outright runs silently through check, reporting findings from both engines under one id with no warning.

Reuse / duplication

  • packages/cli/src/commands/rules.ts: the "build message → print JSON error envelope or console.error → set exitCode → return" shape is repeated at ~line 714, ~725-740, and ~852-860, all reimplementing writeJsonError (packages/cli/src/types/errors.ts), which is already used elsewhere (commands/auth.ts). Worth a small reportCLIError(json, code, message) helper.
  • packages/cli/src/rules/owner.ts's isFile() re-implements the same ENOENT/ENOTDIR-as-absence check as isMissingDirectory (rules/vale/verify.ts) and pathExists (filesystem/migrations/0004-vale-engine.ts) — a third independent copy of "which errors mean not-here."

Simplification

  • dispatch.ts:166 and rules/vale/run.ts:236-247,277: ...(outcome.notice === undefined ? {} : { notice: outcome.notice })-style conditional spreads exist only to hide an already-optional field; every reader treats absent/undefined identically and JSON.stringify drops undefined on its own. Simplifies to a plain notice property.
  • commands/agent.ts:154,158: identical telemetry.capture("cli_help", { topic: key }) duplicated across both branches of the recipe-found/not-found if/else — hoist above the branch.

Efficiency (minor, non-blocking)

  • owner.ts's rulefileOwners probes engines and candidate paths sequentially (for/await in a loop) where they're independent and could run via Promise.all; worst case is 3 sequential stat round-trips. Low-single-digit ms in practice, not worth blocking on.
  • rulefileOwners stats a path, and shortly after verifyRule/verifyValeRule re-reads/re-probes the same candidate paths from scratch — a small amount of duplicated I/O that could be avoided by threading the resolved path through, but not a correctness issue.

Conventions

No CLAUDE.md / STYLEGUIDE-CODE.md violations found: imports are direct (no barrel exports), correctly grouped/ordered, exported types are properly exported, the new rule-verify-dispatch.test.ts exercises the built CLI's output rather than parsing generated artifacts (consistent with the "verify build output in the build" rule), and the changeset absence is consistent with this being a mid-stack PR per the root CLAUDE.md's stacked-PR rules (no skip-changeset label misuse observed).

Not flagged as bugs, but worth a deliberate answer before merge

  • TOPICS (packages/cli/src/prompts/index.ts) drops static/engine-selection with no compatibility shim; the PR body already surfaces this as a known consequence for the platform generator's TOPICS consumption and proposes the MINOR-bump correction to cli-knowledge-prompts — flagging only because nothing in-repo currently guards against it besides the changeset discipline.
  • Vale I/O errors (e.g. EACCES on an unreadable fixture directory) are reported as INVALID_INPUT in verifyValeRuleCommand's catch block, conflating a filesystem problem with a user-input problem.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR renames the CLI’s agent-facing knowledge surface from taskless help to taskless agent, switches topic addressing to single-token names, and fills the previously missing Vale authoring destination by adding new authoring recipes plus related CLI/test updates. It also adjusts Vale scaffolding and runtime behavior (stderr-on-success surfaced as a notice) and extends rule verify to dispatch by owning engine (ast-grep vs Vale) to make the authoring/verification loop reachable for both engines.

Changes:

  • Replace the help subcommand with agent, remove positional-join topic resolution, and update telemetry/tests accordingly.
  • Add/rename rule-authoring recipes (create-sg-rule, create-vale-rule, create-runtime-rule, create-remote-rule, create-legacy-rule) and merge engine-selection reasoning into route.
  • Extend rule verification to route by engine ownership, add Vale verify output shape, and surface Vale stderr diagnostics as non-blocking notices; update OpenSpec change docs/specs.

Reviewed changes

Copilot reviewed 43 out of 47 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/cli/test/rule-verify-dispatch.test.ts Adds coverage for rule verify dispatch by owning engine and Vale fixture validation behaviors.
packages/cli/test/prompts.test.ts Updates prompt/topic typing and parity checks to the renamed topics and agent command.
packages/cli/test/onboard.test.ts Updates onboarding test expectations to use taskless agent recipes.
packages/cli/test/help-telemetry.test.ts Switches telemetry coverage from help command module to agent command module and updates topic capture expectations.
packages/cli/test/help-routing-telemetry.test.ts Adjusts routing telemetry tests for the new command (but still referenced removed topics; flagged in comments).
packages/cli/test/help-extensions.test.ts Updates behavioral tests for topic listing/resolution, removed-topic behavior, and new routing/authoring topics.
packages/cli/test/cli.test.ts Updates “no args” behavior expectation to reflect agent index output.
packages/cli/test/cli-run.test.ts Updates resolveCommandName coverage to treat agent like the old help (command name only).
packages/cli/test/anonymous-flag.test.ts Updates anonymous-flag guidance strings to point to new taskless agent … topics.
packages/cli/src/types/errors.ts Adds ENGINE_UNAVAILABLE error code to distinguish missing engine binaries from execution failures.
packages/cli/src/telemetry-run.ts Updates command-name derivation docs/behavior from help to agent.
packages/cli/src/schemas/rules-verify.ts Adds an engine discriminant to sg verify output and introduces a distinct Vale verify output schema.
packages/cli/src/rules/vale/run.ts Captures Vale stderr on zero-exit runs and returns it as an optional notice on ok outcomes.
packages/cli/src/rules/owner.ts New helper to determine rule ownership by rule file location and format actionable paths for errors.
packages/cli/src/rules/dispatch.ts Forwards Vale “notice” (stderr diagnostics on success) through dispatch results without affecting exit codes.
packages/cli/src/prompts/recipes.ts Renames schema-bearing topics (rule-create/rule-improvecreate-remote-rule/improve-rule).
packages/cli/src/prompts/index.ts Updates exported TOPICS/INTERNAL_TOPICS to match the renamed authoring surface and removed engine-selection export.
packages/cli/src/index.ts Registers agent subcommand and updates non-interactive routing from help to agent.
packages/cli/src/help/verify-rule.txt Adds a verify recipe (but needed updates to match single-token topic naming and new output shape; flagged in comments).
packages/cli/src/help/static.txt Removes the old static authoring recipe (replaced by create-sg-rule).
packages/cli/src/help/rule-create.txt Removes old service-generation recipe (merged into create-remote-rule).
packages/cli/src/help/rule-create.anonymous.txt Removes old anonymous create recipe (folded into create-sg-rule guidance).
packages/cli/src/help/route.txt Rewrites routing to classify into the new destinations and embeds engine reasoning directly.
packages/cli/src/help/remote.txt Removes old “remote boundary” recipe (merged into create-remote-rule).
packages/cli/src/help/improve-rule.txt Adds API-backed improve recipe (but had stale taskless help references/topic header; flagged in comments).
packages/cli/src/help/improve-rule.anonymous.txt Adds anonymous/local improve recipe (but had stale taskless help references/topic header; flagged in comments).
packages/cli/src/help/engine-selection.txt Removes standalone engine-selection recipe (merged into route).
packages/cli/src/help/delete-rule.txt Adds delete recipe (but had stale taskless help references/topic header; flagged in comments).
packages/cli/src/help/create-vale-rule.txt Adds comprehensive Vale authoring recipe (rule + .vale.ini scoping + fixtures).
packages/cli/src/help/create-sg-rule.txt Adds renamed local ast-grep authoring recipe replacing static.
packages/cli/src/help/create-runtime-rule.txt Adds logged-out runtime authoring/gate explanation recipe.
packages/cli/src/help/create-remote-rule.txt Adds merged, single-procedure service-generation recipe.
packages/cli/src/help/create-legacy-rule.txt Renames/retitles legacy-linter authoring recipe (was existing).
packages/cli/src/filesystem/migrations/0004-vale-engine.ts Makes scaffolded .vale.ini section-less to avoid unscoped default linting.
packages/cli/src/commands/rules.ts Dispatches rule verify by owning engine; adds Vale verification command path and JSON output support.
packages/cli/src/commands/agent.ts Implements agent command (index + single-token topic resolution + telemetry), replacing old help behavior.
openspec/changes/agent-command-and-vale-authoring/tasks.md Plan/checkboxes for this OpenSpec change set.
openspec/changes/agent-command-and-vale-authoring/specs/cli-vale-rule-engine/spec.md Specifies section-less scaffold + stderr-on-success notice requirements for Vale.
openspec/changes/agent-command-and-vale-authoring/specs/cli-rule-routing/spec.md Specifies merged routing+engine reasoning and renamed destinations.
openspec/changes/agent-command-and-vale-authoring/specs/cli-knowledge-prompts/spec.md Specifies topic export surface stability and pre-1.0 MINOR semantics.
openspec/changes/agent-command-and-vale-authoring/specs/cli-help/spec.md Updates spec to agent command semantics and no positional-join behavior.
openspec/changes/agent-command-and-vale-authoring/specs/cli-agent-authoring/spec.md Adds requirements ensuring each engine has a reachable authoring recipe.
openspec/changes/agent-command-and-vale-authoring/resume.md Handoff notes for continuing the change set work.
openspec/changes/agent-command-and-vale-authoring/proposal.md Proposal narrative for the rename + Vale authoring destination.
openspec/changes/agent-command-and-vale-authoring/iteration-log.md Execution log of recipe harness runs used to validate authoring prose.
openspec/changes/agent-command-and-vale-authoring/design.md Design decisions (D1–D10) supporting the new command/topic model.
openspec/changes/agent-command-and-vale-authoring/.openspec.yaml OpenSpec change metadata for the proposal directory.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/cli/test/help-routing-telemetry.test.ts Outdated
Comment thread packages/cli/test/help-extensions.test.ts
Comment thread packages/cli/src/rules/owner.ts Outdated
Comment thread packages/cli/src/rules/owner.ts Outdated
Comment thread packages/cli/src/commands/rules.ts Outdated
thecodedrift added a commit that referenced this pull request Aug 14, 2026
The existing migration tests assert where files land and that their
bytes survive. Both can be perfect while the migrated project reports
nothing, which is how the Vale silent-disable shipped: 0004 enables a
check named `rules.<id>`, 0005's StylesPath resolves the same rule as
`<id>.<id>`, and carrying the old assignment forward leaves every Vale
rule present, valid, enabled against a check that does not exist, and
quiet. It was caught by hand, by noticing ast-grep still reported while
Vale had gone silent.

So this seeds a version-4 project and runs the real CLI over it after
migrating: both static engines still report, the assignment is
retargeted, every rule still verifies, the tests that moved still run,
runtime capture bytes are unchanged, and a second run is a no-op. The
ast-grep half is the control, so a silent Vale shows up as a difference
between engines rather than a plausibly-empty run.

The changeset also described a layout this stack has since replaced,
and promised an `engine-selection` topic that #102 merged into `route`.
Rewritten for what actually ships.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
thecodedrift added a commit that referenced this pull request Aug 14, 2026
7.1-7.6 verified. 7.7 is blocked on #102: a change archives once on the
tip, and the gate wants openspec/changes/ empty, but
agent-command-and-vale-authoring still has 14 open tasks. Archiving it
early would drop the spec deltas for work nobody has done. #102's
delta also has to reach specs/ before this change's delta has a target.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
thecodedrift added a commit that referenced this pull request Aug 15, 2026
The stack's second and final archive, which is why #102 had to go
first: this change's `cli-agent-authoring` delta modifies a requirement
that one introduces, so it needed a target in specs/.

The archive refused twice, both times on a rename expressed as a
MODIFIED. Five of the six MODIFIED entries across cli-rule-format and
cli-vale-rule-engine renamed their requirement, which openspec matches
by header, so each became a REMOVE of the old name plus an ADD of the
new. cli-rule-format's MODIFIED section emptied out entirely.

Two requirements the deltas never retired, both now describing code
this change deletes:

- "Both the legacy and engine-partitioned layouts are readable"
  promises a legacy read path that task 1.5 removes. The legacy
  constant now names the same string as the rules root, so a stale
  read path would resolve into the live tree (design D9).
- "The scaffolded Vale config carries no section" describes a file
  that no longer exists. Verified against a fresh `init`: the scaffold
  contains no `.ini` at all, because scope is declared per rule.

`cli-rules` and `cli-update-engine` still fail --strict; both fail on
main and neither is touched by this stack.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
@thecodedrift

Copy link
Copy Markdown
Member Author

@claude /review

@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown

Claude finished @thecodedrift's task in 5m 2s —— View job


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

  • Gather context (diff vs origin/openspec/add-vale-rule-engine-5-integration, changed files, CLAUDE.md conventions)
  • Correctness review (line-by-line diff scan, removed-behavior audit, cross-file tracing)
  • Reuse / simplification / efficiency review
  • Conventions (CLAUDE.md / STYLEGUIDE) review
  • Post inline comments and top-level summary

Overall assessment

This is a second pass on #102 (a prior @claude /review ran on this PR). The good news: the biggest gap from that earlier pass — dangling taskless help cross-references left after the rename — is fixed, and fixed properly: packages/cli/test/recipe-cross-references.test.ts now scans the shipped recipe corpus itself (no taskless help occurrences, no dangling taskless agent <topic> citations, header/filename agreement) rather than relying on a manual sweep. I confirmed grep -rn "taskless help" packages/cli/src/help/ returns nothing, and spot-checked onboard.txt, detect.txt, ci.txt — all correctly point at the new single-token topics.

However, most of the substantive findings from the earlier review pass are still open in the current diff — they weren't stylistic, and re-checking them against the current owner.ts/verify.ts/dispatch.ts shows the same code:

  1. Vale's stderr notice still doesn't reach rule verify (inline comment on packages/cli/src/schemas/rules-verify.ts). D4's stated rationale is that surfacing Vale's stderr is what makes the section-less scaffold safe — but that only holds for taskless check. verifyValeRule (packages/cli/src/rules/vale/verify.ts:262, not touched by this PR) reads outcome.results and never outcome.notice, and valeVerifyOutputSchema has no field to carry it even if it were read. Same repo state (misplaced rules.<id> = YES) reports the W101 warning through check and silently passes through rule verify. Not covered by the new rule-verify-dispatch.test.ts.

  2. rulefileOwners still hardcodes ["sg", "vale"], omitting runtime from ENGINES = ["sg", "vale", "runtime"] (inline comment on packages/cli/src/rules/owner.ts:30). A runtime-owned rule id returns zero owners and falls through to the ast-grep verifier, reporting a bogus "missing rule file" under engine: "sg".

  3. verify-rule.txt wasn't updated for multi-engine dispatch (inline comment). Its Goal/Preconditions/Steps/Errors/JSON example are still ast-grep-only, despite the topic version bump to v2 and commands/rules.ts now dispatching rule verify by ownership with a divergent Vale output shape. create-vale-rule.txt treats verify-rule as authoritative for Vale verification, so the two recipes now disagree.

  4. The changeset's rename table is factually wrong (inline comment on .changeset/vale-rule-engine.md:28). It states static, existing → route; the actual mapping is static → create-sg-rule (now exported via TOPICS) and existing → create-legacy-rule (internal), with only engine-selection merging into route. This is public breaking-change documentation for @taskless/cli/prompts consumers, worth getting right.

  5. help-routing-telemetry.test.ts still iterates the removed topics (inline comment) — it passes today only because agent.ts fires the same cli_help telemetry event on both the found and not-found paths, so the test now silently validates the unknown-topic path instead of routing topics.

Lower-priority, unaddressed since the last pass (not re-flagged inline to keep this focused, but worth a look before merge):

  • owner.ts's ruleFileLocation still reconstructs only the canonical path, while rulefileOwners also matches the legacy ast-grep location — an ambiguity error can name a path that doesn't exist.
  • commands/rules.ts's new verifyValeRuleCommand and the owners.length > 1 branch still hand-roll the JSON error envelope instead of using writeJsonError (packages/cli/src/types/errors.ts), now three sites in this file diverging from the shared helper.
  • commands/agent.ts:154,158 duplicates telemetry.capture("cli_help", { topic: key }) across both branches of the found/not-found if/else.
  • dispatch.ts (used by check) still never performs the cross-engine ambiguity check that owner.ts (used by rule verify) hard-errors on — the same on-disk state (a rule id under both sg and vale) is refused by one entry point and silently merged by the other.

Conventions

No new CLAUDE.md/STYLEGUIDE-CODE.md violations found beyond what's noted above (imports direct and correctly grouped, exported types properly exported, recipe-cross-references.test.ts reads recipe source rather than parsing built output, consistent with the "verify build output in the build" rule).
(branch: openspec/agent-command-and-vale-authoring)

Comment thread packages/cli/src/schemas/rules-verify.ts
Comment thread packages/cli/src/rules/owner.ts Outdated
Comment thread .changeset/vale-rule-engine.md Outdated
Comment thread packages/cli/src/help/verify-rule.txt
Comment thread packages/cli/test/help-routing-telemetry.test.ts Outdated
thecodedrift added a commit that referenced this pull request Aug 17, 2026
`pull_request: branches: [main]` matches the PR's base ref, and GitHub
also resolves a stacked PR's eventual target and matches on that — so the
filter did run on PRs based on another branch. Until it didn't. On the
#71#106 stack every PR up to #102 got a Validate run and #103/#106 got
none, across 16 pull_request events that other workflows handled fine.
#103 is a ~93-file change that has never been linted, typechecked, or
tested in CI.

Lint, typecheck, and tests have no interest in where a PR eventually
merges, so the filter is dropped rather than repaired. `ready_for_review`
is named explicitly because it is not in the default event set; without
it a draft marked ready gets no fresh run until someone pushes again,
which is the state #103 sat in.

Committed directly to main: the fix restores CI coverage, and routing it
through a PR would have it validated by the mechanism it repairs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
thecodedrift added a commit that referenced this pull request Aug 17, 2026
Three docs stated that GitHub's stacked-PR support makes
`pull_request: branches: [main]` match every PR in a stack, and presented
it as dependable. It is not. On the #71#106 stack the filter matched
through #102 and then silently stopped: #103 and #106 produced no
`Validate` and no `Require a changeset` run across 16 pull_request events
that filter-less workflows handled fine. That belief is why a ~93-file
change reached "ready for review" having never been linted, typechecked,
or tested in CI.

The corrected rule is split by intent. A workflow that must run
everywhere carries no `branches:` filter and names `ready_for_review` in
`types:`, since it is absent from the default event set. A workflow that
must act only on the PR merging to `main` establishes that inside the
job, from the base ref or stack position.

Also fixes a factual error: iterate-pr claimed
`pr-check-openspec.yml` is scoped by `branches: [main]`. It has no
`branches:` filter, which is the actual reason it runs on every PR.

Adds the corollary worth internalizing: an absent check reads like a
passing one, so a check you expected and cannot find is a reason to
suspect the trigger, not to assume the PR is clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
thecodedrift and others added 2 commits August 18, 2026 17:42
`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
…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
thecodedrift added a commit that referenced this pull request Aug 19, 2026
7.1-7.6 verified. 7.7 is blocked on #102: a change archives once on the
tip, and the gate wants openspec/changes/ empty, but
agent-command-and-vale-authoring still has 14 open tasks. Archiving it
early would drop the spec deltas for work nobody has done. #102's
delta also has to reach specs/ before this change's delta has a target.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
thecodedrift added a commit that referenced this pull request Aug 19, 2026
The stack's second and final archive, which is why #102 had to go
first: this change's `cli-agent-authoring` delta modifies a requirement
that one introduces, so it needed a target in specs/.

The archive refused twice, both times on a rename expressed as a
MODIFIED. Five of the six MODIFIED entries across cli-rule-format and
cli-vale-rule-engine renamed their requirement, which openspec matches
by header, so each became a REMOVE of the old name plus an ADD of the
new. cli-rule-format's MODIFIED section emptied out entirely.

Two requirements the deltas never retired, both now describing code
this change deletes:

- "Both the legacy and engine-partitioned layouts are readable"
  promises a legacy read path that task 1.5 removes. The legacy
  constant now names the same string as the rules root, so a stale
  read path would resolve into the live tree (design D9).
- "The scaffolded Vale config carries no section" describes a file
  that no longer exists. Verified against a fresh `init`: the scaffold
  contains no `.ini` at all, because scope is declared per rule.

`cli-rules` and `cli-update-engine` still fail --strict; both fail on
main and neither is touched by this stack.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
thecodedrift and others added 21 commits August 18, 2026 19:21
A Vale rule is currently three locations, one of which every rule in the
project shares. That shared .vale.ini is where all five harness runs
found silent failures — an assignment above the first matcher, a glob
that missed the fixture extension, three names that had to agree with
nothing reporting when they didn't. A single write-contended config is
the wrong shape at any scale.

Each rule becomes a directory holding its style and its own .vale.ini;
check assembles the run config from them and gitignores it. Measured
against Vale 3.17.1: rules/<id>/<id>.yml resolves as check <id>.<id>
under StylesPath = rules and resolves to nothing under StylesPath = .,
Vale rejects unknown keys in a style so scope cannot ride along inside
it, and a .yml sidecar in a style directory is loaded as a rule and
fails E201 while a .vale.ini in the same place is ignored.

Also replaces rule verify <id> with path-addressed verify and test. An
id does not name one thing — the same id can exist under two engines,
which is why the id form needed an ambiguity error at all.

Adds an example/ project so a reader can see an install rather than
infer it from tests that build their own fixtures.

Stacked on #102 and merging down: a layout change without its migration
ships a project whose rules silently stop running.
Widens the change from Vale to every engine, and renames it to match.

One directory per rule, identical shape everywhere:
`.taskless/rules/<engine>/<id>/` holding the rule, any config that
engine requires, and its tests in `.tests/`. A rule becomes one path
rather than two, which is what makes `verify <path>` and `test <path>`
work without an id lookup.

The dot on `.tests/` is load-bearing and measured. ast-grep's ruleDirs
recurses and parses every .yml beneath as a rule, so a plain `tests/`
directory fails the scan with "missing field 'language'"; `__tests__/`
fails the same way; a dot-directory is skipped, and `sg test` still
reads it when testDir names it. Vale is unaffected either way — a
`.tests/` inside a style directory is harmless even containing a .yml.

That is a dependency on undocumented behavior, recorded as one in D2
with two mitigations: the failure is loud (a parse error naming the
file, not a silently reinterpreted test), and a test pins it. The
rejected alternative — materializing a rules-only tree for ast-grep —
is written down as the fallback if the assumption ever breaks.

Also drops the per-rule sg config: ast-grep expresses scoping inside
the rule, so the slot would be an empty file every author creates, no
author fills, and every reader learns to ignore. And renames runtime's
capture rules to `captures/`, since "matcher" now means a Vale glob
section in this same tree.

0005 layers on 0004; both are unreleased and both ship in this stack,
so consumers run them as one upgrade and never see the intermediate
layout.
…ting

Started the layout refactor and hit something the proposal missed:
`.taskless/rules/` is the new root and is also LEGACY_RULES_DIRECTORY,
the pre-0004 flat location. Same string, different meaning.

The legacy read paths turn out to be removable rather than renameable,
because they are unreachable: ensureTasklessDirectory runs migrations
before anything reads a rule, so 0004 has already moved
.taskless/rules/*.yml to sg/rules/ and 0005 moves it again. Under the
new layout a legacy lookup would resolve .taskless/rules/<id>.yml
inside a tree whose real contents are rules/<engine>/<id>/ — reading
the new root as though it were the old flat directory. A stale read
path that resolves into the live tree is worse than no fallback.

Also adds the migration precondition that follows: 0005 asserts the new
root holds no top-level *.yml before writing engine directories into
it, since a file still there means 0004 did not complete.

Reverting the partial engines.ts refactor so this PR stays spec-only
and green rather than carrying a tree with twelve broken callers.
Groups 1-3 of self-contained-rules, plus the `rule verify` removal.

ENGINE_LAYOUTS now describes a rule *directory* rather than parallel
rules/ and rule-tests/ trees, and every path derives from it. Tests live
in `.tests/` — the dot is load-bearing, since ast-grep's ruleDirs
recurses and parses every .yml beneath as a rule, and the constant
carries that measurement plus the fallback if it ever breaks.

Both engine configs are now assembled per run and gitignored.
Assembly is deterministic — rules sorted by id, each rule's matcher
order verbatim — because Vale's precedence is positional and a config
built in directory-iteration order would give a rule a different
effective scope per machine.

Migration 0005 moves everything and rewrites nothing: capture bytes
determine reconciliation hashes. It asserts `.taskless/rules/` holds no
loose .yml first, since that path is both the new root and the pre-0004
flat location. Vale matchers split by their `tskl) rule` breadcrumb;
one without a breadcrumb is left in place and reported rather than
guessed at or dropped, because it is a user's hand edit.

Deleted with their layouts: the legacy read paths (unreachable — the
migrations run before any read), filesystem/sgconfig.ts (assembly
replaces it), rules/owner.ts and `rule verify` (an id does not name one
rule; the path form has no ambiguity case).

0004's StylesPath docstring now explains both layouts. It said
`StylesPath = rules` is wrong, which was true flat and is exactly
backwards here — a bare contradiction invites a future reader to revert
it.

Typecheck and build clean. Tests still assert the old layout; they are
next.
Caught by migrating a real 0004 project and running check: ast-grep kept
reporting, Vale went silent.

A Vale check is named <style>.<rule>, and the style is whatever
StylesPath points at. Flat, that was `rules`, so assignments read
`rules.<id> = YES`. Per-rule directories make each rule its own style,
so the same rule is `<id>.<id>`. The migration moved the assignment
across verbatim, producing a config Vale parses happily, reports nothing
for, and exits zero on — every migrated rule silently disabled, which is
precisely the failure this layout exists to prevent.

0005 now rewrites the assignment as it splits, and the mapper collapses
`<id>.<id>` to `<id>` so findings carry the id the user filed under. It
only collapses when both halves match: a `<style>.<check>` where they
differ came from a user's own Vale styles, and halving that name would
report under an id identifying nothing.

Verified end to end — a 0004 project migrates and both engines fire.
Fixture project, engine dispatch, and the Vale suites now build rules as
directories. Adds assemble.test.ts covering what task 2.5 asks for: the
StylesPath header, sorted rule order, byte-identical output across runs,
each rule's own matcher order preserved, and provenance tagging.

Also makes 0005 scaffold rules/<engine>/ — it prunes 0004's directories,
so without this a freshly migrated project had no rules tree at all,
every engine reporting absent and nowhere obvious to write a first rule.

Deletes sg-committed-config.test.ts (the committed config it covers is
now assembled) and rule-verify-dispatch.test.ts (rule verify is gone;
its replacement lands with the path-addressed commands).

55 failures left, all layout paths in the remaining suites.
Also fixes buildIsolatingConfig, which still enabled `rules.<id>` under
a StylesPath that no longer produces that name — the same silent-disable
the migration hit, one file over. Verification would have reported every
rule as not firing.

migrate-engine-layout now asserts the end state of 0004+0005 rather than
0004's intermediate layout: both are unreleased and ship together, so a
user upgrades through the pair and never sees the middle.

90 failures down to 30.
Retires the two tests describing behavior that no longer exists — a rule
at the pre-0004 flat path, and merging two layouts with de-duplication.
There is one tree now, so there is no overlap to collapse.

Drops the `rule verify --anonymous` case with the command it covered.

30 failures down to 21.
566 passing, typecheck and lint clean.

Adds engine-layout.test.ts, which is what design D2 promised: it asserts
a scan stays clean with test YAML inside a rule's .tests/, and that the
same file in a plain tests/ fails. The dot-directory behavior is
undocumented and the binary is version-pinned, so this test is what
fires at a deliberate bump — turning the discovery into a migration task
with a changelog rather than a mystery in CI.

Two reconcile/ingest tests needed rethinking rather than repathing. The
signature test discovered the pre-migration tree to sign it, which no
longer works now that discovery reads one layout; it describes the old
rule directly instead, since the property under test is that the
signature survives the move.

Retires the assertions that only made sense with two layouts.
Group 4. `verify <path>` checks a rule has the components its engine
requires; `test <path>` runs its tests and runs verify first.

The split is about preconditions: an agent mid-authoring has a rule and
no fixtures yet, and needs to know the rule is valid before it can write
a meaningful test. So verify deliberately does not require tests.

Running verify first is the ordering fix. When a rule is both malformed
and under-fixtured, the fixture complaint is the less useful error and
is the one that surfaced first before — the author was told their
fixtures were incomplete while the reason the rule could never have run
went unmentioned. A test pins that.

A path names one thing, so there is no ambiguity case to report: the
same id under two engines is just two paths. The engine comes from the
path's <engine> segment, never from the file, which is what dispatch
already does — so a rule cannot be validated by one engine and run by
another. A directory means everything beneath it, which is the CI form.

Verify also catches the two Vale silent-disables by name: a rule with no
config of its own is scoped to nothing, and a config that never enables
<id>.<id> leaves the rule present but off.

577 tests pass; typecheck and lint clean.
Group 6, and the lint fixes for group 4 that I pushed past.

The tests cover behavior thoroughly but build their fixtures inside the
test that reads them, so nothing in the repo showed the layout as a
reader would meet it. example/ is that: two rules, one per engine, in a
project small enough to take in at once.

Its README explains the two things that are not self-evident — why
.tests/ is dot-prefixed (ast-grep reads every .yml under the rules tree
as a rule) and why only Vale has a per-rule config (it rejects unknown
keys, so scope cannot live in the style file).

example-project.test.ts is the other half. It runs check, verify, and
test against a copy and asserts on the results, including the exact
finding shape the README quotes, so a layout change that breaks the demo
fails the build rather than leaving something misleading in the repo. It
also asserts neither assembled config is committed — an example that
checked in generated files would teach that.

example/ joins the root eslint and prettier ignores: its source calls
eval on purpose and its fixtures are prose written to be flagged.

582 tests pass; typecheck and lint clean.
No em dashes, no antithesis, contractions throughout, shorter sentences.

Extended past the README to the rule messages and the .vale.ini comment,
because those are the example's most-read prose and would have taught
the habit. `Avoid eval — it executes...` became `Avoid eval. It
executes...`, and the no-simply message dropped its "not what to do"
tail. The README's quoted check output was regenerated from a real run
so it still matches byte for byte.

582 tests pass.
Every recipe that named a path still described the flat layout. This
repoints them at `.taskless/rules/<engine>/<id>/` and replaces the
id-addressed `rule verify` with the path-addressed `verify` and `test`.

`create-vale-rule` changes most: the shared `.vale.ini` walkthrough
becomes a per-rule config with its `tskl)` breadcrumb, and the `W101`
outside-a-matcher warning is gone with the situation that caused it.
The old text also warned at length that verification could not catch a
missing assignment. It can now, so that becomes the `<id>.<id>` naming
rule and a pointer at the command.

Two findings from running the recipes rather than reading them:

A `consistency` rule compiles its own name into its pattern as a Go RE2
capture group, and RE2 rejects a hyphen there. Measured against Vale
3.17.1, `ize-ise` fails with E201 and takes every Vale rule in the
project down with it, since Vale reads one config per run: 9 rules, 0
findings. Our own recipes teach kebab-case, so we were teaching the
break. `verify` now rejects it, two tests pin it, and design D10
records it.

A hyphen is a word boundary, so `obviously` fires inside
`obviously-named`. The recipe stated the boundary rule but not that
case, and a harness agent used a hyphenated compound as a `pass/`
near-miss that then fired.

Verified by extracting all nine worked rules from the *rendered* recipe
and executing them, and by re-running the authoring harness with fresh
agents that had no repository access.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
The existing migration tests assert where files land and that their
bytes survive. Both can be perfect while the migrated project reports
nothing, which is how the Vale silent-disable shipped: 0004 enables a
check named `rules.<id>`, 0005's StylesPath resolves the same rule as
`<id>.<id>`, and carrying the old assignment forward leaves every Vale
rule present, valid, enabled against a check that does not exist, and
quiet. It was caught by hand, by noticing ast-grep still reported while
Vale had gone silent.

So this seeds a version-4 project and runs the real CLI over it after
migrating: both static engines still report, the assignment is
retargeted, every rule still verifies, the tests that moved still run,
runtime capture bytes are unchanged, and a second run is a no-op. The
ast-grep half is the control, so a silent Vale shows up as a difference
between engines rather than a plausibly-empty run.

The changeset also described a layout this stack has since replaced,
and promised an `engine-selection` topic that #102 merged into `route`.
Rewritten for what actually ships.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
7.1-7.6 verified. 7.7 is blocked on #102: a change archives once on the
tip, and the gate wants openspec/changes/ empty, but
agent-command-and-vale-authoring still has 14 open tasks. Archiving it
early would drop the spec deltas for work nobody has done. #102's
delta also has to reach specs/ before this change's delta has a target.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
The stack's second and final archive, which is why #102 had to go
first: this change's `cli-agent-authoring` delta modifies a requirement
that one introduces, so it needed a target in specs/.

The archive refused twice, both times on a rename expressed as a
MODIFIED. Five of the six MODIFIED entries across cli-rule-format and
cli-vale-rule-engine renamed their requirement, which openspec matches
by header, so each became a REMOVE of the old name plus an ADD of the
new. cli-rule-format's MODIFIED section emptied out entirely.

Two requirements the deltas never retired, both now describing code
this change deletes:

- "Both the legacy and engine-partitioned layouts are readable"
  promises a legacy read path that task 1.5 removes. The legacy
  constant now names the same string as the rules root, so a stale
  read path would resolve into the live tree (design D9).
- "The scaffolded Vale config carries no section" describes a file
  that no longer exists. Verified against a fresh `init`: the scaffold
  contains no `.ini` at all, because scope is declared per rule.

`cli-rules` and `cli-update-engine` still fail --strict; both fail on
main and neither is touched by this stack.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
Six approved review fixes from PR #103.

`assembleValeConfig` returns undefined when no Vale rule declares a
matcher, writing nothing and deleting nothing. Dispatch gated Vale on
`hasValeRules`, which counts *directories*, so a project with a rule
directory that assembles to nothing ran Vale against a stale config from
a previous run — or one that never existed. `DispatchOptions` now carries
`valeConfigPath`, `check` threads `assembled.vale` through, and
`runValeEngine` returns no results when it is undefined, mirroring the
ast-grep skip.

Also:

- Fix the vacuous "commits no assembled config" test: the pathspec was
  relative to `<repo>/example` and resolved to `example/example/.taskless`,
  so stdout was always empty. Assert on the exact assembled paths rather
  than basenames, since a per-rule `.vale.ini` is committed source.
- Restore the deleted "rule directory present, nothing enabled" coverage
  at the dispatch level, spying on `runVale` so a skip is distinguishable
  from a run that found nothing.
- Narrow the `delete-rule` recipe back to sg. `deleteRuleFiles` hardcodes
  `ruleDirectory(cwd, "sg", id)`; the doc claimed a generic `<engine>`.
- Stop `taskless test` spawning `sg test` twice per rule. `verifyRule`
  takes a `runTests` option, and `verifyOneRule`/`testOneRule` share one
  call through `verifySgRule`. Same results, ordering, and errors.
- Resolve a file sitting where a rule directory belongs as
  `RuleNotFoundError` instead of a rule id that fails two layers down.
- Point the second `bootstrap` stat at `rules/vale` and `rules/runtime`;
  it duplicated `rules/sg`.
- Record in migration 0005's docstring that 0004 ships with it, so no
  published install can hold a hand-written 0004-era `.vale.ini`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
Two specs failed. Fixing them surfaced a third problem that was not
failing, which is the worse one.

`cli-update-engine` is deleted. It was a tombstone: the capability was
decommissioned, every requirement removed, and the file kept "for
historical reference". specs/ describes what is true now, and the
history is already in the 2026-03-30-remove-scaffold-dependency
archive, including that change's own cli-update-engine delta. Nothing
outside the archives referenced it.

`cli-rules` gets scenarios and SHALL keywords on 15 requirements, and
loses the five that specify `taskless rule verify` — a command this
stack removed and replaced with the path-addressed `verify` and `test`,
now owned by cli-rule-validation. A requirement stating the subcommand
is gone replaces them. Two requirements also named pre-layout paths
(`.taskless/rule-tests/<id>.yml`, and deleting "rule and test files"
rather than the rule directory); both repathed while being touched.

The third problem: a second `##` inside a requirements section ends it,
so every requirement below stops being read. `cli-rules` hid 10 behind
`## API Contract`, `infrastructure` hid 19, and `skills` hid 6. Those
last two PASSED --strict throughout, because the single requirement
each still exposed was well-formed. 35 requirements were unread while
the validator reported success. The groupings are preserved as bold
lead-in lines instead, and un-nesting infrastructure immediately
exposed a real requirement with no SHALL, now fixed.

`skills` also still told agents to fetch recipes via
`npx @taskless/cli help <topic>`.

23 specs, 0 failures.

Refs #105

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
Two self-contradictions the strict-validation pass left behind.

The create requirements still wrote rules to the flat
`.taskless/rules/<id>.yml` and tests to `.taskless/rule-tests/<id>.yml`,
while delete had already been repathed to the rule directory. `files.ts`
agrees with delete, so create was the stale one.

The new "`taskless rule verify` SHALL NOT exist" requirement was
contradicted three lines up, where the surface list still advertised it,
and again where two scenarios named it as a live command.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
Work that previously lived only inside a merge commit, reapplied as an
ordinary commit so the branch is linear.

A merge can hold content present in neither parent, and this one did.
Replaying the branch reproduces every commit but not the merge's own
resolutions, so these changes would otherwise be silently dropped:

- `RuleTestResult` carries Vale's config `notice` and `verify`/`test`
  print it, on a pass as well as a failure. Without this the schema
  field survives with nothing reading it, and a rule Vale never enabled
  still verifies clean.
- The `--json` engine-failure fixture and the notice fixture both used
  pre-layout paths, so neither provoked what it claimed. Corrected, plus
  a `taskless.json` in the fixture: without it the tree reads as schema
  version 0 and the migrations relocate a current-layout project, after
  which `check` scans nothing and reports success (issue #109).
- `example/README.md` re-synced with real `check` output.

Verified by tree equality against the merge-based head this replaces:
`git diff` reports no difference. 602 tests, lint, and typecheck pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
@thecodedrift
thecodedrift merged commit 5596e39 into main Aug 19, 2026
4 checks passed
thecodedrift added a commit that referenced this pull request Aug 19, 2026
A Vale rule is currently three locations, one of which every rule in the
project shares. That shared .vale.ini is where all five harness runs
found silent failures — an assignment above the first matcher, a glob
that missed the fixture extension, three names that had to agree with
nothing reporting when they didn't. A single write-contended config is
the wrong shape at any scale.

Each rule becomes a directory holding its style and its own .vale.ini;
check assembles the run config from them and gitignores it. Measured
against Vale 3.17.1: rules/<id>/<id>.yml resolves as check <id>.<id>
under StylesPath = rules and resolves to nothing under StylesPath = .,
Vale rejects unknown keys in a style so scope cannot ride along inside
it, and a .yml sidecar in a style directory is loaded as a rule and
fails E201 while a .vale.ini in the same place is ignored.

Also replaces rule verify <id> with path-addressed verify and test. An
id does not name one thing — the same id can exist under two engines,
which is why the id form needed an ambiguity error at all.

Adds an example/ project so a reader can see an install rather than
infer it from tests that build their own fixtures.

Stacked on #102 and merging down: a layout change without its migration
ships a project whose rules silently stop running.
thecodedrift added a commit that referenced this pull request Aug 19, 2026
The existing migration tests assert where files land and that their
bytes survive. Both can be perfect while the migrated project reports
nothing, which is how the Vale silent-disable shipped: 0004 enables a
check named `rules.<id>`, 0005's StylesPath resolves the same rule as
`<id>.<id>`, and carrying the old assignment forward leaves every Vale
rule present, valid, enabled against a check that does not exist, and
quiet. It was caught by hand, by noticing ast-grep still reported while
Vale had gone silent.

So this seeds a version-4 project and runs the real CLI over it after
migrating: both static engines still report, the assignment is
retargeted, every rule still verifies, the tests that moved still run,
runtime capture bytes are unchanged, and a second run is a no-op. The
ast-grep half is the control, so a silent Vale shows up as a difference
between engines rather than a plausibly-empty run.

The changeset also described a layout this stack has since replaced,
and promised an `engine-selection` topic that #102 merged into `route`.
Rewritten for what actually ships.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
thecodedrift added a commit that referenced this pull request Aug 19, 2026
7.1-7.6 verified. 7.7 is blocked on #102: a change archives once on the
tip, and the gate wants openspec/changes/ empty, but
agent-command-and-vale-authoring still has 14 open tasks. Archiving it
early would drop the spec deltas for work nobody has done. #102's
delta also has to reach specs/ before this change's delta has a target.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
thecodedrift added a commit that referenced this pull request Aug 19, 2026
The stack's second and final archive, which is why #102 had to go
first: this change's `cli-agent-authoring` delta modifies a requirement
that one introduces, so it needed a target in specs/.

The archive refused twice, both times on a rename expressed as a
MODIFIED. Five of the six MODIFIED entries across cli-rule-format and
cli-vale-rule-engine renamed their requirement, which openspec matches
by header, so each became a REMOVE of the old name plus an ADD of the
new. cli-rule-format's MODIFIED section emptied out entirely.

Two requirements the deltas never retired, both now describing code
this change deletes:

- "Both the legacy and engine-partitioned layouts are readable"
  promises a legacy read path that task 1.5 removes. The legacy
  constant now names the same string as the rules root, so a stale
  read path would resolve into the live tree (design D9).
- "The scaffolded Vale config carries no section" describes a file
  that no longer exists. Verified against a fresh `init`: the scaffold
  contains no `.ini` at all, because scope is declared per rule.

`cli-rules` and `cli-update-engine` still fail --strict; both fail on
main and neither is touched by this stack.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
@thecodedrift
thecodedrift deleted the openspec/agent-command-and-vale-authoring branch August 19, 2026 03:16
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.

2 participants