feat(cli): server-owned rule reconciliation - #47
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds the CLI implementation for server-owned rule reconciliation: taskless check can now (when authenticated) report local rule signatures to the server, receive a blessed run set, and run only that subset; it also introduces a canonical hashing/signature format with conformance vectors and supporting docs/spec updates.
Changes:
- Added canonical rule signature hashing (
normalize+canonicalHash+parseSignature) plus conformance vectors fixture + tests to ensure byte-for-byte compatibility with the server reference. - Implemented a reconcile client (
POST /cli/api/reconcile) and updatedcheckto gate rule execution to the server-providedrunset when authenticated, with a degrade-to-local-scan path on expected failures. - Added run-set materialization into
.taskless/.run/rules, updated sgconfig generation to point at alternate rule dirs, and expanded CLI help + OpenSpec specs accordingly.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/cli/src/rules/rule-hash.ts | Canonical normalization + hashing and signature parsing for reconciliation. |
| packages/cli/test/rule-hash.test.ts | Conformance + invariant tests to ensure hash compatibility. |
| packages/cli/test/fixtures/rule-hash.vectors.json | Committed cross-repo vectors fixture used as release gate. |
| packages/cli/scripts/fetch-rule-hash-vectors.ts | Best-effort refresh of vectors from the API with offline cache fallback. |
| packages/cli/package.json | Wires vectors refresh into prebuild and adds a generate script. |
| packages/cli/src/api/reconcile.ts | Hand-typed reconcile client returning ok/unauthorized/unavailable. |
| packages/cli/src/rules/run-set.ts | Signs rule files, selects run set by signature, materializes .run/ rules dir. |
| packages/cli/src/filesystem/sgconfig.ts | Allows targeting ruleDirs at a configurable rules directory. |
| packages/cli/src/commands/check.ts | Auth-state-driven gating/degrade behavior; warnings suppressed under --json. |
| packages/cli/test/run-set.test.ts | Unit tests for signing, selection, and run-dir materialization. |
| packages/cli/test/reconcile-check.test.ts | Integration tests for gating/degrade behavior via mock server + dist CLI. |
| packages/cli/src/help/check.txt | Documents “What runs (auth state)” behavior. |
| packages/cli/src/help/ci.txt | Documents optional CI token backstop as enforcement point. |
| packages/cli/src/types/errors.ts | Adds RECONCILE_FAILED error code (enum extension). |
| openspec/specs/cli-rule-reconciliation/spec.md | New spec for hashing + reconcile buckets + execution rules. |
| openspec/specs/cli-check/spec.md | Updates check spec for auth-state gating and reconcile/degrade behavior. |
| openspec/changes/archive/2026-07-02-server-owned-rule-reconciliation/* | Archived proposal/design/tasks/spec deltas for this change. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
thecodedrift
added a commit
that referenced
this pull request
Jul 4, 2026
Address Copilot review on #47: - fetch-rule-hash-vectors.ts: a 200 response with a non-JSON body threw an uncaught error and broke the prebuild even when a committed cache existed. Wrap response.json() and fall back to the cache like every other failure. - rule-hash.ts: the parseSignature JSDoc claimed it throws on an 'unsupported' envelope, but it deliberately tolerates unknown future algoVersions (forward-compat). Correct the comment to match the implementation and tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
normalize() + canonicalHash() + parseSignature() implementing the algoVersion-1 signature envelope for server-owned rule reconciliation (TSKL-270), on web-standard crypto only to match the server reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Commit the cross-repo vectors fixture and a prebuild-wired fetch that refreshes from GET /cli/api/rule-hash-vectors, falling back to the committed cache offline. Conformance test asserts our hasher reproduces every vector exactly (release-blocking on mismatch). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
POST /cli/api/reconcile client returning a discriminated ok/unauthorized/unavailable outcome that never throws on expected network/auth/not-deployed conditions, so check can degrade to a local scan. Adds the RECONCILE_FAILED error code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
check now picks its behavior from auth state: unauthenticated/--anonymous scans all local rules silently; authenticated reconciles and runs only the blessed run set, warning on unsafe/unknown/missing; a failed reconcile degrades to a local scan with a notice. Warnings are suppressed under --json and never affect the exit code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
check.txt gains a "What runs (auth state)" section; ci.txt step 7 documents the optional TASKLESS_TOKEN backstop as the enforcement point over the server-blessed run set. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Unit tests for the run-set helpers and mock-server integration tests proving check runs only the blessed run set, degrades on an unavailable endpoint, stays silent when logged out or --anonymous, and warns on unsafe/unknown/missing without affecting the exit code or --json output. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Applies the delta specs to the living specs (new cli-rule-reconciliation capability; cli-check gains auth-state gating, warnings, degrade, and run-set-only execution) and archives the completed change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the OpenSpec change defining how a runtime rule executes, stacked on server-owned rule reconciliation. Reconciliation gates which files may run; this proposal defines the local harness that evaluates a runtime rule. A runtime rule is a directory under .taskless/runtime-rules/ (capture *.yml + a check.ts). The harness assembles the capture rules into one ast-grep narrow, gates on matches, then invokes check.ts's default export via a bundled tsx. check.ts is arbitrary code execution, so it runs only when its signature is validated by reconciliation, or under --dangerously-run-scripts. Static ast-grep rules and inert capture *.yml are never gated. Adds the five auth/flag modes, a --timeout bound, and an additive --json skipped field. Refs TSKL-245 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verified against workers/generator/src/actions/add-runtime-rule.ts: runtime rules are written to .taskless/runtime-rules/<slug>-<suffix>/ (one <name>.yml per capture rule + a check.ts), with fixtures under .taskless/runtime-rule-tests/. The check file is always check.ts, and its bytes are hashed with the same canonicalHash envelope reconcile uses — so gating on check.ts matches what the server captures. Tighten the recognition spec (drop the metadata.taskless.check locator claim; check.ts is fixed) and record the confirmation in design.md. Refs TSKL-245 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Group 1 of runtime-rule-execution. Add the harness-side recognition of runtime rules: a directory under .taskless/runtime-rules/ holding kind: runtime capture *.yml plus a check.ts. - src/types/runtime-rule.ts mirrors the structural harness<->check contract (Finding, Match, CheckFunction, CaptureRule, metadata block) from the generator's @taskless/types; a delivered check imports nothing, so the contract is structural. - src/rules/runtime/discover.ts enumerates .taskless/runtime-rules/, parses each capture *.yml, confirms kind: runtime, and returns a typed RuntimeRule (capture rules with id/name/language/match + the check.ts path). .taskless/runtime-rule-tests/ is never enumerated. Refs TSKL-245 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Groups 2-3 of runtime-rule-execution. Implement the local harness that evaluates a runtime rule, plus the bundled tsx loader it runs check.ts under. - narrow.ts: run a rule's capture rules as ONE ast-grep scan (anchor --json=stream, broad --files-with-matches) and normalize matches to the contract shape (0-indexed -> 1-indexed, ruleId -> model name, captures from metaVariables). Uses a temp --config rules dir so multiple captures + full ast-grep config run in a single scan. - invoke.ts: run check.ts's default export (root, matches) via a pinned tsx resolved at runtime (no repo toolchain). An embedded ESM runner writes the returned Finding[] to an out-file; a throw, non-zero exit, or timeout is isolated to an error result. Default 10s bound, overridable. - harness.ts: narrow -> gate-on-matches -> invoke -> map Finding to CheckResult (source: taskless-runtime); process-per-check, sequential. - Add tsx to the CLI dependencies (externalized from the Vite bundle). Verified end-to-end against a temp-dir fixture: discovery, narrow (correct line normalization + rule attribution), tsx invocation, and finding mapping. Refs TSKL-245 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d rules
Group 4 of runtime-rule-execution. Add the runtime reconcile-set: sign each
rule's check.ts (only) for reconciliation, select the rules the server blessed,
and materialize them for read-hash-execute.
- src/rules/runtime/run-set.ts: signRuntimeChecks (check.ts only — capture *.yml
are inert), reportRuntimeChecks -> { file, signature }, selectBlessedRuntimeRules
(content-join: a rule runs iff its check.ts signature is in the run set; the
rest are withheld/advisory), and materializeRuntimeRules (copy blessed rule
dirs into .taskless/.run/runtime-rules/ and re-discover so execution uses the
blessed bytes; .run/ stays gitignored).
- discover.ts: extract discoverRuntimeRulesIn(root) so materialized rules can be
re-discovered from .run/.
- narrow.ts: copy the original capture *.yml bytes into the temp config instead
of re-serializing the parsed object — a YAML round-trip can alter an exotic
ast-grep config.
Verified end-to-end: report only check.ts, bless on signature match, materialize
to .run/, execute the materialized copy; empty run set withholds the rule.
Refs TSKL-245
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ile gating Group 5 of runtime-rule-execution. Rewire `taskless check` around the two rule classes and complete the cutover to signing only untrusted code. - Static ast-grep rules under .taskless/rules/ always run (trusted, no network). - Runtime rules run only on a validated path: planRuntime resolves the mode from auth state + flags — authed reconcile runs blessed rules and withholds the rest (advisory); logged-out / --anonymous / no-remote / reconcile-unavailable skip runtime with a notice; --dangerously-run-scripts runs all runtime rules with no network behind a loud warning. - Add --dangerously-run-scripts and --timeout <seconds>; runtime findings merge into the same results and exit-code logic; --json gains an additive optional `skipped` array (schema updated), warnings/notices stay stderr-only. - Fix a Finding->CheckResult off-by-one: findings are 1-indexed, CheckResult.range is 0-indexed (display/json add 1). Cutover: remove the stacked-under static-reconcile gating — delete src/rules/run-set.ts and the now-obsolete test/reconcile-check.test.ts + test/run-set.test.ts (runtime-dispatch tests land in Group 7). Static rules are no longer signed or gated; only runtime check.ts is. Verified end-to-end via the built CLI: static-only-runs (runtime skipped + notice), --dangerously-run-scripts (both run), and --json (skipped array, warnings suppressed). Full CLI suite green (326 tests). Refs TSKL-245 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Groups 6-7 of runtime-rule-execution. Docs (Group 6): - check.txt (topic v2): static rules always run; runtime check.ts runs only when server-verified; the mode table, --dangerously-run-scripts, --timeout, and the --json skipped array. - ci.txt: unauthenticated CI runs static rules and skips runtime; the TASKLESS_TOKEN backstop is the enforcement point for runtime check.ts. Tests (Group 7): - runtime-harness.test.ts: discovery, gate-on-zero-matches (check never invoked), match normalization + Finding->CheckResult indexing, throwing-check isolation, timeout -> error finding. - runtime-check.test.ts: end-to-end dispatch via the built CLI with a mock reconcile server + git origin — authed-blessed, empty-run withheld, logged-out and --anonymous skip + report, reconcile-unavailable skips, dangerously-run- scripts runs offline; asserts static always runs and only check.ts is reported. Also fix a real timeout bug found by the harness test: tsx re-execs node as a grandchild, so spawn detached and SIGKILL the whole process group — otherwise a runaway check keeps running past the timeout. Full suite green (338 tests). Refs TSKL-245 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ntation Verification (opsx:verify) caught a spec-vs-impl divergence: the narrow requirement, proposal, and design named `--inline-rules --json=stream`, but the harness assembles the capture rules into a temp `--config` (--inline-rules carries only one rule; a runtime rule has multiple capture rules + full ast-grep config). Reworded to "one scan per mode" via a generated config so the spec that gets synced to canonical on archive matches reality. Refs TSKL-245 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sync the runtime-rule-execution delta specs into the canonical specs and archive the change (verify → sync → archive). - cli-runtime-rule-execution: new canonical spec (the runtime harness contract). - cli-check: static-vs-runtime dispatch, the validated-path rule, skip+report, and --dangerously-run-scripts added; auth-state/reconcile/degrade requirements updated for the cutover; the now-obsolete 'warns on reconciliation mismatches' and 'exits cleanly when the run set is empty' requirements removed (their static-reconcile behavior was deleted in the cutover — the delta records the removal with reasons). - cli-rule-reconciliation: reporting + run-set requirements rescoped to each runtime rule's check.ts. Change archived to openspec/changes/archive/2026-07-03-runtime-rule-execution/; no unarchived changes remain, so the tip's check-openspec-archived job passes. Refs TSKL-245 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address Copilot review on #47: - fetch-rule-hash-vectors.ts: a 200 response with a non-JSON body threw an uncaught error and broke the prebuild even when a committed cache existed. Wrap response.json() and fall back to the cache like every other failure. - rule-hash.ts: the parseSignature JSDoc claimed it throws on an 'unsupported' envelope, but it deliberately tolerates unknown future algoVersions (forward-compat). Correct the comment to match the implementation and tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address Copilot review on #49: the 'Blessed runtime rules execute from the materialized run directory' requirement, its scenario, and the design decision referred to the live tree as `.taskless/rules/`, but runtime rules live under `.taskless/runtime-rules/`. Correct all three so the read-hash-execute guarantee names the right tree. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Six fixes from Copilot's review of #50, plus the canonical-spec twin of the #49 doc fix: - invoke.ts: on Windows, kill the check via taskkill /T (negative PIDs aren't supported), so a timeout actually terminates the tsx+node tree. - discover.ts: pin the check file to check.ts inside the rule dir; do not resolve metadata.taskless.check as a path (prevents escaping the dir via ../). - run-set.ts: signRuntimeChecks is now per-rule resilient (returns unreadable rules instead of throwing); reported reconcile paths are POSIX-normalized so Windows backslashes don't defeat the server-side path match. - check.ts: a missing/unreadable check.ts is reported as skipped and materialization errors degrade to a runtime-skip — a malformed runtime rule no longer aborts the whole check (static keeps running). - narrow.ts: run one broad scan per broad capture rule so matches are attributed to the right rule (was mislabeling all broad matches as the first rule); treat a signal-killed narrow (null exit code) as a failure instead of success. - specs/cli-runtime-rule-execution: fix the materialize requirement's live-tree path (.taskless/rules/ -> .taskless/runtime-rules/) to match #49. Add an integration test: a runtime rule missing check.ts is skipped (not fatal) and static rules still run. Full suite green (339). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address the remaining #49 review comments (items 1-4, 6-7): - cli-check: the degrade --json scenario now allows the additive `skipped` array (not 'only { success, results }'); --dangerously-run-scripts is described as 'without server validation' rather than 'trusting local signatures' (which the reconciliation spec forbids as an auth mechanism). - cli-rule-reconciliation: rename the requirement heading to 'Reconcile reports every runtime rule's check.ts' (RENAMED op) so the title matches the scoped body instead of the old 'every held rule file'. - cli-runtime-rule-execution: clarify that match mode is read per capture rule (rules may mix modes); state that a broad path-only match carries line/column = 1 and empty text/captures. - design: note the generator path lives in the internal taskless/taskless repo, not this one. (Item 5 — the 'exactly as before' wording — is left as-is by decision.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Propagate the #49 review-comment fixes into the canonical specs (the archived change copy came via merge): - cli-check: degrade --json scenario allows the additive skipped array; --dangerously-run-scripts described as 'without server validation'. - cli-rule-reconciliation: heading renamed to 'Reconcile reports every runtime rule's check.ts'. - cli-runtime-rule-execution: per-capture match mode; broad matches are path-only (line/column 1, empty text/captures). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thecodedrift
force-pushed
the
jakob/server-owned-rule-reconciliation
branch
from
July 6, 2026 20:48
44cfaa5 to
949565d
Compare
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add the CLI side of server-owned rule reconciliation (TSKL-270). Instead of running whatever YAML sits in
.taskless/rules/,taskless checknow reports the rule files it holds to the server and executes only the subset the server returns asrun.What changed
src/rules/rule-hash.ts) —normalize()+canonicalHash()+parseSignature()for the1;h=sha-256;d=<hex>envelope, built on web-standardcrypto.subtle+TextEncoder(nonode:crypto) so it reproduces the server reference byte-for-byte.test/fixtures/rule-hash.vectors.json) plus aprebuild-wired resilient fetch that refreshes fromGET /cli/api/rule-hash-vectorsand falls back to the committed cache offline. A test asserts our hasher reproduces all 13 vectors exactly (release-blocking on mismatch).src/api/reconcile.ts) —POST /cli/api/reconcilereturning a discriminatedok/unauthorized/unavailableoutcome that never throws on expected network/auth/not-deployed conditions.checkgating — behavior is driven by auth state: logged out (or--anonymous) scans all local rules silently; authenticated reconciles and runs only the blessedrunset (materialized into a gitignored.taskless/.run/rules/), warning onunsafe/unknown/missing; a failed reconcile degrades to a local scan with a notice. Warnings are stderr-only, suppressed under--json, and never affect the exit code.check.txtgains a "What runs (auth state)" section;ci.txtdocuments the optionalTASKLESS_TOKENbackstop as the enforcement point.Why
The backend is moving to server-owned reconciliation: the server, not the CLI, decides which rule files may run. This closes the gap where
checkexecuted any YAML on disk with no notion of authenticity, and is the enabler for a new class of server-blessed rules.Notes for reviewers
checkstill requires no auth. The new network dependency is strictly additive and gated on auth state; the offline/anonymous path is unchanged (all local rules, no network).checkkeeps working. Verified against the live origin (401 for a bad token, 405 on one probe; both degrade).Refs TSKL-270
🤖 Generated with Claude Code
Stack (root → tip):