regen skills

This commit is contained in:
Denis Angell
2026-05-13 19:25:06 +02:00
parent b2ef159aee
commit a0782daf46
11 changed files with 1759 additions and 274 deletions

View File

@@ -20,7 +20,7 @@
"typescript": "^5.7.0"
},
"engines": {
"node": ">=20"
"node": ">=20.12"
}
},
"node_modules/@anthropic-ai/claude-agent-sdk": {

View File

@@ -2,12 +2,16 @@
* Regen-skills mode: rebuild a module's skill file from ai.md inputs.
*
* For a given module (e.g. `protocol`, `ledger`, `consensus`), collect all
* `.ai.md` files under the matching source paths and ask the agent to
* produce an updated `docs/skills/<module>.md`.
* `.ai.md` files under the matching source paths and ask the Agent SDK to
* write an updated `docs/skills/<module>.md`.
*
* The agent writes the file via the `Write` tool rather than returning the
* skill content as text. This avoids hitting the per-turn output token
* limit on large modules (which previously truncated several skill files).
*/
import { existsSync, readdirSync, statSync } from 'node:fs';
import { readFile, writeFile } from 'node:fs/promises';
import { readFile } from 'node:fs/promises';
import { join, relative, resolve } from 'node:path';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { MODEL, MODULE_SKILL_MAP, PROMPTS_DIR, SKILLS_DIR, XRPLD_ROOT } from './config.js';
@@ -60,16 +64,17 @@ async function loadAiFiles(absPaths: readonly string[]): Promise<AiFile[]> {
* Regenerate the skill file for a given module name.
*
* @param moduleName - The skill file name without extension (e.g. "protocol",
* "ledger"). Must match a key in the MODULE_SKILL_MAP value set.
* "ledger"). Must match a value in MODULE_SKILL_MAP.
*/
export async function regenSkills(moduleName: string): Promise<void> {
const skillFile = `${moduleName}.md`;
const prefixes = prefixesForSkill(skillFile);
if (prefixes.length === 0) {
throw new Error(
`Unknown module: ${moduleName}. Valid modules: ${Array.from(new Set(Object.values(MODULE_SKILL_MAP).filter((v): v is string => v !== null))).join(', ')}`,
const known = Array.from(
new Set(Object.values(MODULE_SKILL_MAP).filter((v): v is string => v !== null)),
);
throw new Error(`Unknown module: ${moduleName}. Valid modules: ${known.join(', ')}`);
}
console.log(`Regenerating skill: ${skillFile}`);
@@ -84,6 +89,7 @@ export async function regenSkills(moduleName: string): Promise<void> {
const aiFiles = await loadAiFiles(aiPaths);
const skillPath = resolve(SKILLS_DIR, skillFile);
const skillRelPath = relative(XRPLD_ROOT, skillPath);
const existingSkill = existsSync(skillPath)
? await readFile(skillPath, 'utf8')
: '(no existing skill file — create a new one)';
@@ -94,7 +100,11 @@ export async function regenSkills(moduleName: string): Promise<void> {
.map((f) => `\n### \`${f.sourcePath}\`\n\n${f.content}`)
.join('\n\n---\n');
const userPrompt = `Regenerate the skill file: \`docs/skills/${skillFile}\`
const userPrompt = `Regenerate the skill file at: \`${skillRelPath}\`
Use the **Write** tool to write the new content to that path. Do NOT return
the skill content in your message — write it directly to the file. This
avoids hitting per-turn output token limits.
## Existing skill content
@@ -104,27 +114,31 @@ ${existingSkill}
${aiBlocks}
Produce the new complete skill file content as your final message.`;
When you have written the file, respond with a brief one-line confirmation.`;
let response = '';
const result = query({
prompt: userPrompt,
options: {
model: MODEL,
systemPrompt,
cwd: XRPLD_ROOT,
allowedTools: ['Read', 'Glob', 'Grep'],
allowedTools: ['Write', 'Read', 'Glob', 'Grep'],
permissionMode: 'acceptEdits',
},
});
let wroteFile = false;
for await (const message of result) {
if (message.type === 'assistant') {
const content = message.message?.content;
if (Array.isArray(content)) {
for (const block of content) {
if (block.type === 'text') {
response += block.text;
if (block.type === 'tool_use' && block.name === 'Write') {
const input = block.input as { file_path?: string } | undefined;
if (input?.file_path !== undefined) {
wroteFile = true;
console.log(` Agent wrote: ${input.file_path}`);
}
}
}
}
@@ -135,12 +149,10 @@ Produce the new complete skill file content as your final message.`;
}
}
const trimmed = response.trim();
if (trimmed.length === 0) {
console.error(' Agent returned empty response — skill file not updated.');
if (!wroteFile) {
console.error(' Agent did not call Write — skill file not updated.');
return;
}
await writeFile(skillPath, `${trimmed}\n`);
console.log(` Wrote: ${relative(XRPLD_ROOT, skillPath)}`);
console.log(` Wrote: ${skillRelPath}`);
}