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
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ green on its own; none depends on a later one to be correct.
- [x] 1.2 Make the `delete` not-found message engine-agnostic
- [x] 1.3 Correct the seven stale `.taskless/<engine>/rules/` comments, leaving the two historical ones in the migrations
- [x] 1.4 Test deleting a rule filed under each engine, and an id no engine holds
- [x] 1.5 Correct the same stale layout in `cli-runtime-rule-execution`'s spec text (delta written; lands with this change)
- [x] 1.5 Correct the same stale layout in `cli-runtime-rule-execution`'s spec text. **Partially done by the delta**: it corrected only the requirement the delta itself touched, and left the `Blessed runtime rules execute from the materialized run directory` requirement in the same file still naming `.taskless/runtime-rules/`. The remaining sites, here and in `cli-check` and `cli-rule-reconciliation`, were corrected in the archive PR that promoted this change

## 2. Publish the layout table (slice 2) — unblocks the generator

Expand Down Expand Up @@ -57,10 +57,10 @@ green on its own; none depends on a later one to be correct.

- [x] 6.5 Nothing repaired runs in the pass that repaired it. Restore rewrites the working tree and promotes nothing into the current run: an `unsafe` rule stays withheld, a `missing` rule was never a local candidate, and an `unknown` rule never runs. Fetching code and executing it in the same pass that discovered the drift would move the gate
- [x] 6.6 A repair that fails is a notice, never a failed `check`. A rule that could not be repaired stays withheld, which is already the safe state
- [ ] 6.7 Ask the generator for `ruleId` on a reconcile `unsafe` entry (**N6**). Until then the id is parsed out of `.taskless/rules/runtime/<id>/check.ts`, which works and makes repair depend on a layout that has already moved twice — silently, since a wrong id is a 404 and an unrepaired rule rather than an error
- [x] 6.7 Ask the generator for `ruleId` on a reconcile `unsafe` entry (**N6**). **Done**: `unsafe`, `unknown` and `missing` entries all carry `ruleId`, and `ruleIdFromCheckPath` is deleted. An entry arriving without a usable id is skipped with a notice rather than becoming a request for `undefined`. Until then the id is parsed out of `.taskless/rules/runtime/<id>/check.ts`, which works and makes repair depend on a layout that has already moved twice — silently, since a wrong id is a 404 and an unrepaired rule rather than an error

## 7. Close out

- [ ] 7.1 Reply to **N4**: leave the `engine` tier defined and unused — G2 means no engine but `sg` is deliverable as a single file, so the middle rung has no future occupant either
- [ ] 7.2 Grow the changeset as each slice lands; it stays on the bottom branch
- [ ] 7.3 Archive the change on the final slice
- [x] 7.1 Reply to **N4**: **Done**, confirmed both sides — leave the `engine` tier defined and unused — G2 means no engine but `sg` is deliverable as a single file, so the middle rung has no future occupant either
- [x] 7.2 Grow the changeset as each slice lands; it stays on the bottom branch
- [x] 7.3 Archive the change on the final slice
6 changes: 3 additions & 3 deletions openspec/specs/cli-check/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ authentication.

`taskless check` SHALL reconcile before running runtime rules whenever a bearer token and a
`repositoryUrl` are resolvable and `--anonymous` is not set. It SHALL compute the signature
envelope for the `check.ts` of every runtime rule under `.taskless/runtime-rules/`, call
envelope for the `check.ts` of every runtime rule under `.taskless/rules/runtime/`, call
`POST /cli/api/reconcile` with `{ repositoryUrl, files }`, and then execute **only** the
runtime rules whose `check.ts` is returned in the `run` set, matched back to local files by
signature (per the `cli-rule-reconciliation` capability). Capture `*.yml` and static rules
Expand Down Expand Up @@ -298,11 +298,11 @@ non-zero code solely because reconciliation failed, and the warning SHALL be sup

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

`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.
`taskless check` SHALL dispatch rules to distinct executors by their engine directory: **ast-grep** rules under `.taskless/rules/sg/` via the ast-grep scanner, **Vale** rules under `.taskless/rules/vale/` via the Vale runner (per the `cli-vale-rule-engine` capability), and **runtime** rules under `.taskless/rules/runtime/` 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.

#### Scenario: Mixed corpus runs all executors

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

Expand Down
48 changes: 48 additions & 0 deletions openspec/specs/cli-generated-rule-delivery/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# cli-generated-rule-delivery Specification

## Purpose

TBD - created by archiving change generator-payload-alignment. Update Purpose after archive.

## Requirements

### Requirement: A delivered rule is a file set

The CLI SHALL accept a generated rule as a set of files, each with a path relative to
`.taskless/rules/<engine>/<id>/` and its content as text. One shape SHALL serve every engine,
validated against `ENGINE_LAYOUTS` — the table the CLI already holds — so that "is this a complete
rule" is answered from data rather than from per-engine prose.

A response entry carrying the legacy single `content` object SHALL remain valid and SHALL continue
to be filed as an ast-grep rule. `files` and `content` SHALL be mutually exclusive.

#### Scenario: A runtime rule arrives complete

- **WHEN** a delivered rule declares engine `runtime` and carries `check.ts` and `captures/*.yml`
- **THEN** the CLI SHALL write them under `.taskless/rules/runtime/<id>/`
- **AND** the rule SHALL be discoverable and verifiable without further input

#### Scenario: A Vale rule arrives with its config

- **WHEN** a delivered rule declares engine `vale` and carries `<id>.yml` and `.vale.ini`
- **THEN** the CLI SHALL write both
- **AND** the rule SHALL be scoped by its own `.vale.ini` rather than by a synthesized default

#### Scenario: An incomplete file set is refused

- **WHEN** a delivered file set omits a file the engine layout requires
- **THEN** the CLI SHALL refuse the rule and name what is missing
- **AND** SHALL NOT write a partial rule directory

### Requirement: Delivered paths are refused before they are written

The CLI SHALL reject an absolute path, any `..` segment, and any path the engine layout does not
account for, **before** creating any directory or file. Writing server-supplied paths is a
directory-traversal surface that did not exist while the response carried one structured object
whose destination the client computed itself.

#### Scenario: A traversing path is refused

- **WHEN** a delivered file declares a path containing `..` or an absolute path
- **THEN** the CLI SHALL refuse the entire rule
- **AND** SHALL NOT have created any file or directory for it
35 changes: 35 additions & 0 deletions openspec/specs/cli-layout-export/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# cli-layout-export Specification

## Purpose

TBD - created by archiving change generator-payload-alignment. Update Purpose after archive.

## Requirements

### Requirement: The rule layout is published as data

The CLI SHALL publish its rule-layout table — the known engines, each engine's rule file, config
file and captures directory, and the rules and tests directory names — as an importable module,
so that a service building a rule payload validates against the table rather than transcribing it.

A shape documented in prose drifts: the same layout was described in seven stale code comments and
in the runtime-execution spec, all naming a pre-migration path. A shape published as data does not.

#### Scenario: The layout is importable

- **WHEN** a consumer imports the published layout entry
- **THEN** it SHALL receive the engine list and each engine's layout as values
- **AND** the values SHALL be the same ones the CLI itself dispatches on

### Requirement: The published layout carries no CLI runtime

The published entry SHALL NOT reach the filesystem, the network, telemetry, or the command tree,
so a Worker can import it. This SHALL be enforced by the build — the published chunk graph is
checked at build time, failing the build rather than relying on review — matching how the prompts
entry is already constrained.

#### Scenario: A runtime import fails the build

- **WHEN** the published layout graph reaches a host capability
- **THEN** the build SHALL fail
- **AND** the artifact SHALL NOT be emitted
44 changes: 42 additions & 2 deletions openspec/specs/cli-rule-reconciliation/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,13 @@ inert data, always available, and SHALL NOT be reported to or gated by reconcili
SHALL reconcile by sending `POST /cli/api/reconcile` with an `Authorization: Bearer <cli-token>`
header and a JSON body `{ repositoryUrl, files }`, where `repositoryUrl` is the full repository
URL and `files` is an array of `{ file, signature }` covering the `check.ts` of **every**
runtime rule the CLI holds under `.taskless/runtime-rules/`. `file` SHALL be the `check.ts`'s
runtime rule the CLI holds under `.taskless/rules/runtime/`. `file` SHALL be the `check.ts`'s
delivered path as it exists on disk and `signature` SHALL be the full envelope computed for its
bytes. The CLI SHALL send the whole signature envelope, not a bare digest.

#### Scenario: Every runtime rule's check.ts is reported

- **WHEN** the CLI reconciles with runtime rules present under `.taskless/runtime-rules/`
- **WHEN** the CLI reconciles with runtime rules present under `.taskless/rules/runtime/`
- **THEN** the request body SHALL include one `{ file, signature }` entry for the `check.ts` of each runtime rule

#### Scenario: Inert files are not reported
Expand Down Expand Up @@ -222,3 +222,43 @@ signal. The server-side record is authoritative and the server decides what runs
- **WHEN** a rule file has a locally stored signature that matches its content
- **THEN** the CLI SHALL NOT run the file on that basis alone
- **AND** SHALL rely on the server's `run` set for authorization

### Requirement: The reported file path is contractual

The CLI SHALL report each runtime check to reconcile as a **repo-root-relative POSIX path**,
`.taskless/rules/runtime/<id>/check.ts`, with platform separators normalized so every host
reports the same string for the same rule.

This is a cross-team contract, not an implementation detail. Reconcile matches `run` by content
digest and is path-independent, but the `unsafe` versus `unknown` split is a lookup on the
reported name: a spelling mismatch downgrades a tampered file from `unsafe` ("content changed in
place") to `unknown` ("never issued") — a diagnostic loss on the one path where the diagnosis
matters.

#### Scenario: The reported path is repo-relative and POSIX

- **WHEN** the CLI reports a runtime check to reconcile
- **THEN** the `file` SHALL be `.taskless/rules/runtime/<id>/check.ts`
Comment thread
thecodedrift marked this conversation as resolved.
- **AND** the separators SHALL be `/` regardless of host platform

### Requirement: A withheld rule can be re-fetched

When reconcile reports a rule as `unsafe` or `unknown`, the CLI SHALL be able to request the
blessed bytes for that rule rather than only warning. The request SHALL carry the rule id **and**
the signature the client holds, scoped like reconcile itself, so that a bare digest cannot be used
to retrieve content across organizations.

The response SHALL be the bytes matching the held signature, never the newest generation.
Answering with newer bytes would upgrade a rule in the middle of a `check` without anyone asking;
upgrading is regeneration and SHALL remain an explicit action.

#### Scenario: An unsafe rule is repaired

- **WHEN** reconcile reports a rule as `unsafe`
- **THEN** the CLI SHALL be able to re-fetch the blessed bytes for that rule
- **AND** the rule SHALL reconcile as `run` after the bytes are restored

#### Scenario: Re-fetch does not upgrade

- **WHEN** a newer generation of the same rule exists server-side
- **THEN** re-fetch SHALL still return the bytes matching the signature the client reported
14 changes: 14 additions & 0 deletions openspec/specs/cli-rules/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,26 @@ The API calls for rule generation (`POST /cli/api/request` and `GET /cli/api/req

`taskless rule delete <id>` SHALL remove the rule and everything that defines it. Under the rule-directory layout that is one directory, `.taskless/rules/<engine>/<id>/`, which carries the rule, any per-engine config, and its tests. (Renamed; repathed.) Accepts `--anonymous` as a no-op.

A rule id does not carry its engine, so the CLI SHALL **resolve** which engine directory holds `<id>` rather than assuming one. A rule id is globally unique by construction, so at most one engine can hold it. When no engine holds the id, the CLI SHALL report not-found without naming an engine, because naming one would be a guess.

#### Scenario: Deleting a rule removes its whole directory

- **WHEN** a user runs `taskless rule delete no-eval`
- **THEN** the CLI SHALL remove the rule's directory including its `.tests/`
- **AND** no file belonging to that rule SHALL remain

#### Scenario: Deleting a rule filed under any engine

- **WHEN** a rule with id `<id>` exists under `.taskless/rules/vale/<id>/` or `.taskless/rules/runtime/<id>/`
- **THEN** `taskless rule delete <id>` SHALL remove that directory
- **AND** SHALL NOT report not-found for a rule that is present on disk

#### Scenario: Deleting an id no engine holds

- **WHEN** no engine directory contains `<id>`
- **THEN** the CLI SHALL report the rule was not found under `.taskless/rules/`
- **AND** the message SHALL NOT name a single engine's path

### Requirement: Rules delete does not require authentication

`taskless rule delete` SHALL NOT require authentication. Deleting a local file is not a service operation. (Renamed.)
Expand Down
Loading
Loading