@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 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
- .agent-ready/skills/*/SKILL.md
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
- ### `.agent-ready/skills/*/SKILL.md`
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: .agent-ready/skills/codebase-navigation/SKILL.md
331
- - created: .agent-ready/skills/validation/SKILL.md
332
- - created: .agent-ready/skills/nextjs-hydration/SKILL.md
333
- - created: .agent-ready/skills/supabase-debugging/SKILL.md
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
- console.log(`- would write ${relative}`);
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(`.agent-ready/skills/${skill.slug}/SKILL.md`, skill.content);
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 `.agent-ready/skills/*/SKILL.md` for task-specific instructions that should be loaded on demand, not copied wholesale into this file.", "");
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
- if (!base.includes(`${noisy}/`))
221
- base.push(`${noisy}/`);
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 deny = [
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`?|agentReady|Generated by agent-ready/i.test(text));
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
- if (/^pages\/api\/.+\.(tsx?|jsx?)$/.test(local))
367
- entries.set(source, { path: source, kind: "API route", reason: "Next.js API route" });
368
- if (/^app\/.+\/(page|route)\.(tsx?|jsx?)$/.test(local))
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netanelyasi/agent-ready",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Generate an AI-agent harness for any codebase: CLAUDE.md, CODEMAP.md, skills, ignore rules, and readiness reports.",
5
5
  "type": "module",
6
6
  "bin": {