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
46 changes: 46 additions & 0 deletions .agents/skills/react-doctor/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
name: react-doctor
description: Use when finishing a feature, fixing a bug, before committing React code, or when the user types `/doctor`, asks to scan, triage, or clean up React diagnostics. Covers lint, accessibility, bundle size, architecture. Includes a regression check and a full local-triage workflow that fetches the canonical playbook.
version: "1.1.0"
---

# React Doctor

Scans React codebases for security, performance, correctness, and architecture issues. Outputs a 0–100 health score.

## After making React code changes:

Run `npx react-doctor@latest --verbose --diff` and check the score did not regress.

If the score dropped, fix the regressions before committing.

## For general cleanup or code improvement:

Run `npx react-doctor@latest --verbose` (without `--diff`) to scan the full codebase. Fix issues by severity — errors first, then warnings.

## /doctor — full local triage workflow

When the user types `/doctor`, says "run react doctor", or asks for a full triage / cleanup pass (not just a regression check), fetch the canonical local-triage playbook and follow every step in it:

```bash
curl --fail --silent --show-error \
--header 'Cache-Control: no-cache' \
https://www.react.doctor/prompts/react-doctor-agent.md
```

The playbook is the single source of truth — a scan → filter → triage → fix → validate loop that edits the working tree directly (never commits, never opens PRs). Updating the prompt at its source updates every agent on its next fetch — no skill reinstall needed.

Pair it with the matching per-rule prompts at `https://www.react.doctor/prompts/rules/<plugin>/<rule>.md` (fetched on demand inside the playbook) so each fix uses the canonical, reviewer-tested recipe.

## Command

```bash
npx react-doctor@latest --verbose --diff
```

| Flag | Purpose |
| ----------- | --------------------------------------------- |
| `.` | Scan current directory |
| `--verbose` | Show affected files and line numbers per rule |
| `--diff` | Only scan changed files vs base branch |
| `--score` | Output only the numeric score |
21 changes: 21 additions & 0 deletions .github/workflows/react-doctor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: React Doctor

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

permissions:
contents: read
pull-requests: write
issues: write

concurrency:
group: react-doctor-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
react-doctor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: millionco/react-doctor@main
37 changes: 37 additions & 0 deletions .husky/pre-commit
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1 +1,38 @@
#!/bin/sh

# react-doctor hook start
react_doctor_scan_staged_files() {
if [ -x "./node_modules/.bin/react-doctor" ]; then
"./node_modules/.bin/react-doctor" --staged --fail-on warning
return
fi

if command -v react-doctor >/dev/null 2>&1; then
react-doctor --staged --fail-on warning
return
fi

if command -v pnpm >/dev/null 2>&1; then
pnpm dlx react-doctor@latest --staged --fail-on warning
return
fi

if command -v npx >/dev/null 2>&1; then
npx --yes react-doctor@latest --staged --fail-on warning
return
fi

printf '%s\n' "react-doctor: command not found; skipping staged scan."
}

react_doctor_output=$(mktemp "${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX")
if react_doctor_scan_staged_files > "$react_doctor_output" 2>&1; then
rm -f "$react_doctor_output"
else
cat "$react_doctor_output" >&2
rm -f "$react_doctor_output"
printf '%s\n' "React Doctor found staged regressions." "Run react-doctor --staged --fail-on warning to inspect." "Want them fixed? Ask your agent to run that command and resolve the findings." >&2
exit 1
fi
Comment thread
pedronauck marked this conversation as resolved.
# react-doctor hook end
bunx --no-install lint-staged
357 changes: 298 additions & 59 deletions bun.lock

Large diffs are not rendered by default.

102 changes: 102 additions & 0 deletions internal/e2elane/pre_commit_hook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package e2elane

import (
"context"
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
)

func TestPreCommitHook(t *testing.T) {
t.Parallel()

t.Run("Should fail before lint-staged when react-doctor reports regressions", func(t *testing.T) {
t.Parallel()

repoRoot := repoRoot(t)
workdir := t.TempDir()
hookPath := filepath.Join(workdir, "pre-commit")
hookSource := filepath.Join(repoRoot, ".husky", "pre-commit")

hookContents, err := os.ReadFile(hookSource)
if err != nil {
t.Fatalf("os.ReadFile(%q) error = %v", hookSource, err)
}
if err := os.WriteFile(hookPath, hookContents, 0o755); err != nil {
t.Fatalf("os.WriteFile(%q) error = %v", hookPath, err)
}

binDir := filepath.Join(workdir, "bin")
if err := os.MkdirAll(binDir, 0o755); err != nil {
t.Fatalf("os.MkdirAll(%q) error = %v", binDir, err)
}

markerPath := filepath.Join(workdir, "lint-staged-invoked")
writeExecutable(
t,
filepath.Join(binDir, "react-doctor"),
"#!/bin/sh\nprintf '%s\n' 'react doctor regression' >&2\nexit 1\n",
)
writeExecutable(
t,
filepath.Join(binDir, "bunx"),
"#!/bin/sh\nprintf '%s\n' 'invoked' > \"$BUNX_MARKER\"\nexit 0\n",
)

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
t.Cleanup(cancel)

cmd := exec.CommandContext(ctx, hookPath)
cmd.Dir = workdir
cmd.Env = append(
os.Environ(),
"PATH="+binDir+string(os.PathListSeparator)+os.Getenv("PATH"),
"TMPDIR="+workdir,
"BUNX_MARKER="+markerPath,
)

output, err := cmd.CombinedOutput()
if err == nil {
t.Fatal("expected hook to fail when react-doctor reports regressions")
}
if ctx.Err() != nil {
t.Fatalf("hook execution timed out: %v", ctx.Err())
}

var exitErr *exec.ExitError
if !errors.As(err, &exitErr) {
t.Fatalf("hook error = %T %v, want *exec.ExitError", err, err)
}
if got, want := exitErr.ExitCode(), 1; got != want {
t.Fatalf("hook exit code = %d, want %d", got, want)
}

outputText := string(output)
if !strings.Contains(outputText, "react doctor regression") {
t.Fatalf("hook output = %q, want captured react-doctor stderr", outputText)
}
if !strings.Contains(outputText, "React Doctor found staged regressions.") {
t.Fatalf("hook output = %q, want failure guidance", outputText)
}

_, statErr := os.Stat(markerPath)
if statErr == nil {
t.Fatalf("expected lint-staged marker %q to be absent", markerPath)
}
if !errors.Is(statErr, os.ErrNotExist) {
t.Fatalf("os.Stat(%q) error = %v, want not-exist", markerPath, statErr)
}
})
}

func writeExecutable(t *testing.T, path string, contents string) {
t.Helper()

if err := os.WriteFile(path, []byte(contents), 0o755); err != nil {
t.Fatalf("os.WriteFile(%q) error = %v", path, err)
}
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"test:e2e:runtime": "make test-e2e-runtime",
"test:e2e:web": "make test-e2e-web",
"tests": "bun run test",
"typecheck": "bun run bun:typecheck"
"typecheck": "bun run bun:typecheck",
"doctor": "npx react-doctor@latest"
},
"devDependencies": {
"@commitlint/cli": "^21.0.1",
Expand All @@ -48,6 +49,7 @@
"openapi-typescript": "^7.13.0",
"oxfmt": "^0.50.0",
"oxlint": "^1.65.0",
"react-doctor": "^0.2.14",
"shadcn": "^4.7.0",
"tailwindcss": "^4.3.0",
"turbo": "^2.9.14",
Expand Down
18 changes: 9 additions & 9 deletions packages/site/content/runtime/core/configuration/config-toml.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1240,15 +1240,15 @@ run-enqueue APIs are the execution boundary.
Coordinator launch only applies to workspace-scoped coordinated runs with a stable
`coordination_channel_id`. Global runs and intent-only tasks do not start coordinators in the MVP.

| Field | Type | Default | Valid values | Description |
| ----------------------------------- | -------- | ------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled` | boolean | `false` | `true` or `false` | Allows coordinator bootstrap after a coordinated workspace run is enqueued. |
| `agent_name` | string | `coordinator` | Non-empty when enabled. | Agent definition used for managed coordinator sessions. |
| `provider` | string | empty | Empty or a configured provider key. | Optional provider override for the coordinator agent. |
| `model` | string | empty | Empty or provider-supported model string. | Optional model override. If set, `provider` or provider default resolution must exist. |
| `default_ttl` | duration | `2h` | `1m` through `24h`. | Lifetime assigned to managed coordinator sessions. |
| `max_children` | integer | `5` | `1` through `5`. | Maximum safe-spawn children a coordinator may hold at once. |
| `max_active_sessions_per_workspace` | integer | `5` | Positive integer. | Caps concurrent autonomy-managed sessions (coordinator + spawned workers) per workspace. Coordinator uniqueness is enforced by the daemon singleton check. |
| Field | Type | Default | Valid values | Description |
| ----------------------------------- | -------- | ------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled` | boolean | `false` | `true` or `false` | Allows coordinator bootstrap after a coordinated workspace run is enqueued. |
| `agent_name` | string | `coordinator` | Non-empty when enabled. | Agent definition used for managed coordinator sessions. |
| `provider` | string | empty | Empty or a configured provider key. | Optional provider override for the coordinator agent. |
| `model` | string | empty | Empty or provider-supported model string. | Optional model override. If set, `provider` or provider default resolution must exist. |
| `default_ttl` | duration | `2h` | `1m` through `24h`. | Lifetime assigned to managed coordinator sessions. |
| `max_children` | integer | `5` | `1` through `5`. | Maximum safe-spawn children a coordinator may hold at once. |
| `max_active_sessions_per_workspace` | integer | `5` | Positive integer. | Caps concurrent autonomy-managed sessions (coordinator + spawned workers) per workspace. Coordinator uniqueness is enforced by the daemon singleton check. |

Provider and model resolution is intentionally narrow. The daemon applies:

Expand Down
2 changes: 0 additions & 2 deletions packages/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
"dependencies": {
"@agh/ui": "workspace:*",
"@mdx-js/mdx": "^3.1.1",
"@orama/orama": "^3.1.18",
"@remotion/player": "^4.0.462",
"@vercel/analytics": "^2.0.1",
"@vercel/speed-insights": "^2.0.0",
Expand All @@ -33,7 +32,6 @@
"next": "^16.2.6",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"reading-time": "^1.5.0",
"rehype-pretty-code": "^0.14.3",
"remark": "^15.0.1",
"remark-gfm": "^4.0.1",
Expand Down
36 changes: 36 additions & 0 deletions packages/ui/src/components/__tests__/progress.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { render } from "@testing-library/react";
import { describe, expect, it } from "vitest";

import { Progress, ProgressLabel } from "../progress";

function getProgressIndicator(container: HTMLElement): HTMLElement {
const indicator = container.querySelector('[data-slot="progress-indicator"]');
if (!(indicator instanceof HTMLElement)) {
throw new Error("Expected progress indicator to render");
}
return indicator;
}

describe("Progress", () => {
it("Should apply the requested tone without leaking the accent background", () => {
const { container } = render(
<Progress tone="success" value={42}>
<ProgressLabel>Uploading dataset</ProgressLabel>
</Progress>
);

const indicator = getProgressIndicator(container);
expect(indicator).toHaveClass("bg-success");
expect(indicator).not.toHaveClass("bg-accent");
});

it("Should keep accent as the default indicator tone", () => {
const { container } = render(
<Progress value={64}>
<ProgressLabel>Uploading dataset</ProgressLabel>
</Progress>
);

expect(getProgressIndicator(container)).toHaveClass("bg-accent");
});
});
3 changes: 2 additions & 1 deletion packages/ui/src/components/__tests__/sonner.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { act, render, waitFor } from "@testing-library/react";
import { describe, expect, it } from "vitest";

import { Toaster, toast } from "../sonner";
import { Toaster } from "../sonner";
import { toast } from "../sonner-toast";

describe("Toaster", () => {
it("Should mount the Sonner root region", () => {
Expand Down
28 changes: 28 additions & 0 deletions packages/ui/src/components/alert-variants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { cva } from "class-variance-authority";

const alertVariants = cva(
"group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-small-body has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
Comment thread
pedronauck marked this conversation as resolved.
{
variants: {
variant: {
default: "bg-canvas-soft border-line text-fg",
neutral:
"border-neutral/20 bg-neutral-tint text-fg *:data-[slot=alert-description]:text-muted",
danger:
"border-danger/20 bg-danger-tint text-danger *:data-[slot=alert-description]:text-danger/85",
warning:
"border-warning/20 bg-warning-tint text-warning *:data-[slot=alert-description]:text-warning/85",
success:
"border-success/20 bg-success-tint text-success *:data-[slot=alert-description]:text-success/85",
info: "border-info/20 bg-info-tint text-info *:data-[slot=alert-description]:text-info/85",
accent:
"border-accent/20 bg-accent-tint text-accent *:data-[slot=alert-description]:text-accent/85",
},
},
defaultVariants: {
variant: "default",
},
}
);

export { alertVariants };
30 changes: 3 additions & 27 deletions packages/ui/src/components/alert.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,8 @@
import { cva, type VariantProps } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import type * as React from "react";

import { cn } from "../lib/utils";

const alertVariants = cva(
"group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-small-body has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-canvas-soft border-line text-fg",
neutral:
"border-neutral/20 bg-neutral-tint text-fg *:data-[slot=alert-description]:text-muted",
danger:
"border-danger/20 bg-danger-tint text-danger *:data-[slot=alert-description]:text-danger/85",
warning:
"border-warning/20 bg-warning-tint text-warning *:data-[slot=alert-description]:text-warning/85",
success:
"border-success/20 bg-success-tint text-success *:data-[slot=alert-description]:text-success/85",
info: "border-info/20 bg-info-tint text-info *:data-[slot=alert-description]:text-info/85",
accent:
"border-accent/20 bg-accent-tint text-accent *:data-[slot=alert-description]:text-accent/85",
},
},
defaultVariants: {
variant: "default",
},
}
);
import { alertVariants } from "./alert-variants";

type AlertProps = React.ComponentProps<"div"> & VariantProps<typeof alertVariants>;

Expand Down Expand Up @@ -100,5 +76,5 @@ function AlertActions({ className, ...props }: React.ComponentProps<"div">) {
);
}

export { Alert, AlertAction, AlertActions, AlertDescription, AlertMeta, AlertTitle, alertVariants };
export { Alert, AlertAction, AlertActions, AlertDescription, AlertMeta, AlertTitle };
export type { AlertProps };
Loading
Loading