Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 25 additions & 10 deletions openspec/changes/cli-v2-rule-api/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,29 @@ place, not by staying on raw `fetch`.

### 2. One snapshot per `check`, taken before anything is signed

`check` copies `.taskless/rules/` to `.taskless/.run/rules/` first (replacing
any previous snapshot). Everything after reads only the snapshot: signing,
reporting, config assembly, and all three engines. The assembled configs for a
`check` are written beside it as `.taskless/.run/.vale.ini` and
`.taskless/.run/.sgconfig.yml`, so their root-relative `StylesPath` and
`ruleDirs` resolve into the snapshot unchanged. Runtime rules execute from
`.taskless/.run/rules/runtime/`, replacing `.run/runtime-rules/`.
`check` copies `.taskless/rules/` to `.taskless/.run/snapshot/.taskless/rules/`
first (replacing any previous snapshot). The snapshot **mirrors the project's
layout** under a base directory, `.taskless/.run/snapshot/`, so every existing
path helper and both assemblers work unchanged when handed that base in place of
the project root: the assembled configs land at
`.taskless/.run/snapshot/.taskless/.vale.ini` and `.sgconfig.yml`, and their
root-relative `StylesPath` and `ruleDirs` resolve into the snapshot. The engines
still run from the project root, with only the config path changed. Everything
after the copy reads only the snapshot: signing, reporting, config assembly, and
all three engines. Runtime rules execute from the snapshot, replacing
`.run/runtime-rules/`.

Measured before building on it (task 4.1): the mixed-engine fixture plus a Vale
rule scoped to `[docs/**/*.md]` produced the same seven findings across five
rules from both config locations, and the subdirectory-scoped rule fired on
`docs/deep/a.md` and not on a top-level file in both.

The run directory ignores itself (`.taskless/.run/.gitignore` holds `*`), rather
than `check` adding `.run/` to the tracked `.taskless/.gitignore`: a `check` that
rewrote a tracked file would contradict "check writes only under `.taskless/.run/`",
and the first lint run on this repository after the change did exactly that. git,
ast-grep, and Vale all honor the nested file; a test runs both engines with no
outer ignore entry and gets identical findings.

The snapshot is taken on every path, including unauthenticated and
`--anonymous`, so there is one execution path rather than a verified one and an
Expand Down Expand Up @@ -260,9 +276,8 @@ Free organization is never refused for a just-generated rule.
## Risks / Trade-offs

- **[Risk] Vale section globs might resolve relative to the config file.** The
assembled config moves from `.taskless/` to `.taskless/.run/`. → A test runs
one Vale rule scoped to a subdirectory glob from both locations and asserts
identical findings before the snapshot is wired into `check`.
assembled config moves into the snapshot. → Measured identical before building
(Decision 2), and a test pins it.
- **[Risk] 0.11.x stops working when the floor is set.** Remote generation fails
and reconcile returns `400` once 0.12.0 ships. → Accepted server-side (#229);
0.11.x degrades to "service unavailable" and skips runtime rules without
Expand Down
2 changes: 1 addition & 1 deletion openspec/changes/cli-v2-rule-api/specs/cli-check/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The CLI SHALL exit with code 0 when no error-severity matches are found (includi
- returned an `unsafe` verdict for an `sg` or `vale` rule; or
- left a reported rule unaccounted for (in none, or more than one, of `rules`, `unknown`, and `entitlement.withheld`).

The CLI SHALL also exit with code 1 when two rule directories under different engines share an id. Under `--json`, `success` SHALL be `false` whenever the exit code is non-zero.
On an authenticated run that would reconcile, the CLI SHALL also exit with code 1 when two rule directories under different engines share an id. A logged-out run verifies nothing and does not fail on it. Under `--json`, `success` SHALL be `false` whenever the exit code is non-zero.

#### Scenario: Exit 0 when clean

Expand Down
13 changes: 13 additions & 0 deletions openspec/changes/cli-v2-rule-api/specs/cli-rule-format/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## MODIFIED Requirements

### Requirement: Reconciliation survives the relayout

The CLI SHALL report each rule to the reconcile endpoint by its `ruleId` (the rule's directory
name) with file paths relative to the rule's own directory, so where the engine-partitioned
layout places a rule's directory is not part of what is reported. Moving a rule directory
without renaming or editing it SHALL NOT change its reconciled state.

#### Scenario: Moved rules reconcile unchanged

- **WHEN** `check` reconciles after the migration has moved rules into `.taskless/rules/<engine>/<id>/`
- **THEN** each rule is reported under the same `ruleId` with the same relative paths and signatures, the server resolves it to the same rule, and no rule is reported as new or missing
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,8 @@ outside this check.

### Requirement: The CLI runs the bytes it reported

Before signing anything, `check` SHALL copy `.taskless/rules/` into a snapshot at
`.taskless/.run/rules/`, replacing any previous snapshot and dereferencing symbolic links.
Before signing anything, `check` SHALL copy `.taskless/rules/` into a snapshot under
`.taskless/.run/`, replacing any previous snapshot and dereferencing symbolic links.
It SHALL compute every reported signature from the snapshot and SHALL run every engine
from the snapshot, with the assembled configs written under `.taskless/.run/`. A rule the
verdict excludes SHALL be removed from the snapshot before any engine configuration is
Expand All @@ -246,7 +246,7 @@ assembled. The snapshot SHALL be taken on every path, including unauthenticated
#### Scenario: Static rules run from the snapshot

- **WHEN** `check` runs sg and vale rules
- **THEN** the ast-grep and Vale configs SHALL point into `.taskless/.run/rules/`
- **THEN** the ast-grep and Vale configs SHALL point into the snapshot under `.taskless/.run/`
- **AND** SHALL NOT point into `.taskless/rules/`

#### Scenario: An excluded rule is absent from what runs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
### Requirement: Blessed runtime rules execute from the materialized run directory

When a runtime rule is executed on a validated path, the CLI SHALL execute it from the
snapshot `check` took at `.taskless/.run/rules/runtime/` **before** signing, not from the live
snapshot `check` took under `.taskless/.run/` **before** signing, not from the live
`.taskless/rules/runtime/` tree and not from a copy made after reconciliation, so the bytes
executed are exactly the bytes that were reported and judged (copy, sign, report, execute).

#### Scenario: Execution uses the blessed bytes

- **WHEN** a runtime rule is blessed and executed
- **THEN** the CLI SHALL invoke the `check.ts` in the snapshot under `.taskless/.run/rules/runtime/`
- **THEN** the CLI SHALL invoke the `check.ts` in the snapshot under `.taskless/.run/`
- **AND** SHALL NOT execute a copy modified in `.taskless/rules/runtime/` after reconciliation

#### Scenario: No copy is made between the verdict and execution
Expand Down
48 changes: 26 additions & 22 deletions openspec/changes/cli-v2-rule-api/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,58 +77,57 @@ upgradeUrl }`, strip C0/C1 control characters except newline from

## 4. Snapshot (slice 3)

- [ ] 4.1 Measure first: run one Vale rule whose `.vale.ini` scopes a
subdirectory glob with its assembled config at `.taskless/.vale.ini` and
at `.taskless/.run/.vale.ini`, and assert identical findings. Do the same
for an ast-grep rule with `.sgconfig.yml`. If either differs, stop and
revise design Decision 2 before continuing.
- [ ] 4.2 Add `rules/snapshot.ts`: replace `.taskless/.run/rules/` with a
- [x] 4.1 Measure first: the mixed-engine fixture plus a Vale rule scoped to
`[docs/**/*.md]`, run with configs assembled at `.taskless/` and in the
mirrored snapshot, gives identical findings (7 across 5 rules) and the
same subdirectory scoping. Design Decision 2 records the layout.
- [x] 4.2 Add `rules/snapshot.ts`: replace `.taskless/.run/snapshot/` with a
dereferencing copy of `.taskless/rules/`, skipping `.DS_Store`,
`Thumbs.db`, `desktop.ini`; a dangling link drops the file. Tests cover a
symlinked capture, a dangling link, and an OS metadata file.
- [ ] 4.3 Parameterize `assembleValeConfig` / `assembleSgConfig` (and what they
read through `engines.ts`) by a root, so `check` assembles into
`.taskless/.run/` from the snapshot while `verify` / `test` keep today's
paths. `assemble.test.ts` covers both roots.
- [ ] 4.4 Run runtime rules from `.taskless/.run/rules/runtime/`; delete
- [x] 4.3 Run `check`'s assembly against the snapshot base, so its configs land
inside the snapshot while `verify` / `test` keep today's paths. A test
pins identical findings from both locations, including a
subdirectory-scoped Vale rule.
- [x] 4.4 Run runtime rules from the snapshot; delete
`materializeRuntimeRules` and `RUNTIME_RUN_DIR`. A test edits a live
`check.ts` after signing and asserts the snapshot's bytes executed.

## 5. Per-rule reconcile and the verdict policy (slice 3)

- [ ] 5.1 Add `rules/report.ts`: discover every rule directory of every engine
- [x] 5.1 Add `rules/report.ts`: discover every rule directory of every engine
in the snapshot, refuse duplicate ids across engines (naming both
directories), and build `{ ruleId, files: [{ path, signature }] }` with
POSIX paths, excluding `.tests/**`. Tests: all engines reported,
fixtures excluded, a duplicate id refused, `--rule` not narrowing.
- [ ] 5.2 Add `rules/verdicts.ts`: turn a v2 reconcile response into a per-rule
- [x] 5.2 Add `rules/verdicts.ts`: turn a v2 reconcile response into a per-rule
disposition (run / exclude / fail reason / notice) by engine per the
table in design Decision 5, and compute accounting (a reported rule in
zero or several of `rules`, `unknown`, `withheld` is unaccounted). Pure
function, table-driven tests including an `unsafe` sg rule, a static and
a runtime `unknown`, `missing`, withheld, unaccounted, and double-listed.
- [ ] 5.3 Replace `planRuntime` with a `planCheck` that snapshots, reports,
- [x] 5.3 Replace `planRuntime` with a `planCheck` that snapshots, reports,
reconciles, applies dispositions, and removes excluded rules from the
snapshot before assembly. Degrade paths (no token, `--anonymous`, no
remote, 401, 404 `organization_not_found`, unreachable) run every static
rule unverified and skip runtime rules, as today. Delete
`repairWithheldRules`, `repair.ts`, and `run-set.ts`'s v1 helpers.
- [ ] 5.4 Rewire `commands/check.ts` onto `planCheck`: exit 1 on withheld, an
- [x] 5.4 Rewire `commands/check.ts` onto `planCheck`: exit 1 on withheld, an
`unsafe` static rule, an unaccounted rule, or a duplicate id; one notice
per `unsafe` naming each differing path and `taskless rule restore`; one
notice per `missing`. `check.test.ts`, `runtime-check.test.ts`, and
`mixed-engine-check.test.ts` cover each exit condition.
- [ ] 5.5 Assert `check` never writes `.taskless/rules/`: a test hashes the tree
- [x] 5.5 Assert `check` never writes `.taskless/rules/`: a test hashes the tree
before and after a run with `unsafe` and `missing` verdicts, and asserts
no restore or fetch route was called.

## 6. check --json and recipes (slice 3)

- [ ] 6.1 Add the optional `integrity` array to `schemas/check.ts` and emit it;
- [x] 6.1 Add the optional `integrity` array to `schemas/check.ts` and emit it;
keep `skipped`, `failures`, `notices`, and `entitlement` (withheld names
resolved by rule id). Tests cover an `unsafe` entry with files, an
unaccounted entry, and its omission on a clean run.
- [ ] 6.2 Update the `check` and `ci` recipes: an edited static rule and an
- [x] 6.2 Update the `check` and `ci` recipes: an edited static rule and an
unaccounted rule fail the run; the fix is `rule restore`, not editing the
rule back by hand; `missing` only warns. Update `create-runtime-rule`
where it describes reconcile.
Expand All @@ -153,7 +152,9 @@ upgradeUrl }`, strip C0/C1 control characters except newline from
output.
- [ ] 7.5 Add a `recover-rule` agent recipe (restore versus rollback, what a
refusal means, recovering from git per the refusal's `message`) and link
it from the `check` recipe. `recipe-cross-references.test.ts` passes.
it from the `check` recipe's "An edited rule" section (slice 3 left the
pointer out, since the topic did not exist yet).
`recipe-cross-references.test.ts` passes.

## 8. Retire v1 (slice 5)

Expand All @@ -169,9 +170,12 @@ upgradeUrl }`, strip C0/C1 control characters except newline from
string literal not followed by `v2/`, or delete the idea if the grep in
8.1 plus the types already make a v1 call impossible to write. Record
which, and why, in the PR.
- [ ] 8.3 Remove or rewrite tests that exercised v1 (`api-deprecated-paths`,
`repair`, `repair-integration`, `reconciliation-start`, and the v1 paths
in `entitlement` and `api-rule-errors`). `pnpm test` passes.
- [ ] 8.3 Remove or rewrite what still exercises v1. Already gone in slice 3:
`repair`, `repair-integration`, `runtime-dropped-rules`, the v1 relayout
reconcile test, `api/reconcile.ts`, `api/restore.ts`, and the runtime
`plan` / `repair` / `run-set` modules. Left for here: `api-deprecated-paths`,
the v1 parser in `entitlement.test.ts`, and whatever 8.1 deletes.
`pnpm test` passes.
- [ ] 8.4 Run `pnpm typecheck` and `pnpm lint` (which rebuilds and runs
`pnpm cli check`) from the repository root; both pass.

Expand Down
Loading
Loading