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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ jobs:
with:
node-version: "26"
- run: bun install --frozen-lockfile
- name: Verify the bundled oxlint rule catalog is current
run: bun run oxlint:catalog:check
- run: bun run typecheck
- run: bun run test
- run: bun run build:npm
Expand Down
14 changes: 9 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@

### Added

- `adhere rules create [idea]`: a wizard that writes one rule with an installed coding agent (`claude`, `codex`, or `omp`). The first run, or `--setup`, saves the agent and the organization's shared rules repo, as `org/repo`, to `~/.config/adhere/settings.json`; `--agent` overrides the agent for one run. The agent suggests a linter that already checks it, whether the rule belongs to the project or the shared repo, three names, a `RULE.md` or, when Jev needs state the file cannot show, a `RULE.ts` with `appendState`, and whether the rule needs `typeAware` or `reads`. It then finds code in the repo for the user to judge: each excerpt is shown with three lines around it and the agent's reason, and the user's answers decide which examples the draft uses. adhere checks the draft as `validate` does, sending problems back to the agent once before showing it; a `RULE.ts` is imported, and its code run, only after the user approves it. A project rule is written to `.adhere/rules/`, with a warning when `.adhere/config.ts` lacks the context it needs; an organization rule is committed to a fresh clone of the shared repo on the branch `adhere/<rule-id>`, with the settings installing repos need in the commit message, pushed, and linked for a pull request.
- `adhere rules create [idea]`: a wizard that writes one rule with an installed coding agent (`claude`, `codex`, or `omp`). The first run, or `--setup`, saves the agent and the organization's shared rules repo, as `org/repo`, to `~/.config/adhere/settings.json`; `--agent` overrides the agent for one run. The agent suggests a linter that already checks it, whether the rule belongs to the project or the shared repo, three names, a `RULE.md` or, when Jev needs state the file cannot show, a `RULE.ts` with `appendState`, and whether the rule needs `typeAware` or `reads`. It then finds code in the repo for the user to judge: each excerpt is shown with three lines around it and the agent's reason, and the user's answers decide which examples the draft uses. adhere checks the draft as `validate` does, sending problems back to the agent once before showing it; a `RULE.ts` is imported, and its code run, only after the user approves it. A project rule is written to `.adhere/rules/`, with a warning when `.adhere/config.ts` lacks the context it needs; an organization rule is committed to a fresh clone of the shared repo on the branch `adhere/<rule-id>`, with the settings installing repos need in the commit message, pushed, and linked for a pull request. When the convention is a native oxlint rule, chosen from adhere's catalog, the wizard prints the exact config with the rule's own option values (including a plugin oxlint leaves off and `options.typeAware`) or enables it in the shared repo's `lint/rules.json`; when it is mechanical, the agent writes an oxlint JS plugin that the user approves, with valid and invalid examples, before adhere runs it through the project's own `RuleTester`, after which the plugin and its test are written as `.mjs` files under `.adhere/oxlint/` or as `.js` files under `lint/plugins/`; the test runs on Node 22 or newer, since Oxlint's RuleTester refuses Bun and older Node. A failed run is sent back to the agent once for repair; the user approves the repaired plugin, and it runs again. Nothing is written unless the examples pass.
- `adhere rules readme` refreshes the managed index in a shared rules repo's README from the checkout: every rule under `.adhere/rules/` with the description `adhere list` shows, every enabled native setting in `lint/rules.json` with its severity, and each custom plugin's enabled rules linked to its file. It replaces only the block between `<!-- adhere:rules:start -->` and `<!-- adhere:rules:end -->`, is idempotent, removes entries for deleted or renamed rules on the next run, and refuses malformed markers without writing. A `RULE.ts` is named but never run; a rule with an off setting is omitted.
- `adhere init --shared` writes the managed README index into its scaffold, including the example rules, and refreshes it without overwriting handwritten prose. `adhere rules create` refreshes the index for local writes in a shared repo and stages it with shared-repo rule and plugin branches, so the index travels in the same pull request. In a shared repo, choosing "this project" writes `lint/rules.json` or `lint/plugins/` locally instead of printing a snippet or writing `.adhere/oxlint/`.
- `adhere install` copies a source's `.adhere/instructions/` Markdown into `.adhere/instructions/<org>/<repo>/` as well as its rules, so a shared repo can carry instructions for `adhere postinstall`.
- `adhere init --shared org/repo` scaffolds `.adhere/instructions/00-workflow.md` and a plain ESM `lint/` package (`index.js`, `index.d.ts`, `rules.json`) exported as `@org/repo/lint`, for shared Oxlint settings a consumer spreads into Vite+ or plain Oxlint.
- A bundled Oxlint native-rule catalog (`src/oxlint/catalog.json`), generated from the installed Oxlint by `bun run oxlint:catalog` and verified offline by `bun run oxlint:catalog:check`, which CI runs.
- Docs Reference pages for every command and flag and every config key, generated at build time from the CLI and the config schema's `description` annotations.
- Configurable decision-model providers, including Jev, OpenAI, Cloudflare Workers AI, and custom adapters. OpenAI credentials can be managed separately with `login --openai` and `logout --openai`.
- Provider-aware eval reporting for latency, unanswered questions, and shared repository scoring; rule-detail and provider-comparison studies with expanded labeled datasets.
Expand All @@ -19,11 +24,12 @@
- `dependents` read, enabled with `reads: ["workspacePackages", "dependents"]`: per file, the workspace packages whose `package.json` depends on its package (`dependents.packages`) and up to 50 files that import it by a relative path (`dependents.files`, with `omittedFiles` counting the rest). Like other shared context, a change in dependents does not invalidate cached judgments.
- Rule options: a `RULE.ts` declares settings as an Effect Schema in `options`, and a config sets them as `overrides[id].options`. Unset required or invalid options refuse the run. Jev, and the rule's `appendState`, read the decoded settings as state `options`, in requests of the rule's own; preflight counts them, and changing them re-judges only that rule. adhere re-exports `Schema` for rule files.
- An install script for the standalone executable: `curl -fsSL https://github.com/darkmatter/adhere/raw/main/scripts/install.sh | sh` puts this machine's executable from the latest release in `~/.local/bin`.
- An install script for the standalone executable: `curl -fsSL https://github.com/darkmatter/adhere/raw/main/scripts/install.sh | sh` puts this machine's executable from the latest release in `~/.local/bin`
- `adhere rules create [idea]`: a wizard that writes one rule with an installed coding agent (`claude`, `codex`, or `omp`). The first run, or `--setup`, saves the agent and the organization's shared rules repo, as `org/repo`, to `~/.config/adhere/settings.json`; `--agent` overrides the agent for one run. The agent suggests a linter that already checks it, whether the rule belongs to the project or the shared repo, and three names; finds `must` and `never` examples in the repo for the user to choose from; and drafts the `RULE.md`, which adhere checks for parsing and wording as `validate` does, sending problems back to the agent once before showing it. A project rule is written to `.adhere/rules/`; an organization rule is committed to a fresh clone of the shared repo on the branch `adhere/<rule-id>`, pushed, and linked for a pull request

### Changed

- `adhere install org/repo` now offers its instruction files and rules as a multiselect at a terminal, preselecting what is already installed; `--all` copies every match without a prompt; an exact rule id with nothing deeper under it copies directly; a topic copies only its rules and never instructions; `--force` replaces existing files and directories, and deselecting never deletes. Without a terminal and without `--all`, it fails and names the matches and an exact rule to use.
- `adhere postinstall` renders instruction files whose path has at least three segments first (copied shared files, and local files two directories deep), then files directly in the instructions directory, then files one directory deep, each group sorted by path.
- A shared repo's tags are repo-wide: one tag covers its rules, instructions, and `lint/` package, and there are no per-rule or per-package tags.
- Adopt Vite+ `1.1.0` and Vitest `5.0.3`, with project formatting hooks, shared editor settings, and pinned development runtimes.
- Configure `reads` at the top level instead of per rule. `workspacePackages`, `symbols`, and `references` are enabled by default; use `reads: []` for code-only requests.
- `--limit` counts files needing requests, including all pending rules for each selected file, rather than individual rule checks.
Expand All @@ -37,7 +43,6 @@
### Removed

- The `reference` and `avoid` keys of a config rule, `avoid` as a fence tag or heading in a `RULE.md`, and the refusal of a rule-level `reads`. A config rule whose only example is under `reference` or `avoid` fails for lack of an example, a fence tagged `avoid` or under an `Avoid` heading is code to write, and a rule-level `reads` is ignored.
- The `reference` and `avoid` keys of a config rule, `avoid` as a fence tag or heading in a `RULE.md`, and the refusal of a rule-level `reads`. A config rule whose only example is under `reference` or `avoid` fails for lack of an example, a fence tagged `avoid` or under an `Avoid` heading is code to write, and a rule-level `reads` is ignored

### Fixed

Expand All @@ -46,4 +51,3 @@
- Detect changed source or ignore directives before sending or caching results under a planned content key.
- Fix the native API panic when reading const-asserted empty tuple properties by pinning the SDK and compiler to `7.1.0-dev.20261009.1`, which includes [TypeScript #64080](https://github.com/microsoft/TypeScript/pull/64080).
- Stop refusing to run when `.adhere/` holds a Markdown file outside every rule's directory, such as an instructions file in `.adhere/instructions/`, the default for `adhere postinstall`. Like any file not named `RULE.md` or `RULE.ts`, it is ignored.
- Stop refusing to run when `.adhere/` holds a Markdown file outside every rule's directory, such as an instructions file in `.adhere/instructions/`, the default for `adhere postinstall`. Like any file not named `RULE.md` or `RULE.ts`, it is ignored
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export async function findRestaurants(zipCode: string) {
````

adhere asks [Jev](https://typesafe.ai), TypeSafe AI's System One model, whether
each file breaks each rule. Jev answers with a calibrated probability. adhere
each file breaks each rule which returns a calibrated probability. adhere
reports each rule whose probability is above a threshold, and shows the part of
the file Jev points at. The report looks like Oxlint's:

Expand Down
149 changes: 149 additions & 0 deletions docs/decisions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Decisions

Accepted decisions for adhere's shared-repo and linter integration. These
records explain the choices the README and skills document; they are not a
roadmap.

## Tags are repo-wide

A shared repo releases as a whole. One tag, such as `v1.0.0`, covers its rules,
its instruction files, and its `lint/` package. There are no per-rule or
per-package tags.

The alternatives, tagging each rule or publishing the lint package separately,
would let consumers combine versions the repo never tested together. A single
tag also gives `adhere install org/repo#v1.0.0` and a git-dependency pin the
same snapshot. The cost is coarse updates: any change to one rule needs a new
tag, and a consumer who pins the lint package to that tag takes the repo's
whole lint state, not only that rule's change.

## Copy rules and instructions; consume lint as a package

`adhere install` copies a shared repo's rule directories to
`.adhere/rules/<org>/<repo>/` and its Markdown instruction files to
`.adhere/instructions/<org>/<repo>/`. The shared `lint/` package is not copied;
consumers add the repo as a git dependency and import `@org/repo/lint`.

Rules are data: a copied rule becomes the repository's own, can be edited in
place, and a copied `RULE.ts` runs with the helpers beside it without a package
loader. Instructions are Markdown that `adhere postinstall` renders into
`AGENTS.md`, so a copy keeps the text visible and reviewable in the repository.
Lint settings are configuration: they must resolve plugin paths relative to the
shared package, and multiple projects need the same settings without keeping
duplicate copies. `lint/index.js` performs that resolution and is spread into
Vite+'s lint block or plain Oxlint's config.

The tradeoff is drift. Copied files stop tracking their source, and
`--force` replaces local edits when a consumer takes a newer tag. That is
intentional: local edits should be visible and reversible, not silently
overwritten by an import. Lint settings move with the git-dependency pin
instead.

## Selection by default, `--all` for automation

For a bare repository source, `adhere install` presents instruction files and
rules as a multiselect at a terminal. An exact rule id with nothing deeper under
it copies directly. A topic offers only its rules, never instruction files.
`--all` copies every match without a prompt.

A default of "copy everything" would make an organization's entire rule set
land in every repository, including rules a project does not want. Interactive
selection keeps that choice explicit and preselects what is already installed,
so a rerun shows the current state instead of adding duplicates. Automation has
no terminal, so it must opt into `--all`; without either, the command fails and
names the matches and an exact rule to use instead. Deselecting never deletes
existing files, and `--force` is the only way to replace local edits.

## Shared instructions render first

`adhere postinstall` reads Markdown under the instructions directory
recursively and renders it in this order:

1. files whose path from the instructions directory has at least three
segments, which includes files copied under
`.adhere/instructions/<org>/<repo>/`;
2. files directly in the instructions directory;
3. files one directory deep.

Each group sorts by path.

Organization-wide policy should lead a repository's local fragments, and copied
files live deep enough to be recognized by depth alone. Depth is the only test,
so a local file two directories deep is also grouped with copied files. The
rule is deterministic, so an unchanged set of files always produces the same
`AGENTS.md` block.

## The shared README index is managed, not generated wholesale

A shared repo's README lists what it offers, and that list is generated from
the checkout between `<!-- adhere:rules:start -->` and
`<!-- adhere:rules:end -->`: each rule under `.adhere/rules/` with the
description `adhere list` shows, each enabled setting in `lint/rules.json` with
its severity, and each custom plugin's enabled rules linked to its file. Rules
sort by id and entries render one per line, so the same checkout always writes
the same block. Descriptions come from `RULE.md` frontmatter through the same
discovery `adhere list` uses; a `RULE.ts` is named but never imported, and a
plugin's code is never loaded. A rule with an `off` setting is omitted.

Only the block is replaced. The README also carries setup, install, and release
prose written by hand, so regenerating the whole file would destroy it, and
appending a fresh list on every change would accumulate stale entries. Markers
make the managed region explicit and idempotent, and a missing, duplicated, or
out-of-order marker refuses the refresh before any write rather than guessing
at a repair. `adhere init --shared` writes the block into its scaffold and
refreshes it after the example rules land; re-running it over an existing
README updates only the block.

`adhere rules readme` is the local refresh, for edits, renames, and deletions
made by hand. Shared-repo create flows also refresh before committing: a rule
or plugin pushed for a pull request carries the index in the same commit, and a
local write refreshes it afterward. When the working directory is itself the
shared repo, "this project" means its `lint/` package: native settings are
written to `lint/rules.json` and plugins to `lint/plugins/` locally instead of
printing a consumer snippet. The shared repo is recognized by the metadata
`init --shared` writes, a `package.json` exporting `./lint`, not by the
presence of `.adhere/rules/`, which every project with rules has. A legacy
rules-only org repo without that export keeps its previous create flow and
gets no index or scaffold. Malformed markers fail the refresh before any push,
rather than suppressing the error and publishing a stale index.

## A versioned native catalog, and syntax-only custom plugins

`adhere rules create` may route a convention to one of two linter paths, and
only these:

- A native Oxlint rule must be an id in `src/oxlint/catalog.json`, a snapshot
generated from the Oxlint version adhere's development toolchain installs.
The agent answers `linter` as a plain rule-id string or null; adhere checks
it against the catalog at decode time and rejects unknown ids. The agent
supplies the rule's exact option values; it prints `"error"` when the
defaults enforce the idea, and `["error", ...options]` when they do not,
because a bare `"error"` would enforce nothing. It prints the project config
or enables the same setting in a shared repo's `lint/rules.json`.
- A custom Oxlint JS plugin is for a convention visible in the syntax tree and
scopes. It must be plain ESM, import nothing, and default-export its object.
Its diagnostics are reported literally so the valid and invalid examples can
assert exact messages. The user approves the plugin and its examples before
anything runs. It is then tested with the project's
own Oxlint RuleTester, through `oxlint/plugins-dev` or
`vite-plus/lint/plugins-dev`; a failure gets one agent repair, reapproval,
and another run, and nothing is written unless the tests pass. Project files
live in `.adhere/oxlint/` as `.mjs`, since a `type: commonjs` package treats
`.js` as CommonJS; shared files live in `lint/plugins/` as `.js`, since
`lint/` is `type: module`. Tests run on Node 22 or newer: Oxlint's RuleTester
refuses Bun and older Node, so there is no fallback, and without it nothing
is written and no repair is attempted.

Native rules keep exact checks in the linter that can decide them, and the
catalog prevents the wizard from inventing rule ids. Generating a plugin only
for syntax-level checks keeps the boundary honest: types, other files' contents,
and other parsers remain adhere's job. Approval before execution is required
because the plugin is code. Using the project's installed Oxlint means adhere
does not bundle or choose a second linter version.

The catalog is a snapshot, not a resolver. It follows adhere's development
toolchain, so a consumer on an older Oxlint may lack a suggested rule, and a
newer Oxlint may have rules the catalog does not suggest. Plugin tests always
use the consumer's Oxlint. `bun run oxlint:catalog` regenerates the snapshot
and its documentation summaries; `bun run oxlint:catalog:check` verifies it
offline, and CI enforces the check.
Loading