Swap the acknowledgment requirement for a self-revealing supply-chain
awareness banner: read the AGENTS.md your loops pull before you run them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Enrich the per-scan Sentry run span with the full run/CI/project/outcome
context (scan mode, score, diagnostics by severity/category, top rule,
lint/dead-code state, and in CI the GitHub event, official-action marker,
forwarded action inputs, and the pull-request gate) so usage and CI behavior
are queryable as spans instead of pre-aggregated counters.
Mint a random per-run runId on the Sentry run context (never a tag or metric
dimension) to correlate the spans of a single run; a workspace invocation's
project scans share one id and per-project span attributes disambiguate. The
wide event's wouldBlock/outcome/exitCode mirror the CLI's real fail-on gate
(ciFailure surface, and no gate in score-only mode). The official GitHub
Action forwards its inputs so action configuration is visible in telemetry.
Telemetry stays anonymized — no repo, owner, username, branch, or path is
sent to Sentry — and --no-score / --no-telemetry opts out.
Co-authored-by: Cursor <cursoragent@cursor.com>
The composite Action is versioned independently from the npm packages via
v-prefixed semver tags (vX.Y.Z) plus a floating major (vN), distinct from
the Changesets `<pkg>@X.Y.Z` tags. Documents the bump rules, the signed-tag
gotcha, and the release/floating-major commands.
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds `layerOtlp` in `core/src/observability.ts` and provides it to
both `inspect()` (CLI) and `diagnose()` (programmatic API). The
layer is a strict no-op unless BOTH env vars are set:
REACT_DOCTOR_OTLP_ENDPOINT e.g. https://api.axiom.co
REACT_DOCTOR_OTLP_AUTH_HEADER e.g. Bearer <token>
When configured, it provides `Otlp.layerJson({...})` from
`effect/unstable/observability/Otlp` with
`@effect/platform-node/NodeHttpClient.layerUndici` as the
transport. Every existing `Effect.fn("Service.method")` span and
the top-level `Effect.withSpan("runInspect", ...)` ship to the
configured backend without any further wiring.
Mirrors the canonical eval pattern from
`react-doctor-evals/src/Observability.ts → layerAxiom`. Closes the
"Bigger bet #7" item from the prioritized-improvements list.
**No behavior change for default users** — the layer drops to
`Layer.empty` when env vars are absent, so the unconfigured path
has zero network traffic and zero new dependencies at runtime.
Tests cover the no-op path. AGENTS.md and the CLI README document
the opt-in.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor: collapse @react-doctor/types and @react-doctor/project-info into core
Eliminates two private workspace packages whose only role was to
namespace shared TS types and project-discovery helpers — neither
was a real consumer boundary, and the indirection cost a `workspace:*`
edge per leaf package without saving any code.
**Layout**
- `packages/types/` → `packages/core/src/types/` (10 files, pure TS types)
- `packages/project-info/` → `packages/core/src/project-info/`
(27 files, discoverProject + framework detection + monorepo helpers)
- `packages/types/src/react-native-dependency-names.ts` →
`packages/oxlint-plugin-react-doctor/src/react-native-dependency-names.ts`
(the canonical RN-aware-package list lives next to its heaviest
consumer — the rule gate — and is re-exported from core to avoid
the rule-plugin ↔ core dependency cycle the previous types package
was specifically designed to break).
The two packages are deleted from the workspace; every consumer
(`react-doctor`, `api`, `oxlint-plugin-react-doctor`, `actions/review`,
+130 source/test files) now imports from `@react-doctor/core` directly.
**Errors cleanup**
Dropped the legacy plain-class `ReactDoctorError` and `isReactDoctorError`
from `project-info`. The four narrow errors (`ProjectNotFoundError`,
`NoReactDependencyError`, `PackageJsonNotFoundError`,
`AmbiguousProjectError`) now extend `Error` directly — the only valid
hierarchy is the tagged `Schema.TaggedErrorClass` `ReactDoctorError`
from `core/errors.ts`, which has been the canonical one since the
Effect v4 refactor. Added `isProjectDiscoveryError` as a narrow type
guard for the four leaves.
The discovery errors are thrown synchronously BEFORE the Effect
runtime takes over (at `diagnose()` / CLI entry points); inside the
runtime, the Project service's `translateProjectInfoError` converts
each one to its tagged-reason equivalent.
**Schema vs type name collisions**
`schemas.ts` exports `Diagnostic` / `JsonReport` / `JsonReportMode` /
… as Schema.Class values, while `types/` exports the same names as
TS types. The core barrel re-exports the TS-type versions
(broad-strokes API) and exposes the Schema classes via a new
`@react-doctor/core/schemas` subpath export for the two callers
that need them (`Reporter` service, smoke validator, schema tests).
**Validation**
- `pnpm typecheck` ✓
- `pnpm lint` ✓
- `pnpm format` ✓
- `pnpm test` ✓ (1449 passing, 3 skipped — no regressions)
- `pnpm smoke:json-report` ✓
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(core): keep RN-dependency constants out of types barrel to avoid pulling oxlint plugin into discovery
Bugbot caught (medium severity): re-exporting `isReactNativeDependencyName` from
`oxlint-plugin-react-doctor` via the core types barrel meant every consumer of
`@react-doctor/core` (including `discoverProject` / `discoverReactSubprojects`)
would load the entire 286-rule plugin at module-init.
Adds a tiny standalone leaf at `core/src/project-info/internal-rn-dependency-names.ts`
(zero imports, just the constants the workspace-discovery gate needs) and switches
`is-package-json-react-native-aware.ts` to use it. The types barrel no longer
re-exports the plugin-side constants — rule consumers import them directly from
`oxlint-plugin-react-doctor`.
The duplication is intentional and documented at both files; the two lists are
short enough that a future regression test in the plugin can observe the union.
Validated: typecheck + test (1449 pass) + lint + format all green.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Deep study of ~/Developer/react-doctor-evals against our current
architecture. The eval pioneered the Effect v4 patterns this repo
is modeled on; this PR brings over the lasting structural and
observability wins.
### Lasting docs (the main deliverable)
- **`.specs/inspect-pipeline.md`** — full spec for `runInspect`, the
streaming orchestrator. Documents the 9-service wiring, the Stream
shape, the failure-folding pattern (Ref + Stream.catchTag), the
layer-stack override knobs (`--offline` / `--no-lint` / etc.), the
per-element pipeline, surface filtering, hooks vs services, the
three-layer call story (`inspect()` → `diagnose()` → `runInspect`),
and the playbook for adding a new diagnostic source or lint backend.
Mirrors the eval's `.specs/github-app.md` format — design constraints
→ module map → numbered sections → locked decisions → open follow-ups.
- **`CONTEXT.md`** — domain glossary. Diagnostic, InspectOutput, Service,
Layer stack, per-element pipeline, surface filter, mode, score,
composite action, ReactDoctorError vs legacy thrown errors, Context.Reference,
branded paths. Pairs with AGENTS.md (which is conventions) and
`.specs/inspect-pipeline.md` (which is the deepest dive).
### `Effect.fn("Service.method")` for free observability
Every non-trivial service-method body is now wrapped in
`Effect.fn("Service.method")` — the canonical eval pattern from
`react-doctor-evals/src/Runner.ts` and `WorkerPool.ts`. Production
cost is zero when no tracer layer is provided. With
`Otlp.layerJson(...)` from `effect/unstable/observability/Otlp`
(eval ref: `Observability.ts → layerAxiom`), users see one named
span per service call.
Wrapped today:
- `Project.discover` (was `Effect.try`)
- `Config.resolve` (was direct `Cache.get`)
- `DeadCode.run` (was `Stream.unwrap(Effect.tryPromise)`)
- `Linter.run` (was `Stream.unwrap(Effect.gen)`)
- `Score.compute` (was `Effect.promise`)
- `runInspect` itself, via `Effect.withSpan("runInspect", { attributes })`
so the orchestrator is the parent of the per-service-method spans.
### AGENTS.md updates
- New `Effect.fn("Service.method")` rule in the Services section.
- New `Config.redacted` guidance for secret-shaped env vars (will land
with the first secret-shaped config; the pattern is documented now
so it lands correctly).
- New `Observability` subsection documenting `Effect.withSpan` +
`Effect.fn` composition, attribute-key naming convention, and the
`Otlp.layerJson` opt-in path.
### Not in this PR (tracked for follow-up)
- **Migrate services to canonical `make:` pattern**: the eval uses
`Context.Service<Self>()("name", { make: Effect.gen(...) }) {
static readonly layer = Layer.effect(this, this.make)
}` while we use the shape-in-type-param form
`Context.Service<Self, Shape>()("name")` + Layer.effect outside.
Both are valid v4; the eval's form is more idiomatic for services
that have init work. Defer the wholesale conversion — the current
shape works and the refactor would churn every service file.
- **Adopt `Layer.mock(Service, { method })` in tests**: cleaner test
layers than hand-rolled `Layer.succeed(Service, Service.of({ ... }))`.
Eval pattern from `react-doctor-evals/src/Runner.ts → layerTest`.
- **`Effect.Command` from `effect/unstable/cli`** to replace commander.
Bigger refactor; tracked for the v0.3 CLI overhaul.
- **`Otlp.layerJson` end-to-end**: the spans are now in place, but
no actual exporter layer is wired in production. End users that
want traces would add `Otlp.layerJson({ baseUrl, resource, headers })`
+ `NodeHttpClient.layerUndici` themselves. Document the snippet
in the README when there's demand.
Co-authored-by: Cursor <cursoragent@cursor.com>
Deep audit against the Effect v4 patterns documented in
\`tmp/effect/.patterns/\` and \`tmp/effect/migration/\` (the cloned
reference repo). Three real v3-style holdovers fixed:
### 1. Manual instanceof reason dispatch → Effect.catchReasons
\`get-diff-files.ts\`, \`api/diagnose.ts\`, and \`inspect.ts\`'s
\`restoreLegacyThrow\` all had imperative \`switch (reason._tag)\`
ladders behind an \`if (cause instanceof ReactDoctorError)\` JS
catch block. v4 ships \`Effect.catchReasons(errorTag, cases, orElse?)\`
exactly for this — exhaustive, type-safe narrowing of a
\`Schema.TaggedErrorClass\` reason union, no \`instanceof\` checks.
- \`getDiffInfo\` →
\`.pipe(Effect.catchReasons("ReactDoctorError", { GitBaseBranchInvalid, GitBaseBranchMissing }))\`
- \`diagnose\` + \`inspect\`'s legacy translation →
\`.pipe(Effect.catchReasons("ReactDoctorError", { NoReactDependency, ProjectNotFound, AmbiguousProject }, orElse))\`
The \`orElse\` branch preserves the legacy "any other tagged
reason throws as a plain Error with the tagged-class message
string" contract that grep-stderr callers depend on.
The recovered branches use \`Effect.die(new LegacyClass(...))\` so
the thrown class lands on \`Effect.runPromise\`'s reject channel
unchanged.
### 2. try/catch inside Effect.gen → Effect.try + Effect.orElseSucceed
\`render-summary.ts\`'s "write the dump, swallow if it fails"
block had a literal \`try { ... yield* Console.log(...) } catch {}\`
inside an \`Effect.gen\` — a hard v4 anti-pattern (the catch only
runs for sync throws, never for Effect failures, so the shape is
misleading and the v4 patterns doc forbids it).
Replaced with:
Effect.try({ try: () => writeDiagnosticsDirectory(...), catch: (e) => e })
.pipe(Effect.orElseSucceed(() => null))
### 3. AGENTS.md updates
New \`Error dispatch / recovery — v4 idioms\` and \`Generator hygiene\`
subsections in the Effect v4 Conventions block. Documents:
- When to use \`catchReasons\` / \`catchTag\` / \`catch\` / \`die\`
- Hard rule: NEVER \`try/catch\` inside \`Effect.gen\` — use
\`Effect.try\` and \`Effect.orElseSucceed\` / \`Effect.catch\` instead
- \`return yield* Effect.fail(...)\` for terminal effects
- When \`Effect.fnUntraced\` is justified (hot paths only; not
currently needed in this codebase)
### Misc
- Removed now-unused \`isReactDoctorError\` + \`InspectOutput\`
imports from \`inspect.ts\`.
- Removed unused \`ReactDoctorError\` import from \`api/diagnose.ts\`
(catchReasons does the narrowing).
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor: drop custom Logger service; adopt Effect.Console throughout
Pivot to Effect v4's built-in `Console` (`effect/Console`) as the
canonical logging primitive. The custom `Logger` Context.Service and
`LoggerWriter` interface (introduced in PRs #421 / #427) are removed
in favor of Effect's `Console` Context.Reference (default sink:
`globalThis.console`).
### Renderers → Effect-typed
All 6 renderer files now return `Effect.Effect<void>` and use
`yield* Console.log(...)` / `Console.warn(...)` / `Console.error(...)`
instead of taking a `logger: LoggerWriter` parameter:
- `print-branded-header.ts` Effect.Effect<void>
- `render-score-header.ts` printScoreHeader, printNoScoreHeader,
printBrandingOnlyHeader
- `render-summary.ts` printSummary
- `render-diagnostics.ts` printDiagnostics (helpers now build
pure string arrays; IO happens at
a single Console.log boundary)
- `render-project-detection.ts` printProjectDetection
- `handle-error.ts` handleErrorEffect (+ sync façade)
`inspect.ts` yield*s the renderers from inside its existing Effect.gen
program. Silent mode swaps the Console reference for a no-op:
`Effect.provideService(Console.Console, silentConsole)`.
### Imperative CLI helpers → `cliLogger` shim
The non-Effect-typed CLI helpers (resolve-diff-mode, select-projects,
run-explain, install-skill, prompts, exit-gracefully, resolve-fail-on-level,
json-mode, the legacy paths in cli/commands/inspect.ts) import
`cliLogger as logger` from `cli/utils/cli-logger.ts`. The shim is a
thin sync façade that calls `Effect.runSync(Console.X(...))` so every
log line drains into the same Console pipeline as the Effect-typed
renderers.
### JSON-mode silencing
`enableJsonMode` now monkey-patches `globalThis.console.log` /
`error` / `warn` / `info` / `debug` to no-ops at startup. Effect's
Console reference resolves to `globalThis.console` by default, so
this silences every `yield* Console.*` from the orchestrator and
every `cliLogger.*` from the CLI helpers without any conditional
checks at the call sites.
### Files deleted
- packages/core/src/services/logger.ts
- packages/core/src/logger.ts
- packages/core/tests/services/logger.test.ts
### Core: imperative warn calls
`load-config.ts`, `read-ignore-file.ts`, and `resolve-config-root-dir.ts`
were the three core helpers still importing the deprecated `logger`
shim. They now call `Effect.runSync(Console.warn(...))` directly.
### Tests
Two tests previously did `setLoggerSilent(true)` in `beforeEach`.
They now use `silenceConsoleForTest()` from a new test helper
(`tests/helpers/silence-console.ts`) that monkey-patches the global
console for the duration of the test and returns a restore fn.
### AGENTS.md
Updated the Effect v4 Conventions section with a new `Console / logging`
subsection documenting the always-use-Effect.Console rule, the
`cliLogger` bridge for imperative callers, and the silent-mode pattern.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli-logger): restore highlighter coloring for warn/error/info (bugbot)
Bugbot regression on #432: the new `cliLogger` shim and the
Effect-typed `handleErrorEffect` / `inspect.ts` finalize block all
dropped the `highlighter.warn` / `.error` / `.info` calls that the
legacy `consoleLogger` applied to every message. Result: warnings,
errors, and info lines rendered as plain uncolored text.
Restore the color contract:
- cliLogger.warn → highlighter.warn (yellow)
- cliLogger.error → highlighter.error (red)
- cliLogger.info → highlighter.info (cyan)
- handleErrorEffect block → red across every line
- inspect.ts `Lint checks failed` + `score may be incomplete`
+ `No issues detected, but X failed` → wrapped in highlighter
- resolve-oxlint-node.ts consoleWarn → highlighter.warn
Effect's `Console.warn` / `Console.error` / `Console.info` route to
the matching `console.X` method but don't add color of their own
(unlike browser DevTools), so the wrapping has to happen at the
caller side.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
The runtime is live on main (#405, #410, #411, #412, #414, #417).
Codify the patterns so subsequent agents (and humans) write code that
matches what landed.
## AGENTS.md additions
- Package Layout section enumerating each workspace package's role
- Effect v4 Conventions section covering:
- Import style (always 'import * as X from "effect/X"', never
the umbrella 'from "effect"')
- Errors as tagged Schema.TaggedErrorClass, get message() getters,
Cause.pretty for opaque causes, _tag-based dispatch over string
substring checks
- Services: Context.Service<Self, Interface>()("react-doctor/Name",
{ make }); Effect.fnUntraced for hot paths; Service.of(...);
Layer.effect vs Layer.succeed; object args when >1 parameter
- Layer naming conventions (layerNode, layerOf, layerInMemory,
layerCapture, layerNoop, layerComposite, implementation-specific
layerOxlint / layerHttp / layerNdjson / layerOra)
- Schemas: Schema.Class for wire records, Schema.Literals plural for
unions, Schema.brand via .pipe(), interfaces for arg types to avoid
runtime encode cost
- Ambient config via Context.Reference (core/src/refs.ts)
- Testing section pointing at the per-package tests/ dirs and the
vite-plus/test framework
- Reference reading pointing at tmp/effect/.patterns/ and
~/Developer/react-doctor-evals/src/
## TODOS.md additions
New 'Effect v4 runtime follow-ups' section captures the deferred
consolidation work flagged in the per-PR descriptions:
- Collapse @react-doctor/project-info into core (blocked by
ReactDoctorError name collision between the legacy thrown class
and the new tagged runtime wrapper — decide rename direction at
v1.0)
- Collapse @react-doctor/types into core (same collision + circular
dep risk via oxlint-plugin's use of isReactNativeDependencyName)
- Extract @react-doctor/cli workspace package (mechanical file move
— ~40 cli/utils/* sites)
- Decompose run-oxlint into Stream combinators (kills the
Effect.runSync HACK in Linter.layerOxlint)
- Production Reporter layer choice (currently layerNoop because the
orchestrator returns the array via Stream.runCollect; future LSP /
watch mode would need a different layer)
Co-authored-by: Cursor <cursoragent@cursor.com>
The rule flagged every `indigo-*` / `gray-*` / `slate-*` token in a
className as "the Tailwind template default" and pushed users toward
zinc / neutral / stone (or a brand color). In practice the choice
between those neutral scales is a deliberate brand decision rather
than a code-health signal, and the Radix-Colors carve-out it shipped
with (anchoring the regex to the canonical 50…950 stops) didn't help
teams that customize the default Tailwind palette to a different
numeric scale.
Removes the rule file, the now-unused TAILWIND_DEFAULT_PALETTE_*
/ TAILWIND_PALETTE_UTILITY_PREFIXES constants, the entry in the
generated rule-registry, and the dedicated regression-test describe
block. Also adds an AGENTS.md rule forbidding agents from authoring
changesets or editing CHANGELOG.md — those belong to humans at
release time.
Closes MIL-1543.
Co-authored-by: Cursor <cursoragent@cursor.com>