@netanelyasi/agent-ready 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +457 -0
- package/dist/analyzers/scoreReadiness.d.ts +2 -0
- package/dist/analyzers/scoreReadiness.js +49 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +73 -0
- package/dist/generators/generate.d.ts +2 -0
- package/dist/generators/generate.js +482 -0
- package/dist/scanner/scanProject.d.ts +2 -0
- package/dist/scanner/scanProject.js +544 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.js +1 -0
- package/dist/utils/fs.d.ts +12 -0
- package/dist/utils/fs.js +102 -0
- package/package.json +51 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function generateFiles(scan, score, force) {
|
|
3
|
+
const files = [];
|
|
4
|
+
add("CLAUDE.md", generateClaudeMd(scan));
|
|
5
|
+
add("CODEMAP.md", generateCodemap(scan));
|
|
6
|
+
add(".aiignore", generateAiIgnore(scan));
|
|
7
|
+
add(".claude/settings.json", generateClaudeSettings(scan));
|
|
8
|
+
add(".claude/hooks/prevent-destructive.mjs", generatePreventDestructiveHook());
|
|
9
|
+
add(".claude/hooks/protect-generated.mjs", generateProtectGeneratedHook());
|
|
10
|
+
add(".claude/hooks/suggest-validation.mjs", generateSuggestValidationHook(scan));
|
|
11
|
+
add(".agent-ready/report.md", generateReport(scan, score));
|
|
12
|
+
add(".agent-ready/recommendations.md", generateRecommendations(scan));
|
|
13
|
+
add(".agent-ready/hooks/README.md", generateHooksReadme(scan));
|
|
14
|
+
for (const subdir of subdirClaudeFiles(scan)) {
|
|
15
|
+
add(`${subdir.dir}/CLAUDE.md`, subdir.content);
|
|
16
|
+
}
|
|
17
|
+
for (const skill of generateSkills(scan)) {
|
|
18
|
+
add(`.agent-ready/skills/${skill.slug}/SKILL.md`, skill.content);
|
|
19
|
+
}
|
|
20
|
+
return files;
|
|
21
|
+
function add(relativePath, content) {
|
|
22
|
+
files.push({ path: path.join(scan.root, relativePath), content, kind: force ? "overwrite" : "create" });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function subdirClaudeFiles(scan) {
|
|
26
|
+
if (!scan.monorepo.detected)
|
|
27
|
+
return [];
|
|
28
|
+
return scan.packages
|
|
29
|
+
.filter((pkg) => pkg.path !== "package.json")
|
|
30
|
+
.map((pkg) => {
|
|
31
|
+
const dir = path.posix.dirname(pkg.path);
|
|
32
|
+
const commands = packageCommandLines(pkg, scan.packageManager);
|
|
33
|
+
return {
|
|
34
|
+
dir,
|
|
35
|
+
content: [
|
|
36
|
+
`# ${dir} — Local AI Agent Guide`,
|
|
37
|
+
"",
|
|
38
|
+
"This file should contain only conventions and commands for this workspace. Keep broad repository rules in the root `CLAUDE.md`.",
|
|
39
|
+
"",
|
|
40
|
+
"## Workspace",
|
|
41
|
+
`- Package: ${pkg.name ?? "unnamed"}`,
|
|
42
|
+
`- Path: \`${dir}\``,
|
|
43
|
+
"",
|
|
44
|
+
"## Local Commands",
|
|
45
|
+
...commands,
|
|
46
|
+
"",
|
|
47
|
+
"## Local Navigation",
|
|
48
|
+
"- Start searches inside this directory before searching the whole repository.",
|
|
49
|
+
"- Prefer local tests/builds over root-wide validation unless the change crosses package boundaries.",
|
|
50
|
+
"- Add workspace-specific gotchas here as they are discovered.",
|
|
51
|
+
"",
|
|
52
|
+
].join("\n"),
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function packageCommandLines(pkg, projectPackageManager) {
|
|
57
|
+
const dir = path.posix.dirname(pkg.path) === "." ? "." : path.posix.dirname(pkg.path);
|
|
58
|
+
const pm = pkg.packageManager?.split("@")[0] ?? projectPackageManager ?? "npm";
|
|
59
|
+
const rows = [];
|
|
60
|
+
const candidates = [
|
|
61
|
+
["dev", ["dev", "start:dev", "serve"]],
|
|
62
|
+
["build", ["build", "compile"]],
|
|
63
|
+
["test", ["test", "test:unit", "unit"]],
|
|
64
|
+
["lint", ["lint", "eslint"]],
|
|
65
|
+
["typecheck", ["typecheck", "type-check", "check", "tsc"]],
|
|
66
|
+
["format", ["format", "prettier"]],
|
|
67
|
+
];
|
|
68
|
+
for (const [label, names] of candidates) {
|
|
69
|
+
const scriptName = names.find((name) => pkg.scripts[name]);
|
|
70
|
+
if (scriptName)
|
|
71
|
+
rows.push(`- ${label}: \`cd ${dir} && ${pm} run ${scriptName}\``);
|
|
72
|
+
}
|
|
73
|
+
return rows.length ? rows : ["- No local package scripts detected. Add workspace-specific validation commands here."];
|
|
74
|
+
}
|
|
75
|
+
function generateClaudeMd(scan) {
|
|
76
|
+
const lines = [
|
|
77
|
+
`# ${scan.name} — AI Agent Guide`,
|
|
78
|
+
"",
|
|
79
|
+
"This file is generated by `agent-ready`. Keep it lean: broad project facts, critical gotchas, and validation commands only. Move task-specific expertise into skills.",
|
|
80
|
+
"",
|
|
81
|
+
"## Project Snapshot",
|
|
82
|
+
`- Languages: ${list(scan.languages)}`,
|
|
83
|
+
`- Frameworks: ${list(scan.frameworks)}`,
|
|
84
|
+
`- Databases/tools: ${list(scan.databases)}`,
|
|
85
|
+
`- Deployment: ${list(scan.deployment)}`,
|
|
86
|
+
`- Package manager: ${scan.packageManager ?? "unknown"}`,
|
|
87
|
+
`- Monorepo: ${scan.monorepo.detected ? `yes (${list(scan.monorepo.tools)})` : "no/unclear"}`,
|
|
88
|
+
"",
|
|
89
|
+
"## How to Navigate",
|
|
90
|
+
"- Start from `CODEMAP.md` before broad searches.",
|
|
91
|
+
"- Prefer targeted file reads and symbol/reference search over opening many files.",
|
|
92
|
+
"- Do not inspect generated/build/vendor folders unless the task explicitly requires it.",
|
|
93
|
+
"- In monorepos, work from the relevant subdirectory and use local commands when possible.",
|
|
94
|
+
"",
|
|
95
|
+
"## Important Directories",
|
|
96
|
+
...scan.importantDirs.map((dir) => `- \`${dir.path}\` — ${dir.reason}${dir.children.length ? ` (${dir.children.slice(0, 8).join(", ")})` : ""}`),
|
|
97
|
+
"",
|
|
98
|
+
"## Validation Commands",
|
|
99
|
+
...commandLines(scan),
|
|
100
|
+
"",
|
|
101
|
+
"## Agent Operating Rules",
|
|
102
|
+
"- Make the smallest safe change that satisfies the request.",
|
|
103
|
+
"- Before editing, identify the exact file(s) and local conventions.",
|
|
104
|
+
"- After code changes, run the narrowest relevant validation command listed above.",
|
|
105
|
+
"- If validation is missing or expensive, state what could not be verified.",
|
|
106
|
+
"- Never claim actions were performed unless the tool/command actually succeeded.",
|
|
107
|
+
];
|
|
108
|
+
if (scan.traits.hasHebrewOrRtl) {
|
|
109
|
+
lines.push("- This project appears to contain Hebrew/RTL UI. Preserve RTL semantics and use logical CSS (`start`/`end`) where possible.");
|
|
110
|
+
}
|
|
111
|
+
if (scan.databases.includes("Supabase")) {
|
|
112
|
+
lines.push("- Supabase detected: verify schema/RLS/query behavior before proposing database changes.");
|
|
113
|
+
}
|
|
114
|
+
if (scan.frameworks.includes("Next.js")) {
|
|
115
|
+
lines.push("- Next.js detected: avoid hydration mismatches; access browser-only APIs only in client-safe code.");
|
|
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.", "");
|
|
118
|
+
return lines.join("\n");
|
|
119
|
+
}
|
|
120
|
+
function commandLines(scan) {
|
|
121
|
+
const order = ["dev", "build", "test", "lint", "typecheck", "format"];
|
|
122
|
+
const lines = [];
|
|
123
|
+
for (const name of order) {
|
|
124
|
+
const commands = scan.commands[name];
|
|
125
|
+
if (!commands?.length)
|
|
126
|
+
continue;
|
|
127
|
+
lines.push(`- ${name}:`);
|
|
128
|
+
for (const command of commands.slice(0, 8))
|
|
129
|
+
lines.push(` - \`${command}\``);
|
|
130
|
+
}
|
|
131
|
+
if (!lines.length)
|
|
132
|
+
lines.push("- No standard validation commands were detected. Add project-specific commands here.");
|
|
133
|
+
return lines;
|
|
134
|
+
}
|
|
135
|
+
function generateCodemap(scan) {
|
|
136
|
+
return [
|
|
137
|
+
`# ${scan.name} — CODEMAP`,
|
|
138
|
+
"",
|
|
139
|
+
"Generated by `agent-ready`. Use this as the first stop for AI agents before broad code search.",
|
|
140
|
+
"",
|
|
141
|
+
"## Entry Points",
|
|
142
|
+
...entryPointLines(scan),
|
|
143
|
+
"",
|
|
144
|
+
"## Central Files",
|
|
145
|
+
"Files with the most internal imports pointing at them. These are often shared utilities, public APIs, or architectural choke points.",
|
|
146
|
+
"",
|
|
147
|
+
...centralFileLines(scan),
|
|
148
|
+
"",
|
|
149
|
+
"## Internal Import Graph",
|
|
150
|
+
"Representative resolved internal imports. Read this before doing broad grep-style exploration.",
|
|
151
|
+
"",
|
|
152
|
+
...importEdgeLines(scan),
|
|
153
|
+
"",
|
|
154
|
+
"## External Dependencies in Code",
|
|
155
|
+
...externalImportLines(scan),
|
|
156
|
+
"",
|
|
157
|
+
"## Top-level Map",
|
|
158
|
+
...scan.importantDirs.map((dir) => [
|
|
159
|
+
`### \`${dir.path}\``,
|
|
160
|
+
dir.reason,
|
|
161
|
+
dir.children.length ? `Children: ${dir.children.map((child) => `\`${child}\``).join(", ")}` : "Children: not listed.",
|
|
162
|
+
"",
|
|
163
|
+
].join("\n")),
|
|
164
|
+
"## Package Manifests",
|
|
165
|
+
...scan.packages.map((pkg) => `- \`${pkg.path}\`${pkg.name ? ` — ${pkg.name}` : ""}`),
|
|
166
|
+
"",
|
|
167
|
+
"## Search Guidance",
|
|
168
|
+
"- Start at an entry point, then follow imports to the relevant implementation.",
|
|
169
|
+
"- Use central files to identify shared abstractions before editing duplicated logic.",
|
|
170
|
+
"- Use exact symbols, route names, table names, or component names when possible.",
|
|
171
|
+
"- Start in the directory closest to the task before searching globally.",
|
|
172
|
+
"- Avoid noisy paths listed in `.aiignore` unless directly relevant.",
|
|
173
|
+
"",
|
|
174
|
+
].join("\n");
|
|
175
|
+
}
|
|
176
|
+
function entryPointLines(scan) {
|
|
177
|
+
if (!scan.codeGraph.entryPoints.length)
|
|
178
|
+
return ["- No entry points detected. Add known application, CLI, route, or library entry points here."];
|
|
179
|
+
return scan.codeGraph.entryPoints.map((entry) => `- \`${entry.path}\` — ${entry.kind}; ${entry.reason}`);
|
|
180
|
+
}
|
|
181
|
+
function centralFileLines(scan) {
|
|
182
|
+
if (!scan.codeGraph.centralFiles.length)
|
|
183
|
+
return ["- No central files detected from imports yet."];
|
|
184
|
+
return scan.codeGraph.centralFiles.slice(0, 15).map((file) => `- \`${file.path}\` — inbound: ${file.inbound}, outbound: ${file.outbound}`);
|
|
185
|
+
}
|
|
186
|
+
function importEdgeLines(scan) {
|
|
187
|
+
if (!scan.codeGraph.importEdges.length)
|
|
188
|
+
return ["- No resolved internal imports detected."];
|
|
189
|
+
return scan.codeGraph.importEdges.slice(0, 40).map((edge) => `- \`${edge.from}\` → \`${edge.to}\``);
|
|
190
|
+
}
|
|
191
|
+
function externalImportLines(scan) {
|
|
192
|
+
if (!scan.codeGraph.externalImports.length)
|
|
193
|
+
return ["- No external imports detected in scanned source files."];
|
|
194
|
+
return scan.codeGraph.externalImports.slice(0, 20).map((entry) => `- \`${entry.packageName}\` — used by ${entry.importedBy.slice(0, 5).map((file) => `\`${file}\``).join(", ")}${entry.importedBy.length > 5 ? " …" : ""}`);
|
|
195
|
+
}
|
|
196
|
+
function generateAiIgnore(scan) {
|
|
197
|
+
const base = [
|
|
198
|
+
"# Generated by agent-ready",
|
|
199
|
+
"node_modules/",
|
|
200
|
+
".next/",
|
|
201
|
+
".nuxt/",
|
|
202
|
+
"dist/",
|
|
203
|
+
"build/",
|
|
204
|
+
"coverage/",
|
|
205
|
+
".turbo/",
|
|
206
|
+
".cache/",
|
|
207
|
+
".venv/",
|
|
208
|
+
"venv/",
|
|
209
|
+
"__pycache__/",
|
|
210
|
+
"vendor/",
|
|
211
|
+
"target/",
|
|
212
|
+
"bin/",
|
|
213
|
+
"obj/",
|
|
214
|
+
"*.log",
|
|
215
|
+
"*.lock",
|
|
216
|
+
"generated/",
|
|
217
|
+
"**/*.generated.*",
|
|
218
|
+
];
|
|
219
|
+
for (const noisy of scan.noisyPaths)
|
|
220
|
+
if (!base.includes(`${noisy}/`))
|
|
221
|
+
base.push(`${noisy}/`);
|
|
222
|
+
return `${[...new Set(base)].join("\n")}\n`;
|
|
223
|
+
}
|
|
224
|
+
function generateClaudeSettings(scan) {
|
|
225
|
+
const deny = [
|
|
226
|
+
"node_modules/**",
|
|
227
|
+
".next/**",
|
|
228
|
+
"dist/**",
|
|
229
|
+
"build/**",
|
|
230
|
+
"coverage/**",
|
|
231
|
+
".turbo/**",
|
|
232
|
+
"vendor/**",
|
|
233
|
+
"generated/**",
|
|
234
|
+
"**/*.generated.*",
|
|
235
|
+
];
|
|
236
|
+
const settings = {
|
|
237
|
+
permissions: { deny: [...new Set(deny)] },
|
|
238
|
+
hooks: {
|
|
239
|
+
PreToolUse: [
|
|
240
|
+
{
|
|
241
|
+
matcher: "Bash",
|
|
242
|
+
hooks: [
|
|
243
|
+
{
|
|
244
|
+
type: "command",
|
|
245
|
+
command: "node",
|
|
246
|
+
args: ["${CLAUDE_PROJECT_DIR}/.claude/hooks/prevent-destructive.mjs"],
|
|
247
|
+
timeout: 10,
|
|
248
|
+
statusMessage: "Checking command safety",
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
matcher: "Write|Edit|MultiEdit",
|
|
254
|
+
hooks: [
|
|
255
|
+
{
|
|
256
|
+
type: "command",
|
|
257
|
+
command: "node",
|
|
258
|
+
args: ["${CLAUDE_PROJECT_DIR}/.claude/hooks/protect-generated.mjs"],
|
|
259
|
+
timeout: 10,
|
|
260
|
+
statusMessage: "Checking generated/noisy path safety",
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
PostToolUse: [
|
|
266
|
+
{
|
|
267
|
+
matcher: "Write|Edit|MultiEdit",
|
|
268
|
+
hooks: [
|
|
269
|
+
{
|
|
270
|
+
type: "command",
|
|
271
|
+
command: "node",
|
|
272
|
+
args: ["${CLAUDE_PROJECT_DIR}/.claude/hooks/suggest-validation.mjs"],
|
|
273
|
+
timeout: 10,
|
|
274
|
+
statusMessage: "Suggesting validation",
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
agentReady: { generatedBy: "agent-ready", project: scan.name },
|
|
281
|
+
};
|
|
282
|
+
return `${JSON.stringify(settings, null, 2)}\n`;
|
|
283
|
+
}
|
|
284
|
+
function generatePreventDestructiveHook() {
|
|
285
|
+
return `#!/usr/bin/env node
|
|
286
|
+
import { readFileSync } from "node:fs";
|
|
287
|
+
|
|
288
|
+
const input = JSON.parse(readFileSync(0, "utf8") || "{}");
|
|
289
|
+
const command = String(input.tool_input?.command ?? "");
|
|
290
|
+
|
|
291
|
+
const blockedPatterns = [
|
|
292
|
+
{ pattern: /(^|[;&|]\\s*)rm\\s+-[^\\n;]*r[^\\n;]*f[^\\n;]*(\\/|~|\\.|\\*)?(\\s|$)/i, reason: "Blocks rm -rf style destructive deletion." },
|
|
293
|
+
{ pattern: /git\\s+reset\\s+--hard/i, reason: "Blocks git reset --hard." },
|
|
294
|
+
{ pattern: /git\\s+clean\\s+-[^\\n;]*f/i, reason: "Blocks git clean -f." },
|
|
295
|
+
{ pattern: /git\\s+push\\s+([^\\n;]*\\s)?--force/i, reason: "Blocks force-push." },
|
|
296
|
+
{ pattern: /:\\(\\)\\s*\\{\\s*:\\|:&\\s*\\};:/, reason: "Blocks fork bomb pattern." },
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
const match = blockedPatterns.find((item) => item.pattern.test(command));
|
|
300
|
+
if (!match) process.exit(0);
|
|
301
|
+
|
|
302
|
+
console.log(JSON.stringify({
|
|
303
|
+
hookSpecificOutput: {
|
|
304
|
+
hookEventName: "PreToolUse",
|
|
305
|
+
permissionDecision: "deny",
|
|
306
|
+
permissionDecisionReason: \`agent-ready safety hook denied command. \${match.reason} Ask the user for explicit approval and use a narrower command.\`,
|
|
307
|
+
},
|
|
308
|
+
}));
|
|
309
|
+
`;
|
|
310
|
+
}
|
|
311
|
+
function generateProtectGeneratedHook() {
|
|
312
|
+
return `#!/usr/bin/env node
|
|
313
|
+
import { readFileSync } from "node:fs";
|
|
314
|
+
import path from "node:path";
|
|
315
|
+
|
|
316
|
+
const input = JSON.parse(readFileSync(0, "utf8") || "{}");
|
|
317
|
+
const rawPath = input.tool_input?.file_path ?? input.tool_input?.path ?? "";
|
|
318
|
+
if (!rawPath) process.exit(0);
|
|
319
|
+
|
|
320
|
+
const normalized = String(rawPath).replaceAll("\\\\", "/");
|
|
321
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
322
|
+
const deniedDirs = new Set(["node_modules", ".next", ".nuxt", "dist", "build", "coverage", ".turbo", "vendor", "target", "generated"]);
|
|
323
|
+
const generatedFile = /(^|\\/)generated(\\/|$)|\\.generated\\.[^/]+$/i.test(normalized);
|
|
324
|
+
const noisyDir = parts.some((part) => deniedDirs.has(part));
|
|
325
|
+
|
|
326
|
+
if (!generatedFile && !noisyDir) process.exit(0);
|
|
327
|
+
|
|
328
|
+
console.log(JSON.stringify({
|
|
329
|
+
hookSpecificOutput: {
|
|
330
|
+
hookEventName: "PreToolUse",
|
|
331
|
+
permissionDecision: "deny",
|
|
332
|
+
permissionDecisionReason: \`agent-ready safety hook denied editing generated/noisy path: \${path.basename(normalized)}. If this is intentional, ask the user to approve and edit settings.\`,
|
|
333
|
+
},
|
|
334
|
+
}));
|
|
335
|
+
`;
|
|
336
|
+
}
|
|
337
|
+
function generateSuggestValidationHook(scan) {
|
|
338
|
+
const commands = [...(scan.commands.lint ?? []), ...(scan.commands.typecheck ?? []), ...(scan.commands.test ?? []), ...(scan.commands.build ?? [])].slice(0, 3);
|
|
339
|
+
const message = commands.length
|
|
340
|
+
? `Suggested validation after edits: ${commands.map((command) => `\`${command}\``).join(", ")}. Run the narrowest relevant command before completion.`
|
|
341
|
+
: "No validation command was detected by agent-ready. Before completion, state that validation is undocumented unless the user provides a command.";
|
|
342
|
+
return `#!/usr/bin/env node
|
|
343
|
+
import { readFileSync } from "node:fs";
|
|
344
|
+
|
|
345
|
+
readFileSync(0, "utf8");
|
|
346
|
+
console.log(JSON.stringify({
|
|
347
|
+
systemMessage: ${JSON.stringify(message)},
|
|
348
|
+
}));
|
|
349
|
+
`;
|
|
350
|
+
}
|
|
351
|
+
function generateReport(scan, score) {
|
|
352
|
+
return [
|
|
353
|
+
`# Agent Readiness Report — ${scan.name}`,
|
|
354
|
+
"",
|
|
355
|
+
`Score: **${score.score}/100**`,
|
|
356
|
+
"",
|
|
357
|
+
"## Detected",
|
|
358
|
+
`- Languages: ${list(scan.languages)}`,
|
|
359
|
+
`- Frameworks: ${list(scan.frameworks)}`,
|
|
360
|
+
`- Databases/tools: ${list(scan.databases)}`,
|
|
361
|
+
`- Deployment: ${list(scan.deployment)}`,
|
|
362
|
+
`- Monorepo: ${scan.monorepo.detected ? "yes" : "no/unclear"}`,
|
|
363
|
+
`- Entry points: ${scan.codeGraph.entryPoints.length}`,
|
|
364
|
+
`- Resolved internal imports: ${scan.codeGraph.importEdges.length}`,
|
|
365
|
+
`- External packages imported: ${scan.codeGraph.externalImports.length}`,
|
|
366
|
+
"",
|
|
367
|
+
"## Strengths",
|
|
368
|
+
...bullet(score.strengths),
|
|
369
|
+
"",
|
|
370
|
+
"## Missing / Recommended Fixes",
|
|
371
|
+
...bullet(score.missing),
|
|
372
|
+
"",
|
|
373
|
+
"## Warnings",
|
|
374
|
+
...bullet(score.warnings),
|
|
375
|
+
"",
|
|
376
|
+
].join("\n");
|
|
377
|
+
}
|
|
378
|
+
function generateHooksReadme(scan) {
|
|
379
|
+
return [
|
|
380
|
+
"# Hook Templates",
|
|
381
|
+
"",
|
|
382
|
+
"These are safe starter hook policies. Wire them into your agent harness only after reviewing them for this repository.",
|
|
383
|
+
"",
|
|
384
|
+
"## Start Hook",
|
|
385
|
+
"- Load the nearest `CLAUDE.md` files for the current working directory.",
|
|
386
|
+
"- If the task mentions a framework/library, prefer current docs before implementation.",
|
|
387
|
+
"- For monorepos, identify the affected workspace before broad search.",
|
|
388
|
+
"",
|
|
389
|
+
"## Pre-edit / Pre-delete Hook",
|
|
390
|
+
"- Block edits in ignored/generated paths unless the user explicitly asked for generated output.",
|
|
391
|
+
"- Require confirmation before deleting files, rewriting broad directories, or changing deployment secrets.",
|
|
392
|
+
"",
|
|
393
|
+
"## Post-edit Hook",
|
|
394
|
+
...postEditHookLines(scan),
|
|
395
|
+
"",
|
|
396
|
+
"## Stop Hook",
|
|
397
|
+
"- Summarize what changed and what was validated.",
|
|
398
|
+
"- If a reusable gotcha/pattern was discovered, propose a focused update to the nearest `CLAUDE.md` or a skill.",
|
|
399
|
+
"- Do not store temporary session state as long-term memory.",
|
|
400
|
+
"",
|
|
401
|
+
].join("\n");
|
|
402
|
+
}
|
|
403
|
+
function postEditHookLines(scan) {
|
|
404
|
+
const lines = ["- Run or suggest the narrowest relevant validation command."];
|
|
405
|
+
if (scan.commands.lint?.length)
|
|
406
|
+
lines.push(`- Lint candidate: \`${scan.commands.lint[0]}\``);
|
|
407
|
+
if (scan.commands.typecheck?.length)
|
|
408
|
+
lines.push(`- Typecheck candidate: \`${scan.commands.typecheck[0]}\``);
|
|
409
|
+
if (scan.commands.test?.length)
|
|
410
|
+
lines.push(`- Test candidate: \`${scan.commands.test[0]}\``);
|
|
411
|
+
if (!scan.commands.lint?.length && !scan.commands.typecheck?.length && !scan.commands.test?.length)
|
|
412
|
+
lines.push("- No validation commands detected yet; ask maintainers to document them.");
|
|
413
|
+
return lines;
|
|
414
|
+
}
|
|
415
|
+
function generateRecommendations(scan) {
|
|
416
|
+
const recs = [];
|
|
417
|
+
recs.push("# Agent Harness Recommendations", "");
|
|
418
|
+
recs.push("## LSP", ...bullet(lspRecommendations(scan)), "");
|
|
419
|
+
recs.push("## MCP", ...bullet(mcpRecommendations(scan)), "");
|
|
420
|
+
recs.push("## Hooks", ...bullet(hookRecommendations(scan)), "");
|
|
421
|
+
recs.push("## Maintenance", "- Review CLAUDE.md and generated skills every 3–6 months or after major model/tool releases.", "- Move repeated session learnings into skills or focused subdirectory CLAUDE.md files.", "");
|
|
422
|
+
return recs.join("\n");
|
|
423
|
+
}
|
|
424
|
+
function lspRecommendations(scan) {
|
|
425
|
+
const out = [];
|
|
426
|
+
if (scan.traits.hasTypeScript || scan.languages.includes("JavaScript"))
|
|
427
|
+
out.push("Install/enable TypeScript language server and ESLint language server for symbol-aware navigation.");
|
|
428
|
+
if (scan.languages.includes("Python"))
|
|
429
|
+
out.push("Enable Pyright or basedpyright for Python symbol navigation.");
|
|
430
|
+
if (scan.languages.includes("Java"))
|
|
431
|
+
out.push("Enable Java language server (jdtls). ");
|
|
432
|
+
if (scan.languages.includes("C#"))
|
|
433
|
+
out.push("Enable C# Dev Kit / Roslyn LSP.");
|
|
434
|
+
if (scan.languages.includes("C/C++"))
|
|
435
|
+
out.push("Enable clangd with a compile_commands.json where possible.");
|
|
436
|
+
return out.length ? out : ["No specific LSP recommendation detected; add one for the primary language if available."];
|
|
437
|
+
}
|
|
438
|
+
function mcpRecommendations(scan) {
|
|
439
|
+
const out = ["Use Context7/library-docs MCP for up-to-date framework/library APIs."];
|
|
440
|
+
if (scan.databases.includes("Supabase"))
|
|
441
|
+
out.push("Use Supabase MCP for schema/RLS inspection before DB changes.");
|
|
442
|
+
if (scan.deployment.includes("GitHub Actions"))
|
|
443
|
+
out.push("Expose CI logs/status through GitHub tooling/MCP if available.");
|
|
444
|
+
if (scan.deployment.includes("Vercel"))
|
|
445
|
+
out.push("Use Vercel tooling/MCP for deployments and production logs.");
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
function hookRecommendations(scan) {
|
|
449
|
+
const out = [
|
|
450
|
+
"Stop hook: summarize useful session learnings and propose focused CLAUDE.md/skill updates.",
|
|
451
|
+
"Pre-edit/pre-delete hook: require confirmation for destructive operations and broad rewrites.",
|
|
452
|
+
];
|
|
453
|
+
if (scan.commands.lint?.length)
|
|
454
|
+
out.push("Post-edit hook: suggest or run the narrowest lint command for changed workspace.");
|
|
455
|
+
if (scan.commands.test?.length)
|
|
456
|
+
out.push("Post-edit hook: suggest targeted tests related to changed files.");
|
|
457
|
+
return out;
|
|
458
|
+
}
|
|
459
|
+
function generateSkills(scan) {
|
|
460
|
+
const skills = [
|
|
461
|
+
{ slug: "codebase-navigation", content: skill("codebase-navigation", "Use when starting work in this repository or when a task spans unfamiliar directories.", ["Read CODEMAP.md first.", "Use narrow searches from the relevant directory before global search.", "Prefer symbol/reference search when LSP is available.", "Do not inspect ignored/generated directories unless explicitly needed."]) },
|
|
462
|
+
{ slug: "validation", content: skill("validation", "Use after code edits or before declaring a task complete.", [...commandLines(scan).map((line) => line.replace(/^[- ]+/, "")), "Run the narrowest relevant command first.", "If validation cannot be run, report the exact reason."]) },
|
|
463
|
+
];
|
|
464
|
+
if (scan.frameworks.includes("Next.js"))
|
|
465
|
+
skills.push({ slug: "nextjs-hydration", content: skill("nextjs-hydration", "Use when editing Next.js/React components, routes, or client/server boundaries.", ["Do not read localStorage/sessionStorage/window/document during server render or initial state.", "Use useEffect or guarded client-only code for browser APIs.", "Avoid Math.random() or new Date() in render paths that must hydrate identically.", "Keep server/client boundaries explicit."]) });
|
|
466
|
+
if (scan.databases.includes("Supabase"))
|
|
467
|
+
skills.push({ slug: "supabase-debugging", content: skill("supabase-debugging", "Use when debugging Supabase queries, RLS, migrations, auth, or PostgREST errors.", ["Inspect the actual query shape before changing schema.", "Prefer maybeSingle() unless a row is guaranteed.", "Check RLS policies and authenticated role behavior before assuming frontend bugs.", "For DB changes, verify current schema first and use migrations."]) });
|
|
468
|
+
if (scan.traits.hasHebrewOrRtl)
|
|
469
|
+
skills.push({ slug: "rtl-ui", content: skill("rtl-ui", "Use when changing UI, layout, forms, tables, icons, or mixed Hebrew/English text.", ["Use logical alignment (`start`/`end`) and RTL-aware flex direction.", "Do not remove or hide broader parent containers when only one UI element was requested.", "Check mixed LTR/RTL content such as numbers, currency, percentages, paths, and code.", "Prefer targeted layout changes over broad CSS rewrites."]) });
|
|
470
|
+
if (scan.traits.hasDocker || scan.deployment.length)
|
|
471
|
+
skills.push({ slug: "deployment", content: skill("deployment", "Use when changing Docker, CI, deploy config, environment variables, or release flow.", ["Document required environment variables.", "Keep build-time and runtime environment requirements separate.", "Run local build or the closest available validation before deployment.", "Check git status so new files are not missed."]) });
|
|
472
|
+
return skills;
|
|
473
|
+
}
|
|
474
|
+
function skill(name, description, rules) {
|
|
475
|
+
return [`# ${name}`, "", `Description: ${description}`, "", "## Rules", ...rules.map((rule) => `- ${rule}`), ""].join("\n");
|
|
476
|
+
}
|
|
477
|
+
function list(items) {
|
|
478
|
+
return items.length ? items.join(", ") : "none detected";
|
|
479
|
+
}
|
|
480
|
+
function bullet(items) {
|
|
481
|
+
return items.length ? items.map((item) => `- ${item}`) : ["- None."];
|
|
482
|
+
}
|