Skip to content
This repository was archived by the owner on Oct 20, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/audio-recap/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: audio-recap
description: Turn changes made by an agent into a concise Markdown script for text-to-speech. Use only when the user explicitly asks for an audio recap, spoken recap, TTS-ready change summary, or invokes /audio-recap.
depends-on: []
chains-to: null
suggests: ["using-elevenlabs-tts", "audio-plan", "visual-recap"]
suggests: ["using-elevenlabs-tts", "audio-plan"]
---

# Audio Recap
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/human-typed-plan/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: human-typed-plan
description: Use when a developer wants AI help with a feature, bugfix, refactor, or implementation approach while keeping human ownership of code, typing, decisions, and understanding; especially requests to plan, explain logic, compare approaches, or provide implementation steps for the developer to write.
depends-on: ["test-driven-development"]
depends-on: []
chains-to: null
suggests: ["ideal-example-clone", "refactor-transaction"]
---
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{
"name": "void-grimoire",
"description": "Additive coding-agent skills: session journaling, learned-correction persistence, prompt expansion, context discipline, safety checks, lookup helpers, and workflow assists",
"version": "4.1.0",
"version": "4.1.1",
"source": "./",
"author": {
"name": "Dmytro L.",
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "void-grimoire",
"description": "Additive coding-agent skills: session journaling, learned-correction persistence, prompt expansion, context discipline, safety checks, lookup helpers, and workflow assists",
"version": "4.1.0",
"version": "4.1.1",
"author": {
"name": "Dmytro L.",
"email": "dmltdev@proton.me"
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "void-grimoire",
"version": "4.1.0",
"version": "4.1.1",
"description": "Additive coding-agent skills: session journaling, learned-correction persistence, prompt expansion, context discipline, safety checks, lookup helpers, and workflow assists",
"author": {
"name": "Dmytro L.",
Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/skill-references.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Skill references

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
skill-references:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm test
2 changes: 1 addition & 1 deletion .omp-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{
"name": "void-grimoire",
"description": "Additive coding-agent skills: session journaling, learned-correction persistence, prompt expansion, context discipline, safety checks, lookup helpers, and workflow assists",
"version": "4.1.0",
"version": "4.1.1",
"source": "./",
"author": {
"name": "Dmytro L.",
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ Composition still works:
- `chains-to` — the named skill is invoked after this one completes
- `suggests` — soft recommendation, agent checks if relevant

Composition fields refer to canonical shipped skill names, checked with `npm test`.
If a skill intentionally uses an external helper, its body must identify the provider, installation source, required or optional status, and behavior when the helper is unavailable. Do not encode an external helper as an unresolved internal composition edge.

The categorized `skills/` directory tree is the single domain map for shipped skills. The README mirrors that structure for humans. Claude Code loads relevant skills automatically via their descriptions; invoke any skill by name when you want it explicitly.

Repo-local operational skills live in `.agents/skills`; `.claude/skills` points there by symlink.
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "void-grimoire",
"version": "4.1.0",
"version": "4.1.1",
"description": "Utility skills for coding agents",
"private": true,
"license": "MIT",
Expand All @@ -16,6 +16,9 @@
"session-summary",
"prompt-expansion"
],
"scripts": {
"test": "node --test tests/skill-references.test.mjs"
},
"pi": {
"skills": ["skills", ".agents/skills/void-install-skills"]
}
Expand Down
6 changes: 3 additions & 3 deletions skills-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"audio-recap": {
"source": ".",
"sourceType": "local",
"computedHash": "f72d2b151477cd8bc0db8c5be187f5d1ea6226ec6d66893ddd9b52450758cec9"
"computedHash": "e6d65aa9c0985261e6b4a617c6883ae9dc05a16a009b89d8fd842abce81ed839"
},
"autonomous": {
"source": ".",
Expand All @@ -24,7 +24,7 @@
"autoresearch": {
"source": ".",
"sourceType": "local",
"computedHash": "e8f9e43a768f0e06244c38bdf2cb1f7940450395def9ededff1c5db7d5835b46"
"computedHash": "0c2f92e4ff350d1a95ece74852563804713ebcf33bb9bf4d578ea34d442e11a1"
},
"brief": {
"source": ".",
Expand Down Expand Up @@ -119,7 +119,7 @@
"human-typed-plan": {
"source": ".",
"sourceType": "local",
"computedHash": "5eb9b9e12b09d0147d7de1c34bf07b76d3fd2926b45d00e11a14deee7b25eb15"
"computedHash": "bb52c31f632f801fb6dccc78df5bd83952e66378b80b46ef07f59e1d35fa3e8f"
},
"ideal-example-clone": {
"source": ".",
Expand Down
2 changes: 1 addition & 1 deletion skills/communication/audio-recap/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: audio-recap
description: Turn changes made by an agent into a concise Markdown script for text-to-speech. Use only when the user explicitly asks for an audio recap, spoken recap, TTS-ready change summary, or invokes /audio-recap.
depends-on: []
chains-to: null
suggests: ["using-elevenlabs-tts", "audio-plan", "visual-recap"]
suggests: ["using-elevenlabs-tts", "audio-plan"]
---

# Audio Recap
Expand Down
2 changes: 1 addition & 1 deletion skills/planning/human-typed-plan/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: human-typed-plan
description: Use when a developer wants AI help with a feature, bugfix, refactor, or implementation approach while keeping human ownership of code, typing, decisions, and understanding; especially requests to plan, explain logic, compare approaches, or provide implementation steps for the developer to write.
depends-on: ["test-driven-development"]
depends-on: []
chains-to: null
suggests: ["ideal-example-clone", "refactor-transaction"]
---
Expand Down
180 changes: 180 additions & 0 deletions tests/skill-references.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import assert from 'node:assert/strict';
import { readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';

const root = fileURLToPath(new URL('../', import.meta.url));
const relations = ['depends-on', 'chains-to', 'suggests'];
const tracked = new Set(['name', ...relations]);

function discoverCatalog() {
const directories = path => readdirSync(path, { withFileTypes: true })
.filter(entry => entry.isDirectory()).map(entry => entry.name).sort();
const catalog = [];
for (const domain of directories(join(root, 'skills'))) {
for (const directory of directories(join(root, 'skills', domain))) {
const file = `skills/${domain}/${directory}/SKILL.md`;
if (!readdirSync(join(root, 'skills', domain, directory), { withFileTypes: true })
.some(entry => entry.name === 'SKILL.md' && entry.isFile())) continue;
catalog.push({ file, directory, content: readFileSync(join(root, file), 'utf8') });
}
}
return catalog;
}

// This deliberately supports only the repository's plain keys and simple flow values.
// New graph syntax must fail visibly, not disappear from reference validation.
function parseValue(field, value) {
const skillName = token => {
if (!/^(?:[a-z0-9]+(?:-[a-z0-9]+)*|"[a-z0-9]+(?:-[a-z0-9]+)*"|'[a-z0-9]+(?:-[a-z0-9]+)*')$/.test(token)) {
throw new Error(`unsupported or malformed skill name: ${token}`);
}
return token.replace(/^['"]|['"]$/g, '');
};
value = value.replace(/\s+#.*$/, '').trim();
if (field === 'name') return [skillName(value)];
if (value === '' || value === 'null' || value === '~') return [];
if (value.startsWith('[') && value.endsWith(']')) {
const contents = value.slice(1, -1).trim();
return contents === '' ? [] : contents.split(',').map(token => skillName(token.trim()));
}
if (field === 'chains-to') return [skillName(value)];
throw new Error(`unsupported or malformed graph value: ${value}`);
}

function extractFrontmatter({ file, content }, errors) {
const lines = content.split(/\r?\n/);
const end = lines.indexOf('---', 1);
const fields = new Map();
if (lines[0] !== '---' || end < 0) {
errors.push(`${file}: missing opening or closing frontmatter delimiter`);
return fields;
}
const seen = new Set();
let activeField;
for (const line of lines.slice(1, end)) {
if (line.trim() === '' || line.trimStart().startsWith('#')) continue;
if (/^\s/.test(line)) {
if (tracked.has(activeField)) errors.push(`${file}: ${activeField}: unsupported block or multiline value`);
continue;
}
const key = /^([A-Za-z][\w-]*):(?:[ \t]+(.*))?$/.exec(line);
if (!key) {
errors.push(`${file}: unsupported frontmatter key syntax: ${line}`);
activeField = undefined;
continue;
}
activeField = key[1];
if (!tracked.has(activeField)) continue;
if (seen.has(activeField)) {
errors.push(`${file}: ${activeField}: duplicate tracked key`);
continue;
}
seen.add(activeField);
try {
fields.set(activeField, parseValue(activeField, key[2] ?? ''));
} catch (error) {
errors.push(`${file}: ${activeField}: ${error.message}`);
}
}
return fields;
}

function validateCatalog(catalog) {
const errors = [];
if (catalog.length === 0) errors.push('Canonical skill catalog is empty');
const parsed = catalog.map(source => ({ ...source, fields: extractFrontmatter(source, errors) }));
const names = new Map();
for (const { file, directory, fields } of parsed) {
const name = fields.get('name')?.[0];
if (!name) {
errors.push(`${file}: name: missing valid canonical name`);
continue;
}
if (name !== directory) errors.push(`${file}: name: ${name} does not match directory ${directory}`);
if (names.has(name)) errors.push(`${file}: name: duplicate canonical name ${name} (also ${names.get(name)})`);
names.set(name, file);
}
// Build all names first so file ordering cannot break valid forward references.
for (const { file, fields } of parsed) {
for (const field of relations) {
for (const target of fields.get(field) ?? []) {
if (!names.has(target)) errors.push(`${file}: ${field}: missing canonical target ${target}`);
}
}
}
return errors;
}

const fixture = (directory, metadata = '', body = '') => ({
file: `skills/fixtures/${directory}/SKILL.md`,
directory,
content: `---\nname: ${directory}\n${metadata}\n---\n${body}`,
});

test('every canonical composition target resolves to a shipped skill', () => {
const errors = validateCatalog(discoverCatalog());
assert.deepEqual(errors, [], errors.join('\n'));
});

test('forward references and supported graph values resolve without reading unrelated fields or bodies', () => {
assert.deepEqual(validateCatalog([
fixture('first', 'depends-on: ["second", third]\nchains-to: second\nsuggests: [\'third\']\ndescription: |\n suggests: [not-a-skill]\nargs:\n - name: input'),
fixture('second', 'depends-on: null\nchains-to: []\nsuggests:'),
fixture('third', 'depends-on: []\nchains-to: ~\nsuggests: null', '```yaml\nsuggests: [body-only]\n```'),
]), []);
});

for (const [field, target] of [
['depends-on', 'test-driven-development'],
['chains-to', 'missing-successor'],
['suggests', 'visual-recap'],
]) {
test(`${field} reports its source and unresolved target`, () => {
assert.deepEqual(validateCatalog([fixture('source', `${field}: ["${target}"]`)]), [
`skills/fixtures/source/SKILL.md: ${field}: missing canonical target ${target}`,
]);
});
}

test('all unresolved edges are reported in one run', () => {
assert.deepEqual(validateCatalog([
fixture('source', 'depends-on: [missing-prerequisite]\nchains-to: missing-successor\nsuggests: [missing-helper]'),
]), [
'skills/fixtures/source/SKILL.md: depends-on: missing canonical target missing-prerequisite',
'skills/fixtures/source/SKILL.md: chains-to: missing canonical target missing-successor',
'skills/fixtures/source/SKILL.md: suggests: missing canonical target missing-helper',
]);
});

for (const [label, metadata, expected] of [
['block list', 'suggests:\n - absent', /suggests: unsupported block/],
['unterminated flow list', 'depends-on: [absent', /depends-on: unsupported or malformed/],
['empty flow entry', 'suggests: [known,,absent]', /suggests: unsupported or malformed/],
['mapping value', 'chains-to: {next: absent}', /chains-to: unsupported or malformed/],
['anchor', 'suggests: &helpers [absent]', /suggests: unsupported or malformed/],
['alias', 'suggests: *helpers', /suggests: unsupported or malformed/],
['merge indirection', 'defaults: &helpers\n suggests: [absent]\n<<: *helpers', /unsupported frontmatter key syntax: <</],
['quoted tracked key', '"suggests": [absent]', /unsupported frontmatter key syntax: "suggests"/],
['duplicate key', 'suggests: [absent]\nsuggests: []', /suggests: duplicate tracked key/],
]) {
test(`${label} cannot silently bypass graph validation`, () => {
const errors = validateCatalog([fixture('source', metadata), fixture('known')]);
assert.match(errors.join('\n'), expected);
if (label === 'duplicate key') {
assert.ok(errors.includes('skills/fixtures/source/SKILL.md: suggests: missing canonical target absent'));
}
});
}

for (const [label, catalog, expected] of [
['empty catalog', [], /catalog is empty/],
['duplicate canonical names', [fixture('source'), { ...fixture('other'), content: '---\nname: source\n---' }], /duplicate canonical name source/],
['name mismatch', [{ ...fixture('source'), content: '---\nname: other\n---' }], /other does not match directory source/],
['missing frontmatter', [{ ...fixture('source'), content: '# Skill\nsuggests: [absent]' }], /frontmatter delimiter/],
]) {
test(`${label} rejects an invalid canonical catalog`, () => {
assert.match(validateCatalog(catalog).join('\n'), expected);
});
}
Loading