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 +50 -19
- package/dist/{chunk-GWF7PSLR.js → chunk-WHGIRQPX.js} +121 -120
- package/dist/cli.js +514 -21
- package/dist/index.js +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,37 +1,72 @@
|
|
|
1
1
|
# agentready-design-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Score a design system against the [Agent-Ready Design rubric](https://github.com/hgillispie/Agent-Ready-Design) in one command.
|
|
4
4
|
|
|
5
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
--
|
|
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
|
-
|
|
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
|
-
|
|
31
|
+
## Flags
|
|
26
32
|
|
|
27
33
|
```
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
44
|
+
## What gets scored
|
|
33
45
|
|
|
34
|
-
|
|
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-
|
|
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("
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
85
|
-
if (pendingCount > 0) {
|
|
547
|
+
if (remoteMeta) {
|
|
86
548
|
console.log(
|
|
87
|
-
kleur.
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentready-design-cli",
|
|
3
|
-
"version": "0.1
|
|
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",
|