agentready-design-cli 0.1.0 → 0.2.1

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/README.md CHANGED
@@ -1,37 +1,72 @@
1
1
  # agentready-design-cli
2
2
 
3
- Deterministic checks for the [Agent-Ready Design rubric](https://github.com/hgillispie/Agent-Ready-Design). Walks a design-system repo and scores everything that can be scored without an LLM. Emits Markdown + JSON in the [shared report format](../../schema/).
3
+ Score a design system against the [Agent-Ready Design rubric](https://github.com/hgillispie/Agent-Ready-Design) in one command.
4
4
 
5
- ## Usage
5
+ ## Quick start
6
+
7
+ ### Deterministic + paste-into-agent (zero config)
6
8
 
7
9
  ```bash
8
10
  npx agentready-design-cli ./path/to/your/design-system
9
11
  ```
10
12
 
11
- Writes `agent-ready-report.md` and `agent-ready-report.json` next to the target. Anything that requires judgment is emitted as `status: "pending"` the report stays complete.
13
+ Walks the repo, scores everything that can be scored without a model, and writes a paste-ready prompt for the rest:
12
14
 
13
- ### Flags
15
+ ```
16
+ ✓ ./agent-ready-report.json
17
+ ✓ ./agent-ready-report.md
18
+ ✓ ./agent-ready-prompt.md ← paste this into your agent
14
19
 
20
+ Tier 1 — Human-ready, AI-hostile · floor 2/4 · retrieval 1/4 · median 2.0/4
15
21
  ```
16
- --format markdown|json|both # default both
17
- --out <dir> # default = target path
18
- --target <name> # friendly name in the report header
19
- --merge <existing.json> # only re-run pending criteria from a prior run
20
- --help
22
+
23
+ ### Fully auto-scored (hosted API, no agent needed)
24
+
25
+ ```bash
26
+ npx agentready-design-cli ./your-ds --remote
21
27
  ```
22
28
 
23
- ### Output
29
+ Same as above, but also calls the hosted scoring endpoint (Claude Haiku) to fill in the judgment-required rows. **You don't need an API key** — the endpoint is hosted by the project. Privacy notes below.
24
30
 
25
- A Markdown heatmap with a `0`–`4` cell per criterion (and `?` for pending), and a JSON file conforming to [`schema/report.schema.json`](../../schema/report.schema.json).
31
+ ## Flags
26
32
 
27
33
  ```
28
- Tier 1 — Human-ready, AI-hostile · floor 2/4 · retrieval 1/4 · median 2.0/4
29
- 12 criteria require an agent. Paste prompts/self-assessment.md to complete the report.
34
+ --format markdown|json|both default both
35
+ --out <dir> default = target path
36
+ --target <name> friendly name in the report header
37
+ --merge <existing.json> only re-run pending criteria from a prior run
38
+ --remote auto-fill pending rows via the hosted scoring API
39
+ --api <url> override the API endpoint (default agentreadydesign.com)
40
+ --no-prompt skip writing agent-ready-prompt.md
41
+ --help
30
42
  ```
31
43
 
32
- ## What it checks
44
+ ## What gets scored
33
45
 
34
- Roughly half the rubric is deterministically checkable; the rest is `pending: requires-agent`. See [`CRITERIA.md`](./CRITERIA.md) for the full split.
46
+ | | |
47
+ |---|---|
48
+ | Deterministic checks (no model needed) | tokens dir, DTCG format, `AGENTS.md`, `llms.txt`, component manifest, layout primitives, raw `<button>`/`<input>` in components, hex literals in component CSS, Storybook config, Chromatic/Percy, axe, semver, … |
49
+ | Judgment-required (filled by `--remote` or paste-prompt) | prop-naming consistency, anti-pattern docs, composition rules, doc structure, hand-curation of AGENTS.md, drift detection, eval suite, trust levels, governance |
50
+
51
+ See [`CRITERIA.md`](./CRITERIA.md) for the full split.
52
+
53
+ ## Privacy — what `--remote` sends
54
+
55
+ When you pass `--remote`, the CLI sends a curated sample (up to ~60 KB) of your repo to `https://agentreadydesign.com/api/score`. Files included:
56
+
57
+ - `package.json`, `README.md`, `tsconfig.json`
58
+ - `AGENTS.md`, `.builder/AGENTS.md`, `CLAUDE.md`, `.cursorrules`, `.windsurfrules`, `.clinerules`
59
+ - `llms.txt`, `llms-full.txt`
60
+ - `custom-elements.json`, `registry.json`, Storybook main config
61
+ - Token files under `tokens/`, `src/tokens/`, `packages/tokens/`
62
+ - Up to 5 component files from each of `components/`, `src/components/`, `packages/ui/src/`
63
+ - Up to 3 files each from `patterns/` and `recipes/`
64
+
65
+ Hard caps: ≤40 files, ≤4 KB per file, ≤60 KB total. Files matching `.env*`, `*.key`, `*.pem`, etc. are never matched by the included glob patterns.
66
+
67
+ The endpoint forwards your sample to Anthropic Claude Haiku for scoring and returns only the deltas. No content is logged or retained. Source: [`site/netlify/functions/score.ts`](https://github.com/hgillispie/Agent-Ready-Design/blob/main/site/netlify/functions/score.ts).
68
+
69
+ **If you'd rather not send anything**: omit `--remote`. The default flow writes a paste-ready prompt locally and your AI tool reads your repo directly.
35
70
 
36
71
  ## Programmatic use
37
72
 
@@ -42,10 +77,6 @@ const report = await runAssessment({ target: "./packages/ui" });
42
77
  console.log(renderMarkdown(report));
43
78
  ```
44
79
 
45
- ## Pair it with the prompt
46
-
47
- After running the CLI, paste [`prompts/self-assessment.md`](https://github.com/hgillispie/Agent-Ready-Design/blob/main/prompts/self-assessment.md) into your agent. It will detect `agent-ready-report.json` at the repo root and fill in only the `pending` rows, marking `producer.kind: "merged"` in the output.
48
-
49
80
  ## License
50
81
 
51
82
  MIT.
@@ -1,5 +1,125 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/context.ts
4
+ import { readFile, stat } from "fs/promises";
5
+ import { resolve, relative, sep } from "path";
6
+ import fg from "fast-glob";
7
+ var IGNORES = [
8
+ "**/node_modules/**",
9
+ "**/dist/**",
10
+ "**/build/**",
11
+ "**/.next/**",
12
+ "**/.astro/**",
13
+ "**/.cache/**",
14
+ "**/.turbo/**",
15
+ "**/coverage/**",
16
+ "**/.git/**"
17
+ ];
18
+ var CheckContext = class {
19
+ root;
20
+ fileCache = /* @__PURE__ */ new Map();
21
+ globCache = /* @__PURE__ */ new Map();
22
+ pkgJsonCache;
23
+ constructor(root) {
24
+ this.root = resolve(root);
25
+ }
26
+ resolve(...p) {
27
+ return resolve(this.root, ...p);
28
+ }
29
+ relative(p) {
30
+ return relative(this.root, p).split(sep).join("/");
31
+ }
32
+ async exists(rel) {
33
+ try {
34
+ await stat(this.resolve(rel));
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+ async existsAny(rels) {
41
+ for (const r of rels) {
42
+ if (await this.exists(r)) return r;
43
+ }
44
+ return null;
45
+ }
46
+ async readFile(rel) {
47
+ if (this.fileCache.has(rel)) return this.fileCache.get(rel) ?? null;
48
+ try {
49
+ const content = await readFile(this.resolve(rel), "utf8");
50
+ this.fileCache.set(rel, content);
51
+ return content;
52
+ } catch {
53
+ this.fileCache.set(rel, null);
54
+ return null;
55
+ }
56
+ }
57
+ async glob(pattern, opts) {
58
+ const key = JSON.stringify({ p: pattern, i: opts?.ignore ?? null });
59
+ const cached = this.globCache.get(key);
60
+ if (cached) return cached;
61
+ const matches = await fg(pattern, {
62
+ cwd: this.root,
63
+ ignore: [...IGNORES, ...opts?.ignore ?? []],
64
+ dot: true,
65
+ onlyFiles: true,
66
+ followSymbolicLinks: false
67
+ });
68
+ matches.sort();
69
+ this.globCache.set(key, matches);
70
+ return matches;
71
+ }
72
+ async firstMatch(pattern) {
73
+ const matches = await this.glob(pattern);
74
+ return matches[0] ?? null;
75
+ }
76
+ async pkgJson() {
77
+ if (this.pkgJsonCache !== void 0) return this.pkgJsonCache;
78
+ const raw = await this.readFile("package.json");
79
+ if (!raw) {
80
+ this.pkgJsonCache = null;
81
+ return null;
82
+ }
83
+ try {
84
+ this.pkgJsonCache = JSON.parse(raw);
85
+ } catch {
86
+ this.pkgJsonCache = null;
87
+ }
88
+ return this.pkgJsonCache;
89
+ }
90
+ /**
91
+ * Heuristic "component directories" — places to look for component source.
92
+ * Covers common conventions: top-level components/, ui/, lib/, src/, packages/*\/src.
93
+ */
94
+ async componentDirs() {
95
+ const candidates = [
96
+ "components",
97
+ "src/components",
98
+ "src/ui",
99
+ "ui",
100
+ "lib/components",
101
+ "packages/ui/src",
102
+ "packages/components/src",
103
+ "packages/ui/src/components"
104
+ ];
105
+ const found = [];
106
+ for (const c of candidates) {
107
+ if (await this.exists(c)) found.push(c);
108
+ }
109
+ if (found.length === 0) {
110
+ const pkgs = await this.glob("packages/*/package.json");
111
+ for (const p of pkgs) {
112
+ const base = p.replace(/\/package\.json$/, "");
113
+ for (const sub of ["src", "src/components", "components"]) {
114
+ const full = `${base}/${sub}`;
115
+ if (await this.exists(full)) found.push(full);
116
+ }
117
+ }
118
+ }
119
+ return found;
120
+ }
121
+ };
122
+
3
123
  // src/rubric.ts
4
124
  var RUBRIC = [
5
125
  // Category 1 — Tokens & Foundations
@@ -151,126 +271,6 @@ function generateBacklog(criteria) {
151
271
  }));
152
272
  }
153
273
 
154
- // src/context.ts
155
- import { readFile, stat } from "fs/promises";
156
- import { resolve, relative, sep } from "path";
157
- import fg from "fast-glob";
158
- var IGNORES = [
159
- "**/node_modules/**",
160
- "**/dist/**",
161
- "**/build/**",
162
- "**/.next/**",
163
- "**/.astro/**",
164
- "**/.cache/**",
165
- "**/.turbo/**",
166
- "**/coverage/**",
167
- "**/.git/**"
168
- ];
169
- var CheckContext = class {
170
- root;
171
- fileCache = /* @__PURE__ */ new Map();
172
- globCache = /* @__PURE__ */ new Map();
173
- pkgJsonCache;
174
- constructor(root) {
175
- this.root = resolve(root);
176
- }
177
- resolve(...p) {
178
- return resolve(this.root, ...p);
179
- }
180
- relative(p) {
181
- return relative(this.root, p).split(sep).join("/");
182
- }
183
- async exists(rel) {
184
- try {
185
- await stat(this.resolve(rel));
186
- return true;
187
- } catch {
188
- return false;
189
- }
190
- }
191
- async existsAny(rels) {
192
- for (const r of rels) {
193
- if (await this.exists(r)) return r;
194
- }
195
- return null;
196
- }
197
- async readFile(rel) {
198
- if (this.fileCache.has(rel)) return this.fileCache.get(rel) ?? null;
199
- try {
200
- const content = await readFile(this.resolve(rel), "utf8");
201
- this.fileCache.set(rel, content);
202
- return content;
203
- } catch {
204
- this.fileCache.set(rel, null);
205
- return null;
206
- }
207
- }
208
- async glob(pattern, opts) {
209
- const key = JSON.stringify({ p: pattern, i: opts?.ignore ?? null });
210
- const cached = this.globCache.get(key);
211
- if (cached) return cached;
212
- const matches = await fg(pattern, {
213
- cwd: this.root,
214
- ignore: [...IGNORES, ...opts?.ignore ?? []],
215
- dot: true,
216
- onlyFiles: true,
217
- followSymbolicLinks: false
218
- });
219
- matches.sort();
220
- this.globCache.set(key, matches);
221
- return matches;
222
- }
223
- async firstMatch(pattern) {
224
- const matches = await this.glob(pattern);
225
- return matches[0] ?? null;
226
- }
227
- async pkgJson() {
228
- if (this.pkgJsonCache !== void 0) return this.pkgJsonCache;
229
- const raw = await this.readFile("package.json");
230
- if (!raw) {
231
- this.pkgJsonCache = null;
232
- return null;
233
- }
234
- try {
235
- this.pkgJsonCache = JSON.parse(raw);
236
- } catch {
237
- this.pkgJsonCache = null;
238
- }
239
- return this.pkgJsonCache;
240
- }
241
- /**
242
- * Heuristic "component directories" — places to look for component source.
243
- * Covers common conventions: top-level components/, ui/, lib/, src/, packages/*\/src.
244
- */
245
- async componentDirs() {
246
- const candidates = [
247
- "components",
248
- "src/components",
249
- "src/ui",
250
- "ui",
251
- "lib/components",
252
- "packages/ui/src",
253
- "packages/components/src",
254
- "packages/ui/src/components"
255
- ];
256
- const found = [];
257
- for (const c of candidates) {
258
- if (await this.exists(c)) found.push(c);
259
- }
260
- if (found.length === 0) {
261
- const pkgs = await this.glob("packages/*/package.json");
262
- for (const p of pkgs) {
263
- const base = p.replace(/\/package\.json$/, "");
264
- for (const sub of ["src", "src/components", "components"]) {
265
- const full = `${base}/${sub}`;
266
- if (await this.exists(full)) found.push(full);
267
- }
268
- }
269
- }
270
- return found;
271
- }
272
- };
273
-
274
274
  // src/checks/category-1-tokens.ts
275
275
  var TOKEN_DIRS = ["tokens", "src/tokens", "design-tokens", "src/design-tokens", "packages/tokens"];
276
276
  async function findTokenSources(ctx) {
@@ -1197,6 +1197,7 @@ function renderMarkdown(report) {
1197
1197
  }
1198
1198
 
1199
1199
  export {
1200
+ CheckContext,
1200
1201
  RUBRIC,
1201
1202
  TOTAL_CRITERIA,
1202
1203
  computeRollup,
package/dist/cli.js CHANGED
@@ -1,15 +1,411 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ CheckContext,
3
4
  TIER_LABELS,
5
+ computeRollup,
6
+ generateBacklog,
4
7
  renderMarkdown,
5
8
  runAssessment
6
- } from "./chunk-GWF7PSLR.js";
9
+ } from "./chunk-WHGIRQPX.js";
7
10
 
8
11
  // src/cli.ts
9
12
  import { writeFile, mkdir } from "fs/promises";
10
- import { dirname, resolve } from "path";
13
+ import { dirname, resolve, relative } from "path";
11
14
  import { existsSync } from "fs";
12
15
  import kleur from "kleur";
16
+
17
+ // src/embedded.ts
18
+ var SELF_ASSESSMENT_PROMPT = `# Agent-Ready Design \u2014 Self-Assessment Prompt
19
+
20
+ > Paste this prompt into Builder.io Fusion, Cursor, Codex, Claude Code, or any agentic coding tool with your design system repository open as the working directory. The agent will score it against the v0.1 Agent-Ready Design rubric and emit a Markdown heatmap and a JSON sidecar in the format defined at [schema/report.schema.json](https://github.com/hgillispie/Agent-Ready-Design/blob/main/schema/report.schema.json).
21
+
22
+ ---
23
+
24
+ You are auditing the current repository against the **Agent-Ready Design v0.1 rubric**. The canonical rubric is at \`https://github.com/hgillispie/Agent-Ready-Design/blob/main/RUBRIC.md\`. If you have web access, fetch it. Otherwise rely on the inline summary below \u2014 it is sufficient for scoring.
25
+
26
+ ## Your job
27
+
28
+ 1. Traverse the repository. Identify it as a design system (or a repo *containing* one \u2014 e.g. a monorepo with a \`packages/ui\` package).
29
+ 2. **If a file named \`agent-ready-report.json\` exists at the repo root, load it.** It was produced by \`agentready-design-cli\`. Trust its \`score\`/\`evidence\` for any criterion with \`status: "scored"\` unless you find clear contradicting evidence. Complete every criterion with \`status: "pending"\`. Set \`producer.kind\` to \`"merged"\` in your output.
30
+ 3. Otherwise, score every criterion 0\u20134 from scratch.
31
+ 4. Emit **both** a Markdown heatmap AND a JSON file conforming to the shared schema. Write the JSON to \`agent-ready-report.json\` at the repo root. Print the Markdown in your response.
32
+ 5. Surface the **foundational floor** (lowest score in Categories 1\u20134) prominently. Below 2 means the system will ship "uncanny valley" output regardless of any retrieval infrastructure.
33
+ 6. Produce a **prioritized 5\u201310 item action backlog**. Rank by leverage on the foundational floor first, retrieval level second.
34
+
35
+ ## Scoring scale
36
+
37
+ | Level | Meaning |
38
+ |---|---|
39
+ | 0 | Absent. Does not exist or is not discoverable. |
40
+ | 1 | Ad hoc. Exists but incomplete, inconsistent, or human-only. |
41
+ | 2 | Documented. Human-readable, consistent. |
42
+ | 3 | Machine-readable. Structured data agents can ingest, discoverable from a single canonical entry. |
43
+ | 4 | Agent-native. Exposed via MCP/registry + validated by an automated eval loop. |
44
+
45
+ ## The rubric \u2014 all 31 criteria
46
+
47
+ ### Category 1 \u2014 Tokens & Foundations
48
+
49
+ - **1.1** Tokens exist as exported, versioned source files (canonical \`/tokens/\` dir, JSON/YAML/CSS, not Figma-only).
50
+ - **1.2** Semantic tokens exist on top of primitives; components reference semantics.
51
+ - **1.3** Tokens are closed and enumerable from a single source.
52
+ - **1.4** Token usage is enforced (lint/CI rejects raw hex/px in component CSS).
53
+
54
+ ### Category 2 \u2014 Component API Contract
55
+
56
+ - **2.1** Every component has a machine-readable manifest (CEM 2.x, Storybook Component Manifest, custom JSON, or react-docgen) covering props/variants/slots/events.
57
+ - **2.2** Prop naming is consistent across the library (e.g. visual prominence always \`variant\`, size always \`size\`).
58
+ - **2.3** Composition and slot rules are explicit (compound-component children/parents enumerated).
59
+ - **2.4** Anti-patterns are documented per component (Do/Don't, "when not to use").
60
+ - **2.5** Accessibility metadata travels with the component (ARIA, keyboard, focus, in a structured field).
61
+
62
+ ### Category 3 \u2014 Documentation Format
63
+
64
+ - **3.1** Single canonical docs site, no scattered duplicates.
65
+ - **3.2** Component pages have a stable, predictable structure (front-matter + section ordering).
66
+ - **3.3** At least 3 concrete usage examples per component, including anti-patterns.
67
+ - **3.4** \`llms.txt\` (and ideally \`llms-full.txt\`) is published at the docs root.
68
+
69
+ ### Category 4 \u2014 Composition & Layout
70
+
71
+ - **4.1** Layout primitives (\`Stack\`, \`Inline\`, \`Grid\`, \`Box\`, \`Container\`) exist as first-class components.
72
+ - **4.2** Responsive conventions are explicit and uniform (single canonical pattern, tokenized breakpoints).
73
+ - **4.3** A "patterns" or "recipes" layer exists for compositions agents should reuse.
74
+
75
+ ### Category 5 \u2014 Retrieval & Discoverability
76
+
77
+ - **5.1** \`AGENTS.md\` (or \`CLAUDE.md\`) exists and is **hand-curated** (per ETH Zurich evidence, auto-generated ones often *hurt* performance).
78
+ - **5.2** An MCP server (or registry endpoint) exposes the system to agents.
79
+ - **5.3** Always-on foundational rules (spacing/color/type) are injected into every agent run via rules files.
80
+ - **5.4** Components are addressable by stable, predictable identifiers (one canonical import path).
81
+
82
+ ### Category 6 \u2014 Distribution & Versioning
83
+
84
+ - **6.1** Installable through a single well-known mechanism (npm package, shadcn registry, monorepo workspace \u2014 not "copy from Confluence").
85
+ - **6.2** Breaking changes and deprecations are machine-readable (manifest \`deprecated: true\`, codemods).
86
+ - **6.3** A clear "current vs. latest" version is communicated to agents (AGENTS.md states target version).
87
+
88
+ ### Category 7 \u2014 Quality Signals
89
+
90
+ - **7.1** Every component has interaction tests / stories.
91
+ - **7.2** Visual regression is in place.
92
+ - **7.3** Accessibility checks are automated (axe/Lighthouse in CI).
93
+ - **7.4** Code quality and consistency within the library itself (no raw hex codes internally).
94
+
95
+ ### Category 8 \u2014 Evaluation & Governance for Agents
96
+
97
+ - **8.1** An eval suite tests agent output against the design system.
98
+ - **8.2** Trust levels are defined for agent actions (suggest / PR / auto-merge).
99
+ - **8.3** Drift detection between docs, tokens, and code.
100
+ - **8.4** Generated-code attribution and review (PRs from agents are labeled and tracked).
101
+
102
+ ## How to score
103
+
104
+ For each criterion:
105
+
106
+ 1. State the **observable signal** you'd look for (the rubric is specific about this; the inline summary above lists the headline signal \u2014 read it carefully).
107
+ 2. Look. Use \`grep\`, \`find\`, \`ls\`, \`cat\`, AST traversal, whatever is appropriate. Prefer evidence from the repo over training-data priors.
108
+ 3. Pick a score 0\u20134. **Be strict.** A \`custom-elements.json\` with \`variant: string\` instead of an enumerated union is a 2, not a 3. An auto-generated \`AGENTS.md\` is a 1, not a 3. \`llms.txt\` pointing to outdated MDX is gaming the rubric.
109
+ 4. Record one sentence of rationale and at least one file path (with a line number where useful) as evidence.
110
+
111
+ ## Output \u2014 Markdown
112
+
113
+ Print this format in your response:
114
+
115
+ \`\`\`markdown
116
+ # Agent-Ready Design \u2014 Assessment
117
+
118
+ **Target:** \`<repo name or path>\`
119
+ **Producer:** <agent name> (paste-prompt)
120
+ **Generated:** <ISO 8601 timestamp>
121
+
122
+ ## Roll-up
123
+
124
+ - **Foundational floor (Categories 1\u20134): N / 4** \u2014 <one-line interpretation>
125
+ - **Retrieval level (Category 5 max): N / 4**
126
+ - **Overall median: N.N / 4**
127
+ - **Tier: N \u2014 <label>** (per RUBRIC.md \xA74)
128
+
129
+ ## Heatmap
130
+
131
+ | Cat | 1 | 2 | 3 | 4 | 5 | Floor | Median |
132
+ |---|---|---|---|---|---|---|---|
133
+ | **1. Tokens & Foundations** | N | N | N | N | \u2013 | N | N |
134
+ | **2. Component API Contract** | N | N | N | N | N | N | N |
135
+ | **3. Documentation Format** | N | N | N | N | \u2013 | N | N |
136
+ | **4. Composition & Layout** | N | N | N | \u2013 | \u2013 | N | N |
137
+ | **5. Retrieval & Discoverability** | N | N | N | N | \u2013 | N | N |
138
+ | **6. Distribution & Versioning** | N | N | N | \u2013 | \u2013 | N | N |
139
+ | **7. Quality Signals** | N | N | N | N | \u2013 | N | N |
140
+ | **8. Evaluation & Governance** | N | N | N | N | \u2013 | N | N |
141
+
142
+ (Use the criterion number, not stars or emoji. Use \`\u2013\` for cells that don't exist \u2014 e.g. Category 4 only has 3 criteria.)
143
+
144
+ ## Per-criterion
145
+
146
+ ### Category 1 \u2014 Tokens & Foundations
147
+
148
+ - **1.1 Tokens exist as exported, versioned source files. \u2014 N/4**
149
+ - Rationale: <one sentence>
150
+ - Evidence: \`tokens/colors.json:1\` \u2026
151
+ - Suggestion: <how to raise one level>
152
+
153
+ \u2026 (continue for every criterion)
154
+
155
+ ## Prioritized backlog
156
+
157
+ 1. **<Action title>** \u2014 Criteria <ids>. <One-sentence rationale.> _Effort: hours/days/weeks/months._
158
+ 2. \u2026
159
+ \`\`\`
160
+
161
+ ## Output \u2014 JSON
162
+
163
+ Also write \`agent-ready-report.json\` at the repo root, conforming to the schema below. **Every criterion must be present** \u2014 use \`status: "pending"\` if you cannot score one. Use \`status: "skipped"\` for criteria that don't apply (e.g. a Vue-only repo skipping \`2.1\` CEM-vs-react-docgen distinctions \u2014 but still attempt scoring first).
164
+
165
+ \`\`\`jsonc
166
+ {
167
+ "schemaVersion": "0.1.0",
168
+ "rubricVersion": "0.1.0",
169
+ "generatedAt": "<ISO timestamp>",
170
+ "target": { "path": "<repo path>", "name": "<optional>" },
171
+ "producer": { "kind": "prompt", "name": "self-assessment.md", "version": "0.1.0" },
172
+ "criteria": [
173
+ {
174
+ "id": "1.1",
175
+ "category": 1,
176
+ "title": "Tokens exist as exported, versioned source files.",
177
+ "score": 2,
178
+ "status": "scored",
179
+ "rationale": "tokens/colors.css exists but no DTCG JSON export.",
180
+ "evidence": [
181
+ { "kind": "file", "path": "tokens/colors.css", "line": 1 },
182
+ { "kind": "glob", "path": "**/*.tokens.json", "detail": "no matches" }
183
+ ],
184
+ "suggestion": "Add Style Dictionary v4 with DTCG JSON output."
185
+ }
186
+ // \u2026 one entry per criterion (31 total) \u2026
187
+ ],
188
+ "rollup": {
189
+ "foundationalFloor": 1,
190
+ "retrievalLevel": 0,
191
+ "overallMedian": 1.5,
192
+ "tier": 1,
193
+ "categoryFloors": { "1": 1, "2": 2, "3": 1, "4": 2, "5": 0, "6": 2, "7": 1, "8": 0 },
194
+ "categoryMedians": { "1": 2, "2": 2, "3": 1.5, "4": 2, "5": 0.5, "6": 2, "7": 1, "8": 0 }
195
+ },
196
+ "backlog": [
197
+ {
198
+ "title": "Publish tokens as DTCG-formatted JSON",
199
+ "criteria": ["1.1", "1.3"],
200
+ "priority": 1,
201
+ "estimatedEffort": "days",
202
+ "rationale": "Foundational floor \u2014 unblocks Categories 2 and 5."
203
+ }
204
+ ]
205
+ }
206
+ \`\`\`
207
+
208
+ ## Roll-up rules
209
+
210
+ - \`foundationalFloor\` = min(scores in Categories 1\u20134). If any criterion in 1\u20134 is \`pending\`, treat the floor as unknown and explicitly say so.
211
+ - \`retrievalLevel\` = max(scores in Category 5).
212
+ - \`overallMedian\` = median of all scored criteria (ignore pending/skipped).
213
+ - Tier mapping (per RUBRIC.md \xA74):
214
+ - **0 \u2014 Pre-agentic.** Foundational floor 0\u20131.
215
+ - **1 \u2014 Human-ready, AI-hostile.** Foundational floor 2, retrieval 0\u20131.
216
+ - **2 \u2014 Agent-legible.** Foundational floor 2, retrieval 2, overall median 2.
217
+ - **3 \u2014 Agent-collaborative.** Foundational floor 3, retrieval 3, overall median 3.
218
+ - **4 \u2014 Self-healing.** All categories \u2265 3 and Category 8 \u2265 3.
219
+
220
+ ## Rules of engagement
221
+
222
+ - **Be honest.** A confidently wrong "3" is worse than a humble "1, here's what's missing."
223
+ - **Be specific.** Every score needs a file path or a "I looked for X and did not find it" note.
224
+ - **Be brief.** One sentence of rationale. The evidence array does the talking.
225
+ - **Do not invent files.** If you can't find a manifest, score the criterion \`1\` or \`0\` \u2014 do not pretend it exists.
226
+ - **Use the repo's own language.** If they say "Tokens v2" call it that, not "your design token system."
227
+
228
+ When you're done, print the Markdown heatmap, confirm that you've written \`agent-ready-report.json\`, and stop. Do not modify any other repo files.
229
+ `;
230
+
231
+ // src/remote.ts
232
+ var DEFAULT_API_URL = "https://agentreadydesign.com/api/score";
233
+ var MAX_FILE_BYTES = 4 * 1024;
234
+ var MAX_TOTAL_BYTES = 60 * 1024;
235
+ var MAX_FILES = 40;
236
+ var ALWAYS_PATHS = [
237
+ "package.json",
238
+ "README.md",
239
+ "AGENTS.md",
240
+ ".builder/AGENTS.md",
241
+ "CLAUDE.md",
242
+ ".cursorrules",
243
+ ".windsurfrules",
244
+ ".clinerules",
245
+ "llms.txt",
246
+ "llms-full.txt",
247
+ "custom-elements.json",
248
+ ".storybook/main.ts",
249
+ ".storybook/main.tsx",
250
+ ".storybook/main.js",
251
+ ".storybook/main.cjs",
252
+ ".storybook/main.mjs",
253
+ "tsconfig.json",
254
+ "registry.json"
255
+ ];
256
+ var SAMPLE_GLOBS = [
257
+ { pattern: "tokens/**/*.{json,css,scss}", max: 5 },
258
+ { pattern: "src/tokens/**/*.{json,css,scss}", max: 5 },
259
+ { pattern: "packages/tokens/**/*.json", max: 3 },
260
+ { pattern: "components/*.{ts,tsx,jsx,vue,svelte}", max: 5 },
261
+ { pattern: "src/components/*.{ts,tsx,jsx,vue,svelte}", max: 5 },
262
+ { pattern: "src/components/**/index.{ts,tsx,jsx}", max: 5 },
263
+ { pattern: "packages/ui/src/**/*.{ts,tsx,jsx}", max: 5 },
264
+ { pattern: "patterns/**/*.{md,tsx,jsx}", max: 3 },
265
+ { pattern: "recipes/**/*.{md,tsx,jsx}", max: 3 }
266
+ ];
267
+ async function gatherSample(ctx) {
268
+ const sample = [];
269
+ const seen = /* @__PURE__ */ new Set();
270
+ let totalBytes = 0;
271
+ const tryAdd = async (rel) => {
272
+ if (sample.length >= MAX_FILES) return false;
273
+ if (totalBytes >= MAX_TOTAL_BYTES) return false;
274
+ if (seen.has(rel)) return false;
275
+ const content = await ctx.readFile(rel);
276
+ if (!content) return false;
277
+ const truncated = content.length > MAX_FILE_BYTES ? content.slice(0, MAX_FILE_BYTES) + "\n\u2026[truncated]" : content;
278
+ if (totalBytes + truncated.length > MAX_TOTAL_BYTES) return false;
279
+ sample.push({ path: rel, content: truncated });
280
+ seen.add(rel);
281
+ totalBytes += truncated.length;
282
+ return true;
283
+ };
284
+ for (const p of ALWAYS_PATHS) await tryAdd(p);
285
+ for (const { pattern, max } of SAMPLE_GLOBS) {
286
+ const matches = await ctx.glob(pattern);
287
+ for (const m of matches.slice(0, max)) await tryAdd(m);
288
+ }
289
+ return sample;
290
+ }
291
+ var PENDING_PER_CHUNK = 6;
292
+ async function callRemote(opts) {
293
+ const pending = opts.report.criteria.filter((c) => c.status === "pending");
294
+ if (pending.length === 0) return { deltas: [] };
295
+ const scored = opts.report.criteria.filter((c) => c.status === "scored");
296
+ const chunks = [];
297
+ for (let i = 0; i < pending.length; i += PENDING_PER_CHUNK) {
298
+ chunks.push(pending.slice(i, i + PENDING_PER_CHUNK));
299
+ }
300
+ const results = await Promise.all(
301
+ chunks.map(
302
+ (chunk) => callRemoteOnce({
303
+ apiUrl: opts.apiUrl,
304
+ sample: opts.sample,
305
+ signal: opts.signal,
306
+ // Each chunk gets its own pending subset plus the full scored context.
307
+ report: {
308
+ ...opts.report,
309
+ criteria: [...chunk, ...scored]
310
+ }
311
+ })
312
+ )
313
+ );
314
+ const deltas = results.flatMap((r) => r.deltas ?? []);
315
+ const totalInput = results.reduce(
316
+ (n, r) => n + (r.meta?.inputTokens ?? 0),
317
+ 0
318
+ );
319
+ const totalOutput = results.reduce(
320
+ (n, r) => n + (r.meta?.outputTokens ?? 0),
321
+ 0
322
+ );
323
+ return {
324
+ deltas,
325
+ meta: {
326
+ model: results[0]?.meta?.model,
327
+ pendingScored: deltas.length,
328
+ inputTokens: totalInput,
329
+ outputTokens: totalOutput
330
+ }
331
+ };
332
+ }
333
+ async function callRemoteOnce(opts) {
334
+ const url = opts.apiUrl ?? process.env.AGENT_READY_API_URL ?? DEFAULT_API_URL;
335
+ const res = await fetch(url, {
336
+ method: "POST",
337
+ headers: { "content-type": "application/json" },
338
+ body: JSON.stringify({ report: opts.report, sample: opts.sample }),
339
+ signal: opts.signal
340
+ });
341
+ const text = await res.text();
342
+ if (!res.ok) {
343
+ let detail = text;
344
+ try {
345
+ const parsed = JSON.parse(text);
346
+ detail = parsed.error ? `${parsed.error}${parsed.detail ? ": " + parsed.detail : ""}` : text;
347
+ } catch {
348
+ }
349
+ throw new Error(`API ${res.status} ${res.statusText}: ${detail}`);
350
+ }
351
+ try {
352
+ return JSON.parse(text);
353
+ } catch {
354
+ throw new Error(`API returned non-JSON: ${text.slice(0, 200)}`);
355
+ }
356
+ }
357
+ function clampScore(s) {
358
+ if (typeof s !== "number") return null;
359
+ const r = Math.round(s);
360
+ if (r < 0 || r > 4) return null;
361
+ return r;
362
+ }
363
+ function sanitizeEvidence(e) {
364
+ if (!Array.isArray(e)) return [];
365
+ const out = [];
366
+ for (const item of e.slice(0, 5)) {
367
+ if (!item || typeof item !== "object") continue;
368
+ const obj = item;
369
+ const kind = obj.kind;
370
+ if (kind !== "file" && kind !== "glob" && kind !== "command" && kind !== "url" && kind !== "note")
371
+ continue;
372
+ out.push({
373
+ kind,
374
+ ...typeof obj.path === "string" ? { path: obj.path } : {},
375
+ ...typeof obj.detail === "string" ? { detail: obj.detail.slice(0, 500) } : {},
376
+ ...typeof obj.snippet === "string" ? { snippet: obj.snippet.slice(0, 500) } : {}
377
+ });
378
+ }
379
+ return out;
380
+ }
381
+ function mergeDeltas(report, response) {
382
+ const byId = new Map(response.deltas.map((d) => [d.id, d]));
383
+ const criteria = report.criteria.map((c) => {
384
+ if (c.status !== "pending") return c;
385
+ const delta = byId.get(c.id);
386
+ if (!delta) return c;
387
+ const score = clampScore(delta.score);
388
+ if (score === null) return c;
389
+ return {
390
+ ...c,
391
+ score,
392
+ status: "scored",
393
+ pending: void 0,
394
+ rationale: typeof delta.rationale === "string" ? delta.rationale : c.rationale,
395
+ evidence: sanitizeEvidence(delta.evidence),
396
+ ...typeof delta.suggestion === "string" ? { suggestion: delta.suggestion } : {}
397
+ };
398
+ });
399
+ return {
400
+ ...report,
401
+ criteria,
402
+ rollup: computeRollup(criteria),
403
+ backlog: generateBacklog(criteria),
404
+ producer: { ...report.producer, kind: "merged" }
405
+ };
406
+ }
407
+
408
+ // src/cli.ts
13
409
  function parseArgs(argv) {
14
410
  const args = argv.slice(2);
15
411
  let target = ".";
@@ -17,6 +413,9 @@ function parseArgs(argv) {
17
413
  let out = ".";
18
414
  let name;
19
415
  let merge;
416
+ let noPrompt = false;
417
+ let remote = false;
418
+ let apiUrl;
20
419
  let help = false;
21
420
  for (let i = 0; i < args.length; i++) {
22
421
  const a = args[i];
@@ -25,12 +424,15 @@ function parseArgs(argv) {
25
424
  else if (a === "--out") out = args[++i] ?? ".";
26
425
  else if (a === "--target") name = args[++i];
27
426
  else if (a === "--merge") merge = args[++i];
427
+ else if (a === "--no-prompt") noPrompt = true;
428
+ else if (a === "--remote") remote = true;
429
+ else if (a === "--api") apiUrl = args[++i];
28
430
  else if (!a.startsWith("-")) target = a;
29
431
  }
30
- return { target, format, out, name, merge, help };
432
+ return { target, format, out, name, merge, noPrompt, remote, apiUrl, help };
31
433
  }
32
434
  var HELP = `
33
- ${kleur.bold("agent-ready")} \u2014 score a design system against the Agent-Ready Design rubric.
435
+ ${kleur.bold("agentready-design-cli")} \u2014 score a design system against the Agent-Ready Design rubric.
34
436
 
35
437
  ${kleur.bold("Usage")}
36
438
  npx agentready-design-cli [path] [options]
@@ -40,13 +442,46 @@ ${kleur.bold("Options")}
40
442
  --out <dir> Directory to write reports into. Default: <path>.
41
443
  --target <name> Friendly name for the report header.
42
444
  --merge <file.json> Existing agent-ready-report.json to merge into (re-runs only pending rows).
445
+ --no-prompt Don't write agent-ready-prompt.md alongside the report.
446
+ --remote Auto-fill pending rows by calling the hosted scoring API.
447
+ (sends a sample of your repo files; see PRIVACY note in the README)
448
+ --api <url> Override the API endpoint URL (default ${DEFAULT_API_URL}).
43
449
  -h, --help Show this help.
44
450
 
45
451
  ${kleur.bold("Examples")}
46
452
  npx agentready-design-cli ./packages/ui
453
+ npx agentready-design-cli ./ds --remote
47
454
  npx agentready-design-cli ./ds --format markdown --target "Acme UI"
48
455
  npx agentready-design-cli ./ds --merge ./ds/agent-ready-report.json
49
456
  `;
457
+ function displayPath(p, cwd) {
458
+ const rel = relative(cwd, p);
459
+ return rel === "" ? "." : rel.startsWith("..") ? p : `./${rel}`;
460
+ }
461
+ async function writeReports(outDir, report, format, writePromptFile) {
462
+ const written = [];
463
+ await mkdir(outDir, { recursive: true });
464
+ if (format === "json" || format === "both") {
465
+ const p = resolve(outDir, "agent-ready-report.json");
466
+ await mkdir(dirname(p), { recursive: true });
467
+ await writeFile(p, JSON.stringify(report, null, 2) + "\n", "utf8");
468
+ written.push(p);
469
+ }
470
+ if (format === "markdown" || format === "both") {
471
+ const p = resolve(outDir, "agent-ready-report.md");
472
+ await writeFile(p, renderMarkdown(report) + "\n", "utf8");
473
+ written.push(p);
474
+ }
475
+ const pendingCount = report.criteria.filter(
476
+ (c) => c.status === "pending"
477
+ ).length;
478
+ if (pendingCount > 0 && writePromptFile) {
479
+ const p = resolve(outDir, "agent-ready-prompt.md");
480
+ await writeFile(p, SELF_ASSESSMENT_PROMPT, "utf8");
481
+ written.push(p);
482
+ }
483
+ return written;
484
+ }
50
485
  async function main() {
51
486
  const opts = parseArgs(process.argv);
52
487
  if (opts.help) {
@@ -60,19 +495,47 @@ async function main() {
60
495
  return;
61
496
  }
62
497
  console.log(kleur.dim(`Scoring ${target} against Agent-Ready Design v0.1...`));
63
- const report = await runAssessment({ target, name: opts.name, merge: opts.merge });
64
- const outDir = resolve(opts.out);
65
- await mkdir(outDir, { recursive: true });
66
- if (opts.format === "json" || opts.format === "both") {
67
- const p = resolve(outDir, "agent-ready-report.json");
68
- await mkdir(dirname(p), { recursive: true });
69
- await writeFile(p, JSON.stringify(report, null, 2) + "\n", "utf8");
70
- console.log(kleur.green(`\u2713 ${p}`));
498
+ let report = await runAssessment({ target, name: opts.name, merge: opts.merge });
499
+ let remoteMeta = null;
500
+ if (opts.remote) {
501
+ const pending = report.criteria.filter((c) => c.status === "pending");
502
+ if (pending.length > 0) {
503
+ console.log(
504
+ kleur.dim(
505
+ `Calling hosted scoring API for ${pending.length} pending criteria...`
506
+ )
507
+ );
508
+ try {
509
+ const ctx = new CheckContext(target);
510
+ const sample = await gatherSample(ctx);
511
+ const response = await callRemote({
512
+ apiUrl: opts.apiUrl,
513
+ report,
514
+ sample
515
+ });
516
+ report = mergeDeltas(report, response);
517
+ remoteMeta = response.meta ?? null;
518
+ } catch (err) {
519
+ const msg = err instanceof Error ? err.message : String(err);
520
+ console.error(kleur.red(`\u2716 Remote scoring failed: ${msg}`));
521
+ console.error(
522
+ kleur.dim(
523
+ " Falling back to deterministic-only output. Pending rows kept as-is."
524
+ )
525
+ );
526
+ }
527
+ }
71
528
  }
72
- if (opts.format === "markdown" || opts.format === "both") {
73
- const p = resolve(outDir, "agent-ready-report.md");
74
- await writeFile(p, renderMarkdown(report) + "\n", "utf8");
75
- console.log(kleur.green(`\u2713 ${p}`));
529
+ const outDir = resolve(opts.out);
530
+ const written = await writeReports(
531
+ outDir,
532
+ report,
533
+ opts.format,
534
+ !opts.noPrompt
535
+ );
536
+ const cwd = process.cwd();
537
+ for (const p of written) {
538
+ console.log(kleur.green(`\u2713 ${displayPath(p, cwd)}`));
76
539
  }
77
540
  const r = report.rollup;
78
541
  console.log("");
@@ -81,15 +544,45 @@ async function main() {
81
544
  `Tier ${r.tier} \u2014 ${TIER_LABELS[r.tier]} \xB7 floor ${r.foundationalFloor}/4 \xB7 retrieval ${r.retrievalLevel}/4 \xB7 median ${r.overallMedian.toFixed(1)}/4`
82
545
  )
83
546
  );
84
- const pendingCount = report.criteria.filter((c) => c.status === "pending").length;
85
- if (pendingCount > 0) {
547
+ if (remoteMeta) {
86
548
  console.log(
87
- kleur.yellow(
88
- `
89
- ${pendingCount} criteria require an agent. Paste prompts/self-assessment.md from the rubric repo to complete the report.`
549
+ kleur.dim(
550
+ ` (remote: ${remoteMeta.pendingScored ?? 0} criteria scored by ${remoteMeta.model ?? "API"})`
90
551
  )
91
552
  );
92
553
  }
554
+ const pendingCount = report.criteria.filter(
555
+ (c) => c.status === "pending"
556
+ ).length;
557
+ if (pendingCount > 0) {
558
+ console.log("");
559
+ console.log(kleur.yellow(`${pendingCount} criteria require an agent to score.`));
560
+ const promptFile = written.find((p) => p.endsWith("agent-ready-prompt.md"));
561
+ if (promptFile) {
562
+ const promptRel = displayPath(promptFile, cwd);
563
+ console.log("");
564
+ if (!opts.remote) {
565
+ console.log(kleur.bold("Two ways to finish:"));
566
+ console.log(
567
+ ` ${kleur.cyan("--remote")} Re-run with ${kleur.cyan("--remote")} to auto-fill via the hosted scoring API.`
568
+ );
569
+ console.log(
570
+ ` ${kleur.cyan("Paste the prompt")} Open ${kleur.cyan(promptRel)} and paste it into your AI tool`
571
+ );
572
+ console.log(
573
+ ` (Builder.io Fusion, Cursor, Claude Code, Codex, ...).`
574
+ );
575
+ } else {
576
+ console.log(kleur.bold("Next step:"));
577
+ console.log(
578
+ ` Open ${kleur.cyan(promptRel)} and paste it into your AI tool to fill the remaining rows.`
579
+ );
580
+ }
581
+ }
582
+ } else {
583
+ console.log("");
584
+ console.log(kleur.green("\u2713 All criteria scored."));
585
+ }
93
586
  }
94
587
  main().catch((err) => {
95
588
  console.error(kleur.red("\u2716 "), err);
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  generateBacklog,
8
8
  renderMarkdown,
9
9
  runAssessment
10
- } from "./chunk-GWF7PSLR.js";
10
+ } from "./chunk-WHGIRQPX.js";
11
11
  export {
12
12
  RUBRIC,
13
13
  TIER_LABELS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentready-design-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Deterministic checks for the Agent-Ready Design rubric. Run against a design-system repo to score what can be scored without an LLM.",
5
5
  "license": "MIT",
6
6
  "author": "Hunter Gillispie <hunter@builder.io>",
@@ -37,6 +37,10 @@
37
37
  "README.md"
38
38
  ],
39
39
  "scripts": {
40
+ "embed": "node scripts/embed-prompt.mjs",
41
+ "prebuild": "pnpm embed",
42
+ "pretest": "pnpm embed",
43
+ "pretypecheck": "pnpm embed",
40
44
  "build": "tsup",
41
45
  "dev": "tsup --watch",
42
46
  "test": "vitest run",