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 @@ -17,8 +17,8 @@ hashed, globally-unique `id` (`${ruleSlug}-${sha1(ruleBody).slice(0,8)}`) for sc
attribution, and a stable model-assigned `name` the check branches on. Capture rules may carry
full ast-grep config (`constraints`/`utils`/`transform` as **siblings** of `rule`).

The local harness is resolved in TSKL-245: assemble a rule's capture rules → **one**
`ast-grep scan` (anchor `--inline-rules --json=stream`; broad `--files-with-matches`) → gate
The local harness is resolved in TSKL-245: assemble a rule's capture rules → an
`ast-grep scan` (one per mode: anchor `--json=stream`; broad `--files-with-matches`) → gate
on matches → invoke `check.ts`'s **default export** with `(root, matches)` via a bundled,
pinned `tsx`; use the **returned** `Finding[]`. `Finding.severity ∈ error|warning|info` maps
onto static-rule gating with no translation. Measured cost is `tsx` per-worker startup
Expand Down Expand Up @@ -126,9 +126,13 @@ would run unverified code the user never opted into.

### Decision: The narrow → gate → `check.ts` harness, per TSKL-245

For each executable runtime rule: collect its capture rules and run **one** `ast-grep` scan —
`--inline-rules --json=stream` in anchor mode, `--files-with-matches` in broad mode
(`kind: program` enumerators). Normalize each match to
For each executable runtime rule: collect its capture rules into a generated ast-grep config
(a temp `ruleDirs` config — `--inline-rules` carries only a single rule, and a runtime rule has
multiple capture rules plus full `constraints`/`utils`/`transform`) and run **one scan per
mode** — `--json=stream` for anchor capture rules, `--files-with-matches` for broad
(`kind: program`) enumerators. An all-anchor rule is a single scan; mixing modes is one scan
per mode (broad is kept separate so a `kind: program` rule isn't streamed as whole-file text).
Normalize each match to
`{ rule, ruleId, file (root-relative), line (1-indexed), column, text, captures }`, mapping the
hashed `ruleId` back to the model `name` as `match.rule`. If there are **zero** matches,
`check.ts` is not invoked. Otherwise invoke its **default export** as a function with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ the default safety mechanism is a server-validated signature, not a local sandbo
## What Changes

- Add a **`cli-runtime-rule-execution`** capability: recognize a runtime-rule directory
(`metadata.taskless.kind: runtime`), run its capture rules as **one** `ast-grep` narrow
(anchor `--inline-rules --json=stream`; broad `--files-with-matches` for `kind: program`
(`metadata.taskless.kind: runtime`), run its capture rules as an `ast-grep` narrow (one scan
per mode: anchor `--json=stream`; broad `--files-with-matches` for `kind: program`
enumerators), **gate on matches**, and only then invoke `check.ts`'s default export with
`(root, matches)` via a CLI-bundled, pinned `tsx`. Zero matches ⇒ `check.ts` is never
invoked. Normalize matches to `{ rule, ruleId, file, line, column, text, captures }`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,18 @@ non-zero code solely because reconciliation failed, and the warning SHALL be sup
- **WHEN** the CLI degrades and `--json` is set
- **THEN** stdout SHALL contain the machine JSON shape (`{ success, results }` plus the additive optional `skipped` array for the skipped runtime rules)
- **AND** SHALL NOT contain the human-readable degrade warning

## REMOVED Requirements

### Requirement: Check warns on reconciliation mismatches

**Reason**: The cutover scopes reconciliation to runtime `check.ts` only; static rules are no
longer reconciled. Runtime-rule mismatches (a `check.ts` that is `unsafe`/`unknown`/`missing`)
are now surfaced as withheld/skipped notices and the `--json` `skipped` array (see the ADDED
requirements), so the static-rule drift/unknown/missing warnings this described no longer exist.

### Requirement: Check exits cleanly when the run set is empty

**Reason**: Static rules are no longer gated by a server `run` set — they always scan. An empty
runtime allow-list simply runs no runtime rules (reported via `skipped`/withheld); there is no
static empty-run-set skip behavior left to specify.
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,23 @@ SHALL NOT be executed by `check`.

### Requirement: The harness narrows with one ast-grep scan and gates on matches

For a runtime rule the CLI SHALL assemble the rule's capture rules and run **one** `ast-grep`
scan as the narrow: `--inline-rules --json=stream` in `anchor` mode, and
`--files-with-matches` in `broad` mode (whole-language `kind: program` enumerators). When the
narrow produces **zero** matches the CLI SHALL NOT invoke `check.ts`.
For a runtime rule the CLI SHALL assemble the rule's capture rules into an ast-grep
configuration and run **one scan per mode** as the narrow: `--json=stream` for `anchor` capture
rules, and `--files-with-matches` for `broad` capture rules (whole-language `kind: program`
enumerators). A rule with only `anchor` capture rules therefore runs in a single scan; a rule
mixing modes runs one scan per mode. When the narrow produces **zero** matches the CLI SHALL
NOT invoke `check.ts`.

#### Scenario: Zero matches skips the check

- **WHEN** a runtime rule's narrow scan produces no matches
- **THEN** the CLI SHALL NOT invoke that rule's `check.ts`
- **AND** the rule SHALL contribute no findings

#### Scenario: One scan per rule
#### Scenario: Capture rules of a mode run together

- **WHEN** a runtime rule has multiple capture rules
- **THEN** the CLI SHALL run them as a single `ast-grep` scan, not one scan per capture rule
- **WHEN** a runtime rule has multiple `anchor` capture rules
- **THEN** the CLI SHALL run them in a single `ast-grep` scan, not one scan per capture rule

### Requirement: Matches are normalized and attributed to the model name

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## 1. Runtime-rule recognition

- [x] 1.1 Add `packages/cli/src/rules/runtime/discover.ts`: enumerate `.taskless/runtime-rules/` for rule directories, parse each capture `*.yml`'s `metadata.taskless` (`kind`, `name`, `check`, `match`), and confirm the class via `kind: runtime`. Return a typed `RuntimeRule` (`{ dir, captureFiles, checkFile, match }`). `.taskless/runtime-rule-tests/` is not enumerated for execution. (Also mirrored the harness↔check contract types into `src/types/runtime-rule.ts`.)
- [x] 1.2 Static rules stay sourced from `.taskless/rules/`; runtime rules from `.taskless/runtime-rules/` — location is the class split.

## 2. Narrow → gate → check harness

- [x] 2.1 Add `packages/cli/src/rules/runtime/narrow.ts`: assemble a rule's capture rules and run ONE `ast-grep` scan per mode — anchor `--json=stream`, broad `--files-with-matches` (`kind: program`). (Used a temp `--config` rules dir rather than `--inline-rules` — a runtime rule has multiple capture rules + full ast-grep config, which `--inline-rules` can't carry; **copies the original capture `*.yml` bytes** to avoid a YAML round-trip. Grouped by mode: all-anchor = 1 scan, mixed = 1 per mode to avoid broad's whole-file streaming. Reuses `findSgBinary`/`buildPath`.)
- [x] 2.2 Add match normalization to `{ rule, ruleId, file (root-relative), line (1-indexed), column, text, captures }`, mapping the hashed capture `id` back to the model `name` surfaced as `match.rule`. (ast-grep 0-indexed → 1-indexed; captures from `metaVariables.single`.)
- [x] 2.3 Gate on matches: when the narrow yields zero matches, do NOT invoke `check.ts`.
- [x] 2.4 Add `packages/cli/src/rules/runtime/invoke.ts`: call `check.ts`'s default export with `(root, matches)` via a CLI-bundled, pinned `tsx`; use the returned `Finding[]`. Isolate a throwing check to a single error-severity finding for that rule. (Runs an embedded ESM runner under `tsx`; findings via an out-file so the check's stdout can't pollute the channel.)
- [x] 2.5 Decide and implement scheduling: process-per-check (each invoke spawns a `tsx` process), rules run sequentially; bound each check with a default wall-clock timeout (`DEFAULT_CHECK_TIMEOUT_MS`, overridable via `--timeout`) that SIGKILLs the check and records a single error-severity finding.
- [x] 2.6 Map each `Finding` (`severity ∈ error|warning|info`, omitted → warning) onto `CheckResult` with `source: taskless-runtime` (`src/rules/runtime/harness.ts`); feeds the existing aggregation and exit-code logic.

## 3. tsx bundling

- [x] 3.1 Add a pinned `tsx` to the CLI `dependencies` and resolve its bin at runtime via `createRequire`/`tsx/package.json` (no repo-local toolchain assumed); externalized from the Vite bundle (spawned as a subprocess).
- [x] 3.2 Verify `check.ts` executes with no `node_modules` and no precompile — smoke-tested against a temp-dir fixture (discovery → narrow → `tsx` invoke → finding); automated coverage lands in Group 7.

## 4. Reconcile scoping & materialization

- [x] 4.1 Add `src/rules/runtime/run-set.ts` (runtime-cohesive rather than bloating the static `run-set.ts`): enumerate via `discoverRuntimeRules`, `signRuntimeChecks` signs each rule's `check.ts` only, `reportRuntimeChecks` maps to `{ file, signature }` (capture `*.yml` and static rules are inert — not reported).
- [x] 4.2 `selectBlessedRuntimeRules(signed, run)` computes per-rule eligibility by content-join: a rule is blessed iff its `check.ts` signature is in `run`; the rest are `withheld` (advisory).
- [x] 4.3 `materializeRuntimeRules` copies blessed rule dirs into `.taskless/.run/runtime-rules/<name>/` and re-discovers them (via `discoverRuntimeRulesIn`) so the narrow + `check.ts` execute the blessed bytes; `addToGitignore` keeps `.run/` ignored.

## 5. check dispatch & modes

- [x] 5.1 Rewrote `src/commands/check.ts`: static rules under `.taskless/rules/` always scan; runtime rules route through the harness only on a validated path. **Cutover** — removed the stacked-under static-reconcile gating (deleted `src/rules/run-set.ts`).
- [x] 5.2 `planRuntime` implements the mode table: authed → reconcile & run blessed rules, withhold the rest (advisory); logged-out/`--anonymous`/no-remote/reconcile-unavailable → skip runtime + report skipped; `--dangerously-run-scripts` → run all runtime rules (no network).
- [x] 5.3 Added `--dangerously-run-scripts` (skips reconciliation, runs all runtime rules, prominent stderr warning suppressed under `--json`) and `--timeout <seconds>` (→ `parseTimeoutMs`).
- [x] 5.4 The former degrade path now scans static rules but **never executes runtime `check.ts`** unverified — skip-with-notice instead.
- [x] 5.5 Skipped-runtime notices are human-output; under `--json` an additive optional `skipped: [{ rule, reason }]` field is added (schema updated) leaving `success`/`results` unchanged. Fixed a `Finding`→`CheckResult` off-by-one (findings are 1-indexed; `CheckResult.range` is 0-indexed). Exit code stays governed solely by error-severity findings. Removed the now-obsolete `test/reconcile-check.test.ts` + `test/run-set.test.ts` (Group 7 adds runtime-dispatch tests).

## 6. Help & docs

- [x] 6.1 Updated `check.txt` (topic v2): the two rule kinds, what runs per mode, the `--dangerously-run-scripts` warning, `--timeout`, and the `--json` `skipped` array.
- [x] 6.2 Updated `ci.txt` step 7: static rules always run unauthenticated; the `TASKLESS_TOKEN` backstop is the authoritative enforcement point for runtime `check.ts`.

## 7. Tests & verification

- [x] 7.1 `test/runtime-harness.test.ts` (imports src, real ast-grep + tsx): discovery, gate-on-zero-matches (check never invoked), match normalization + `Finding`→`CheckResult` indexing, throwing-check isolation, timeout → error finding.
- [x] 7.2 `test/runtime-check.test.ts` (subprocess against `dist/`, temp fixtures + mock reconcile server + git origin): authed runs blessed rules; empty-run withholds; logged-out and `--anonymous` skip + report; reconcile-unavailable (503) skips; `--dangerously-run-scripts` runs offline with a warning.
- [x] 7.3 Static-always-run asserted in every mode; reconcile receives ONLY the runtime `check.ts` (never the static YAML).
- [x] 7.4 `--json` shape: warnings/notices suppressed; runtime findings share the `results` shape; the optional `skipped` array present when runtime rules don't run.
- [x] 7.5 `pnpm --filter @taskless/cli typecheck`, `pnpm lint`, and full `pnpm test` (338 tests) — all green. Also fixed a real timeout bug (SIGKILL the tsx process _group_, not just the wrapper, so a runaway check is actually terminated).
45 changes: 0 additions & 45 deletions openspec/changes/runtime-rule-execution/tasks.md

This file was deleted.

Loading
Loading