@drkmttr/adhere 0.18.1 → 0.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drkmttr/adhere",
3
- "version": "0.18.1",
3
+ "version": "0.21.2",
4
4
  "private": false,
5
5
  "description": "Lints a repository against the rules in .adhere/, judged by Jev.",
6
6
  "license": "MIT",
@@ -38,32 +38,32 @@
38
38
  "prepare": "vp config"
39
39
  },
40
40
  "dependencies": {
41
- "effect": "4.0.0-rc.118"
41
+ "effect": "4.0.3"
42
42
  },
43
43
  "devDependencies": {
44
- "@effect/platform-bun": "4.0.0-rc.118",
44
+ "@effect/platform-bun": "4.0.3",
45
45
  "@types/bun": "^1.4.2",
46
- "@types/node": "^24.13.6",
46
+ "@types/node": "^26.0.0",
47
47
  "release-it": "^21.1.0",
48
- "typescript": "7.1.0-dev.20261009.1",
48
+ "typescript": "7.1.0-dev.20261012.1",
49
49
  "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
50
50
  "vite-plus": "1.1.0",
51
- "vitest": "5.0.3"
51
+ "vitest": "5.0.5"
52
52
  },
53
53
  "overrides": {
54
- "@effect/platform-node-shared": "4.0.0-rc.118",
54
+ "@effect/platform-node-shared": "4.0.3",
55
55
  "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
56
- "vitest": "5.0.3"
56
+ "vitest": "5.0.5"
57
57
  },
58
58
  "devEngines": {
59
59
  "packageManager": {
60
60
  "name": "bun",
61
- "version": "1.4.2",
61
+ "version": "1.4.3",
62
62
  "onFail": "download"
63
63
  },
64
64
  "runtime": {
65
65
  "name": "node",
66
- "version": "24.21.0",
66
+ "version": "26.11.1",
67
67
  "onFail": "download"
68
68
  }
69
69
  },
@@ -90,10 +90,10 @@
90
90
  "--force"
91
91
  ],
92
92
  "optionalDependencies": {
93
- "@drkmttr/adhere-darwin-arm64": "0.18.1",
94
- "@drkmttr/adhere-darwin-x64": "0.18.1",
95
- "@drkmttr/adhere-linux-arm64": "0.18.1",
96
- "@drkmttr/adhere-linux-x64": "0.18.1",
97
- "@drkmttr/adhere-win32-x64": "0.18.1"
93
+ "@drkmttr/adhere-darwin-arm64": "0.21.2",
94
+ "@drkmttr/adhere-darwin-x64": "0.21.2",
95
+ "@drkmttr/adhere-linux-arm64": "0.21.2",
96
+ "@drkmttr/adhere-linux-x64": "0.21.2",
97
+ "@drkmttr/adhere-win32-x64": "0.21.2"
98
98
  }
99
99
  }
@@ -0,0 +1,132 @@
1
+ # Presets
2
+
3
+ Every rule in adhere's built-in presets is listed below, with a one-line
4
+ summary. Each id links to the full rule, and is the id that reports,
5
+ `overrides`, and `adhere-ignore` comments use. [Presets](../README.md#presets)
6
+ says how to name a preset or one of its topics.
7
+
8
+ ## typescript
9
+
10
+ - **Source.** The TypeScript Handbook and the Google TypeScript Style Guide.
11
+ - **Checked by a linter instead.** The preset has no rules for these, since
12
+ typescript-eslint and ESLint check them exactly: unhandled promises
13
+ (`no-floating-promises`), throwing non-errors (`only-throw-error`), `any`
14
+ (`no-explicit-any`), exhaustive switches (`switch-exhaustiveness-check`), a
15
+ lost `cause` (`preserve-caught-error`), and empty catch blocks (`no-empty`).
16
+
17
+ - [`typescript/async/concurrent-independent-work`](./typescript/async/concurrent-independent-work/RULE.md): Independent awaited calls run concurrently, not one after another.
18
+ - [`typescript/async/network-calls-have-timeouts`](./typescript/async/network-calls-have-timeouts/RULE.md): HTTP requests carry a timeout or an abort signal.
19
+ - [`typescript/design/no-argument-mutation`](./typescript/design/no-argument-mutation/RULE.md): Functions return changed copies instead of mutating arguments.
20
+ - [`typescript/errors/no-swallowed-errors`](./typescript/errors/no-swallowed-errors/RULE.md): Caught errors are handled, rethrown, or returned as failures.
21
+ - [`typescript/errors/release-on-every-path`](./typescript/errors/release-on-every-path/RULE.md): Acquired resources are released on every path, including errors.
22
+ - [`typescript/testing/assert-outcomes`](./typescript/testing/assert-outcomes/RULE.md): Tests assert results and effects, not only that code ran.
23
+ - [`typescript/testing/independent-tests`](./typescript/testing/independent-tests/RULE.md): Each test sets up its own state and relies on no other test.
24
+ - [`typescript/types/assertions-only-when-proven`](./typescript/types/assertions-only-when-proven/RULE.md): Type assertions claim only what surrounding code has proven.
25
+ - [`typescript/types/derive-dont-restate`](./typescript/types/derive-dont-restate/RULE.md): Types are derived from their source instead of restated by hand.
26
+ - [`typescript/types/unrepresentable-invalid-states`](./typescript/types/unrepresentable-invalid-states/RULE.md): Types make invalid field combinations unrepresentable.
27
+ - [`typescript/types/validate-external-data`](./typescript/types/validate-external-data/RULE.md): Data from outside is checked at runtime before use.
28
+
29
+ ## react
30
+
31
+ - **Source.** react.dev, mostly
32
+ [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
33
+ and
34
+ [Choosing the State Structure](https://react.dev/learn/choosing-the-state-structure).
35
+ - **Checked by a linter instead.** The preset has no rules for these, since
36
+ the React Compiler's lint rules check them, in
37
+ [eslint-plugin-react-hooks](https://react.dev/reference/eslint-plugin-react-hooks)'s
38
+ recommended set and in Oxlint: pure render (`purity`), props and state
39
+ never mutated (`immutability`), refs not read during render (`refs`), no
40
+ `setState` in render or synchronously in an effect (`set-state-in-render`,
41
+ `set-state-in-effect`), which covers state derived in an effect, and no
42
+ component defined inside another (`static-components`).
43
+
44
+ - [`react/effects/clean-up-what-they-start`](./react/effects/clean-up-what-they-start/RULE.md): Effects return cleanup that undoes what they started.
45
+ - [`react/effects/event-logic-in-handlers`](./react/effects/event-logic-in-handlers/RULE.md): Code responding to a user action runs in its event handler.
46
+ - [`react/effects/external-stores-use-sync-external-store`](./react/effects/external-stores-use-sync-external-store/RULE.md): External stores are read with `useSyncExternalStore`.
47
+ - [`react/effects/fetches-ignore-stale-responses`](./react/effects/fetches-ignore-stale-responses/RULE.md): Effects that fetch ignore or abort stale responses.
48
+ - [`react/effects/notify-parents-in-handlers`](./react/effects/notify-parents-in-handlers/RULE.md): Parents are notified in the handler that makes the change.
49
+ - [`react/server/no-private-data-to-client`](./react/server/no-private-data-to-client/RULE.md): Client Components receive only the fields they show.
50
+ - [`react/server/server-functions-authorize-and-validate`](./react/server/server-functions-authorize-and-validate/RULE.md): Server Functions authorize callers and validate arguments.
51
+ - [`react/state/no-contradictory-state`](./react/state/no-contradictory-state/RULE.md): One status value replaces booleans that can contradict.
52
+ - [`react/state/no-props-copied-into-state`](./react/state/no-props-copied-into-state/RULE.md): Props are read during render, not copied into state.
53
+ - [`react/state/store-ids-not-copies`](./react/state/store-ids-not-copies/RULE.md): State holds a selected item's id, not a copy of the item.
54
+
55
+ ## security
56
+
57
+ - **Source.** OWASP's cheat sheets.
58
+
59
+ - [`security/constant-time-secret-compare`](./security/constant-time-secret-compare/RULE.md): Received secrets are compared in constant time.
60
+ - [`security/no-secrets-in-output`](./security/no-secrets-in-output/RULE.md): Secrets stay out of logs, errors, and HTTP responses.
61
+ - [`security/no-untrusted-input-in-commands-or-paths`](./security/no-untrusted-input-in-commands-or-paths/RULE.md): Untrusted input stays out of command strings and unchecked paths.
62
+ - [`security/parameterized-queries`](./security/parameterized-queries/RULE.md): SQL values from outside are passed as bound parameters.
63
+
64
+ ## effect
65
+
66
+ - **Source.** The
67
+ [effect-solutions](https://github.com/kitlangton/effect-solutions) docs and
68
+ the [effect/platform](https://effect.website/docs/platform/introduction/)
69
+ docs.
70
+
71
+ - [`effect/basics/gen-for-sequencing`](./effect/basics/gen-for-sequencing/RULE.md): Sequential effectful steps use `Effect.gen` and `yield*`.
72
+ - [`effect/basics/instrument-with-pipe`](./effect/basics/instrument-with-pipe/RULE.md): Timeouts, retries, logging, and spans attach with `.pipe`.
73
+ - [`effect/config/business-logic-depends-on-config-service`](./effect/config/business-logic-depends-on-config-service/RULE.md): Business logic depends on a config service, not Config reads.
74
+ - [`effect/config/secrets-are-redacted`](./effect/config/secrets-are-redacted/RULE.md): Tokens, passwords, and keys are read with `Config.Redacted`.
75
+ - [`effect/config/tests-provide-values-directly`](./effect/config/tests-provide-values-directly/RULE.md): Tests supply config with `Layer.succeed` on the config service.
76
+ - [`effect/config/validate-with-schema`](./effect/config/validate-with-schema/RULE.md): Constrained config values are validated as they are read.
77
+ - [`effect/data/brand-meaningful-primitives`](./effect/data/brand-meaningful-primitives/RULE.md): Meaningful primitives in domain types are branded schemas.
78
+ - [`effect/data/variants-are-tagged-unions`](./effect/data/variants-are-tagged-unions/RULE.md): Domain variants are tagged schemas in a union.
79
+ - [`effect/errors/catch-defects-at-boundaries-only`](./effect/errors/catch-defects-at-boundaries-only/RULE.md): Defects are caught only at system boundaries.
80
+ - [`effect/errors/defects-for-bugs`](./effect/errors/defects-for-bugs/RULE.md): Bugs and invariant violations are defects, not typed errors.
81
+ - [`effect/errors/wrap-external-errors`](./effect/errors/wrap-external-errors/RULE.md): External errors are wrapped in tagged errors.
82
+ - [`effect/services/dependencies-through-layers`](./effect/services/dependencies-through-layers/RULE.md): Service dependencies are yielded inside the service's Layer.
83
+ - [`effect/services/fresh-layer-per-test`](./effect/services/fresh-layer-per-test/RULE.md): Each `it.effect` test provides its own layer.
84
+ - [`effect/services/memoize-parameterized-layers`](./effect/services/memoize-parameterized-layers/RULE.md): Parameterized layers are built once and stored in a constant.
85
+ - [`effect/services/no-mutable-state`](./effect/services/no-mutable-state/RULE.md): Service interfaces expose only readonly members.
86
+ - [`effect/testing/test-clock-for-time`](./effect/testing/test-clock-for-time/RULE.md): Tests that depend on time use `TestClock`.
87
+
88
+ ## alchemy
89
+
90
+ - **Source.** alchemy's docs and blog.
91
+
92
+ - [`alchemy/apis/match-remote-errors-by-tag`](./alchemy/apis/match-remote-errors-by-tag/RULE.md): Remote errors are matched by tag and data, not `instanceof`.
93
+ - [`alchemy/apis/schemas-at-trust-boundaries`](./alchemy/apis/schemas-at-trust-boundaries/RULE.md): Outside input is schema-validated at trust boundaries.
94
+ - [`alchemy/data/commit-migrations-then-deploy`](./alchemy/data/commit-migrations-then-deploy/RULE.md): Migrations are committed ahead of time and applied as committed.
95
+ - [`alchemy/data/hyperdrive-direct-origin`](./alchemy/data/hyperdrive-direct-origin/RULE.md): Hyperdrive points at the database's direct endpoint.
96
+ - [`alchemy/data/migrations-use-direct-url`](./alchemy/data/migrations-use-direct-url/RULE.md): Migration runners connect with the database's direct URL.
97
+ - [`alchemy/data/never-disable-tls-verification`](./alchemy/data/never-disable-tls-verification/RULE.md): Database connections verify the server's TLS certificate.
98
+ - [`alchemy/data/private-endpoint-at-runtime`](./alchemy/data/private-endpoint-at-runtime/RULE.md): Service code reaches its database through the private binding.
99
+ - [`alchemy/data/single-migration-owner`](./alchemy/data/single-migration-owner/RULE.md): Each database has exactly one migration owner.
100
+ - [`alchemy/durable/idempotent-event-handlers`](./alchemy/durable/idempotent-event-handlers/RULE.md): Handlers for at-least-once events are idempotent.
101
+ - [`alchemy/durable/io-inside-steps`](./alchemy/durable/io-inside-steps/RULE.md): Workflow I/O runs inside steps; replayed code stays deterministic.
102
+ - [`alchemy/durable/no-self-triggering-bucket-writes`](./alchemy/durable/no-self-triggering-bucket-writes/RULE.md): Bucket handlers filter out events from their own writes.
103
+ - [`alchemy/durable/stable-step-names`](./alchemy/durable/stable-step-names/RULE.md): Workflow step names stay stable across replays.
104
+ - [`alchemy/durable/webhook-state-is-truth`](./alchemy/durable/webhook-state-is-truth/RULE.md): Paid access depends on webhook-recorded state, not success URLs.
105
+ - [`alchemy/providers/diff-guards-unresolved-props`](./alchemy/providers/diff-guards-unresolved-props/RULE.md): Provider diffs guard unresolved props with `isResolved`.
106
+ - [`alchemy/providers/idempotent-delete`](./alchemy/providers/idempotent-delete/RULE.md): Provider delete treats an already-gone resource as success.
107
+ - [`alchemy/providers/lazy-layer-construction`](./alchemy/providers/lazy-layer-construction/RULE.md): Provider layers defer credentials and connections until use.
108
+ - [`alchemy/providers/reconcile-from-observed-state`](./alchemy/providers/reconcile-from-observed-state/RULE.md): Provider reconcile converges from the resource's live state.
109
+ - [`alchemy/resources/anchor-paths-with-import-meta`](./alchemy/resources/anchor-paths-with-import-meta/RULE.md): Path props of resources declared outside the run directory use `import.meta`.
110
+ - [`alchemy/resources/idempotent-actions-and-seeds`](./alchemy/resources/idempotent-actions-and-seeds/RULE.md): Actions and seed files are safe to run again.
111
+ - [`alchemy/resources/previews-reference-shared-resources`](./alchemy/resources/previews-reference-shared-resources/RULE.md): Preview stages reference shared resources and add cheap pieces.
112
+ - [`alchemy/resources/protect-irreplaceable-data`](./alchemy/resources/protect-irreplaceable-data/RULE.md): Buckets and databases with irreplaceable data are retained.
113
+ - [`alchemy/resources/remote-state-for-ci`](./alchemy/resources/remote-state-for-ci/RULE.md): CI and multi-machine stacks use a shared state store.
114
+ - [`alchemy/resources/stable-logical-ids`](./alchemy/resources/stable-logical-ids/RULE.md): Resource logical ids stay the same across deploys.
115
+ - [`alchemy/resources/stage-unique-physical-names`](./alchemy/resources/stage-unique-physical-names/RULE.md): Global physical names are derived per stage.
116
+ - [`alchemy/runtime/bind-tag-not-implementation`](./alchemy/runtime/bind-tag-not-implementation/RULE.md): Other runtimes bind the Tag; only the Stack provides the Layer.
117
+ - [`alchemy/runtime/deterministic-config-defaults`](./alchemy/runtime/deterministic-config-defaults/RULE.md): Config defaults and fallbacks are deterministic.
118
+ - [`alchemy/runtime/disposables-in-request-scope`](./alchemy/runtime/disposables-in-request-scope/RULE.md): Disposables are acquired inside handlers, within request scope.
119
+ - [`alchemy/runtime/lambda-no-post-response-work`](./alchemy/runtime/lambda-no-post-response-work/RULE.md): Lambda handlers write durable work before responding.
120
+ - [`alchemy/runtime/no-per-request-work-at-construction`](./alchemy/runtime/no-per-request-work-at-construction/RULE.md): Construction builds resources and handlers, not per-request work.
121
+ - [`alchemy/runtime/resolve-config-and-bindings-at-construction`](./alchemy/runtime/resolve-config-and-bindings-at-construction/RULE.md): Config and bindings are resolved at construction, not in handlers.
122
+ - [`alchemy/secrets/let-alchemy-mint-infra-secrets`](./alchemy/secrets/let-alchemy-mint-infra-secrets/RULE.md): Infrastructure tokens are minted once by alchemy, not hardcoded.
123
+ - [`alchemy/secrets/none-in-browser-env`](./alchemy/secrets/none-in-browser-env/RULE.md): Browser-inlined env values hold only public configuration.
124
+ - [`alchemy/secrets/redacted-until-use`](./alchemy/secrets/redacted-until-use/RULE.md): Secrets stay `Redacted` until the call that needs the raw string.
125
+ - [`alchemy/secrets/unwrap-before-compare-or-send`](./alchemy/secrets/unwrap-before-compare-or-send/RULE.md): `Redacted` values are unwrapped before compare, send, or serialize.
126
+ - [`alchemy/security/authorize-public-endpoints`](./alchemy/security/authorize-public-endpoints/RULE.md): Public handlers authenticate and authorize every caller.
127
+ - [`alchemy/security/github-secret-not-variable`](./alchemy/security/github-secret-not-variable/RULE.md): Sensitive CI values use `GitHub.Secret`, not `GitHub.Variable`.
128
+ - [`alchemy/security/identity-from-verified-session`](./alchemy/security/identity-from-verified-session/RULE.md): The acting user comes from the verified session or token.
129
+ - [`alchemy/security/keep-auth-protections-on`](./alchemy/security/keep-auth-protections-on/RULE.md): Auth checks and protections stay enabled.
130
+ - [`alchemy/security/least-privilege-bindings`](./alchemy/security/least-privilege-bindings/RULE.md): Runtimes bind the narrowest capability their calls need.
131
+ - [`alchemy/security/least-privilege-ci-credentials`](./alchemy/security/least-privilege-ci-credentials/RULE.md): CI credentials are scoped to what the stack deploys.
132
+ - [`alchemy/security/lookup-failure-is-not-anonymous`](./alchemy/security/lookup-failure-is-not-anonymous/RULE.md): A failed session lookup is an error, not an anonymous user.
@@ -23,6 +23,7 @@ adhere init [--force] # .adhere/config.ts, two example rules, and the
23
23
  adhere init --shared org/repo # a repo of rules other repos install, with CI and an alchemy stack
24
24
  adhere login # save a TypeSafe AI API key; TYPESAFE_API_KEY takes precedence
25
25
  adhere validate # rule wording, rules a linter could check, contradictions
26
+ adhere rules create [idea] # wizard: claude, codex, or omp drafts a RULE.md or RULE.ts from excerpts you judge; org rules pushed for a PR
26
27
  adhere lint # judge the working directory; exit 1 on an error finding
27
28
  adhere lint --limit 0 # compiler-free counts/cache plan; no budgeting
28
29
  adhere lint --limit 100 # first 100 files needing requests; all pending rules per file
@@ -173,9 +174,9 @@ Opting in with `typeAware: true` or `--type-aware` enables both `symbols` and
173
174
  `references`, regardless of `reads`. Legacy native-reader entries in `reads`
174
175
  alone do not enable the compiler; leaving them out does not disable it when
175
176
  type awareness is on. Code alone requires `reads: []` and type awareness off.
176
- Presets do not own or override this selection. Rule-level `reads`, in Markdown
177
- front matter, `defineRule`, or inline rules, is rejected with guidance to move
178
- it to the config. The context maps are:
177
+ Presets do not own or override this selection. A rule-level `reads`, in
178
+ Markdown front matter, `defineRule`, or an inline rule, has no effect. The
179
+ context maps are:
179
180
 
180
181
  - `workspacePackages`: only packages matched by this file's literal imports,
181
182
  as `{ canonicalPackageName: directory }`. Subpaths match the owning package;
@@ -25,10 +25,10 @@ the `packageManager` field in `package.json` names it; with neither, **Ask**.
25
25
 
26
26
  | Manager | Add adhere | Run adhere |
27
27
  | ------- | -------------------------------- | ------------------ |
28
- | bun | `bun add -D @drkmttr/adhere` | `bunx adhere` |
28
+ | bun | `bun add -D @drkmttr/adhere` | `bun run adhere` |
29
29
  | pnpm | `pnpm add -D @drkmttr/adhere` | `pnpm exec adhere` |
30
30
  | yarn | `yarn add -D @drkmttr/adhere` | `yarn adhere` |
31
- | npm | `npm install -D @drkmttr/adhere` | `npx adhere` |
31
+ | npm | `npm install -D @drkmttr/adhere` | `npm exec adhere` |
32
32
 
33
33
  In a repo with a `package.json`, add adhere as a dev dependency with its
34
34
  manager, so the repo pins the version the team and CI run. From then on,
@@ -56,18 +56,19 @@ and validate do not start the compiler.
56
56
  In a repo without a `package.json`, inspect any `adhere` on PATH with
57
57
  `adhere --version`, then **Ask** whether to use the npm/Bun package launcher
58
58
  or a raw executable for the same default compiler-free audit. The raw release
59
- (`adhere-Windows-x86_64.exe` on Windows) needs no Node or package manager and
60
- runs with workspace context without special configuration. It cannot run a
61
- type-aware audit: use the package launcher or a checkout if the user opts in.
62
- Do not silently turn off an explicitly chosen `typeAware` setting. Only fetch
63
- the raw release after the user chooses that alternative:
59
+ needs no Node or package manager and runs with workspace context without
60
+ special configuration. It cannot run a type-aware audit: use the package
61
+ launcher or a checkout if the user opts in. Do not silently turn off an
62
+ explicitly chosen `typeAware` setting. Only fetch the raw release after the
63
+ user chooses that alternative. On macOS and Linux:
64
64
 
65
65
  ```sh
66
- curl -fsSL --create-dirs -o ~/.local/bin/adhere \
67
- https://github.com/darkmatter/adhere/releases/latest/download/adhere-$(uname -s)-$(uname -m)
68
- chmod +x ~/.local/bin/adhere
66
+ curl -fsSL https://github.com/darkmatter/adhere/raw/main/scripts/install.sh | sh
69
67
  ```
70
68
 
69
+ On Windows, download `adhere-Windows-x86_64.exe` from
70
+ https://github.com/darkmatter/adhere/releases/latest.
71
+
71
72
  Validate and lint's paid requests need a TypeSafe AI API key: `TYPESAFE_API_KEY`,
72
73
  or one saved by `adhere login`, which the user runs; never ask for the key in
73
74
  chat or print it. Compiler-free planning with `--limit 0` needs no key.
package/src/cli.ts CHANGED
@@ -24,12 +24,18 @@ import {
24
24
  type Progress,
25
25
  progressAfter,
26
26
  progressLine,
27
+ renderJson,
28
+ renderMarkdown,
29
+ renderRuleReports,
27
30
  sendQuestion,
28
31
  } from "#workflows/format.ts";
29
32
  import type { Flags } from "#config.ts";
30
33
  import { findContradictions, formatContradictions } from "#contradictions.ts";
34
+ import { createRule } from "#create/wizard.ts";
35
+ import { AGENTS } from "#create/agents.ts";
31
36
  import { type Install, initProject } from "#init.ts";
32
37
  import { installRules, listRules, parseSource } from "#install.ts";
38
+ import { renderInstructions } from "#instructions.ts";
33
39
  import { formatLinterCheck, tallyProjectRules } from "#mechanical.ts";
34
40
  import { type PresetName, presetNames } from "#presets.ts";
35
41
  import type { SavedKey } from "#providers/provider.ts";
@@ -39,6 +45,7 @@ import {
39
45
  Console,
40
46
  Effect,
41
47
  Exit,
48
+ FileSystem,
42
49
  Layer,
43
50
  LogLevel,
44
51
  Option,
@@ -49,8 +56,9 @@ import {
49
56
  Schema,
50
57
  Stdio,
51
58
  Stream,
59
+ Terminal,
52
60
  } from "effect";
53
- import { Argument, Command, Flag, Prompt } from "effect/cli";
61
+ import { Argument, CliError, Command, Flag, Prompt } from "effect/cli";
54
62
  // A Bun text import (https://bun.sh/docs/bundler/loaders#text): the file's
55
63
  // contents become a string at bundle time, so the compiled binary carries the
56
64
  // skill without an --asset flag. `skills/` sits beside `src/`, outside the `#`
@@ -246,9 +254,43 @@ export const auditLayer = (flags: Flags, filter: ReadonlyArray<string> = []) =>
246
254
  /** `adhere lint`: the audit. */
247
255
  export const lintCommand = Command.make(
248
256
  "lint",
249
- { preset, threshold, sufficiencyThreshold, typeAware, yes, limit, rpm, filter, denyWarnings },
257
+ {
258
+ preset,
259
+ threshold,
260
+ sufficiencyThreshold,
261
+ typeAware,
262
+ yes,
263
+ limit,
264
+ rpm,
265
+ filter,
266
+ denyWarnings,
267
+ json: Flag.Boolean("json").pipe(
268
+ Flag.withDefault(false),
269
+ Flag.withDescription(
270
+ "Write one JSON file per violated rule to .adhere/reports/. Use --stdout for one combined report instead.",
271
+ ),
272
+ ),
273
+ md: Flag.Boolean("md").pipe(
274
+ Flag.withDefault(false),
275
+ Flag.withDescription(
276
+ "Write one Markdown file per violated rule to .adhere/reports/. Use --stdout for one combined report instead.",
277
+ ),
278
+ ),
279
+ stdout: Flag.Boolean("stdout").pipe(
280
+ Flag.withDefault(false),
281
+ Flag.withDescription(
282
+ "Print all findings together instead of writing files. Select either --json or --md for a formatted report.",
283
+ ),
284
+ ),
285
+ },
250
286
  (input) =>
251
287
  Effect.gen(function* () {
288
+ if (input.stdout && input.json && input.md) {
289
+ return yield* CliError.UserError.make({
290
+ cause:
291
+ "Choose either --json or --md with --stdout; both formats can be written to files together.",
292
+ });
293
+ }
252
294
  const stdio = yield* Stdio.Stdio;
253
295
  const plan = yield* planAudit({ limit: Option.getOrUndefined(input.limit) });
254
296
  const limits = { filter: input.filter, rpm: Option.getOrUndefined(input.rpm) };
@@ -258,8 +300,7 @@ export const lintCommand = Command.make(
258
300
  if (input.filter.length === 0) yield* pruneCache(plan);
259
301
  return;
260
302
  }
261
- // A prompt draws on stdout, so it needs a terminal at both ends: none when
262
- // the report is redirected, or in CI.
303
+ // Keep prompting limited to interactive runs, including when JSON is requested.
263
304
  const interactive = (yield* stdio.stdinIsTerminal) && (yield* stdio.stdoutIsTerminal);
264
305
  if (
265
306
  (plan.requests > 0 || (plan.cachedLocateRequests ?? 0) > 0) &&
@@ -268,6 +309,11 @@ export const lintCommand = Command.make(
268
309
  ) {
269
310
  const send = yield* Prompt.run(
270
311
  Prompt.Confirm({ message: sendQuestion(plan), initial: true }),
312
+ ).pipe(
313
+ Effect.updateService(Terminal.Terminal, (terminal) => ({
314
+ ...terminal,
315
+ display: toStderr,
316
+ })),
271
317
  );
272
318
  if (!send) {
273
319
  yield* toStderr("Nothing sent.\n");
@@ -283,16 +329,33 @@ export const lintCommand = Command.make(
283
329
  if (input.filter.length === 0) yield* pruneCache(plan);
284
330
  const color = yield* stdio.stdoutIsTerminal;
285
331
  const path = yield* Path.Path;
286
- // One write: separate Console.log calls have interleaved out of order here.
287
- yield* Console.log(
288
- render(result, {
289
- color,
290
- root: path.resolve(),
291
- threshold: plan.threshold,
292
- sufficiencyThreshold: plan.sufficiencyThreshold,
293
- ...(plan.provider === undefined ? {} : { provider: plan.provider }),
294
- }).join("\n"),
295
- );
332
+ const options = {
333
+ color,
334
+ root: path.resolve(),
335
+ threshold: plan.threshold,
336
+ sufficiencyThreshold: plan.sufficiencyThreshold,
337
+ ...(plan.provider === undefined ? {} : { provider: plan.provider }),
338
+ };
339
+ if (!input.stdout && (input.json || input.md)) {
340
+ const fs = yield* FileSystem.FileSystem;
341
+ const directory = path.join(options.root, ".adhere", "reports");
342
+ const reports = renderRuleReports(result, input, options);
343
+ yield* fs.remove(directory, { recursive: true, force: true });
344
+ yield* fs.makeDirectory(directory, { recursive: true });
345
+ for (const [name, report] of reports) {
346
+ yield* fs.writeFileString(path.join(directory, name), report);
347
+ }
348
+ yield* toStderr(`Wrote ${reports.size} report files to .adhere/reports/.\n`);
349
+ } else {
350
+ // One write: separate Console.log calls have interleaved out of order here.
351
+ yield* Console.log(
352
+ input.json
353
+ ? renderJson(result, options)
354
+ : input.md
355
+ ? renderMarkdown(result, options)
356
+ : render(result, options).join("\n"),
357
+ );
358
+ }
296
359
  // Only errors fail the run, unless --deny-warnings: a warning is for a nit or a noisy rule.
297
360
  const failed = failing(result.findings, input.denyWarnings);
298
361
  if (failed.length > 0) {
@@ -556,6 +619,48 @@ export const skillCommand = Command.make("skill", { name: skillName }, ({ name }
556
619
  ),
557
620
  );
558
621
 
622
+ const agent = Flag.Literals("agent", AGENTS).pipe(
623
+ Flag.optional,
624
+ Flag.withDescription(
625
+ "The coding agent to ask this time: claude, codex, or omp, in place of the one setup saved.",
626
+ ),
627
+ );
628
+
629
+ /** `adhere rules create [idea]`: a wizard that writes one rule, with a coding agent's help. */
630
+ const rulesCreateCommand = Command.make(
631
+ "create",
632
+ {
633
+ idea: Argument.String("idea").pipe(
634
+ Argument.optional,
635
+ Argument.withDescription("What the rule should enforce. Asked for when left out."),
636
+ ),
637
+ agent,
638
+ setup: Flag.Boolean("setup").pipe(
639
+ Flag.withDefault(false),
640
+ Flag.withDescription(
641
+ "Choose again which agent to ask and which GitHub repo, as org/repo, holds the organization's shared rules. The first run asks without it.",
642
+ ),
643
+ ),
644
+ },
645
+ (input) => createRule(input.idea, input.agent, input.setup).pipe(Effect.provide(statusLayer)),
646
+ ).pipe(
647
+ Command.withDescription(
648
+ "Write a rule with a coding agent's help: it suggests whether the rule belongs to this project or the organization's shared rules repo, names it, chooses a RULE.md or, when Jev needs state the file cannot show, a RULE.ts, and says which config context (typeAware, reads) it needs. It finds code in the repo for you to judge, with the lines around it, and drafts the rule from your answers; the draft is checked as adhere validate checks wording. A project rule is written to .adhere/rules/; an organization rule is pushed to the shared repo on a branch for a pull request.",
649
+ ),
650
+ );
651
+
652
+ export const rulesCommand = Command.make("rules").pipe(
653
+ Command.withDescription("Work on this repo's rules."),
654
+ Command.withSubcommands([rulesCreateCommand]),
655
+ );
656
+
657
+ export const postinstallCommand = Command.make("postinstall", {}, () =>
658
+ Effect.gen(function* () {
659
+ const path = yield* Path.Path;
660
+ yield* Console.log(yield* renderInstructions(path.resolve()));
661
+ }),
662
+ ).pipe(Command.withDescription("Render instruction files into a managed block in AGENTS.md."));
663
+
559
664
  /** The whole CLI. Bare `adhere` prints its help. */
560
665
  export const cli = Command.make("adhere").pipe(
561
666
  Command.withDescription("Lint a repository against the rules in .adhere/rules/, judged by Jev."),
@@ -580,10 +685,12 @@ export const cli = Command.make("adhere").pipe(
580
685
  lintCommand,
581
686
  validateCommand,
582
687
  initCommand,
688
+ postinstallCommand,
583
689
  listCommand,
584
690
  installCommand,
585
691
  loginCommand,
586
692
  logoutCommand,
587
693
  skillCommand,
694
+ rulesCommand,
588
695
  ]),
589
696
  );