move skills

This commit is contained in:
Denis Angell
2026-05-13 19:08:27 +02:00
parent 9032a31e26
commit b2ef159aee
23 changed files with 571 additions and 93 deletions

View File

@@ -3,6 +3,7 @@
*/
import { existsSync, readdirSync, statSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { join, relative, resolve } from 'node:path';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { MODEL, XRPLD_ROOT } from './config.js';
@@ -47,6 +48,21 @@ function findCppFiles(target: string): string[] {
return results;
}
/**
* Read the sibling .ai.md file for a source file, if one exists.
*
* The athenah-ai pipeline produces a `<file>.ai.md` companion for every
* documented source file (e.g., `Slice.h` -> `Slice.h.ai.md`). When present,
* it is high-signal prose describing the file's purpose, design, and
* non-obvious behavior — the agent should use it as the authoritative
* source of intent.
*/
async function readAiContext(absPath: string): Promise<string | null> {
const aiPath = `${absPath}.ai.md`;
if (!existsSync(aiPath)) return null;
return await readFile(aiPath, 'utf8');
}
/**
* Document a single file by running the documentation agent against it.
*/
@@ -55,13 +71,19 @@ async function documentFile(absPath: string): Promise<void> {
console.log(`\n=== Documenting: ${relPath} ===`);
const systemPrompt = await loadSystemPrompt('document-file', relPath);
const aiContext = await readAiContext(absPath);
const aiContextBlock =
aiContext === null
? ''
: `\n\n## Authoritative AI Context (${relPath}.ai.md)\n\nThe following is high-signal prose describing this file's purpose, design,\nand non-obvious behavior. Treat it as the source of truth for intent and\nbehavior. Your job is to translate this into structured Doxygen \`/** */\`\ncomments on the actual declarations.\n\n---\n\n${aiContext}\n---`;
const userPrompt = `Add Doxygen documentation to: ${relPath}
The file is rooted at ${XRPLD_ROOT}. Use the Read tool to read it, the Edit
tool to add documentation, and Glob/Grep to find related tests or callers
when needed.
Do not modify any code logic — only add documentation comments.`;
Do not modify any code logic — only add documentation comments.${aiContextBlock}`;
const result = query({
prompt: userPrompt,

View File

@@ -7,9 +7,11 @@
* doc-agent document include/xrpl/basics/
* doc-agent review develop..HEAD
* doc-agent review --pr 1234
* doc-agent regen-skills protocol
*/
import { documentTarget } from './document.js';
import { regenSkills } from './regen-skills.js';
import { reviewDiff } from './review.js';
const USAGE = `
@@ -19,6 +21,8 @@ Usage:
doc-agent document <file-or-directory> Add Doxygen documentation
doc-agent review <base>..<head> Detect doc drift in range
doc-agent review --pr <number> Detect doc drift for a PR
doc-agent regen-skills <module> Regenerate docs/skills/soul/<module>.md
from sibling .ai.md files
Environment:
ANTHROPIC_API_KEY (required) Anthropic API key
@@ -58,6 +62,13 @@ async function main(): Promise<void> {
return;
}
if (mode === 'regen-skills') {
const moduleName = args[0];
if (moduleName === undefined) printUsageAndExit(1);
await regenSkills(moduleName);
return;
}
console.error(`Unknown mode: ${mode}`);
printUsageAndExit(1);
}

View File

@@ -24,7 +24,7 @@ export async function loadSystemPrompt(promptName: string, sourcePath: string):
return basePrompt;
}
const skillPath = resolve(SKILLS_DIR, 'soul', skillFile);
const skillPath = resolve(SKILLS_DIR, skillFile);
if (!existsSync(skillPath)) {
return basePrompt;
}

View File

@@ -0,0 +1,146 @@
/**
* 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`.
*/
import { existsSync, readdirSync, statSync } from 'node:fs';
import { readFile, writeFile } 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';
interface AiFile {
readonly sourcePath: string;
readonly content: string;
}
/** Resolve which source-tree prefixes feed a given skill file. */
function prefixesForSkill(skillFile: string): string[] {
return Object.entries(MODULE_SKILL_MAP)
.filter(([, mapped]) => mapped === skillFile)
.map(([prefix]) => prefix);
}
/** Walk a directory and collect all sibling .ai.md files. */
function collectAiFiles(prefix: string): string[] {
const absDir = resolve(XRPLD_ROOT, prefix);
if (!existsSync(absDir) || !statSync(absDir).isDirectory()) return [];
const results: string[] = [];
const walk = (dir: string): void => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) {
walk(full);
} else if (entry.isFile() && entry.name.endsWith('.ai.md')) {
results.push(full);
}
}
};
walk(absDir);
return results;
}
async function loadAiFiles(absPaths: readonly string[]): Promise<AiFile[]> {
const files: AiFile[] = [];
for (const absPath of absPaths) {
const content = await readFile(absPath, 'utf8');
files.push({
sourcePath: relative(XRPLD_ROOT, absPath).replace(/\.ai\.md$/, ''),
content,
});
}
return files;
}
/**
* 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.
*/
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(', ')}`,
);
}
console.log(`Regenerating skill: ${skillFile}`);
console.log(` Source prefixes: ${prefixes.join(', ')}`);
const aiPaths = prefixes.flatMap((prefix) => collectAiFiles(prefix));
if (aiPaths.length === 0) {
console.warn(' No .ai.md files found for this module. Skipping.');
return;
}
console.log(` Found ${aiPaths.length} .ai.md file(s)`);
const aiFiles = await loadAiFiles(aiPaths);
const skillPath = resolve(SKILLS_DIR, skillFile);
const existingSkill = existsSync(skillPath)
? await readFile(skillPath, 'utf8')
: '(no existing skill file — create a new one)';
const systemPrompt = await readFile(resolve(PROMPTS_DIR, 'regen-skill.md'), 'utf8');
const aiBlocks = aiFiles
.map((f) => `\n### \`${f.sourcePath}\`\n\n${f.content}`)
.join('\n\n---\n');
const userPrompt = `Regenerate the skill file: \`docs/skills/${skillFile}\`
## Existing skill content
${existingSkill}
## AI context files for this module
${aiBlocks}
Produce the new complete skill file content as your final message.`;
let response = '';
const result = query({
prompt: userPrompt,
options: {
model: MODEL,
systemPrompt,
cwd: XRPLD_ROOT,
allowedTools: ['Read', 'Glob', 'Grep'],
permissionMode: 'acceptEdits',
},
});
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 (message.type === 'result') {
const cost = message.total_cost_usd?.toFixed(4) ?? '?';
console.log(` [Cost: $${cost}]`);
}
}
const trimmed = response.trim();
if (trimmed.length === 0) {
console.error(' Agent returned empty response — skill file not updated.');
return;
}
await writeFile(skillPath, `${trimmed}\n`);
console.log(` Wrote: ${relative(XRPLD_ROOT, skillPath)}`);
}