mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-08 02:37:55 +02:00
fix(continuous-learning-v2): emit loadable frontmatter from evolve --generate
Artifacts written by `evolve --generate` are inert: Claude Code (and every
spec-compliant Agent Skills client) injects only `name` + `description` at
startup and will not load an artifact missing them.
Today the generator writes:
- skills: `# {name}` with no frontmatter block at all
- commands: `# {cmd_name}` with no frontmatter block at all
- agents: `model`/`tools` only, no `name`, no `description`
So the whole evolve pipeline terminates in files that can never load. I hit
this on a real install: 12 generated artifacts across two projects, none of
which Claude Code had ever seen.
This adds a `_evolved_description()` helper and emits proper frontmatter for
all three artifact kinds. The description is sanitised for the two things that
break loaders: `: ` in an unquoted scalar (rejected by strict YAML parsers)
and `<`/`>` (system-prompt injection risk).
Adds two tests to tests/scripts/instinct-cli-evolve-generate.test.js. Both
fail against current main and pass with this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
committed by
Alex Schmitt
co-authored by
Claude Opus 5
parent
dac72d1997
commit
7aa071c5e9
@@ -1934,6 +1934,24 @@ def _cmd_projects_merge(args) -> int:
|
||||
# Generate Evolved Structures
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
def _evolved_description(trigger: str, instincts: list, kind: str) -> str:
|
||||
"""Build the frontmatter `description` for a generated artifact.
|
||||
|
||||
Claude Code (and every spec-compliant Agent Skills client) injects only
|
||||
`name` + `description` at startup and will not load an artifact that lacks
|
||||
them, so a generated skill/agent without frontmatter is inert on disk.
|
||||
"""
|
||||
ids = ', '.join(i.get('id', 'unnamed') for i in instincts[:6])
|
||||
trig = (trigger or '').strip().rstrip('.') or 'a recurring situation'
|
||||
description = (
|
||||
f"Evolved {kind} covering {len(instincts)} learned instinct(s). "
|
||||
f"Use {trig}. Source instincts - {ids}."
|
||||
)
|
||||
# `: ` breaks strict YAML parsers in an unquoted scalar; `<`/`>` can inject
|
||||
# into the system prompt.
|
||||
return description.replace(': ', ' - ').replace('<', '(').replace('>', ')')
|
||||
|
||||
|
||||
def _generate_evolved(skill_candidates: list, workflow_instincts: list, agent_candidates: list, evolved_dir: Path, limit: int = 0) -> list[str]:
|
||||
"""Generate skill/command/agent files from analyzed instinct clusters.
|
||||
|
||||
@@ -1966,7 +1984,11 @@ def _generate_evolved(skill_candidates: list, workflow_instincts: list, agent_ca
|
||||
skill_dir = evolved_dir / "skills" / name
|
||||
skill_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
content = f"# {name}\n\n"
|
||||
content = "---\n"
|
||||
content += f"name: {name}\n"
|
||||
content += f"description: {_evolved_description(trigger, cand['instincts'], 'skill')}\n"
|
||||
content += "---\n\n"
|
||||
content += f"# {name}\n\n"
|
||||
content += f"Evolved from {len(cand['instincts'])} instincts "
|
||||
content += f"(avg confidence: {cand['avg_confidence']:.0%})\n\n"
|
||||
content += f"## When to Apply\n\n"
|
||||
@@ -1993,7 +2015,10 @@ def _generate_evolved(skill_candidates: list, workflow_instincts: list, agent_ca
|
||||
continue
|
||||
|
||||
cmd_file = evolved_dir / "commands" / f"{cmd_name}.md"
|
||||
content = f"# {cmd_name}\n\n"
|
||||
content = "---\n"
|
||||
content += f"description: {_evolved_description(inst.get('trigger', ''), [inst], 'command')}\n"
|
||||
content += "---\n\n"
|
||||
content += f"# {cmd_name}\n\n"
|
||||
content += f"Evolved from instinct: {inst.get('id', 'unnamed')}\n"
|
||||
content += f"Confidence: {inst.get('confidence', 0.5):.0%}\n\n"
|
||||
content += inst.get('content', '')
|
||||
@@ -2016,7 +2041,10 @@ def _generate_evolved(skill_candidates: list, workflow_instincts: list, agent_ca
|
||||
domains = ', '.join(cand['domains'])
|
||||
instinct_ids = [i.get('id', 'unnamed') for i in cand['instincts']]
|
||||
|
||||
content = f"---\nmodel: sonnet\ntools: Read, Grep, Glob\n---\n"
|
||||
content = "---\n"
|
||||
content += f"name: {agent_name}\n"
|
||||
content += f"description: {_evolved_description(str(cand.get('trigger', '')), cand['instincts'], 'agent')}\n"
|
||||
content += "model: sonnet\ntools: Read, Grep, Glob\n---\n"
|
||||
content += f"# {agent_name}\n\n"
|
||||
content += f"Evolved from {len(cand['instincts'])} instincts "
|
||||
content += f"(avg confidence: {cand['avg_confidence']:.0%})\n"
|
||||
|
||||
@@ -243,6 +243,71 @@ test('preview names match the files --generate writes', () => {
|
||||
}
|
||||
});
|
||||
|
||||
function parseFrontmatter(filePath) {
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
const match = /^---\n([\s\S]*?)\n---\n/.exec(raw);
|
||||
if (!match) return null;
|
||||
const fm = {};
|
||||
for (const line of match[1].split('\n')) {
|
||||
const idx = line.indexOf(':');
|
||||
if (idx > 0 && !line.startsWith(' ')) {
|
||||
fm[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
||||
}
|
||||
}
|
||||
return fm;
|
||||
}
|
||||
|
||||
test('generated skills carry loadable name + description frontmatter', () => {
|
||||
const root = createTempDir();
|
||||
try {
|
||||
writeInstinct(root, 'first', 'when investigating complex systems');
|
||||
writeInstinct(root, 'second', 'when investigating complex systems');
|
||||
writeInstinct(root, 'third', 'when running tests');
|
||||
|
||||
assert.strictEqual(runCli(root, ['evolve', '--generate']).status, 0);
|
||||
|
||||
const skillsDir = path.join(root, 'evolved', 'skills');
|
||||
const skillDirs = fs.existsSync(skillsDir) ? fs.readdirSync(skillsDir) : [];
|
||||
assert.ok(skillDirs.length > 0, 'expected at least one generated skill');
|
||||
|
||||
for (const name of skillDirs) {
|
||||
const skillFile = path.join(skillsDir, name, 'SKILL.md');
|
||||
const fm = parseFrontmatter(skillFile);
|
||||
assert.ok(fm, `${name}/SKILL.md has no frontmatter block`);
|
||||
assert.strictEqual(fm.name, name, `${name}: frontmatter name must match its folder`);
|
||||
assert.ok(fm.description && fm.description.length > 0, `${name}: description must not be empty`);
|
||||
assert.ok(!/[<>]/.test(fm.description), `${name}: description must not contain < or >`);
|
||||
}
|
||||
} finally {
|
||||
cleanupDir(root);
|
||||
}
|
||||
});
|
||||
|
||||
test('generated agents carry name + description alongside model/tools', () => {
|
||||
const root = createTempDir();
|
||||
try {
|
||||
writeInstinct(root, 'a', 'when reviewing pull requests');
|
||||
writeInstinct(root, 'b', 'when reviewing pull requests');
|
||||
writeInstinct(root, 'c', 'when reviewing pull requests');
|
||||
|
||||
assert.strictEqual(runCli(root, ['evolve', '--generate']).status, 0);
|
||||
|
||||
const agentsDir = path.join(root, 'evolved', 'agents');
|
||||
const agents = fs.existsSync(agentsDir) ? fs.readdirSync(agentsDir) : [];
|
||||
assert.ok(agents.length > 0, 'expected at least one generated agent');
|
||||
|
||||
for (const file of agents) {
|
||||
const fm = parseFrontmatter(path.join(agentsDir, file));
|
||||
assert.ok(fm, `${file} has no frontmatter block`);
|
||||
assert.strictEqual(fm.name, path.basename(file, '.md'));
|
||||
assert.ok(fm.description && fm.description.length > 0, `${file}: description must not be empty`);
|
||||
assert.strictEqual(fm.model, 'sonnet');
|
||||
}
|
||||
} finally {
|
||||
cleanupDir(root);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\nPassed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user