@netanelyasi/agent-ready 0.2.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 +22 -6
- package/dist/cli.js +33 -3
- package/dist/generators/generate.js +31 -23
- package/dist/scanner/scanProject.js +34 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ CODEMAP.md # repository map for navigation
|
|
|
76
76
|
.agent-ready/report.md # readiness score and findings
|
|
77
77
|
.agent-ready/recommendations.md
|
|
78
78
|
.agent-ready/hooks/README.md
|
|
79
|
-
.
|
|
79
|
+
.claude/skills/*/SKILL.md # Claude Code-loadable skills
|
|
80
80
|
apps/*/CLAUDE.md # generated for detected monorepo workspaces
|
|
81
81
|
```
|
|
82
82
|
|
|
@@ -122,6 +122,14 @@ Preview generated files:
|
|
|
122
122
|
agent-ready init /path/to/project --dry-run
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
+
Preview generated file contents:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
agent-ready init /path/to/project --dry-run --verbose
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
> Run `agent-ready init` manually from your terminal, not from inside an autonomous coding agent. It writes agent harness files such as `CLAUDE.md`, `.claude/settings.json`, and `.claude/hooks/*`, which some agent security classifiers correctly treat as self-modification.
|
|
132
|
+
|
|
125
133
|
Generate the harness:
|
|
126
134
|
|
|
127
135
|
```bash
|
|
@@ -194,7 +202,7 @@ Generated hooks include:
|
|
|
194
202
|
- `PreToolUse` for `Write|Edit|MultiEdit` — blocks edits to generated/noisy paths such as `node_modules`, `dist`, `build`, `coverage`, `.next`, `vendor`, and `*.generated.*`.
|
|
195
203
|
- `PostToolUse` for `Write|Edit|MultiEdit` — reminds the agent which local validation command to run after edits.
|
|
196
204
|
|
|
197
|
-
### `.
|
|
205
|
+
### `.claude/skills/*/SKILL.md`
|
|
198
206
|
|
|
199
207
|
On-demand task expertise. Examples:
|
|
200
208
|
|
|
@@ -327,10 +335,10 @@ Generating 14 files:
|
|
|
327
335
|
- created: .agent-ready/report.md
|
|
328
336
|
- created: .agent-ready/recommendations.md
|
|
329
337
|
- created: .agent-ready/hooks/README.md
|
|
330
|
-
- created: .
|
|
331
|
-
- created: .
|
|
332
|
-
- created: .
|
|
333
|
-
- created: .
|
|
338
|
+
- created: .claude/skills/codebase-navigation/SKILL.md
|
|
339
|
+
- created: .claude/skills/validation/SKILL.md
|
|
340
|
+
- created: .claude/skills/nextjs-hydration/SKILL.md
|
|
341
|
+
- created: .claude/skills/supabase-debugging/SKILL.md
|
|
334
342
|
- created: apps/web/CLAUDE.md
|
|
335
343
|
- created: packages/db/CLAUDE.md
|
|
336
344
|
```
|
|
@@ -355,12 +363,20 @@ agent-ready init [path] --dry-run
|
|
|
355
363
|
|
|
356
364
|
Show which files would be generated without writing anything.
|
|
357
365
|
|
|
366
|
+
```bash
|
|
367
|
+
agent-ready init [path] --dry-run --verbose
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Show the generated contents too. Use this before deciding whether to run `init` for real or manually merge proposed files.
|
|
371
|
+
|
|
358
372
|
```bash
|
|
359
373
|
agent-ready init [path] --force
|
|
360
374
|
```
|
|
361
375
|
|
|
362
376
|
Overwrite existing files instead of writing `*.agent-ready-proposed`.
|
|
363
377
|
|
|
378
|
+
When existing harness files are present, default `init` writes `*.agent-ready-proposed` files. Review and manually merge them; `agent-ready` does not assume its generated `CLAUDE.md` is better than a maintainer-authored one.
|
|
379
|
+
|
|
364
380
|
## Development
|
|
365
381
|
|
|
366
382
|
Requirements:
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { scoreReadiness } from "./analyzers/scoreReadiness.js";
|
|
4
4
|
import { generateFiles } from "./generators/generate.js";
|
|
5
5
|
import { scanProject } from "./scanner/scanProject.js";
|
|
6
|
-
import { safeWriteFile } from "./utils/fs.js";
|
|
6
|
+
import { pathExists, safeWriteFile } from "./utils/fs.js";
|
|
7
7
|
async function main() {
|
|
8
8
|
const args = parseArgs(process.argv.slice(2));
|
|
9
9
|
if (args.command === "help") {
|
|
@@ -18,17 +18,47 @@ async function main() {
|
|
|
18
18
|
return;
|
|
19
19
|
const files = generateFiles(scan, score, args.force);
|
|
20
20
|
console.log(`\nGenerating ${files.length} files${args.dryRun ? " (dry-run)" : ""}:`);
|
|
21
|
+
let proposedCount = 0;
|
|
21
22
|
for (const file of files) {
|
|
22
23
|
const relative = path.relative(root, file.path).replaceAll(path.sep, "/");
|
|
23
24
|
if (args.dryRun) {
|
|
24
|
-
|
|
25
|
+
const result = await plannedWriteResult(file.path, args.force);
|
|
26
|
+
if (result === "proposed")
|
|
27
|
+
proposedCount += 1;
|
|
28
|
+
const target = result === "proposed" ? `${relative}.agent-ready-proposed` : relative;
|
|
29
|
+
console.log(`- would ${writeVerb(result)}: ${target}`);
|
|
30
|
+
if (args.verbose)
|
|
31
|
+
printDryRunContent(target, file.content);
|
|
25
32
|
continue;
|
|
26
33
|
}
|
|
27
34
|
const result = await safeWriteFile(file.path, file.content, args.force);
|
|
35
|
+
if (result === "proposed")
|
|
36
|
+
proposedCount += 1;
|
|
28
37
|
const target = result === "proposed" ? `${relative}.agent-ready-proposed` : relative;
|
|
29
38
|
console.log(`- ${result}: ${target}`);
|
|
30
39
|
}
|
|
31
40
|
console.log("\nDone. Start with CODEMAP.md and .agent-ready/report.md.");
|
|
41
|
+
if (proposedCount > 0)
|
|
42
|
+
console.log(`Review and manually merge ${proposedCount} *.agent-ready-proposed file(s); existing harness files were not overwritten.`);
|
|
43
|
+
}
|
|
44
|
+
async function plannedWriteResult(filePath, force) {
|
|
45
|
+
const exists = await pathExists(filePath);
|
|
46
|
+
if (force)
|
|
47
|
+
return exists ? "overwritten" : "created";
|
|
48
|
+
return exists ? "proposed" : "created";
|
|
49
|
+
}
|
|
50
|
+
function writeVerb(result) {
|
|
51
|
+
if (result === "created")
|
|
52
|
+
return "create";
|
|
53
|
+
if (result === "overwritten")
|
|
54
|
+
return "overwrite";
|
|
55
|
+
return "propose";
|
|
56
|
+
}
|
|
57
|
+
function printDryRunContent(target, content) {
|
|
58
|
+
console.log(` --- ${target} begin ---`);
|
|
59
|
+
for (const line of content.trimEnd().split("\n"))
|
|
60
|
+
console.log(` ${line}`);
|
|
61
|
+
console.log(` --- ${target} end ---`);
|
|
32
62
|
}
|
|
33
63
|
function parseArgs(argv) {
|
|
34
64
|
const command = argv[0] === "init" || argv[0] === "analyze" ? argv[0] : argv[0] ? "help" : "help";
|
|
@@ -65,7 +95,7 @@ function printSummary(scan, score) {
|
|
|
65
95
|
}
|
|
66
96
|
}
|
|
67
97
|
function printHelp() {
|
|
68
|
-
console.log(`agent-ready\n\nUsage:\n agent-ready analyze [path]\n agent-ready init [path] [--dry-run] [--force]\n\nCommands:\n analyze Scan project and print readiness summary\n init Generate CLAUDE.md, CODEMAP.md, .aiignore, settings, skills, and reports\n\nOptions:\n --dry-run Show files that would be written\n --force Overwrite existing files instead of writing *.agent-ready-proposed\n`);
|
|
98
|
+
console.log(`agent-ready\n\nUsage:\n agent-ready analyze [path]\n agent-ready init [path] [--dry-run] [--verbose] [--force]\n\nCommands:\n analyze Scan project and print readiness summary\n init Generate CLAUDE.md, CODEMAP.md, .aiignore, settings, skills, and reports\n\nOptions:\n --dry-run Show files that would be written\n --verbose With --dry-run, print generated file contents\n --force Overwrite existing files instead of writing *.agent-ready-proposed\n`);
|
|
69
99
|
}
|
|
70
100
|
main().catch((error) => {
|
|
71
101
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -15,7 +15,7 @@ export function generateFiles(scan, score, force) {
|
|
|
15
15
|
add(`${subdir.dir}/CLAUDE.md`, subdir.content);
|
|
16
16
|
}
|
|
17
17
|
for (const skill of generateSkills(scan)) {
|
|
18
|
-
add(`.
|
|
18
|
+
add(`.claude/skills/${skill.slug}/SKILL.md`, skill.content);
|
|
19
19
|
}
|
|
20
20
|
return files;
|
|
21
21
|
function add(relativePath, content) {
|
|
@@ -114,7 +114,7 @@ function generateClaudeMd(scan) {
|
|
|
114
114
|
if (scan.frameworks.includes("Next.js")) {
|
|
115
115
|
lines.push("- Next.js detected: avoid hydration mismatches; access browser-only APIs only in client-safe code.");
|
|
116
116
|
}
|
|
117
|
-
lines.push("", "## Generated Skills", "See `.
|
|
117
|
+
lines.push("", "## Generated Skills", "See `.claude/skills/*/SKILL.md` for task-specific instructions that Claude Code can load on demand. Keep skills focused and avoid copying them wholesale into this file.", "");
|
|
118
118
|
return lines.join("\n");
|
|
119
119
|
}
|
|
120
120
|
function commandLines(scan) {
|
|
@@ -196,33 +196,40 @@ function externalImportLines(scan) {
|
|
|
196
196
|
function generateAiIgnore(scan) {
|
|
197
197
|
const base = [
|
|
198
198
|
"# Generated by agent-ready",
|
|
199
|
-
"node_modules/",
|
|
200
|
-
".next/",
|
|
201
|
-
".nuxt/",
|
|
202
|
-
"dist/",
|
|
203
|
-
"build/",
|
|
204
199
|
"coverage/",
|
|
205
|
-
".turbo/",
|
|
206
200
|
".cache/",
|
|
207
|
-
".venv/",
|
|
208
|
-
"venv/",
|
|
209
|
-
"__pycache__/",
|
|
210
|
-
"vendor/",
|
|
211
|
-
"target/",
|
|
212
|
-
"bin/",
|
|
213
|
-
"obj/",
|
|
214
201
|
"*.log",
|
|
215
202
|
"*.lock",
|
|
216
203
|
"generated/",
|
|
217
204
|
"**/*.generated.*",
|
|
218
205
|
];
|
|
206
|
+
const add = (...patterns) => {
|
|
207
|
+
for (const pattern of patterns)
|
|
208
|
+
if (!base.includes(pattern))
|
|
209
|
+
base.push(pattern);
|
|
210
|
+
};
|
|
211
|
+
if (scan.packageManager || scan.languages.some((language) => ["TypeScript", "JavaScript", "Svelte"].includes(language)))
|
|
212
|
+
add("node_modules/", "dist/", "build/", ".turbo/");
|
|
213
|
+
if (scan.frameworks.includes("Next.js"))
|
|
214
|
+
add(".next/");
|
|
215
|
+
if (scan.frameworks.includes("Nuxt"))
|
|
216
|
+
add(".nuxt/");
|
|
217
|
+
if (scan.languages.includes("Python"))
|
|
218
|
+
add(".venv/", "venv/", "__pycache__/");
|
|
219
|
+
if (scan.languages.includes("Rust"))
|
|
220
|
+
add("target/");
|
|
221
|
+
if (scan.languages.includes("Go"))
|
|
222
|
+
add("bin/");
|
|
223
|
+
if (scan.languages.includes("C#"))
|
|
224
|
+
add("bin/", "obj/");
|
|
225
|
+
if (scan.languages.includes("PHP") || scan.frameworks.some((framework) => framework.includes("Composer")))
|
|
226
|
+
add("vendor/");
|
|
219
227
|
for (const noisy of scan.noisyPaths)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return `${[...new Set(base)].join("\n")}\n`;
|
|
228
|
+
add(`${noisy}/`);
|
|
229
|
+
return `${base.join("\n")}\n`;
|
|
223
230
|
}
|
|
224
231
|
function generateClaudeSettings(scan) {
|
|
225
|
-
const
|
|
232
|
+
const deniedPathPatterns = [
|
|
226
233
|
"node_modules/**",
|
|
227
234
|
".next/**",
|
|
228
235
|
"dist/**",
|
|
@@ -233,6 +240,8 @@ function generateClaudeSettings(scan) {
|
|
|
233
240
|
"generated/**",
|
|
234
241
|
"**/*.generated.*",
|
|
235
242
|
];
|
|
243
|
+
const deniedWriteTools = ["Write", "Edit", "MultiEdit"];
|
|
244
|
+
const deny = deniedWriteTools.flatMap((tool) => deniedPathPatterns.map((pattern) => `${tool}(${pattern})`));
|
|
236
245
|
const settings = {
|
|
237
246
|
permissions: { deny: [...new Set(deny)] },
|
|
238
247
|
hooks: {
|
|
@@ -245,7 +254,7 @@ function generateClaudeSettings(scan) {
|
|
|
245
254
|
command: "node",
|
|
246
255
|
args: ["${CLAUDE_PROJECT_DIR}/.claude/hooks/prevent-destructive.mjs"],
|
|
247
256
|
timeout: 10,
|
|
248
|
-
statusMessage: "Checking command safety",
|
|
257
|
+
statusMessage: "agent-ready: Checking command safety",
|
|
249
258
|
},
|
|
250
259
|
],
|
|
251
260
|
},
|
|
@@ -257,7 +266,7 @@ function generateClaudeSettings(scan) {
|
|
|
257
266
|
command: "node",
|
|
258
267
|
args: ["${CLAUDE_PROJECT_DIR}/.claude/hooks/protect-generated.mjs"],
|
|
259
268
|
timeout: 10,
|
|
260
|
-
statusMessage: "Checking generated/noisy path safety",
|
|
269
|
+
statusMessage: "agent-ready: Checking generated/noisy path safety",
|
|
261
270
|
},
|
|
262
271
|
],
|
|
263
272
|
},
|
|
@@ -271,13 +280,12 @@ function generateClaudeSettings(scan) {
|
|
|
271
280
|
command: "node",
|
|
272
281
|
args: ["${CLAUDE_PROJECT_DIR}/.claude/hooks/suggest-validation.mjs"],
|
|
273
282
|
timeout: 10,
|
|
274
|
-
statusMessage: "Suggesting validation",
|
|
283
|
+
statusMessage: "agent-ready: Suggesting validation",
|
|
275
284
|
},
|
|
276
285
|
],
|
|
277
286
|
},
|
|
278
287
|
],
|
|
279
288
|
},
|
|
280
|
-
agentReady: { generatedBy: "agent-ready", project: scan.name },
|
|
281
289
|
};
|
|
282
290
|
return `${JSON.stringify(settings, null, 2)}\n`;
|
|
283
291
|
}
|
|
@@ -10,6 +10,7 @@ const LANGUAGE_EXTS = {
|
|
|
10
10
|
"C#": [".cs"],
|
|
11
11
|
Go: [".go"],
|
|
12
12
|
Rust: [".rs"],
|
|
13
|
+
Svelte: [".svelte"],
|
|
13
14
|
"C/C++": [".c", ".cc", ".cpp", ".h", ".hpp"],
|
|
14
15
|
};
|
|
15
16
|
export async function scanProject(rootInput) {
|
|
@@ -263,12 +264,12 @@ async function harnessFileState(root, relativePath) {
|
|
|
263
264
|
if (!exists)
|
|
264
265
|
return { exists: false, generatedByAgentReady: false, countsAsMaintainerAuthored: false };
|
|
265
266
|
const text = await readText(fullPath);
|
|
266
|
-
const generatedByAgentReady = Boolean(text && /generated by `?agent-ready`?|
|
|
267
|
+
const generatedByAgentReady = Boolean(text && /generated by `?agent-ready`?|agent-ready:|Generated by agent-ready/i.test(text));
|
|
267
268
|
return { exists: true, generatedByAgentReady, countsAsMaintainerAuthored: !generatedByAgentReady };
|
|
268
269
|
}
|
|
269
270
|
async function analyzeCodeGraph(root, files, packages) {
|
|
270
271
|
const sourceFiles = files
|
|
271
|
-
.filter((file) => /\.(tsx?|jsx?|mjs|cjs|py|go|rs)$/.test(file))
|
|
272
|
+
.filter((file) => /\.(tsx?|jsx?|mjs|cjs|py|go|rs|svelte)$/.test(file))
|
|
272
273
|
.filter((file) => !file.endsWith(".d.ts"))
|
|
273
274
|
.filter((file) => !file.includes(`${path.sep}dist${path.sep}`) && !file.includes(`${path.sep}node_modules${path.sep}`) && !file.includes(`${path.sep}target${path.sep}`))
|
|
274
275
|
.slice(0, 2000);
|
|
@@ -342,10 +343,6 @@ function detectEntryPoints(root, packages, sourceSet) {
|
|
|
342
343
|
["src/main.js", "application entry", "conventional app entry"],
|
|
343
344
|
["src/server.ts", "server entry", "conventional server entry"],
|
|
344
345
|
["src/server.js", "server entry", "conventional server entry"],
|
|
345
|
-
["app/page.tsx", "Next.js route", "App Router page"],
|
|
346
|
-
["app/layout.tsx", "Next.js layout", "App Router layout"],
|
|
347
|
-
["src/app/page.tsx", "Next.js route", "App Router page"],
|
|
348
|
-
["pages/index.tsx", "Next.js route", "Pages Router index"],
|
|
349
346
|
["main.py", "Python entry", "conventional Python entry"],
|
|
350
347
|
["app.py", "Python app", "conventional Python app entry"],
|
|
351
348
|
["src/main.py", "Python entry", "conventional Python entry"],
|
|
@@ -363,10 +360,9 @@ function detectEntryPoints(root, packages, sourceSet) {
|
|
|
363
360
|
if (!source.startsWith(packageRoot))
|
|
364
361
|
continue;
|
|
365
362
|
const local = source.slice(packageRoot.length);
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
entries.set(source, { path: source, kind: "Next.js route", reason: "App Router route/page" });
|
|
363
|
+
const frameworkEntry = frameworkEntryPoint(local);
|
|
364
|
+
if (frameworkEntry)
|
|
365
|
+
entries.set(source, { path: source, ...frameworkEntry });
|
|
370
366
|
if (/^cmd\/[^/]+\/main\.go$/.test(local))
|
|
371
367
|
entries.set(source, { path: source, kind: "Go command", reason: "cmd/*/main.go" });
|
|
372
368
|
if (/^src\/bin\/[^/]+\.rs$/.test(local))
|
|
@@ -380,6 +376,34 @@ function detectEntryPoints(root, packages, sourceSet) {
|
|
|
380
376
|
}
|
|
381
377
|
return [...entries.values()].sort((a, b) => a.path.localeCompare(b.path)).slice(0, 40);
|
|
382
378
|
}
|
|
379
|
+
function frameworkEntryPoint(localPath) {
|
|
380
|
+
if (/^(src\/)?app\/(page|layout|route)\.(tsx?|jsx?)$/.test(localPath)) {
|
|
381
|
+
const file = localPath.includes("/layout.") ? "layout" : localPath.includes("/route.") ? "route handler" : "page";
|
|
382
|
+
return { kind: `Next.js ${file}`, reason: "App Router root entry" };
|
|
383
|
+
}
|
|
384
|
+
if (/^(src\/)?app\/.+\/(page|layout|route|loading|error|not-found)\.(tsx?|jsx?)$/.test(localPath)) {
|
|
385
|
+
return { kind: "Next.js route", reason: "App Router route segment entry" };
|
|
386
|
+
}
|
|
387
|
+
if (/^(src\/)?pages\/index\.(tsx?|jsx?)$/.test(localPath))
|
|
388
|
+
return { kind: "Next.js route", reason: "Pages Router index" };
|
|
389
|
+
if (/^(src\/)?pages\/(api\/.+|.+)\.(tsx?|jsx?)$/.test(localPath))
|
|
390
|
+
return { kind: "Next.js route", reason: "Pages Router route/API entry" };
|
|
391
|
+
if (/^(src\/)?middleware\.(tsx?|jsx?)$/.test(localPath))
|
|
392
|
+
return { kind: "Next.js middleware", reason: "Next.js request middleware entry" };
|
|
393
|
+
if (/^next\.config\.(tsx?|jsx?|mjs|cjs)$/.test(localPath))
|
|
394
|
+
return { kind: "Next.js config", reason: "Next.js configuration entry" };
|
|
395
|
+
if (/^app\/(root|entry\.(client|server))\.(tsx?|jsx?)$/.test(localPath))
|
|
396
|
+
return { kind: "Remix entry", reason: "Remix root/client/server entry" };
|
|
397
|
+
if (/^app\/routes\/.+\.(tsx?|jsx?)$/.test(localPath))
|
|
398
|
+
return { kind: "Remix route", reason: "Remix route module" };
|
|
399
|
+
if (/^src\/routes\/(\+page|\+layout|\+server)\.(svelte|tsx?|jsx?)$/.test(localPath))
|
|
400
|
+
return { kind: "SvelteKit route", reason: "SvelteKit root route entry" };
|
|
401
|
+
if (/^src\/routes\/.+\/(\+page|\+layout|\+server)\.(svelte|tsx?|jsx?)$/.test(localPath))
|
|
402
|
+
return { kind: "SvelteKit route", reason: "SvelteKit route entry" };
|
|
403
|
+
if (/^src\/hooks(\.server)?\.(tsx?|jsx?)$/.test(localPath))
|
|
404
|
+
return { kind: "SvelteKit hook", reason: "SvelteKit lifecycle hook entry" };
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
383
407
|
function extractImportSpecifiers(text, from) {
|
|
384
408
|
const specifiers = new Set();
|
|
385
409
|
const ext = path.posix.extname(from);
|
package/package.json
CHANGED