Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
cf72b0a
docs(openspec): propose the agent command and the Vale authoring path
thecodedrift Aug 13, 2026
864bd67
docs(openspec): route on login state, and merge remote into create-re…
thecodedrift Aug 13, 2026
8eebe1a
docs(openspec): state the engine criterion once, and defer login to auth
thecodedrift Aug 13, 2026
93f1877
docs(openspec): record why the prompts are exported, and soften the r…
thecodedrift Aug 13, 2026
adc26ff
ref(cli): rename the `help` command to `agent`, one token per topic
thecodedrift Aug 13, 2026
0e940d3
docs(openspec): add a harness that executes the authoring recipes
thecodedrift Aug 13, 2026
0330d4f
docs(openspec): add resume notes for picking this change back up
thecodedrift Aug 13, 2026
1a14a46
feat(cli): give every routed engine an authoring recipe
thecodedrift Aug 13, 2026
19bf277
docs(openspec): rewrite the resume notes for group 2 and the 2b handoff
thecodedrift Aug 13, 2026
12f84cb
docs(openspec): record the first 2b harness run and its triage
thecodedrift Aug 13, 2026
d2bef45
docs(openspec): point the resume notes at the 2b iteration log
thecodedrift Aug 13, 2026
224acee
docs(openspec): record the second 2b harness run
thecodedrift Aug 13, 2026
12f88a1
fix(cli): correct create-vale-rule against the harness findings
thecodedrift Aug 13, 2026
34d2909
fix(cli): close the round-2 gaps in create-vale-rule
thecodedrift Aug 13, 2026
440a536
docs(openspec): record 2b as complete but for its control run
thecodedrift Aug 13, 2026
9463f10
docs(cli): add nine worked Vale rules, each with its near-miss
thecodedrift Aug 14, 2026
bdeff2e
feat(cli): verify Vale rules, enforcing the fixture layout
thecodedrift Aug 14, 2026
bd3e55d
feat(cli)!: rename `taskless help` to `taskless agent`, single-token …
thecodedrift Aug 15, 2026
e04bd0b
docs(openspec): archive agent-command-and-vale-authoring
thecodedrift Aug 15, 2026
6e7d8cd
fix(cli): surface Vale's config notice through rule verify
thecodedrift Aug 17, 2026
b508bb9
docs(openspec): propose self-contained Vale rules
thecodedrift Aug 14, 2026
fbaed06
docs(openspec): unify the rule layout across all three engines
thecodedrift Aug 14, 2026
cfa4c93
docs(openspec): record the legacy-path collision found while implemen…
thecodedrift Aug 14, 2026
ab56636
docs(openspec): carry #103's state into the resume notes
thecodedrift Aug 14, 2026
5c79a6a
feat(cli): one directory per rule, with assembled engine configs
thecodedrift Aug 14, 2026
c1fceab
fix(cli): retarget Vale check names when 0005 splits the config
thecodedrift Aug 14, 2026
2c032f4
test(cli): move the suites onto the rule-directory layout
thecodedrift Aug 14, 2026
3e1fad3
test(cli): move migration, runtime, and vale-verify suites to the layout
thecodedrift Aug 14, 2026
349bdd2
test(cli): continue moving suites onto the rule-directory layout
thecodedrift Aug 14, 2026
4d3ea10
test(cli): green on the rule-directory layout, and pin the dot
thecodedrift Aug 14, 2026
a6a37f0
feat(cli): path-addressed verify and test
thecodedrift Aug 14, 2026
abed3f5
feat: add example/, a Taskless install you can read
thecodedrift Aug 14, 2026
be26d62
docs: rewrite the example's prose to Jakob's style guide
thecodedrift Aug 14, 2026
f780a3e
docs(cli): rewrite the recipes for the rule-directory layout
thecodedrift Aug 14, 2026
b1d9964
test(cli): assert the migration behaviorally, and correct the changeset
thecodedrift Aug 14, 2026
d5f3351
docs(openspec): record group 7 results and the archive ordering
thecodedrift Aug 14, 2026
8d6f742
docs(openspec): archive self-contained-rules
thecodedrift Aug 15, 2026
8ce69c5
fix(cli): skip Vale when assembly yields no config
thecodedrift Aug 17, 2026
f23dac0
docs(openspec): make every spec pass validate --all --strict
thecodedrift Aug 15, 2026
cbfadff
docs(openspec): align cli-rules with the rule-directory layout
thecodedrift Aug 16, 2026
dc57dbe
fix(cli): carry the notice through verify, and repair two stale fixtures
thecodedrift Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 58 additions & 13 deletions .changeset/vale-rule-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,61 @@
"@taskless/cli": minor
---

Add Vale as a second static-tier rule engine.

`check` now dispatches by engine directory and runs ast-grep, Vale, and runtime
rules concurrently, merging their findings into one result set. Vale rules live
in `.taskless/vale/` and execute against the committed `.vale.ini`; an
unavailable Vale reports itself and the other engines still return, while a Vale
that times out or rejects its config fails the check rather than passing as a
clean run. Vale rules are verified from `rule-tests/<rule>/pass|fail` fixtures
against a generated per-rule config.

Adds the `engine-selection` knowledge topic — which engine enforces a given
rule, and why — available from `taskless help engine-selection` and exported
through `@taskless/cli/prompts`.
Add Vale as a second static-tier rule engine, give every engine one rule layout, and rename the agent-facing command.

`check` now dispatches by engine and runs ast-grep, Vale, and runtime rules
concurrently, merging their findings into one result set. An unavailable Vale
reports itself and the other engines still return. A Vale that times out or
rejects its config fails the check rather than passing as a clean run.

**Every rule is now one directory**, `.taskless/rules/<engine>/<id>/`, holding
the rule, any per-engine config, and its tests in `.tests/`. Writing a rule
means creating a directory and deleting one means `rm -rf`. Nothing outside it
is touched either way, so concurrent authors never collide on a shared file.

Vale rules carry their own `.vale.ini` declaring which files they apply to.
The single config Vale reads is assembled from those per-rule files on each
run, gitignored, and regenerated, so hand edits to it have no effect. ast-grep
keeps its `files`/`ignores` inside the rule and needs no second file.

**`rule verify` is replaced by two path-addressed commands.** `verify <path>`
checks that a rule has the components its engine requires and needs no tests,
so it works while you're still authoring. `test <path>` runs the rule's tests,
after running `verify` and stopping if that fails. Both take a rule directory,
an engine directory, or nothing at all for the whole project, and both report
one result per rule. Addressing by path rather than id removes the ambiguity
that arose when two engines held the same rule id.

Projects on an older layout migrate automatically on the next command.

**BREAKING: `taskless help <topic>` is now `taskless agent <topic>`.** The
command is named for who reads it. Agents fetching a procedure are not asking
for help, and the old name is gone rather than aliased.

**BREAKING: topics are addressed by a single token.** `taskless help rule
create` becomes `taskless agent create-sg-rule`; multiple positionals are no
longer joined into a topic key. A topic name is now a literal string an agent
copies rather than a phrase it can reorder. The renames:

| Was | Now |
| ------------------ | --------------------------------------- |
| `rule create` | `create-sg-rule` / `create-remote-rule` |
| `rule improve` | `improve-rule` |
| `rule delete` | `delete-rule` |
| `rule verify` | `verify-rule` |
| `rule meta` | `rule-meta` |
| `static` | `create-sg-rule` |
| `existing` | `create-legacy-rule` |
| `engine-selection` | `route` |

`route` now applies the engine reasoning itself and names a concrete
`create-*-rule` topic, so `engine-selection` is removed rather than renamed —
its criterion is stated once, in `route`. Every authoring recipe is rewritten
for the rule-directory layout.

**BREAKING for `@taskless/cli/prompts` consumers.** `engine-selection` is no
longer exported. `TOPICS` is now `create-sg-rule`, `create-vale-rule`, and
`create-runtime-rule`, so a consumer that decides an engine can reach the
procedure for each destination. Because the export is a string union, a
consumer passing the removed name dynamically breaks on upgrade rather than at
build time.
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ __generated__

# Worktrees are second checkouts; formatting them would touch other branches
worktrees/

# The demo project: deliberately-wrong source and prose fixtures.
example/
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Starting in v0.7, Taskless ships a **single consolidated skill** (`taskless`) pl
| | | delete, check, auth, CI). Fetches the canonical recipe |
| | | for the user's intent and follows it. |

Available `taskless help` topics: `rule create`, `rule improve`, `rule delete`, `check`, `auth`, `ci`, `info`, `init`, `update`. Append `--anonymous` for the local-only flow on rule create/improve.
Available `taskless agent` topics: `route`, `create-sg-rule`, `create-vale-rule`, `create-runtime-rule`, `create-remote-rule`, `improve-rule`, `delete-rule`, `check`, `auth`, `ci`, `info`, `init`, `update`. Run `taskless agent` with no topic for the index. Append `--anonymous` for the local-only flow on improve.

## CLI

Expand Down
5 changes: 5 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export default tseslint.config(
// Zero-dependency CommonJS workflow scripts (covered by their own
// node:test suite); the app's TS/ESM-oriented rules don't apply.
".github/scripts/",
// The demo project. Its source is deliberately wrong — `example.cjs`
// calls `eval` so a rule has something to find — and its fixtures are
// prose written to be flagged. Linting it fails on content nobody wrote
// as source. `example-project.test.ts` is what keeps it honest.
"example/",
],
},
eslint.configs.recommended,
Expand Down
2 changes: 2 additions & 0 deletions example/.taskless/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/.vale.ini
/.sgconfig.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
id: no-eval
valid:
- "JSON.parse(raw)"
- "const evaluate = () => 1"
invalid:
- "eval(raw)"
- 'eval("(" + raw + ")")'
9 changes: 9 additions & 0 deletions example/.taskless/rules/sg/no-eval/no-eval.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
id: no-eval
language: JavaScript
severity: error
message: Avoid eval. It executes whatever string it's handed.
note: |
`eval` runs arbitrary code with the caller's permissions. Parse the value
instead: `JSON.parse` for JSON, a real parser for anything else.
rule:
pattern: eval($$$ARGS)
3 changes: 3 additions & 0 deletions example/.taskless/rules/vale/no-simply/.tests/fail/hedged.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
You can simply drop a rule in.

Just run the check.
3 changes: 3 additions & 0 deletions example/.taskless/rules/vale/no-simply/.tests/pass/direct.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Drop a rule in, then run the check.

The adjustment took three releases.
6 changes: 6 additions & 0 deletions example/.taskless/rules/vale/no-simply/.vale.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Which files this rule applies to. Adding a rule edits nothing outside this
# directory. That's the point of the layout.
[*.{html,md}]
tskl) rule = no-simply
BasedOnStyles =
no-simply.no-simply = YES
7 changes: 7 additions & 0 deletions example/.taskless/rules/vale/no-simply/no-simply.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
extends: existence
message: "Avoid '%s'. It tells the reader the work was easy."
level: warning
ignorecase: true
tokens:
- simply
- just
3 changes: 3 additions & 0 deletions example/.taskless/taskless.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"version": 5
}
106 changes: 106 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# A Taskless install, as it actually looks

This is a small project with Taskless rules in it. Everything here is real: the
same layout you get after installing, so you can read it before you commit to
anything.

Two rules, one per engine.

## The files

| Path | What it is |
| -------------- | ------------------------------------------------------- |
| `example.cjs` | A CommonJS module that calls `eval` on file contents |
| `example.html` | A page with a Title Case heading and some hedging prose |
| `.taskless/` | The rules. No build output, no cached state. |

## What a rule looks like

A rule is **one directory**. It holds everything that defines it. Adding a rule
means adding a directory. Removing one means removing that directory. No shared
file gets edited either way.

```
.taskless/rules/
sg/no-eval/
no-eval.yml the rule
.tests/no-eval-20260814-test.yml its test cases
vale/no-simply/
no-simply.yml the rule
.vale.ini which files it applies to
.tests/fail/hedged.md prose it must flag
.tests/pass/direct.md prose it must leave alone
```

Two details in there need explaining.

**`.tests/` is dot-prefixed on purpose.** ast-grep discovers rules by walking
the rules tree, and it reads every `.yml` it finds as a rule. A plain `tests/`
directory would make it parse the test files as rules and fail the whole scan.
A dot-directory gets skipped by that walk. The test runner still finds it.

**Only Vale has a per-rule `.vale.ini`.** Vale can't express "which files does
this apply to" inside the rule file, because it rejects unknown keys. Scope
needs somewhere else to live. ast-grep puts its equivalent (`files`, `ignores`)
inside the rule, so an `sg` rule gets no second file.

You won't find a project-wide `.vale.ini` or `sgconfig.yml` here. Both get
assembled from the per-rule configs when a check runs, and both are gitignored.
They're build output.

## What `check` reports

```
$ npx @taskless/cli check

example.cjs:7:10
error[no-eval] Avoid eval. It executes whatever string it's handed.
> eval("(" + raw + ")")
note: `eval` runs arbitrary code with the caller's permissions. Parse the value
instead: `JSON.parse` for JSON, a real parser for anything else.


example.html:7:11
warning[no-simply] Avoid 'simply'. It tells the reader the work was easy.
> simply

2 issues (1 error, 1 warning) across 2 files
```

One finding from each engine, merged into one report. The exit code follows
severity, so this run exits 1 on the `error`.

## Running it yourself, in this repo

`npx @taskless/cli` fetches the published CLI. To run the one in this
checkout, build it first and then call it from here:

```
pnpm --filter @taskless/cli build # from the repo root
cd example
../packages/cli/dist/index.js check
```

Substitute that path for `npx @taskless/cli` in every command below.
Note that `check` writes the two assembled configs into `.taskless/`, so
expect them to appear after the first run. Both are gitignored.

## Checking the rules themselves

`check` runs rules against your code. Two other commands run against the rules:

```
$ npx @taskless/cli verify # are these rules well-formed?
$ npx @taskless/cli test # do they fire where they should, and only there?
```

Both take a path: a rule directory, an engine directory, or nothing at all for
everything. `test` runs `verify` first and stops if it fails. That way a broken
rule tells you what's broken.

## This example is tested

`packages/cli/test/example-project.test.ts` runs `check`, `verify`, and `test`
against this directory and asserts on what comes back. A demo that's drifted
from the layout it demonstrates is worse than no demo. If the layout changes
and this stops being true, the build fails.
14 changes: 14 additions & 0 deletions example/example.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// A small CommonJS module with something the ast-grep rule has to say about.
const { readFileSync } = require("node:fs");

function loadConfig(path) {
const raw = readFileSync(path, "utf8");
// `eval` on file contents is the pattern `no-eval` exists to catch.
return eval("(" + raw + ")");
}

function greet(name) {
return `Hello, ${name}`;
}

module.exports = { loadConfig, greet };
11 changes: 11 additions & 0 deletions example/example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<title>Taskless example</title>

<h1>Getting Started With The Example</h1>

<p>
You can simply drop a rule into this project and run a check. The heading
above is Title Case, which the capitalization rule has an opinion about.
</p>

<p>Read the README for what each file is for.</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-13
Loading
Loading