four-doctors 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ArtisansCompany
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # doctors
2
+
3
+ The whole suite, one command. Runs `rails-doctor`, `design-doctor`, `qa-doctor`, and (optionally) `react-doctor`, and aggregates the scores.
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ npx -y doctors@latest scan .
9
+ ```
10
+
11
+ Auto-runs the doctors that apply to your project:
12
+
13
+ | Doctor | Runs when |
14
+ |-----------------|-----------------------------------------------------|
15
+ | `rails-doctor` | `Gemfile` + `config/routes.rb` exist |
16
+ | `design-doctor` | `package.json` has react / inertia / tanstack-router |
17
+ | `qa-doctor` | only with `--browser` (needs a running dev server) |
18
+ | `react-doctor` | only with `--react-doctor` |
19
+
20
+ ## Examples
21
+
22
+ ```bash
23
+ # Static-only (default) — Rails + React audit
24
+ npx -y doctors scan .
25
+
26
+ # Add browser QA against a running app
27
+ npx -y doctors scan . --browser --url http://localhost:3000
28
+
29
+ # Add the LLM vision pass
30
+ npx -y doctors scan . --vision --url http://localhost:3000
31
+
32
+ # Full sweep — every doctor, every flag
33
+ npx -y doctors scan . --browser --vision --fill --url http://localhost:3000 --react-doctor
34
+
35
+ # CI gate — fail under suite-average 80
36
+ npx -y doctors scan . --min-score 80
37
+ ```
38
+
39
+ ## Output
40
+
41
+ ```
42
+ doctors — suite report
43
+
44
+ ok rails-doctor 98/100 Great (895ms)
45
+ ok design-doctor 83/100 Great (1467ms)
46
+
47
+ Suite average: 91/100 across 2 doctors
48
+ ```
49
+
50
+ `--markdown` for PR-friendly output:
51
+
52
+ ```
53
+ ## doctors report
54
+
55
+ **Suite average: 91/100** across 2 doctors.
56
+
57
+ | Doctor | Status | Score | Notes |
58
+ |---|---|---|---|
59
+ | `rails-doctor` | ok | 98/100 | Great |
60
+ | `design-doctor` | ok | 83/100 | Great |
61
+ ```
62
+
63
+ ## Install all the skills
64
+
65
+ ```bash
66
+ npx -y doctors install # installs the meta skill
67
+ npx -y rails-doctor install
68
+ npx -y design-doctor install
69
+ npx -y qa-doctor install
70
+ ```
71
+
72
+ After that, your agent can call any doctor by name — and `doctors` itself when you want everything in one shot.
73
+
74
+ ## Source
75
+
76
+ - [doctors](https://github.com/artisanscompany/doctors)
77
+ - [rails-doctor](https://github.com/artisanscompany/rails-doctor)
78
+ - [design-doctor](https://github.com/artisanscompany/design-doctor)
79
+ - [qa-doctor](https://github.com/artisanscompany/qa-doctor)
80
+ - [react-doctor](https://github.com/millionco/react-doctor) (third-party)
81
+
82
+ ## License
83
+
84
+ MIT.
package/bin/doctors.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ require("../dist/cli.js").start(process.argv.slice(2));
package/dist/cli.js ADDED
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.start = start;
4
+ const detect_js_1 = require("./detect.js");
5
+ const orchestrator_js_1 = require("./orchestrator.js");
6
+ const tty_js_1 = require("./reporters/tty.js");
7
+ const markdown_js_1 = require("./reporters/markdown.js");
8
+ const installer_js_1 = require("./installer.js");
9
+ const version_js_1 = require("./version.js");
10
+ const USAGE = `
11
+ Usage: doctors <command> [options]
12
+
13
+ Commands:
14
+ scan [path] Run every applicable doctor and aggregate the scores.
15
+ install Install all four agent skills (rails-doctor, design-doctor,
16
+ qa-doctor, doctors) into detected agent dirs.
17
+ version Print version
18
+ help Show this help
19
+
20
+ Default doctors run automatically when their target is detected:
21
+ rails-doctor if Gemfile + config/routes.rb exist
22
+ design-doctor if package.json has react / @inertiajs/react / @tanstack/react-router
23
+
24
+ Opt-in:
25
+ --browser Also run qa-doctor (needs a running dev server).
26
+ --vision Also run design-doctor's vision pass (needs running app).
27
+ --fill Pass --fill through to qa-doctor (smart-fill forms).
28
+ --url URL Forwarded to qa-doctor and design-doctor's vision pass.
29
+ --diff [BASE] Forwarded to rails-doctor and design-doctor.
30
+ --react-doctor Also run react-doctor (third-party).
31
+ --markdown Emit combined markdown report (else TTY).
32
+ --no-color Disable ANSI colours.
33
+
34
+ CI gates:
35
+ --fail-on LEVEL error|warning|none — fails if any doctor exits non-zero
36
+ at that level. (Currently the meta uses minScore only.)
37
+ --min-score N Fail if the suite average is below N.
38
+ `;
39
+ function start(argv) {
40
+ const args = argv.slice();
41
+ if (args.length === 0) {
42
+ process.stdout.write(USAGE);
43
+ return;
44
+ }
45
+ const head = args[0];
46
+ if (head === "version" || head === "-v" || head === "--version") {
47
+ console.log(`doctors ${version_js_1.VERSION}`);
48
+ return;
49
+ }
50
+ if (head === "help" || head === "-h" || head === "--help") {
51
+ process.stdout.write(USAGE);
52
+ return;
53
+ }
54
+ if (head === "install")
55
+ return runInstall(args.slice(1));
56
+ if (head === "scan")
57
+ return runScan(args.slice(1));
58
+ return runScan(args);
59
+ }
60
+ function runInstall(args) {
61
+ const dryRun = args.includes("--dry-run");
62
+ const out = (0, installer_js_1.install)({ dryRun });
63
+ if (out.length === 0) {
64
+ console.log("No agent skill directories detected.");
65
+ return;
66
+ }
67
+ for (const r of out)
68
+ console.log(`${r.copied ? "✓" : " "} ${r.dest}${dryRun ? " (dry-run)" : ""}`);
69
+ console.log("");
70
+ console.log("To install the individual doctors as skills too:");
71
+ console.log(" npx -y rails-doctor@latest install");
72
+ console.log(" npx -y design-doctor@latest install");
73
+ console.log(" npx -y qa-doctor@latest install");
74
+ }
75
+ function runScan(args) {
76
+ const opts = parseScan(args);
77
+ const det = (0, detect_js_1.detect)(opts.path);
78
+ (async () => {
79
+ const runs = await (0, orchestrator_js_1.runDoctors)(det, {
80
+ vision: opts.vision,
81
+ browser: opts.browser,
82
+ fillForms: opts.fillForms,
83
+ reactDoctor: opts.reactDoctor,
84
+ url: opts.url,
85
+ diff: opts.diff,
86
+ failOn: opts.failOn,
87
+ minScore: opts.minScore,
88
+ });
89
+ if (opts.markdown)
90
+ process.stdout.write((0, markdown_js_1.renderMarkdown)(runs) + "\n");
91
+ else
92
+ (0, tty_js_1.renderTty)(runs, { noColor: opts.noColor });
93
+ const scored = runs.filter((r) => r.enabled && typeof r.score === "number");
94
+ if (opts.minScore !== null && scored.length > 0) {
95
+ const avg = Math.round(scored.reduce((s, r) => s + (r.score ?? 0), 0) / scored.length);
96
+ if (avg < opts.minScore)
97
+ process.exit(1);
98
+ }
99
+ process.exit(0);
100
+ })();
101
+ }
102
+ function parseScan(args) {
103
+ const opts = {
104
+ path: ".",
105
+ vision: false,
106
+ browser: false,
107
+ fillForms: false,
108
+ reactDoctor: false,
109
+ url: null,
110
+ diff: null,
111
+ markdown: false,
112
+ noColor: !process.stdout.isTTY,
113
+ failOn: "none",
114
+ minScore: null,
115
+ };
116
+ for (let i = 0; i < args.length; i++) {
117
+ const a = args[i];
118
+ switch (a) {
119
+ case "--vision":
120
+ opts.vision = true;
121
+ break;
122
+ case "--browser":
123
+ opts.browser = true;
124
+ break;
125
+ case "--fill":
126
+ opts.fillForms = true;
127
+ break;
128
+ case "--react-doctor":
129
+ opts.reactDoctor = true;
130
+ break;
131
+ case "--url":
132
+ opts.url = args[++i] ?? null;
133
+ break;
134
+ case "--diff": {
135
+ const next = args[i + 1];
136
+ if (next && !next.startsWith("--")) {
137
+ opts.diff = next;
138
+ i++;
139
+ }
140
+ else {
141
+ opts.diff = "main";
142
+ }
143
+ break;
144
+ }
145
+ case "--markdown":
146
+ opts.markdown = true;
147
+ break;
148
+ case "--no-color":
149
+ opts.noColor = true;
150
+ break;
151
+ case "--fail-on": {
152
+ const v = args[++i];
153
+ if (v === "error" || v === "warning" || v === "none")
154
+ opts.failOn = v;
155
+ break;
156
+ }
157
+ case "--min-score":
158
+ opts.minScore = parseInt(args[++i] ?? "0", 10);
159
+ break;
160
+ default:
161
+ if (!a.startsWith("--"))
162
+ opts.path = a;
163
+ }
164
+ }
165
+ return opts;
166
+ }
package/dist/detect.js ADDED
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detect = detect;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ function detect(root) {
7
+ const rootAbs = (0, node_path_1.resolve)(root);
8
+ const hasRails = (0, node_fs_1.existsSync)((0, node_path_1.join)(rootAbs, "Gemfile")) && (0, node_fs_1.existsSync)((0, node_path_1.join)(rootAbs, "config", "routes.rb"));
9
+ const pkgPath = (0, node_path_1.join)(rootAbs, "package.json");
10
+ let hasFrontend = false;
11
+ let hasReactDoctorTarget = false;
12
+ if ((0, node_fs_1.existsSync)(pkgPath)) {
13
+ try {
14
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)(pkgPath, "utf8"));
15
+ const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
16
+ hasFrontend = !!(deps["react"] || deps["@inertiajs/react"] || deps["@tanstack/react-router"] || deps["next"] || deps["vue"]);
17
+ hasReactDoctorTarget = !!deps["react"];
18
+ }
19
+ catch { }
20
+ }
21
+ return { root: rootAbs, hasRails, hasFrontend, hasReactDoctorTarget };
22
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.install = install;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const TARGET_DIRS = [
7
+ "~/.claude/skills",
8
+ "~/.agents/skills",
9
+ "~/.cursor/skills",
10
+ "~/.codeium/windsurf/skills",
11
+ "~/.config/github-copilot/skills",
12
+ "~/.config/opencode/skills",
13
+ ];
14
+ function install(opts = {}) {
15
+ const skillSrc = (0, node_path_1.resolve)(__dirname, "..", "skills", "doctors");
16
+ if (!(0, node_fs_1.existsSync)(skillSrc))
17
+ throw new Error(`Skill source missing at ${skillSrc}`);
18
+ const home = process.env.HOME || "";
19
+ const out = [];
20
+ for (const target of TARGET_DIRS) {
21
+ const expanded = target.replace(/^~/, home);
22
+ const parent = (0, node_path_1.dirname)(expanded);
23
+ if (!(0, node_fs_1.existsSync)(parent))
24
+ continue;
25
+ const dest = (0, node_path_1.join)(expanded, "doctors");
26
+ if (opts.dryRun) {
27
+ out.push({ dest, copied: false });
28
+ continue;
29
+ }
30
+ (0, node_fs_1.mkdirSync)(dest, { recursive: true });
31
+ for (const file of (0, node_fs_1.readdirSync)(skillSrc)) {
32
+ (0, node_fs_1.copyFileSync)((0, node_path_1.join)(skillSrc, file), (0, node_path_1.join)(dest, file));
33
+ }
34
+ out.push({ dest, copied: true });
35
+ }
36
+ return out;
37
+ }
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runDoctors = runDoctors;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ async function runDoctors(detected, opts) {
8
+ const runs = [];
9
+ // rails-doctor — when Rails is present
10
+ runs.push(await runOne({
11
+ name: "rails-doctor",
12
+ enabled: detected.hasRails,
13
+ reason: detected.hasRails ? "Gemfile + config/routes.rb detected" : "no Gemfile",
14
+ cwd: detected.root,
15
+ cmd: "npx",
16
+ args: ["-y", "rails-doctor@latest", "scan", ".", "--json", ...(opts.diff ? ["--diff", opts.diff] : [])],
17
+ }));
18
+ // design-doctor — when React frontend is present
19
+ runs.push(await runOne({
20
+ name: "design-doctor",
21
+ enabled: detected.hasFrontend,
22
+ reason: detected.hasFrontend ? "React frontend detected" : "no React frontend",
23
+ cwd: detected.root,
24
+ cmd: "npx",
25
+ args: ["-y", "design-doctor@latest", "scan", ".", "--json", ...(opts.diff ? ["--diff", opts.diff] : []), ...(opts.vision ? ["--vision"] : [])],
26
+ cacheFile: ".design-doctor/result.json",
27
+ }));
28
+ // react-doctor — opt-in (third-party, separate maintainer)
29
+ if (opts.reactDoctor) {
30
+ runs.push(await runOne({
31
+ name: "react-doctor",
32
+ enabled: detected.hasReactDoctorTarget,
33
+ reason: detected.hasReactDoctorTarget ? "React project + --react-doctor flag" : "no React",
34
+ cwd: detected.root,
35
+ cmd: "npx",
36
+ args: ["-y", "react-doctor@latest", ".", "--json"],
37
+ }));
38
+ }
39
+ // qa-doctor — opt-in (--browser), needs running dev server
40
+ if (opts.browser) {
41
+ runs.push(await runOne({
42
+ name: "qa-doctor",
43
+ enabled: detected.hasFrontend,
44
+ reason: detected.hasFrontend ? "React frontend + --browser flag" : "no React",
45
+ cwd: detected.root,
46
+ cmd: "npx",
47
+ args: ["-y", "qa-doctor@latest", "scan", ".", "--json", ...(opts.url ? ["--url", opts.url] : []), ...(opts.fillForms ? ["--fill"] : [])],
48
+ cacheFile: ".qa-doctor/result.json",
49
+ }));
50
+ }
51
+ return runs;
52
+ }
53
+ function runOne(o) {
54
+ return new Promise((resolveP) => {
55
+ if (!o.enabled) {
56
+ resolveP({ name: o.name, enabled: false, reason: o.reason });
57
+ return;
58
+ }
59
+ const t0 = Date.now();
60
+ const child = (0, node_child_process_1.spawn)(o.cmd, o.args, { cwd: o.cwd, stdio: ["ignore", "pipe", "pipe"] });
61
+ const stdoutChunks = [];
62
+ const stderrChunks = [];
63
+ child.stdout.on("data", (b) => { stdoutChunks.push(b); });
64
+ child.stderr.on("data", (b) => { stderrChunks.push(b); });
65
+ child.on("close", () => {
66
+ const durationMs = Date.now() - t0;
67
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8");
68
+ const stderr = Buffer.concat(stderrChunks).toString("utf8");
69
+ const run = { name: o.name, enabled: true, reason: o.reason, durationMs };
70
+ // Prefer the cache file if it exists and is newer than the run start.
71
+ // stdout pipe buffers can truncate large JSON on macOS; the cache file
72
+ // is the authoritative source.
73
+ const cachePath = o.cacheFile ? (0, node_path_1.join)(o.cwd, o.cacheFile) : null;
74
+ let json = null;
75
+ if (cachePath && (0, node_fs_1.existsSync)(cachePath)) {
76
+ try {
77
+ json = (0, node_fs_1.readFileSync)(cachePath, "utf8");
78
+ }
79
+ catch { }
80
+ }
81
+ if (!json)
82
+ json = stdout;
83
+ try {
84
+ const parsed = JSON.parse(json);
85
+ run.score = parsed.score ?? parsed.summary?.score;
86
+ run.grade = parsed.grade ?? parsed.summary?.grade;
87
+ run.output = json;
88
+ }
89
+ catch {
90
+ run.error = stderr.split("\n").find((l) => l.trim()) ?? "(unparseable JSON output)";
91
+ }
92
+ resolveP(run);
93
+ });
94
+ });
95
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderMarkdown = renderMarkdown;
4
+ function renderMarkdown(runs) {
5
+ const lines = [];
6
+ lines.push(`## doctors report`);
7
+ lines.push("");
8
+ const enabled = runs.filter((r) => r.enabled);
9
+ const scored = enabled.filter((r) => typeof r.score === "number");
10
+ const avg = scored.length ? Math.round(scored.reduce((s, r) => s + (r.score ?? 0), 0) / scored.length) : null;
11
+ if (avg !== null)
12
+ lines.push(`**Suite average: ${avg}/100** across ${scored.length} doctor${scored.length === 1 ? "" : "s"}.`);
13
+ lines.push("");
14
+ lines.push(`| Doctor | Status | Score | Notes |`);
15
+ lines.push(`|---|---|---|---|`);
16
+ for (const r of runs) {
17
+ if (!r.enabled) {
18
+ lines.push(`| \`${r.name}\` | skipped | — | ${r.reason ?? ""} |`);
19
+ }
20
+ else if (r.error) {
21
+ lines.push(`| \`${r.name}\` | failed | — | ${r.error} |`);
22
+ }
23
+ else {
24
+ lines.push(`| \`${r.name}\` | ok | ${r.score ?? "—"}/100 | ${r.grade ?? ""} |`);
25
+ }
26
+ }
27
+ lines.push("");
28
+ for (const r of enabled) {
29
+ if (r.error || !r.output)
30
+ continue;
31
+ lines.push(`### ${r.name}`);
32
+ lines.push("");
33
+ lines.push("Run individually for full output:");
34
+ lines.push("```");
35
+ lines.push(`npx -y ${r.name}@latest scan .`);
36
+ lines.push("```");
37
+ lines.push("");
38
+ }
39
+ return lines.join("\n");
40
+ }
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderTty = renderTty;
4
+ const C = {
5
+ reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
6
+ red: "\x1b[31m", yellow: "\x1b[33m", green: "\x1b[32m", cyan: "\x1b[36m",
7
+ };
8
+ function renderTty(runs, opts = { noColor: false }) {
9
+ const c = opts.noColor ? blank() : C;
10
+ const out = (s) => process.stdout.write(s + "\n");
11
+ out(`${c.bold}doctors${c.reset} ${c.dim}— suite report${c.reset}`);
12
+ out("");
13
+ // Aggregate
14
+ const enabled = runs.filter((r) => r.enabled);
15
+ const scored = enabled.filter((r) => typeof r.score === "number");
16
+ const avg = scored.length ? Math.round(scored.reduce((s, r) => s + (r.score ?? 0), 0) / scored.length) : null;
17
+ for (const r of runs) {
18
+ if (!r.enabled) {
19
+ out(` ${c.dim}skip${c.reset} ${r.name.padEnd(15)} ${c.dim}— ${r.reason}${c.reset}`);
20
+ continue;
21
+ }
22
+ if (r.error) {
23
+ out(` ${c.red}fail${c.reset} ${r.name.padEnd(15)} ${c.red}${r.error}${c.reset} ${c.dim}(${r.durationMs}ms)${c.reset}`);
24
+ continue;
25
+ }
26
+ const score = r.score ?? 0;
27
+ const sc = score >= 75 ? c.green : score >= 50 ? c.yellow : c.red;
28
+ out(` ${c.green}ok${c.reset} ${r.name.padEnd(15)} ${sc}${c.bold}${score}${c.reset}/100 ${sc}${r.grade ?? ""}${c.reset} ${c.dim}(${r.durationMs}ms)${c.reset}`);
29
+ }
30
+ out("");
31
+ if (avg !== null) {
32
+ const sc = avg >= 75 ? c.green : avg >= 50 ? c.yellow : c.red;
33
+ out(` ${c.bold}Suite average:${c.reset} ${sc}${c.bold}${avg}/100${c.reset} ${c.dim}across ${scored.length} doctor${scored.length === 1 ? "" : "s"}${c.reset}`);
34
+ }
35
+ else {
36
+ out(` ${c.dim}No doctors produced a parseable score.${c.reset}`);
37
+ }
38
+ }
39
+ function blank() {
40
+ const o = { reset: "", bold: "", dim: "", red: "", yellow: "", green: "", cyan: "" };
41
+ return o;
42
+ }
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VERSION = void 0;
4
+ exports.VERSION = "0.1.0";
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "four-doctors",
3
+ "version": "0.1.0",
4
+ "description": "Run rails-doctor, design-doctor, react-doctor, and qa-doctor with one command. Auto-detects which apply, aggregates the scores into a single report, and installs all four agent skills at once.",
5
+ "keywords": [
6
+ "rails-doctor",
7
+ "design-doctor",
8
+ "qa-doctor",
9
+ "react-doctor",
10
+ "static-analysis",
11
+ "browser-testing",
12
+ "claude-code",
13
+ "codex",
14
+ "agent-skill",
15
+ "monorepo",
16
+ "audit"
17
+ ],
18
+ "homepage": "https://github.com/artisanscompany/doctors",
19
+ "bugs": { "url": "https://github.com/artisanscompany/doctors/issues" },
20
+ "repository": { "type": "git", "url": "git+https://github.com/artisanscompany/doctors.git" },
21
+ "license": "MIT",
22
+ "author": "ArtisansCompany",
23
+ "bin": { "four-doctors": "bin/doctors.js" },
24
+ "engines": { "node": ">=18" },
25
+ "files": ["bin/", "dist/", "skills/", "README.md", "LICENSE"],
26
+ "scripts": {
27
+ "build": "tsc",
28
+ "prepublishOnly": "npm run build"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^20.0.0",
32
+ "typescript": "^5.4.0"
33
+ }
34
+ }
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: doctors
3
+ description: Use when the user wants a full health check of a Rails+Inertia or TanStack codebase — runs rails-doctor, design-doctor, qa-doctor (browser), and optionally react-doctor, and aggregates the scores into one report. Auto-skips inapplicable doctors. Useful before committing, before a release, or when onboarding a new codebase.
4
+ version: "1.0.0"
5
+ ---
6
+
7
+ # Doctors
8
+
9
+ The whole suite, one command. Runs every applicable doctor against the project, aggregates the scores, and emits a single report.
10
+
11
+ ## Quick start
12
+
13
+ ```bash
14
+ npx -y doctors@latest scan .
15
+ ```
16
+
17
+ That runs `rails-doctor` (if a Gemfile is present) and `design-doctor` (if React is in `package.json`), shows individual scores plus the suite average.
18
+
19
+ ## Add browser QA
20
+
21
+ ```bash
22
+ npx -y doctors@latest scan . --browser --url http://localhost:3000
23
+ ```
24
+
25
+ Adds `qa-doctor` to the run. Needs a running dev server.
26
+
27
+ ## Add vision pass
28
+
29
+ ```bash
30
+ npx -y doctors@latest scan . --vision --url http://localhost:3000
31
+ ```
32
+
33
+ Runs `design-doctor` with the LLM vision pass enabled.
34
+
35
+ ## Run all four
36
+
37
+ ```bash
38
+ npx -y doctors@latest scan . --browser --vision --fill --url http://localhost:3000 --react-doctor
39
+ ```
40
+
41
+ ## Companion install
42
+
43
+ Install every individual doctor's skill at once:
44
+
45
+ ```bash
46
+ npx -y rails-doctor@latest install
47
+ npx -y design-doctor@latest install
48
+ npx -y qa-doctor@latest install
49
+ npx -y doctors@latest install
50
+ ```
51
+
52
+ After that, an agent can call any of them by name; this skill just helps the agent decide which to run together.
53
+
54
+ ## CI gating
55
+
56
+ ```bash
57
+ npx -y doctors@latest scan . --min-score 80
58
+ ```
59
+
60
+ Fails the build when the suite average drops below 80.
61
+
62
+ ## What each doctor scores
63
+
64
+ | Doctor | Surface |
65
+ |-----------------|-------------------------------------------------------------------|
66
+ | `rails-doctor` | Ruby / Rails — routes, models, migrations, security, Inertia, perf |
67
+ | `design-doctor` | React — design tokens, microcopy, a11y, shadcn use, vision |
68
+ | `qa-doctor` | Live browser — feature pass/fail, form-fill, retries |
69
+ | `react-doctor` | Third-party — general React lint, dead code, bundle |
70
+
71
+ ## Distribution
72
+
73
+ npm. Pure Node. Works with Node 18+.