Skip to content

feat: enforce the house-style rules on main, including the agent recipes - #187

Merged
thecodedrift merged 15 commits into
mainfrom
feat/dogfood-house-style
Aug 26, 2026
Merged

feat: enforce the house-style rules on main, including the agent recipes#187
thecodedrift merged 15 commits into
mainfrom
feat/dogfood-house-style

Conversation

@thecodedrift

@thecodedrift thecodedrift commented Aug 26, 2026

Copy link
Copy Markdown
Member

Lands the Taskless house-style rules on main and makes them enforced rather than advisory.

The rules themselves were written in #168 and #176 and merged to nightly/test, which nothing builds from (release-cli-nightly.yml only fires when head_branch == 'main'). They have therefore never run against the branch they were meant to police. This carries that work onto main, brings it up to date, and adds the piece that was missing: a CI step that fails the build.

What is here

Commits
9 the rules, the onboarding pass, and the README/docs cleanup, rebased from nightly/test
1 the three em dashes main added to CLAUDE.md after the style pass was written
1 the Check house style step in Validate
1 ci-uses-workspace-cli, replacing a comment that asked for the same thing

The check blocks, and that is the point

Fixes #104 asks for enforcement that is automatic. A warn-only step is not enforcement, so this fails the build on any error-severity finding.

It is a step inside the existing Validate job rather than a standalone workflow, for two reasons. Branch protection requires Validate and nothing else, so a separate workflow would report and block nothing. And Validate already runs pnpm install --frozen-lockfile and pnpm build before its test block, so the step costs no extra setup and dist/ cannot be stale or missing.

It runs pnpm cli check, the workspace build, not npx @taskless/cli. This repository is the CLI: checking its rules with a published build would let a commit that breaks rule evaluation pass its own CI and surface a release later, in someone else's project. That is a deliberate divergence from the ci.txt recipe, which is written for consumers installing the published CLI.

That divergence was originally a comment asking readers not to change it back. It is now ci-uses-workspace-cli, an ast-grep rule over .github/workflows/*.yml. Three things it had to get right, each measured:

  • It must not flag its own rationale. npx @taskless/cli appears in prose in validate.yml and release-cli-nightly.yml, explaining this decision. The matcher is anchored to a run: key's value, not to file text; the clean tree reports nothing.
  • A run: value is a flow_node inline and a block_scalar under |. Both are ordinary here, so both match, with fixtures for each.
  • @taskless/cli-nightly is excluded. It is a published artifact by design. The pattern ends at ($|[@\s]) rather than a negative lookahead, which ast-grep's Rust regex engine does not support.

The first version of that rule put regex on a matcher with no sibling kind and was rejected by this repo's own regex-requires-kind check.

Verification

pnpm cli check No issues found., exit 0
pnpm typecheck / pnpm lint pass
pnpm test 55 files, 895 tests

The gate was proven non-vacuous rather than assumed. Each probe was introduced, the exit observed, then reverted:

Probe Exit
em dash in CLAUDE.md 1, no-em-dashes
em dash in README.md 1, no-em-dashes
branches: [main] under pull_request: 1, pr-workflow-no-branches-filter
step reverted to npx @taskless/cli check 1, ci-uses-workspace-cli
packages/cli/dist moved away 1, fails loudly rather than passing empty
clean tree 0

The em-dash probes prove Vale ran; the other two prove ast-grep ran.

Two things not visible in the diff

The CLAUDE.md rebase conflict. Eight of the nine commits replayed clean. The widening commit conflicted in 4 hunks, because main had rewritten those passages. Resolved by taking the incoming (de-em-dashed) side per hunk, not by taking the whole file, which would have discarded main's cleanly-merged additions. Before resolving, the two sides were diffed at word level: they were identical apart from punctuation, capitalisation forced by a dash becoming a period, and one simplyjust (the no-hedging rule). main-only content was confirmed present afterwards.

Roughly 2300 error-level findings remain on the un-broadened surfaces (TypeScript comments, recipe text), recorded in e0889df. They are out of scope here because the rules are scoped to **/README.md, CLAUDE.md, and .conventions/*.md. But the gate is live as of this PR, so every future widening slice must carry its own remediation in the same PR or it turns main red.

Known gap

If Vale ever fails to install, its five prose rules skip non-blockingly (vale/run.ts, status: "unavailable", blocking: false) and the check passes green with only the ast-grep rules running. It is safe today: the platform package is an os/cpu-gated optional dependency pinned in pnpm-lock.yaml, so --frozen-lockfile on ubuntu-latest installs it. Closing it properly needs a --require-engines flag on check, which belongs in the CLI rather than in CI. Not attempted here.

Scope widened to the recipes

Rebased onto main after #190 landed, then widened deliberately: packages/cli/src/agent/*.txt is bundled into the published CLI and served on every taskless agent <topic> call, which made it the largest prose surface the rules did not reach. All three house-style rules now cover it, and the 270 dashes already there are rewritten.

Rewritten by what each dash was doing. A list or table gloss becomes a colon (it was a definition), an independent clause becomes a period, an appositive becomes a comma, and the six paired dashes become parentheses.

A first pass replaced every dash with a comma. It produced comma splices and flattened the routing table's glosses into its lists, so it was reverted rather than patched — a transform needing 20 hand-corrections is the wrong transform. The second pass is clause-aware, and its 14 remaining splices were fixed by hand.

Meaning is unchanged, and the tests are the evidence: the recipe-content suite asserts exact phrases from route.txt, create-sg-rule.txt, create-vale-rule.txt and others, and all 929 pass.

Three genuine hedges were fixed. create-vale-rule.txt and verify-rule.txt are excluded from no-hedging, because both teach through a worked example named no-simply and the token appears throughout as an identifier rather than as hedging; excluding beats ~34 known-benign warnings on every run. Four warnings remain elsewhere, all sample rule requests.

Worth knowing for the next widening: these are .txt, so Vale applies no markdown parser. Fenced blocks and code spans are checked as prose, unlike the .md surfaces.

#169 work remains beyond thisopenspec/ (1584) and source comments (449) are still out of scope, and openspec/changes/archive/** should probably stay that way.

Changeset

Added. The original PR shipped none, correctly, because it changed only CI config and repo-local rules. The recipes are different: they are bundled into the published package, so rewriting them is user-visible.

Fixes #104
Refs #169

@thecodedrift

Copy link
Copy Markdown
Member Author

@claude /review

@claude

claude Bot commented Aug 26, 2026

Copy link
Copy Markdown

Claude finished @thecodedrift's task in 4m 33s —— View job


Review complete

Review mode: incremental — read 0 prior review thread(s) before reviewing. .prior-review.json contained no reviewThreads, reviews, or top-level comments other than the @claude /review trigger itself, so this is the first review of this PR — nothing to classify as already-addressed.

  • Read .prior-review.json
  • Gather PR diff (origin/main...HEAD)
  • Run correctness/cleanup/altitude/conventions finder passes
  • Verify candidate findings
  • Post final review

Summary

The PR does what it says: it moves the house-style rules from advisory to a blocking Check house style step in Validate, and the reasoning in the workflow comment and rule note: fields is unusually well-documented and cross-checked against the probes described in the PR body. The em-dash removal pass, README/CLAUDE.md restructuring, and taskless.json/.gitignore updates all look mechanical and consistent with the stated intent — I didn't find anything wrong in those.

Two [New] findings on the new ast-grep rules themselves, posted inline:

  1. .taskless/rules/sg/ci-uses-workspace-cli/ci-uses-workspace-cli.yml (line 46) — the regex only matches npx @taskless/cli.... It does not match pnpm dlx @taskless/cli@latest, which is the exact invocation CLAUDE.md and this PR's own description call out as the thing pnpm cli replaces. Changing the CI step to that form (or pnpm exec/yarn dlx/a global install) would silently defeat the enforcement this PR is built around, and the rule's .tests/ fixtures don't cover that case either.
  2. .taskless/rules/sg/pr-workflow-ready-for-review/pr-workflow-ready-for-review.yml (lines 23-27) — the rule only fires when a pull_request: block already has an explicit types: key missing ready_for_review. A pull_request: trigger with no types: key at all falls back to GitHub's default set (opened/synchronize/reopened), which has exactly the same gap, but isn't flagged since there's no types: node to inspect. Not currently triggered on main (all three pull_request: triggers here declare types: explicitly), so this is latent rather than active.

No correctness issues found in validate.yml, stack-breadcrumb.yml's new ready_for_review type, the Vale rule .vale.ini scopes, or the other new sg rules (no-pii-in-telemetry, no-index-imports, no-regex-over-build-output). Didn't run pnpm cli check/typecheck/lint/test myself — CI reports those.

Comment thread .taskless/rules/sg/ci-uses-workspace-cli/ci-uses-workspace-cli.yml Outdated
Comment thread .taskless/rules/sg/pr-workflow-ready-for-review/pr-workflow-ready-for-review.yml Outdated
thecodedrift added a commit that referenced this pull request Aug 26, 2026
From review of #187. Both findings verified before acting, and both real.

`ci-uses-workspace-cli` matched `npx @taskless/cli` and nothing else, so the
invocation CLAUDE.md names by name, `pnpm dlx @taskless/cli@latest`, went
straight through the rule written to forbid it. `yarn dlx` and a global install
did too. A rule that permits the exact string its own repository documents as
the thing to avoid is worse than no rule, because it reads as coverage.

Widening it to the bare package name overcorrected, and the dogfooding caught
that within one run: it fired on `pnpm --filter @taskless/cli build:nightly` in
`release-cli-nightly.yml`, which is the WORKSPACE package addressed as a
filter. `pnpm exec @taskless/cli` is the same story, running the local binary.

What separates a published build is being FETCHED, so the rule matches the
fetch verbs: `npx`, `dlx`, and `-g`. Fixtures now carry `pnpm dlx`, `yarn dlx`
and the two-step global install on the invalid side, and `pnpm exec` and
`--filter` on the valid side, so neither the hole nor the overcorrection can
come back. Measured: `pnpm dlx` and `npx` both exit 1, the clean tree exits 0.

`pr-workflow-ready-for-review` anchored on a `types:` node, so it only fired on
an explicit list that omitted `ready_for_review`. A `pull_request:` with no
`types:` at all falls through to GitHub's default set, which omits
`ready_for_review` just as surely — and the rule said nothing, because there
was no node to anchor to. That is the #103 gap the rule exists to prevent,
reachable by writing less rather than more.

It now also matches a `pull_request:` with no `types:` descendant. Nothing on
`main` hits it today (all three triggers declare `types:`), so this was latent;
a probe workflow with a bare `pull_request:` is now reported where it
previously was not. Severity stays `warning`, unchanged.
@thecodedrift

Copy link
Copy Markdown
Member Author

Re: @claude[bot] — "Review complete — Review mode: incremental — read 0 prior review thread(s)…"
#187 (comment)

Both findings verified before acting, both real, both fixed in 28436d0. Replied inline on each thread and resolved them.

The first is the one that mattered: a rule written to forbid reaching for a published CLI permitted pnpm dlx @taskless/cli@latest, the exact string this repository documents as the thing pnpm cli replaces. A rule that permits its own counterexample is worse than no rule, because it reads as coverage.

Worth recording what happened next, since it argues for the dogfooding this PR is about. Widening the pattern to the bare package name overcorrected, and pnpm cli check caught it in a single run: it fired on pnpm --filter @taskless/cli build:nightly in release-cli-nightly.yml, which is the workspace package addressed as a filter rather than a published install. The final rule matches the fetch verbs (npx, dlx, -g), because being fetched is what distinguishes a published build from a local one. Fixtures now pin both directions.

The second was latent exactly as described, and is now reported.

Verification after the fixes: pnpm cli check exit 0 on a clean tree, pnpm typecheck, pnpm lint, and 895 tests pass. Probed both directions rather than assuming — pnpm dlx and npx each exit 1, a bare pull_request: workflow is now reported where it previously was not.

— AI Coding Agent

Seven rules mined from CLAUDE.md, the .conventions styleguides, and the
posthog conventions — the places this repo already states its rules in
prose. Each one is verified and tested locally; `check` reports two real
findings, both READMEs telling external consumers to run `pnpm dlx`.

sg:
  no-index-imports              relative imports ending in /index
  no-pii-in-telemetry           PII in capture/identify/groupIdentify
  prefer-do-rpc                 `as DurableObjectStub<T>` casts
  no-regex-over-build-output    a test function that reads dist/ and regexes it
  no-pull-request-branches-filter  a branches: filter under pull_request:

vale:
  docs-npx-cli                  pnpm invocations in READMEs
  no-hedging                    simply / obviously / of course

Also backfills tests for the pre-existing no-eval rule, which was
failing `verify` for a missing test file, and puts every rule's
`language:` on ast-grep's own spelling.

The install churn — the rules/ partition, taskless.json, the embedded
SKILL.md — was already in the tree and is carried along rather than
authored here.
A sweep of merged-PR review threads and the OSS Linear backlog turned up
OSS-49, which names two planned GitHub Actions rules and records that a
prior onboarding pass stalled on whether ast-grep handles YAML. It does,
so both are buildable locally rather than runtime-tier.

Adds pr-workflow-ready-for-review, and renames the branches-filter rule
to the pr-workflow-* name OSS-49 already uses for the pair.

One finding on the real workflows: stack-breadcrumb.yml names
types: [opened, reopened, edited, closed] with no ready_for_review. That
is a question rather than a defect — it reacts to PR metadata, not to
readiness — and is left for review to settle.
OSS-28 specifies the house writing style as Vale rules. This adds the two
that can be expressed cleanly, scoped to READMEs rather than the whole
repository: broadening is a separate decision with roughly 2200 findings
attached.

no-em-dashes needs `nonword: true`, which the snippet in OSS-28 does not
carry. Measured: without it the rule parses, passes verify, and does not
fire, because Vale wraps every token in word boundaries and an em dash is
non-word on both sides.

no-blocklist-phrases drops the `landed on` token OSS-28 proposed. Measured
firing on "the plane landed on time", which is the literal sense the issue
says to protect; `we landed` already covers "we landed on the second
option", so the broader token earned nothing.

Not included: the "not X, Y" antithesis rule. OSS-28 is undecided on how to
express it and calls it a heuristic, so it wants a human judgement this pass
cannot make.

24 findings in READMEs, all em dashes, left unfixed pending a call on
whether to rewrite the prose or relax the level.
Brings the nine READMEs in line with no-em-dashes, which this branch
scopes to `**/README.md`. Twenty-four instances, rewritten as a period,
comma, colon, or parentheses per the rule's own message rather than
mechanically swapped for one substitute.
docs-npx-cli says docs use `npx @taskless/cli`, the normalized invocation
for external consumers. Two README blocks offered `pnpm dlx` alongside it,
which is the same claim the rule exists to prevent, so the alternative is
removed rather than annotated.

Annotating was measured and is not available here: a rule scoped to `raw`
reads the unparsed document, so Vale's `<!-- vale Rule = NO -->` directive
is inert for it. That scope is what lets the rule see fenced blocks at all,
and every command in a README lives in one.
The most repeated finding in the merged-PR review threads: a comment that
cites a version or a claim the code moved out from under it. On #161 alone,
five threads — "we pin 3.17.1" left behind after VALE_VERSION went to
3.18.0, a comment naming .rmd as plaintext after the diff moved it to
markup, a block still framing 3.18.0 as "the known incoming bump" after it
shipped. Same shape on #155 and #157.

The rule flags the construction rather than the stale number, which is the
half a static check can see. Vale reads .ts in its comments-only tier, so it
lints the comment text and never the code.

Zero findings on packages/cli/src today, because #161 fixed them all. Reach
verified separately by planting a forecast comment in a real source file:
fixtures run under an isolating config, so a passing test does not prove the
matcher glob reaches anything.

Also ignores .taskless/ in eslint and prettier. A rule about source comments
needs .ts fixtures, Vale decides its comments-only tier by extension, and
those files belong to no tsconfig — so lint-staged failed the commit on
"not found by the project service". Rule fixtures are inputs written to be
flagged; verify and test are what keep them honest.
pr-workflow-ready-for-review flagged this as the only finding on the repo,
and it is a real gap rather than the intended exception. `ready_for_review`
is not in the default event set, so a draft marked ready fired nothing here:
the breadcrumb went on describing the PR as a draft until some later event
happened to run.

The existing comment says tree SHAPE only, which is still true —
`synchronize` stays out because a head push never changes membership. A
draft becoming ready does change the stack, so it is named explicitly and
the comment now says why.

`taskless check` is clean at 12 rules.
prefer-do-rpc targets `$EXPR as DurableObjectStub<$T>`, a Cloudflare
Durable Objects pattern. The Taskless services need it; this repository
has no Cloudflare code for it to reach, so the rule and its synthetic
fixtures are removed rather than carried pre-emptively. Nothing else in
the tree referenced it.

docs-npx-cli stays as written, and the root README now records that
`package.json` carries a `cli` script pointing at
`./packages/cli/dist/index.js` for running the local build.

The note names the script instead of reproducing its shell invocation,
because docs-npx-cli has no per-case escape hatch: its `raw` scope is
what reaches a fenced code block, and Vale evaluates a raw scope against
the unparsed markup, so in-file directives are ignored. Measured on Vale
3.18.0, both the rule-specific comment and a blanket `vale off` fail to
suppress it, and dropping `raw` makes them work at the cost of every
fenced-block finding. The rule keeps its full reach; the finding is
recorded in its .vale.ini.
First slice of the broadening in #169. The three prose rules were scoped
to READMEs only, because landing them repo-wide meant roughly 2300
findings at level: error. This adds the two smallest surfaces and fixes
what they catch, so the slice is green on its own.

41 findings: 39 em dashes rewritten as a period, comma, colon, or
parentheses per the rule's own message rather than swapped mechanically
for one substitute, and two uses of "simply" where the sentence was
describing a real distinction ("merely lives further down", "just out of
date") rather than hedging.

Fenced code blocks are untouched. These rules carry Vale's default
scope, which does not read them, so the em dashes in the shell comments
at CLAUDE.md:155 and STYLEGUIDE-CODE.md:222 are out of scope and stay.

Remaining surfaces, in the order #169 proposes: TypeScript comments
(Vale's comments-only tier), then the agent-facing recipe text, which is
the largest. openspec/changes/archive/ gets a permanent exclusion rather
than a slice.

Refs #169
The previous commit widened no-em-dashes to CLAUDE.md and fixed the 41
findings that existed then. These three arrived on main afterwards, in
the `pnpm cli` / stale-`dist/` section, and are the entire remaining
delta between the rule's scope and the tree.

Rewritten per the rule's own message rather than swapped mechanically
for one substitute: a comma where the clause is an aside ("serves stale
behavior, including stale `agent <topic>` recipes"), a period where it
was two sentences joined ("reach the right parser. A name ast-grep does
not recognize"), and a colon where the second half explains the first
("prefer `pnpm cli`: it is the only one that can reflect uncommitted
work"). No other line of the file is touched, and no measurement or
instruction changes meaning.

`taskless check` is clean at 11 rules.

Refs #169
The rules in `.taskless/` have been in the tree for nine commits with
nothing running them. `validate.yml` had no Taskless step at all, so
every finding they can produce depended on a person remembering to run
`pnpm cli check` locally. #104 asks for enforcement, and a rule nothing
runs is documentation.

A step inside the existing `Validate` job rather than the standalone
`.github/workflows/taskless.yml` the `taskless agent ci` recipe writes.
The recipe is right for a consumer being onboarded, where touching a
pipeline you do not own is the larger risk; here the two things the
check needs only exist inside this job. Branch protection requires
`Validate` and nothing else, so a separate workflow would run, report,
and block nothing. And the job has already built `dist/` from this exact
commit by the time the step runs, so the check costs one process rather
than a second checkout, install, and build.

It runs `pnpm cli check`, the workspace build, not `npx @taskless/cli`.
This repository IS the CLI: checking our own documents with a published
release would exempt the engine at HEAD from its own rules and defer a
rule-evaluation regression to whenever the next version ships. The step
comment says this is a deliberate divergence from the recipe so a future
reader does not correct it back.

It blocks. `check` exits non-zero on an error-severity finding and on an
engine that failed or timed out, so an em dash added to a README,
CLAUDE.md, or `.conventions/*.md` turns the build red. That is the
intended cost. Warning-severity rules still only report, which is what
`severity: warning` means.

Verified non-vacuous rather than assumed. Appending an em dash to
CLAUDE.md and to README.md each exits 1; a missing `dist/` exits 1
rather than passing empty; a `branches:` filter added under
`pull_request:` exits 1, which is the ast-grep engine reporting on this
very file. Clean tree exits 0. Both engines are therefore live, not
silently skipped.

Full scan, not the recipe's diff scan, for the reason the `Validate
specs` step gives: rot accumulates in the files a PR does not touch, and
the scoped corpus is small enough that a diff scan buys nothing.

No secrets. Static rules run unauthenticated; `.taskless/rules/runtime/`
is empty, so there is nothing a `TASKLESS_TOKEN` would unlock yet.

Fixes #104
The check step added in the previous commit carried a comment asking a
reader not to "fix" `pnpm cli check` back to `npx @taskless/cli check`. A
request is not a boundary, and this repository lints its own prose precisely
because comments do not hold. So the invariant becomes a rule.

`ci-uses-workspace-cli` fails any `.github/workflows/*.yml` step whose `run:`
invokes a published `@taskless/cli`. The repository IS the CLI: checking its
rules with a published build would let a commit that breaks rule evaluation
pass its own CI and surface a release later, in someone else's project.

Three things the matcher has to get right, each measured rather than assumed:

`npx @taskless/cli` already appears in PROSE in `validate.yml` and
`release-cli-nightly.yml`, explaining this same decision. A rule that flagged
its own rationale would be worse than no rule, so the matcher is anchored to
a `run:` key's value rather than to file text. Verified: the clean tree
reports nothing.

A `run:` value is a `flow_node` inline and a `block_scalar` under `|`, and
both spellings are ordinary here, so both are matched. The first attempt
carried one `regex` with no sibling `kind` and was rejected by this repo's
own `regex-requires-kind` check, which is the dogfooding working.

`@taskless/cli-nightly` is deliberately excluded. The nightly is a published
artifact by design and the release workflows reference it on purpose. The
pattern ends at `($|[@\s])` rather than a negative lookahead, which the Rust
regex engine ast-grep uses does not support.

Proven both ways: with the step reverted to `npx @taskless/cli check`,
`check` exits 1 naming this rule; restored, it exits 0.
From review of #187. Both findings verified before acting, and both real.

`ci-uses-workspace-cli` matched `npx @taskless/cli` and nothing else, so the
invocation CLAUDE.md names by name, `pnpm dlx @taskless/cli@latest`, went
straight through the rule written to forbid it. `yarn dlx` and a global install
did too. A rule that permits the exact string its own repository documents as
the thing to avoid is worse than no rule, because it reads as coverage.

Widening it to the bare package name overcorrected, and the dogfooding caught
that within one run: it fired on `pnpm --filter @taskless/cli build:nightly` in
`release-cli-nightly.yml`, which is the WORKSPACE package addressed as a
filter. `pnpm exec @taskless/cli` is the same story, running the local binary.

What separates a published build is being FETCHED, so the rule matches the
fetch verbs: `npx`, `dlx`, and `-g`. Fixtures now carry `pnpm dlx`, `yarn dlx`
and the two-step global install on the invalid side, and `pnpm exec` and
`--filter` on the valid side, so neither the hole nor the overcorrection can
come back. Measured: `pnpm dlx` and `npx` both exit 1, the clean tree exits 0.

`pr-workflow-ready-for-review` anchored on a `types:` node, so it only fired on
an explicit list that omitted `ready_for_review`. A `pull_request:` with no
`types:` at all falls through to GitHub's default set, which omits
`ready_for_review` just as surely — and the rule said nothing, because there
was no node to anchor to. That is the #103 gap the rule exists to prevent,
reachable by writing less rather than more.

It now also matches a `pull_request:` with no `types:` descendant. Nothing on
`main` hits it today (all three triggers declare `types:`), so this was latent;
a probe workflow with a bare `pull_request:` is now reported where it
previously was not. Severity stays `warning`, unchanged.
The recipes are bundled into the published CLI and served on every authoring
run, which made them the largest prose surface the house-style rules did not
reach. `no-em-dashes`, `no-blocklist-phrases` and `no-hedging` now cover
`packages/cli/src/agent/*.txt`, and the 270 dashes already there are rewritten.

Rewritten by what each dash was doing, not by one substitution. A list or table
gloss (`- term — description`) becomes a colon, because it was a definition. An
independent clause after the dash becomes a period. An appositive becomes a
comma. The six paired dashes become parentheses, which is what an aside inside
a sentence wants.

A first pass replaced every dash with a comma and produced comma splices
("It is deterministic and offline, use it as ground truth") and flattened the
routing table's glosses into its lists. That pass was reverted rather than
patched: a transform that needs 20 hand-corrections is the wrong transform. The
second is clause-aware, and the 14 splices it still produced were fixed by
hand.

Meaning is unchanged, and the recipe-content tests are the evidence: they assert
exact phrases from `route.txt`, `create-sg-rule.txt`, `create-vale-rule.txt` and
others, and all 929 tests pass.

Three genuine hedges were fixed rather than reworded around: "the rule is simply
inert", "would obviously do better", "or simply stops responding".

`create-vale-rule.txt` and `verify-rule.txt` are excluded from `no-hedging`.
Both teach rule authoring through a worked example named `no-simply`, so the
token appears dozens of times as an identifier and as sample output. Vale cannot
tell a word being used from a word being quoted, and rewording the example to
dodge its own subject would make the recipe worse. Excluding beats leaving ~34
known-benign warnings on every run, which is how a warning becomes something
people stop reading. The four that remain elsewhere are sample rule requests.

These files are `.txt`, so Vale applies no markdown parser: fenced blocks and
code spans are checked as prose, unlike in the `.md` surfaces already covered.

Refs #169
@thecodedrift
thecodedrift force-pushed the feat/dogfood-house-style branch from 28436d0 to c719bf0 Compare August 26, 2026 18:30
@thecodedrift thecodedrift changed the title feat: enforce the house-style rules on main instead of documenting them feat: enforce the house-style rules on main, including the agent recipes Aug 26, 2026
@thecodedrift

Copy link
Copy Markdown
Member Author

@claude /review

@claude

claude Bot commented Aug 26, 2026

Copy link
Copy Markdown

Claude finished @thecodedrift's task in 9m 11s —— View job


Review complete

Review mode: incremental — read 2 prior review thread(s) before reviewing.

Both prior threads are resolved with an *— AI Coding Agent* reply confirming the fix, so I treated them as ADDRESSED and did not re-raise the original reports:

  • ci-uses-workspace-cli matching only npx (fixed in 804d5dc)
  • pr-workflow-ready-for-review not catching a bare pull_request: with no types: (fixed in 804d5dc)

That leaves two commits as new content since the last review: 804d5dc (the two fixes above) and c719bf0 (widening the house-style rules to packages/cli/src/agent/*.txt, rewriting ~270 dashes). Everything below is [New].

Findings (posted inline)

  1. .taskless/rules/vale/no-hedging/.vale.ini:38-46 — the exclusion for create-vale-rule.txt/verify-rule.txt is written before the broad packages/cli/src/agent/*.txt inclusion. This repo's own create-vale-rule.txt recipe (rewritten in this same commit) documents Vale precedence as positional, last-matcher-wins, and the sibling no-blocklist-phrases/docs-npx-cli .vale.ini files in this same commit put the broad default first and the exclusion after. Here it's reversed, so if that precedence rule holds, the trailing *.txt = YES re-enables no-hedging for exactly the two files meant to be excluded. Worth confirming with a real pnpm cli check run that those two files actually produce zero no-hedging findings.
  2. packages/cli/src/agent/create-vale-rule.txt:65 — the dash-removal pass merged prose and an opening code-fence marker onto one line (set back at you , \```), breaking the fenced block that quotes Vale's literal error message.
  3. packages/cli/src/agent/ci.txt:208, create-remote-rule.txt:123, improve-rule.txt:80 — en-dash numeric ranges (10–15, 30–60) were mechanically turned into commas (10, 15, 30, 60), changing "a range of N" into a malformed list. This is a real meaning change, contrary to the commit's "meaning is unchanged" claim.
  4. .taskless/rules/sg/ci-uses-workspace-cli/ci-uses-workspace-cli.yml:63 — the fix for the earlier npx-only gap widened to fetch verbs (npx|dlx|-g), but still misses the long-form --global flag (npm install --global @taskless/cli) and yarn global add @taskless/cli, both ordinary ways to reach a published build.
  5. packages/cli/src/agent/check.txt:108 (and create-legacy-rule.txt:49, create-runtime-rule.txt:69, create-vale-rule.txt:250,705,742, info.txt:60) — stray "word , word" (space before comma) left behind by the same automated transform; minor on its own but the same class of bug as fix: ast-grep binary not found when CLI installed via pnpm dlx #2 and fix: Resolve ast-grep binary directly for pnpm dlx compatibility #3.

Not re-raised / looks fine

  • Both previously-flagged rule gaps are genuinely fixed for the cases they targeted (pnpm dlx, yarn dlx, global two-step install; bare pull_request:), per the fixtures added in 804d5dc.
  • The pr-workflow-ready-for-review second alternative (matching a pull_request: with no types: descendant) is correctly disjoint from the first and doesn't double-fire.
  • The bulk of the c719bf0 prose rewrite (auth.txt, delete-rule.txt, detect.txt, rule.txt, rule-meta.txt, update.txt, verify-rule.txt, onboard.txt, create-sg-rule.txt, create-legacy-rule.txt, create-runtime-rule.txt, improve-rule*.txt, route.txt tables) reads as intended: list/table glosses to colons, independent clauses to periods, appositives to commas — consistent with the stated approach, and I didn't find meaning changes beyond the range/fence issues above.
  • The new .vale.ini glob additions for no-em-dashes/no-blocklist-phrases (plain append, no exclusion) and the changeset are straightforward and match the stated scope.

Comment thread .taskless/rules/vale/no-hedging/.vale.ini Outdated
Comment thread packages/cli/src/agent/create-vale-rule.txt Outdated
Comment thread packages/cli/src/agent/ci.txt Outdated
Comment thread packages/cli/src/agent/create-remote-rule.txt Outdated
Comment thread .taskless/rules/sg/ci-uses-workspace-cli/ci-uses-workspace-cli.yml Outdated
Comment thread packages/cli/src/agent/check.txt Outdated
From review of #187. Six findings, all verified, all real. Four are damage the
automated dash pass did and I did not catch.

**It swallowed newlines.** A dash at end of line became `, ` and pulled the
next line up with it, losing 9 lines across 5 files. In `create-vale-rule.txt`
that joined prose to an opening code fence, so the fenced block was malformed
and the recipe rendered wrong. Each is restored with its line break and a
colon, which is what the dash was doing.

**It destroyed three numeric ranges.** `10–15 line excerpt` became `10, 15
line excerpt`, and `30–60 seconds` became `30, 60 seconds` twice. An en dash in
a range is not a clause boundary. They are hyphens now, which the rule does not
flag.

Both classes had the same signature, a space before a comma, and my post-pass
check would have found them: I ran the mechanical scan after the first
transform and not after the second, then leaned on `check` and the test suite,
neither of which can see this. Prose damage is invisible to a rule that only
counts dashes and to tests that assert phrases elsewhere in the file. The scan
is now part of verifying a prose pass, and it reports zero.

Line counts match the originals exactly across all 20 files, which is the
check that would have caught the joins immediately.

**The `no-hedging` exclusion sat above the section it narrows.** Vale
precedence here is positional and a later matcher wins, which
`create-vale-rule` documents at step 2, so the exclusion was relying on
something this repository's own recipe says is false. Measured on 3.18.0, both
orders exclude the two files today, so this is robustness rather than a bug
fix, and the measurement is recorded next to it.

**The fetch-verb list missed the long flag.** `-g\s+` cannot fire inside
`--global`, because what follows `-g` there is `lobal` rather than whitespace,
so `npm install --global @taskless/cli` slipped through. `yarn global add`
carries no matching token at all. Both are ordinary ways to install a published
package, and both are now invalid fixtures. Proven: all three forms exit 1, and
`pnpm --filter @taskless/cli` still exits 0.

Refs #169
@thecodedrift

Copy link
Copy Markdown
Member Author

Re: @claude[bot] — "Review complete — Review mode: incremental…"
#187 (comment)

All six verified, all six real, all fixed in 44c224c. Replied inline and resolved each thread.

Four of the six were damage my automated dash pass did, and the review caught what I did not. That is worth stating plainly, because I claimed in the PR body that meaning was unchanged and offered the passing test suite as evidence. The tests were not evidence for this. They assert specific phrases in specific files; they cannot see a swallowed newline three paragraphs away, and check only counts dashes, so a rule that is satisfied says nothing about whether the prose still reads.

Two distinct bugs, both with the same signature:

Newline swallowing. A dash at end of line became , and pulled the next line up with it — 9 lines lost across 5 files. In create-vale-rule.txt it joined prose to an opening code fence, leaving the block malformed and the recipe rendering wrong. Every instance carried a space before the comma, which is exactly what the low-severity finding spotted; it was the signature, not a cosmetic artifact.

Numeric ranges. 10–15 and 30–60 (twice) became 10, 15 and 30, 60. An en dash in a range is not a clause boundary.

The check that would have caught both immediately is a line-count comparison against the originals. Line counts now match exactly across all 20 files, and the mechanical scan (space-before-comma, doubled punctuation, digit, digit) reports zero. I ran that scan after the first transform and not after the second, which is precisely how this got through.

The other two findings were correct too: the no-hedging exclusion sat above the section it narrows, contradicting the positional-precedence rule create-vale-rule documents at step 2 — measured, both orders work today, so it is moved for robustness with the measurement recorded rather than because it was broken. And the fetch-verb list could not match --global, since -g is followed by lobal there rather than whitespace; npm install --global, pnpm add --global and yarn global add are now caught and pinned, with pnpm --filter verified still uncaught.

Verification after: pnpm cli check exit 0, typecheck, lint, 929 tests.

— AI Coding Agent

@thecodedrift
thecodedrift merged commit b3c098d into main Aug 26, 2026
8 checks passed
thecodedrift added a commit that referenced this pull request Aug 26, 2026
From review of #187. Both findings verified before acting, and both real.

`ci-uses-workspace-cli` matched `npx @taskless/cli` and nothing else, so the
invocation CLAUDE.md names by name, `pnpm dlx @taskless/cli@latest`, went
straight through the rule written to forbid it. `yarn dlx` and a global install
did too. A rule that permits the exact string its own repository documents as
the thing to avoid is worse than no rule, because it reads as coverage.

Widening it to the bare package name overcorrected, and the dogfooding caught
that within one run: it fired on `pnpm --filter @taskless/cli build:nightly` in
`release-cli-nightly.yml`, which is the WORKSPACE package addressed as a
filter. `pnpm exec @taskless/cli` is the same story, running the local binary.

What separates a published build is being FETCHED, so the rule matches the
fetch verbs: `npx`, `dlx`, and `-g`. Fixtures now carry `pnpm dlx`, `yarn dlx`
and the two-step global install on the invalid side, and `pnpm exec` and
`--filter` on the valid side, so neither the hole nor the overcorrection can
come back. Measured: `pnpm dlx` and `npx` both exit 1, the clean tree exits 0.

`pr-workflow-ready-for-review` anchored on a `types:` node, so it only fired on
an explicit list that omitted `ready_for_review`. A `pull_request:` with no
`types:` at all falls through to GitHub's default set, which omits
`ready_for_review` just as surely — and the rule said nothing, because there
was no node to anchor to. That is the #103 gap the rule exists to prevent,
reachable by writing less rather than more.

It now also matches a `pull_request:` with no `types:` descendant. Nothing on
`main` hits it today (all three triggers declare `types:`), so this was latent;
a probe workflow with a bare `pull_request:` is now reported where it
previously was not. Severity stays `warning`, unchanged.
@thecodedrift
thecodedrift deleted the feat/dogfood-house-style branch August 26, 2026 19: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.

Dogfood: enforce the house writing style with Taskless rules

1 participant