Skip to content

Commit e773da4

Browse files
committed
docs(openspec): archive partition-rules-by-engine
Also corrects the cli-check delta, which filed two rewrites of existing requirements under ADDED. The archive refused it — both headers already exist in the spec — so they move to MODIFIED, which is what they always were. Adds the minor changeset for the engine-partitioned layout.
1 parent 271431a commit e773da4

11 files changed

Lines changed: 147 additions & 51 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@taskless/cli": minor
3+
---
4+
5+
Partition `.taskless/` by rule engine. Migration `0004` moves ast-grep rules to `sg/rules/` and `sg/rule-tests/`, the runtime tree to `runtime/rules/` and `runtime/rule-tests/`, and scaffolds an inert `vale/`. Files move byte-for-byte, so runtime rule signatures survive.
6+
7+
The directory a rule sits in now **is** its engine: dispatch reads the path and never parses a rule file to decide who owns it. `check` runs ast-grep against the committed `.taskless/sg/sgconfig.yml` instead of generating an ephemeral config each run.
8+
9+
Existing projects keep working without action. The pre-`0004` `.taskless/rules/` still runs as ast-grep, and a delivered rule that names no engine is still treated as ast-grep — a rule engine this CLI does not recognize is rejected rather than guessed at.

openspec/changes/partition-rules-by-engine/.openspec.yaml renamed to openspec/changes/archive/2026-08-02-partition-rules-by-engine/.openspec.yaml

File renamed without changes.

openspec/changes/partition-rules-by-engine/design.md renamed to openspec/changes/archive/2026-08-02-partition-rules-by-engine/design.md

File renamed without changes.

openspec/changes/partition-rules-by-engine/proposal.md renamed to openspec/changes/archive/2026-08-02-partition-rules-by-engine/proposal.md

File renamed without changes.

openspec/changes/partition-rules-by-engine/specs/cli-check/spec.md renamed to openspec/changes/archive/2026-08-02-partition-rules-by-engine/specs/cli-check/spec.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## ADDED Requirements
1+
## MODIFIED Requirements
22

33
### Requirement: Check subcommand executes ast-grep scan
44

openspec/changes/partition-rules-by-engine/specs/cli-rule-format/spec.md renamed to openspec/changes/archive/2026-08-02-partition-rules-by-engine/specs/cli-rule-format/spec.md

File renamed without changes.

openspec/changes/partition-rules-by-engine/specs/cli-runtime-rule-execution/spec.md renamed to openspec/changes/archive/2026-08-02-partition-rules-by-engine/specs/cli-runtime-rule-execution/spec.md

File renamed without changes.

openspec/changes/partition-rules-by-engine/tasks.md renamed to openspec/changes/archive/2026-08-02-partition-rules-by-engine/tasks.md

File renamed without changes.

openspec/specs/cli-check/spec.md

Lines changed: 9 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,6 @@ The `check` command SHALL NOT require `.taskless/taskless.json` to exist. The co
2727
- **THEN** the CLI SHALL print a warning that no rules were found
2828
- **AND** the CLI SHALL exit with code 0
2929

30-
### Requirement: Check subcommand generates ephemeral sgconfig.yml
31-
32-
The `check` command SHALL generate an `sgconfig.yml` file in `.taskless/` before invoking ast-grep. The generated config SHALL set `ruleDirs` to `['rules']` and `testConfigs` to `[{testDir: 'rule-tests'}]`. The file SHALL be written to `.taskless/sgconfig.yml` which is gitignored via `.taskless/.gitignore`. If `.taskless/.gitignore` does not exist, the CLI SHALL create it before writing the config.
33-
34-
#### Scenario: sgconfig.yml is generated at check time
35-
36-
- **WHEN** a user runs `taskless check`
37-
- **AND** `.taskless/rules/` contains rule files
38-
- **THEN** the CLI SHALL write `.taskless/sgconfig.yml` with `ruleDirs: ['rules']`
39-
- **AND** the CLI SHALL pass `--config .taskless/sgconfig.yml` to ast-grep
40-
41-
#### Scenario: Existing sgconfig.yml is overwritten
42-
43-
- **WHEN** `.taskless/sgconfig.yml` already exists (from a previous run or legacy scaffold)
44-
- **THEN** the CLI SHALL overwrite it with the freshly generated content
45-
4630
### Requirement: Check subcommand warns when no rules exist
4731

4832
The CLI SHALL check for the presence of YAML rule files in the `.taskless/rules/` directory. When no rule files are found, the CLI SHALL warn the user and exit cleanly.
@@ -60,27 +44,15 @@ The CLI SHALL check for the presence of YAML rule files in the `.taskless/rules/
6044

6145
### Requirement: Check subcommand executes ast-grep scan
6246

63-
The CLI SHALL generate an ephemeral `sgconfig.yml` in `.taskless/` and execute
64-
`sg scan --config .taskless/sgconfig.yml --json=stream` using `child_process.spawn` with
65-
`shell: true` for cross-platform binary resolution. The `sg` binary SHALL be resolved from
66-
the `@ast-grep/cli` dependency via PATH. When reconciliation succeeds, the scan SHALL cover
67-
only the blessed `run`-set rule files; on the unauthenticated/`--anonymous` path, or when an
68-
authenticated reconciliation degrades to a local scan, the scan SHALL cover all local rule
69-
files as before.
47+
The CLI SHALL execute `sg scan --config .taskless/sg/sgconfig.yml --json=stream` using `child_process.spawn` with `shell: true` for cross-platform binary resolution, reading the **committed** ast-grep config at `.taskless/sg/sgconfig.yml`. No `sgconfig.yml` is generated at check time. The `sg` binary SHALL be resolved from the `@ast-grep/cli` dependency via PATH. Reconciliation/run-set semantics for runtime rules are unchanged.
7048

71-
#### Scenario: ast-grep scan runs with generated config
49+
#### Scenario: ast-grep scan runs with the committed config
7250

73-
- **WHEN** the CLI executes the scanner
74-
- **THEN** it SHALL first write `.taskless/sgconfig.yml`
75-
- **AND** it SHALL invoke `sg scan` with `--config .taskless/sgconfig.yml` and `--json=stream`
51+
- **WHEN** the CLI executes the ast-grep scanner
52+
- **THEN** it SHALL invoke `sg scan` with `--config .taskless/sg/sgconfig.yml` and `--json=stream`
53+
- **AND** it SHALL NOT write or generate a config file
7654
- **AND** the working directory for the spawned process SHALL be the resolved project directory
7755

78-
#### Scenario: Scan is limited to the run set when reconciled
79-
80-
- **WHEN** reconciliation succeeded and returned a `run` set
81-
- **THEN** the generated scan configuration SHALL cause `sg scan` to evaluate only the
82-
`run`-set rule files
83-
8456
#### Scenario: ast-grep binary is not found
8557

8658
- **WHEN** the `sg` binary cannot be resolved from PATH
@@ -321,16 +293,12 @@ non-zero code solely because reconciliation failed, and the warning SHALL be sup
321293

322294
### Requirement: Check dispatches static and runtime rules to distinct executors
323295

324-
`taskless check` SHALL execute **static** ast-grep rules under `.taskless/rules/` with the
325-
ast-grep scanner as before, and **runtime** rules under `.taskless/runtime-rules/` (directories
326-
with `metadata.taskless.kind: runtime`, per the `cli-runtime-rule-execution` capability) with
327-
the runtime harness. Findings from both executors SHALL be aggregated into the same result set
328-
and SHALL count toward the exit code identically.
296+
`taskless check` SHALL dispatch rules to distinct executors by their engine directory: **ast-grep** rules under `.taskless/sg/` via the ast-grep scanner, **Vale** rules under `.taskless/vale/` via the Vale runner (per the `cli-vale-rule-engine` capability), and **runtime** rules under `.taskless/runtime/rules/` via the runtime harness (per the `cli-runtime-rule-execution` capability). Findings from all executors SHALL be aggregated into the same result set and SHALL count toward the exit code identically.
329297

330-
#### Scenario: Mixed corpus runs both executors
298+
#### Scenario: Mixed corpus runs all executors
331299

332-
- **WHEN** `.taskless/rules/` contains static rules and `.taskless/runtime-rules/` contains runtime rules
333-
- **THEN** the CLI SHALL run static rules through `sg scan` and runtime rules through the runtime harness
300+
- **WHEN** `.taskless/sg/` contains ast-grep rules, `.taskless/vale/` contains Vale rules, and `.taskless/runtime/rules/` contains runtime rules
301+
- **THEN** the CLI SHALL run ast-grep rules through `sg scan`, Vale rules through the Vale runner, and runtime rules through the runtime harness
334302
- **AND** SHALL merge their findings into one result set
335303

336304
### Requirement: Check runs runtime rules only on a signature-validated path
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# cli-rule-format Specification
2+
3+
## Purpose
4+
5+
TBD - created by archiving change partition-rules-by-engine. Update Purpose after archive.
6+
7+
## Requirements
8+
9+
### Requirement: Rules are partitioned into per-engine directories
10+
11+
The system SHALL store rules under a top-level engine directory `.taskless/<engine>/`, each with a `rules/` directory and a `rule-tests/` directory. The `sg` engine SHALL use `sgconfig.yml`; the `vale` engine SHALL use `.vale.ini`; the `runtime` engine SHALL store each rule as a directory `rules/<rule>/` (capture `*.yml` + `check.ts`) with fixtures under `rule-tests/<rule>/`.
12+
13+
#### Scenario: ast-grep engine directory
14+
15+
- **WHEN** the CLI resolves `.taskless/`
16+
- **THEN** ast-grep rules are found under `.taskless/sg/rules/`, the config is `.taskless/sg/sgconfig.yml`, and tests are under `.taskless/sg/rule-tests/`
17+
18+
#### Scenario: Vale engine directory
19+
20+
- **WHEN** the CLI resolves `.taskless/`
21+
- **THEN** Vale styles are found under `.taskless/vale/rules/`, the config is `.taskless/vale/.vale.ini`, and tests are under `.taskless/vale/rule-tests/`
22+
23+
### Requirement: A rule's engine is determined by its containing directory
24+
25+
The system SHALL dispatch each rule to the engine named by its top-level `.taskless/<engine>/` directory, and SHALL NOT parse a rule file to determine its engine.
26+
27+
#### Scenario: Directory-based dispatch
28+
29+
- **WHEN** a rule file exists at `.taskless/sg/rules/no-eval.yml` and another at `.taskless/vale/rules/no-simply.yml`
30+
- **THEN** the first is executed by ast-grep and the second by Vale, based solely on directory
31+
32+
### Requirement: Each engine's committed native config is the source of truth
33+
34+
The system SHALL treat each engine's committed native config as the authoritative definition of its rules, their scoping, and their metadata. The system SHALL NOT require a separate Taskless sidecar or metadata file for a rule, and SHALL NOT generate an engine config at check time.
35+
36+
#### Scenario: No sidecar or generated config
37+
38+
- **WHEN** the CLI runs a check
39+
- **THEN** it reads the committed `sg/sgconfig.yml` and `vale/.vale.ini` as-is, and neither writes nor generates an engine config
40+
41+
#### Scenario: Native scoping is applied by the engine
42+
43+
- **WHEN** an ast-grep rule declares native `files`/`ignores`, or a Vale `.vale.ini` declares per-rule include/exclude sections
44+
- **THEN** the engine applies that scoping directly, with no Taskless-side rule transformation
45+
46+
### Requirement: Migration preserves existing ast-grep rules by moving them under sg
47+
48+
The migration to the engine-partitioned layout SHALL move the existing `.taskless/rules/`, `.taskless/rule-tests/`, and `.taskless/sgconfig.yml` under `.taskless/sg/` without editing file contents, relying on `sgconfig.yml`'s relative `ruleDirs: [rules]` remaining valid after the move. It SHALL scaffold `.taskless/vale/` and SHALL move `.taskless/runtime-rules/` to `.taskless/runtime/rules/` and `.taskless/runtime-rule-tests/` to `.taskless/runtime/rule-tests/` without editing file contents (preserving runtime capture-rule hashes). Every scaffolded directory that would otherwise be empty SHALL contain a `.gitkeep` file so the structure is tracked reliably.
49+
50+
#### Scenario: Mechanical move of legacy rules
51+
52+
- **WHEN** the migration runs against a `.taskless/` containing `rules/`, `rule-tests/`, and `sgconfig.yml`
53+
- **THEN** those become `sg/rules/`, `sg/rule-tests/`, and `sg/sgconfig.yml`, and `sg scan --config .taskless/sg/sgconfig.yml` runs the same rules as before the move
54+
55+
#### Scenario: Vale scaffolded, runtime moved
56+
57+
- **WHEN** the migration runs
58+
- **THEN** `.taskless/vale/` is created with empty `rules/` and `rule-tests/`, and `.taskless/runtime-rules/` becomes `.taskless/runtime/rules/` with byte-identical contents
59+
60+
### Requirement: Service-delivered rules without an engine are written as ast-grep
61+
62+
The rule ingest path SHALL write a service-delivered rule into the engine directory its payload identifies. The current API carries **no** engine discriminator — `/cli/api/rule/{ruleId}` returns `rules[].content` documented as an ast-grep rule definition — so a payload that does not identify an engine SHALL be written as ast-grep, under `.taskless/sg/rules/<id>.yml`, with its tests under `.taskless/sg/rule-tests/`.
63+
64+
This default is permanent, not a migration window: published CLIs and stored payloads without an engine field continue to exist indefinitely, and the default matches what the migration does to the same rules already on disk.
65+
66+
Absence of an engine and an **unrecognized** engine are distinct. If a payload identifies an engine the installed CLI does not know, ingest SHALL fail with an error naming the engine and instructing the user to upgrade, and SHALL NOT fall back to ast-grep.
67+
68+
#### Scenario: Engine-less payload is filed under sg
69+
70+
- **WHEN** a rule is delivered by the service with no engine identified in its payload
71+
- **THEN** it is written to `.taskless/sg/rules/<id>.yml` and its tests to `.taskless/sg/rule-tests/`, and a subsequent `check` dispatches it to ast-grep
72+
73+
#### Scenario: Ingest and migration agree on destination
74+
75+
- **WHEN** a rule that predates the engine-partitioned layout is migrated, and an equivalent rule is delivered fresh by the service
76+
- **THEN** both come to rest at the same path under `.taskless/sg/rules/`
77+
78+
#### Scenario: Unrecognized engine fails loudly
79+
80+
- **WHEN** a payload identifies an engine the installed CLI does not support
81+
- **THEN** ingest exits with an error naming the engine and directing the user to upgrade, and no rule file is written under any engine directory
82+
83+
### Requirement: Both the legacy and engine-partitioned layouts are readable
84+
85+
The CLI SHALL dispatch rules found at the legacy `.taskless/rules/` path as ast-grep, in addition to `.taskless/sg/rules/`, so a checkout that has not yet been migrated — or a rule delivered by a service that still names the legacy location — is executed rather than ignored.
86+
87+
This tolerance is what decouples the CLI's release from any consumer's: a producer may continue to use the pre-migration layout indefinitely and its rules keep running.
88+
89+
#### Scenario: Unmigrated checkout still runs its rules
90+
91+
- **WHEN** `check` runs against a `.taskless/` containing `rules/` but no `sg/`
92+
- **THEN** those rules are dispatched to ast-grep and reported, not silently skipped
93+
94+
#### Scenario: Both layouts present
95+
96+
- **WHEN** rules exist under both `.taskless/rules/` and `.taskless/sg/rules/`
97+
- **THEN** both are dispatched to ast-grep and their findings merged, with no duplicate reporting of the same rule
98+
99+
### Requirement: Reconciliation survives the relayout
100+
101+
The CLI SHALL report rule files to the reconcile endpoint at their post-migration repo-relative paths. Because the server joins reported files by content signature rather than by path, moving a rule without editing it SHALL NOT change its reconciled state.
102+
103+
#### Scenario: Moved rules reconcile unchanged
104+
105+
- **WHEN** `check` reconciles after the migration has moved rules from `.taskless/rules/` to `.taskless/sg/rules/` and runtime rules to `.taskless/runtime/rules/`
106+
- **THEN** each file's signature is unchanged, the server resolves it to the same rule, and no rule is reported as new or missing
107+
108+
### Requirement: The CLI refuses a scaffold newer than it understands unless overridden
109+
110+
When `taskless.json`'s `version` exceeds the highest migration the installed CLI knows, the system SHALL exit with an error instructing the user to upgrade the CLI, unless `--allow-version-mismatches` is passed, in which case it SHALL proceed without applying migrations.
111+
112+
#### Scenario: Newer scaffold blocks
113+
114+
- **WHEN** `taskless.json` has a `version` greater than the CLI's maximum known migration
115+
- **THEN** the CLI exits with an error telling the user to upgrade the CLI
116+
117+
#### Scenario: Override proceeds
118+
119+
- **WHEN** the same condition holds and `--allow-version-mismatches` is set
120+
- **THEN** the CLI proceeds without applying migrations

0 commit comments

Comments
 (0)