From 488c1ee06e83d70a66eaf2246b29ad47785c82ef Mon Sep 17 00:00:00 2001 From: ayobamiseun Date: Sat, 3 Oct 2026 10:48:32 +0100 Subject: [PATCH 01/19] feat(validator): lint personas for name, description and Composition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The smaller version asked for when #610 was closed. docs/agents.md states three rules for agents/.md — the same frontmatter format as a skill with name equal to the file stem (the name is the subagent_type commands spawn) and a description, and a closing Composition block — and no validator checked them. Add lintPersonaContent to scripts/lib/skill-lint.js, reusing the existing frontmatter parsing and YAML-validity rules, and have validate-skills.js walk agents/*.md after the skills, skipping an index README.md. Headings are read from fence-stripped content so an example block after Composition does not count as a later section. No spawn-reference scanning, no listing-table checks, no new script, no workflow change. All four personas pass: 25 skills and 4 personas checked, 0 errors. --- scripts/lib/skill-lint-test.js | 68 +++++++++++++++++++++++++++++++++- scripts/lib/skill-lint.js | 55 +++++++++++++++++++++++++++ scripts/validate-skills.js | 29 +++++++++++++-- 3 files changed, 148 insertions(+), 4 deletions(-) diff --git a/scripts/lib/skill-lint-test.js b/scripts/lib/skill-lint-test.js index ba08cb2a..5f206186 100644 --- a/scripts/lib/skill-lint-test.js +++ b/scripts/lib/skill-lint-test.js @@ -9,7 +9,7 @@ const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); -const { lintSkillContent, lintSkillLayout, topLevelFrontmatterKeys } = require('./skill-lint.js'); +const { lintSkillContent, lintSkillLayout, topLevelFrontmatterKeys, lintPersonaContent } = require('./skill-lint.js'); const KNOWN = new Set(['alpha', 'beta']); @@ -648,3 +648,69 @@ test('the spec-key check tolerates CRLF frontmatter', () => { const { errors } = lintSkillContent('alpha', content, KNOWN); assert.equal(errors.filter(e => /key 'temperature'/.test(e)).length, 1); }); + +// ─── Personas (agents/.md) ───────────────────────────────────────────── +// +// docs/agents.md: same frontmatter format as a skill, name equal to the file +// stem, and the file ends with a Composition block. Nothing more is checked. + +function personaFile({ name = 'code-reviewer', description = 'Reviews changes. Use before merging.', body } = {}) { + const fm = ['---', name === null ? null : `name: ${name}`, description === null ? null : `description: ${description}`, '---'] + .filter(l => l !== null).join('\n'); + return `${fm}\n\n${body ?? '# Persona\n\n## Rules\n\nBe precise.\n\n## Composition\n\n- **Invoke via:** `/ship`.\n'}`; +} + +test('a conforming persona produces no errors', () => { + assert.deepEqual(lintPersonaContent('code-reviewer', personaFile()).errors, []); +}); + +test('persona: frontmatter name must match the file stem', () => { + const { errors } = lintPersonaContent('code-reviewer', personaFile({ name: 'reviewer' })); + assert.equal(errors.length, 1); + assert.match(errors[0], /name 'reviewer' does not match file name 'code-reviewer'/); +}); + +test('persona: description is required', () => { + const { errors } = lintPersonaContent('code-reviewer', personaFile({ description: null })); + assert.equal(errors.length, 1); + assert.match(errors[0], /missing required field: 'description'/); +}); + +test('persona: a missing frontmatter block is reported and nothing else is attempted', () => { + const { errors } = lintPersonaContent('code-reviewer', '# Persona\n\n## Composition\n'); + assert.equal(errors.length, 1); + assert.match(errors[0], /Missing or malformed YAML frontmatter/); +}); + +test('persona: frontmatter a host would reject as YAML is reported', () => { + const { errors } = lintPersonaContent('code-reviewer', personaFile({ description: 'Reviews: everything. Use before merging.' })); + assert.equal(errors.length, 1); + assert.match(errors[0], /Frontmatter line/); +}); + +test('persona: the Composition section is required', () => { + const { errors } = lintPersonaContent('code-reviewer', personaFile({ body: '# Persona\n\n## Rules\n\nBe precise.\n' })); + assert.equal(errors.length, 1); + assert.match(errors[0], /Missing "## Composition" section/); +}); + +test('persona: Composition must be the last section', () => { + const { errors } = lintPersonaContent('code-reviewer', personaFile({ + body: '# Persona\n\n## Composition\n\n- **Invoke via:** `/ship`.\n\n## Output Format\n\nA report.\n', + })); + assert.equal(errors.length, 1); + assert.match(errors[0], /must be the last section, but "## Output Format" follows it/); +}); + +test('persona: a heading inside a fenced example after Composition is not a later section', () => { + const body = [ + '# Persona', '', '## Composition', '', '- **Invoke via:** `/ship`. Report in this shape:', '', + '```markdown', '## Review: [title]', 'Findings…', '```', '', + ].join('\n'); + assert.deepEqual(lintPersonaContent('code-reviewer', personaFile({ body })).errors, []); +}); + +test('persona: the Composition heading match is case-insensitive and tolerates trailing spaces', () => { + const body = '# Persona\n\n## composition \n\n- **Invoke via:** `/ship`.\n'; + assert.deepEqual(lintPersonaContent('code-reviewer', personaFile({ body })).errors, []); +}); diff --git a/scripts/lib/skill-lint.js b/scripts/lib/skill-lint.js index 8779c9c0..691f25e2 100644 --- a/scripts/lib/skill-lint.js +++ b/scripts/lib/skill-lint.js @@ -543,6 +543,59 @@ function lintSkillLayout(skillDir) { return errors; } +/** + * Lint a persona file (agents/.md) against the three rules docs/agents.md + * states for them: frontmatter in the same format as a skill, with `name` + * equal to the file stem (the name is the `subagent_type` commands spawn) and + * a `description`; and a closing `## Composition` section. Headings inside + * fenced blocks are ignored, so an example after Composition does not count as + * a later section. Pure: no filesystem access. Returns { errors }. + */ +function lintPersonaContent(stem, content) { + const errors = []; + + const fm = parseFrontmatter(content); + if (!fm) { + errors.push('Missing or malformed YAML frontmatter (expected --- block at top of file)'); + return { errors }; + } + errors.push(...frontmatterYamlErrors(content)); + + if (!fm.name) { + errors.push("Frontmatter missing required field: 'name'"); + } else if (fm.name !== stem) { + errors.push(`Frontmatter name '${fm.name}' does not match file name '${stem}' (the name is the subagent_type commands spawn)`); + } + if (!fm.description) { + errors.push("Frontmatter missing required field: 'description'"); + } + + const headings = [...stripFencedCodeBlocks(content).matchAll(/^## +(.+?)\s*$/gm)].map(m => m[1]); + const compositionAt = headings.findIndex(h => /^Composition$/i.test(h)); + if (compositionAt === -1) { + errors.push('Missing "## Composition" section (docs/agents.md: every persona file ends with a Composition block)'); + } else if (compositionAt !== headings.length - 1) { + errors.push(`"## Composition" must be the last section, but "## ${headings[headings.length - 1]}" follows it`); + } + + return { errors }; +} + +/** + * Lint a persona by file name: reads agents/, then delegates to + * lintPersonaContent. Returns { errors }. + */ +function lintPersona(fileName, agentsDir) { + const stem = fileName.replace(/\.md$/, ''); + let content; + try { + content = fs.readFileSync(path.join(agentsDir, fileName), 'utf8'); + } catch (err) { + return { errors: [`Unreadable persona file: ${err.message}`] }; + } + return lintPersonaContent(stem, content); +} + /** * Lint a skill by directory name: reads its SKILL.md, then delegates to * lintSkillContent. This is the thin filesystem wrapper the CLI uses. @@ -581,4 +634,6 @@ module.exports = { lintSkillContent, lintSkillLayout, lintSkill, + lintPersonaContent, + lintPersona, }; diff --git a/scripts/validate-skills.js b/scripts/validate-skills.js index b73a431d..d1722a40 100644 --- a/scripts/validate-skills.js +++ b/scripts/validate-skills.js @@ -3,7 +3,8 @@ * validate-skills.js * * CLI that validates every skill in skills/ against the rules in - * docs/skill-anatomy.md. The rules themselves live in scripts/lib/skill-lint.js + * docs/skill-anatomy.md, and every persona in agents/ against the three rules + * in docs/agents.md. The rules themselves live in scripts/lib/skill-lint.js * (a single source of truth, importable and unit-testable); this file is a thin * wrapper that walks the skills directory, runs the linter, prints the report, * and sets the exit code. @@ -16,9 +17,10 @@ const fs = require('fs'); const path = require('path'); -const { lintSkill } = require('./lib/skill-lint'); +const { lintSkill, lintPersona } = require('./lib/skill-lint'); const SKILLS_DIR = path.resolve(__dirname, '..', 'skills'); +const AGENTS_DIR = path.resolve(__dirname, '..', 'agents'); // ─── Main ──────────────────────────────────────────────────────────────────── @@ -53,8 +55,29 @@ function main() { } } + // ── Personas ───────────────────────────────────────────────────────────── + // agents/.md files are consumed as subagents and spawned by name from + // commands, so they get the same frontmatter contract plus a closing + // Composition section (docs/agents.md). An index file such as README.md is + // not a persona and is skipped. + const personaFiles = fs.existsSync(AGENTS_DIR) + ? fs.readdirSync(AGENTS_DIR).filter(f => f.endsWith('.md') && !/^readme\.md$/i.test(f)).sort() + : []; + + if (personaFiles.length > 0) console.log(''); + for (const fileName of personaFiles) { + const { errors } = lintPersona(fileName, AGENTS_DIR); + totalErrors += errors.length; + if (errors.length === 0) { + console.log(` ✓ agents/${fileName}`); + } else { + console.log(` ✗ agents/${fileName}`); + for (const msg of errors) console.log(` ERROR: ${msg}`); + } + } + const status = totalErrors > 0 ? 'FAILED' : totalWarnings > 0 ? 'PASSED WITH WARNINGS' : 'PASSED'; - console.log(`\n${skillDirs.length} skills checked — ${totalErrors} error(s), ${totalWarnings} warning(s) — ${status}`); + console.log(`\n${skillDirs.length} skills and ${personaFiles.length} personas checked — ${totalErrors} error(s), ${totalWarnings} warning(s) — ${status}`); if (totalErrors > 0) process.exit(1); } From aff59bccaed8571235528ed6784c721c781562c2 Mon Sep 17 00:00:00 2001 From: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Sat, 3 Oct 2026 10:58:21 -0400 Subject: [PATCH 02/19] Reject duplicate behavioral evaluation IDs Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> --- scripts/run-evals-test.js | 29 +++++++++++++++++++++++++++++ scripts/run-evals.js | 20 ++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/scripts/run-evals-test.js b/scripts/run-evals-test.js index 6d71e75d..a0cb0917 100644 --- a/scripts/run-evals-test.js +++ b/scripts/run-evals-test.js @@ -621,3 +621,32 @@ test('materializes a git baseline and applies a working-tree patch', () => { fs.rmSync(workspace, { recursive: true, force: true }); } }); + + +test('rejects duplicate behavioral IDs before they can overwrite a grading slot', () => { + const root = makeSandbox(); + try { + writeSkill(root, 'alpha-skill', 'Handles alpha widgets. Use when changing alpha widgets.'); + const evalCase = completeCase('alpha-skill', 'change alpha widget'); + evalCase.evals.push({ ...behavioralEval(), prompt: 'A different evaluation' }); + writeJson(path.join(root, 'evals', 'cases', 'alpha-skill.json'), evalCase); + const result = run(root); + assert.equal(result.status, 1, result.stdout + result.stderr); + assert.match(result.stdout, /duplicate behavioral eval id=1/); + const dryRun = run(root, ['--behavioral', 'alpha-skill', '--dry-run']); + assert.equal(dryRun.status, 1, dryRun.stdout + dryRun.stderr); + assert.match(dryRun.stderr, /duplicate behavioral eval id=1/); + } finally { fs.rmSync(root, { recursive: true, force: true }); } +}); + +test('accepts distinct behavioral IDs including zero', () => { + const root = makeSandbox(); + try { + writeSkill(root, 'alpha-skill', 'Handles alpha widgets. Use when changing alpha widgets.'); + const evalCase = completeCase('alpha-skill', 'change alpha widget'); + evalCase.evals.push({ ...behavioralEval(), id: 0 }); + writeJson(path.join(root, 'evals', 'cases', 'alpha-skill.json'), evalCase); + const result = run(root); + assert.equal(result.status, 0, result.stdout + result.stderr); + } finally { fs.rmSync(root, { recursive: true, force: true }); } +}); diff --git a/scripts/run-evals.js b/scripts/run-evals.js index 765d6af7..9f8002b3 100644 --- a/scripts/run-evals.js +++ b/scripts/run-evals.js @@ -193,6 +193,16 @@ function resolveFixturePath(root, rel) { return resolvedPath; } +function duplicateEvalIds(evals) { + const seen = new Set(); + const duplicates = new Set(); + for (const ev of evals) { + if (seen.has(ev.id)) duplicates.add(ev.id); + seen.add(ev.id); + } + return [...duplicates]; +} + // ---------- tier 2 ---------- function runDeterministic(minRank1) { @@ -235,6 +245,11 @@ function runDeterministic(minRank1) { continue; } + for (const id of duplicateEvalIds(d.evals || [])) { + console.log(` ✗ ${c.file}: duplicate behavioral eval id=${id}`); + errors++; + } + // Schema: behavioral evals (skill-creator evals.json shape) for (const ev of d.evals || []) { const kind = ev.kind || 'execution'; @@ -522,6 +537,11 @@ function runBehavioral(skillName, dryRun) { console.error(`"${skillName}" has no behavioral evals`); process.exit(1); } + const duplicateIds = duplicateEvalIds(d.evals); + if (duplicateIds.length) { + console.error(`"${skillName}" has duplicate behavioral eval id=${duplicateIds.join(', ')}; each grading slot needs a unique id`); + process.exit(1); + } if (!dryRun) fs.mkdirSync(RESULTS_DIR, { recursive: true }); let failures = 0; From 2d3fbcd2d21a0a3fa8b4eb7aee1a00581d1f7282 Mon Sep 17 00:00:00 2001 From: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Sat, 3 Oct 2026 10:58:25 -0400 Subject: [PATCH 03/19] Keep reference heading anchors unique Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> --- scripts/validate-reference-links-test.js | 18 ++++++++++++++++++ scripts/validate-reference-links.js | 6 ++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/scripts/validate-reference-links-test.js b/scripts/validate-reference-links-test.js index 7971609a..891e472e 100644 --- a/scripts/validate-reference-links-test.js +++ b/scripts/validate-reference-links-test.js @@ -493,3 +493,21 @@ test('a missing file is reported once, not again for its anchor', () => { assert.match(result.stdout, /L1: references\/renamed\.md — resolves to skills\/hardening\/references\/renamed\.md, which does not exist/); assert.match(result.stdout, /1 skills checked — 1 error\(s\) — FAILED/); }); + + +test('heading slugs remain unique when a literal suffix collides with an allocated slug', () => { + const root = makeSandbox(); + hardeningSkill(root, 'references/patterns.md#example-1-1'); + // A hyphen in the literal heading makes its base slug collide with Example's suffix. + writeFile(root, 'skills/hardening/references/patterns.md', '## Example\n\n## Example\n\n## Example-1\n'); + const result = run(root); + assert.equal(result.status, 0, result.stdout + result.stderr); +}); + +test('a duplicate skips a slug occupied by an earlier literal heading', () => { + const root = makeSandbox(); + hardeningSkill(root, 'references/patterns.md#example-2'); + writeFile(root, 'skills/hardening/references/patterns.md', '## Example\n\n## Example-1\n\n## Example\n'); + const result = run(root); + assert.equal(result.status, 0, result.stdout + result.stderr); +}); diff --git a/scripts/validate-reference-links.js b/scripts/validate-reference-links.js index b9d3b245..b659bcdc 100644 --- a/scripts/validate-reference-links.js +++ b/scripts/validate-reference-links.js @@ -77,8 +77,10 @@ function headingAnchors(file) { const match = line.match(/^ {0,3}#{1,6}[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$/); if (!match) continue; const slug = slugify(match[1]); - const count = seen.get(slug) || 0; - anchors.add(count === 0 ? slug : `${slug}-${count}`); + let count = seen.get(slug) || 0; + let anchor = count === 0 ? slug : `${slug}-${count}`; + while (anchors.has(anchor)) anchor = `${slug}-${++count}`; + anchors.add(anchor); seen.set(slug, count + 1); } return anchors; From 938a7860765b27f97af841d9b344ba4d499a054b Mon Sep 17 00:00:00 2001 From: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Sat, 3 Oct 2026 10:58:28 -0400 Subject: [PATCH 04/19] Keep fixture commits independent of user signing settings Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> --- scripts/run-evals-test.js | 21 +++++++++++++++++++++ scripts/run-evals.js | 1 + 2 files changed, 22 insertions(+) diff --git a/scripts/run-evals-test.js b/scripts/run-evals-test.js index 6d71e75d..071dc133 100644 --- a/scripts/run-evals-test.js +++ b/scripts/run-evals-test.js @@ -621,3 +621,24 @@ test('materializes a git baseline and applies a working-tree patch', () => { fs.rmSync(workspace, { recursive: true, force: true }); } }); + + +test('fixture baseline commits ignore inherited user signing requirements', () => { + const root = makeSandbox(); + const config = path.join(root, 'global.gitconfig'); + const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'fixture-signing-test-')); + try { + fs.writeFileSync(config, '[commit]\n\tgpgsign = true\n[gpg]\n\tprogram = definitely-missing-eval-signer\n'); + const result = spawnSync(process.execPath, ['-e', ` + const fs = require('node:fs'); + const { materializeWorkspace } = require(${JSON.stringify(path.join(root, 'scripts', 'run-evals.js'))}); + let workspace; + try { workspace = materializeWorkspace({ files: ['project/context.txt'] }); } + finally { if (workspace) fs.rmSync(workspace, { recursive: true, force: true }); } + `], { cwd: root, encoding: 'utf8', env: { ...process.env, GIT_CONFIG_GLOBAL: config, TMPDIR: temp, TMP: temp, TEMP: temp } }); + assert.equal(result.status, 0, result.stdout + result.stderr); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + fs.rmSync(temp, { recursive: true, force: true }); + } +}); diff --git a/scripts/run-evals.js b/scripts/run-evals.js index 765d6af7..48e991e5 100644 --- a/scripts/run-evals.js +++ b/scripts/run-evals.js @@ -412,6 +412,7 @@ function materializeWorkspace(ev) { // throwaway workspace. execFileSync('git', ['init', '--quiet'], { cwd: workspace }); execFileSync('git', ['config', 'core.autocrlf', 'false'], { cwd: workspace }); + execFileSync('git', ['config', 'commit.gpgsign', 'false'], { cwd: workspace }); execFileSync('git', ['config', 'user.name', 'Skill Eval'], { cwd: workspace }); execFileSync('git', ['config', 'user.email', 'skill-eval@example.invalid'], { cwd: workspace }); execFileSync('git', ['add', '--all'], { cwd: workspace }); From efd49d48c3b4590b3aaf2882473759125620bebd Mon Sep 17 00:00:00 2001 From: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Sat, 3 Oct 2026 10:58:23 -0400 Subject: [PATCH 05/19] Clean up incomplete evaluation workspaces Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> --- scripts/run-evals-test.js | 19 +++++++++++++++++++ scripts/run-evals.js | 9 +++++++++ 2 files changed, 28 insertions(+) diff --git a/scripts/run-evals-test.js b/scripts/run-evals-test.js index 6d71e75d..ef88bf51 100644 --- a/scripts/run-evals-test.js +++ b/scripts/run-evals-test.js @@ -621,3 +621,22 @@ test('materializes a git baseline and applies a working-tree patch', () => { fs.rmSync(workspace, { recursive: true, force: true }); } }); + + +test('removes the throwaway workspace when fixture setup fails', () => { + const root = makeSandbox(); + const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'workspace-cleanup-test-')); + try { + const result = spawnSync(process.execPath, ['-e', ` + const { materializeWorkspace } = require(${JSON.stringify(path.join(root, 'scripts', 'run-evals.js'))}); + try { materializeWorkspace({ files: ['project/context.txt', 'missing.txt'] }); } + catch (error) { console.error(error.message); } + `], { cwd: root, encoding: 'utf8', env: { ...process.env, TMPDIR: temp, TMP: temp, TEMP: temp } }); + assert.equal(result.status, 0, result.stdout + result.stderr); + assert.match(result.stderr, /fixture listed in files\[\] not found/); + assert.deepEqual(fs.readdirSync(temp), [], 'partially copied fixture data must be removed'); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + fs.rmSync(temp, { recursive: true, force: true }); + } +}); diff --git a/scripts/run-evals.js b/scripts/run-evals.js index 765d6af7..46597159 100644 --- a/scripts/run-evals.js +++ b/scripts/run-evals.js @@ -389,6 +389,15 @@ function materializeWorkspace(ev) { // Fresh throwaway project dir per eval; fixtures (if any) copied in so the // agent has real code to operate on rather than describing what it would do. const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-skills-eval-')); + try { + return initializeWorkspace(ev, workspace); + } catch (error) { + fs.rmSync(workspace, { recursive: true, force: true }); + throw error; + } +} + +function initializeWorkspace(ev, workspace) { const setupDirs = new Set(); for (const rel of ev.files || []) { const src = resolveFixturePath(FIXTURES_DIR, rel); From a31135a26d4adbfb0687544f0bd736a73163e78a Mon Sep 17 00:00:00 2001 From: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Sat, 3 Oct 2026 11:10:52 -0400 Subject: [PATCH 06/19] Make the shipping error boundary render its fallback Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> --- skills/shipping-and-launch/SKILL.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/skills/shipping-and-launch/SKILL.md b/skills/shipping-and-launch/SKILL.md index 900ba7f3..f94bb487 100644 --- a/skills/shipping-and-launch/SKILL.md +++ b/skills/shipping-and-launch/SKILL.md @@ -190,6 +190,12 @@ Client metrics: ```typescript // Set up error boundary with reporting class ErrorBoundary extends React.Component { + state = { hasError: false }; + + static getDerivedStateFromError() { + return { hasError: true }; + } + componentDidCatch(error: Error, info: React.ErrorInfo) { // Report to error tracking service reportError(error, { From 3e1e0a2db595ab2abe89b1204f7eabb67a24eb90 Mon Sep 17 00:00:00 2001 From: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Sat, 3 Oct 2026 11:35:42 -0400 Subject: [PATCH 07/19] Bind the frontend error-state retry callback Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> --- skills/frontend-ui-engineering/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/frontend-ui-engineering/SKILL.md b/skills/frontend-ui-engineering/SKILL.md index eaf0fa16..3c772568 100644 --- a/skills/frontend-ui-engineering/SKILL.md +++ b/skills/frontend-ui-engineering/SKILL.md @@ -79,7 +79,7 @@ export function TaskItem({ task, onToggle, onDelete }: TaskItemProps) { ```tsx // Container: handles data export function TaskListContainer() { - const { tasks, isLoading, error } = useTasks(); + const { tasks, isLoading, error, refetch } = useTasks(); if (isLoading) return ; if (error) return ; From 229e5764c4ea9f2c76de216ffe57d5c62b71ecce Mon Sep 17 00:00:00 2001 From: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Sat, 3 Oct 2026 11:35:43 -0400 Subject: [PATCH 08/19] Validate the task title minimum after trimming Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> --- skills/security-and-hardening/references/hardening-patterns.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/security-and-hardening/references/hardening-patterns.md b/skills/security-and-hardening/references/hardening-patterns.md index 9af26044..e5fba73c 100644 --- a/skills/security-and-hardening/references/hardening-patterns.md +++ b/skills/security-and-hardening/references/hardening-patterns.md @@ -159,7 +159,7 @@ The `range() !== 'unicast'` check covers loopback, link-local `169.254.169.254` import { z } from 'zod'; const CreateTaskSchema = z.object({ - title: z.string().min(1).max(200).trim(), + title: z.string().max(200).trim().min(1), description: z.string().max(2000).optional(), priority: z.enum(['low', 'medium', 'high']).default('medium'), dueDate: z.string().datetime().optional(), From b5a203d020aa9614670e463534c8ddc4bf59fb92 Mon Sep 17 00:00:00 2001 From: Val Neekman Date: Sat, 3 Oct 2026 14:44:55 -0400 Subject: [PATCH 09/19] docs: list Dojo Workspace in other hosts --- docs/other-hosts.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/other-hosts.md b/docs/other-hosts.md index c81195fc..c053df8e 100644 --- a/docs/other-hosts.md +++ b/docs/other-hosts.md @@ -4,4 +4,5 @@ These hosts install the pack but are not in the README's install section, becaus Skills are plain `SKILL.md` folders, so any agent that reads the Agent Skills layout can load them. See [getting-started.md](getting-started.md). +- **Dojo Workspace**: `dojo skills add addyosmani/agent-skills` - **Oh My Pi**: `omp plugin marketplace add addyosmani/agent-skills`, then `omp plugin install agent-skills@addy-agent-skills` From c45732d2093992b76691e6772f7888e2321c5173 Mon Sep 17 00:00:00 2001 From: Val Neekman Date: Sat, 3 Oct 2026 14:48:00 -0400 Subject: [PATCH 10/19] docs: preserve existing host listing order --- docs/other-hosts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/other-hosts.md b/docs/other-hosts.md index c053df8e..a044e080 100644 --- a/docs/other-hosts.md +++ b/docs/other-hosts.md @@ -4,5 +4,5 @@ These hosts install the pack but are not in the README's install section, becaus Skills are plain `SKILL.md` folders, so any agent that reads the Agent Skills layout can load them. See [getting-started.md](getting-started.md). -- **Dojo Workspace**: `dojo skills add addyosmani/agent-skills` - **Oh My Pi**: `omp plugin marketplace add addyosmani/agent-skills`, then `omp plugin install agent-skills@addy-agent-skills` +- **Dojo Workspace**: `dojo skills add addyosmani/agent-skills` From 807bba68af6876c8782582947410ebb055c6067a Mon Sep 17 00:00:00 2001 From: Sunil Date: Mon, 5 Oct 2026 23:55:43 +0530 Subject: [PATCH 11/19] fix(hooks): simplify-ignore never loses a block whose placeholder was removed --- hooks/SIMPLIFY-IGNORE.md | 2 +- hooks/simplify-ignore-test.sh | 52 +++++++++++++++++++++++++++++++++- hooks/simplify-ignore.sh | 53 ++++++++++++++++++++++++++++++----- 3 files changed, 98 insertions(+), 9 deletions(-) diff --git a/hooks/SIMPLIFY-IGNORE.md b/hooks/SIMPLIFY-IGNORE.md index 09ac71a0..53ea3403 100644 --- a/hooks/SIMPLIFY-IGNORE.md +++ b/hooks/SIMPLIFY-IGNORE.md @@ -83,7 +83,7 @@ Backups are stored in `.claude/.simplify-ignore-cache/` within your project dire - **Single-line blocks hide the entire line.** If `simplify-ignore-start` and `simplify-ignore-end` appear on the same line as other code, the whole line is hidden from the model, not just the annotated portion. Use dedicated lines for annotations. - **Comment suffix detection covers `*/` and `-->` only.** Template engines with non-standard comment closers (ERB `%>`, Blade `--}}`) may produce unbalanced placeholders. Use `#` or `//` style comments instead. - **Fallback expansion is progressive, not exact.** If the model alters a placeholder's formatting (e.g. changes the reason text), the hook tries progressively simpler matches: full placeholder → prefix+hash+suffix → hash-only. The hash-only fallback may leave cosmetic debris (e.g. stray `:` or reason text). A warning is printed to stderr when this happens. -- **A wholesale rewrite falls back to the backup.** If something replaces a filtered file's entire contents and removes every `BLOCK_` placeholder, the hook can no longer tell where the protected block belongs. At `Stop` it restores the backup and prints a warning; the rewrite is not lost but is kept in the cache as `.claude/.simplify-ignore-cache/.recovered`, whose path the warning prints, and you merge it back by hand. Changes that leave the placeholders in place — a `Bash` edit, a formatter, an external editor — are kept in the file itself. +- **A removed placeholder falls back to the backup.** If an edit deletes a `BLOCK_` line, or something replaces the whole file, the hook can no longer tell where that protected block belongs. An `Edit` or `Write` that does this is reported back to the model (exit 2) with the placeholder to restore, and the backup is not updated. If the placeholder is still missing at `Stop`, the hook restores the backup and prints a warning; the changed file is not lost but is kept in the cache as `.claude/.simplify-ignore-cache/.recovered`, whose path the warning prints, and you merge it back by hand. Changes that leave the placeholders in place — a `Bash` edit, a formatter, an external editor — are kept in the file itself. - **File renaming leaves placeholders.** If the model renames or moves a file via a shell command, the new file will retain `BLOCK_` placeholders. The original code is saved as `.recovered` when the session stops. You must manually restore the recovered code into the new file. ## Requirements diff --git a/hooks/simplify-ignore-test.sh b/hooks/simplify-ignore-test.sh index aae1fe6a..d7d6a347 100755 --- a/hooks/simplify-ignore-test.sh +++ b/hooks/simplify-ignore-test.sh @@ -466,8 +466,58 @@ else assert_eq "Stop restores original + edit byte-identically" "identical" "differs" fi +# ── Test 17: a removed placeholder never takes its protected block with it ─ +printf '\nTest 17: Removing a placeholder line does not lose the protected block\n' +PROJ="$TMPDIR/rt17"; mkdir -p "$PROJ" +RT="$PROJ/dropped.js" +cat > "$RT" <<'EOF' +const a = 1; +// simplify-ignore-start: first +const keepOne = 42; +// simplify-ignore-end +const b = 2; +// simplify-ignore-start: second +const keepTwo = 43; +// simplify-ignore-end +EOF +cp "$RT" "$PROJ/dropped.orig" + +rt_hook_event "$PROJ" "Read" "$RT" +# The model deletes the first placeholder line (it looks like a stray comment) +# and edits another line in the same Edit. +awk 'f||!/BLOCK_/{print;next}{f=1}' "$RT" | sed 's/const a = 1;/const a = 9;/' > "$PROJ/t" +cat "$PROJ/t" > "$RT" +edit_rc=0 +edit_out=$(rt_hook_event "$PROJ" "Edit" "$RT" 2>&1) || edit_rc=$? +BAK=$(ls "$PROJ/.claude/.simplify-ignore-cache"/*.bak 2>/dev/null | head -1) || true +assert_eq "Edit that drops a placeholder exits 2 (stderr goes to the model)" "2" "$edit_rc" +assert_eq "message names the dropped placeholder" "1" "$(printf '%s' "$edit_out" | grep -c 'BLOCK_.*: first' || true)" +assert_eq "backup still holds the protected block" "1" "$(grep -c 'const keepOne = 42' "${BAK:-/dev/null}" || true)" + +stop_out=$(rt_hook_event "$PROJ" "" "" 2>&1) || true +RECOVERED=$(ls "$PROJ/.claude/.simplify-ignore-cache"/*.recovered 2>/dev/null | head -1) || true +assert_eq "Stop puts the protected block back" "1" "$(grep -c 'const keepOne = 42' "$RT" || true)" +if cmp -s "$RT" "$PROJ/dropped.orig"; then + assert_eq "Stop restores the last good backup" "identical" "identical" else - printf '\nTests 14-16 skipped: jq not available (hook no-ops without it)\n' + assert_eq "Stop restores the last good backup" "identical" "differs" +fi +assert_eq "the edit is kept as .recovered" "1" "$(grep -c 'const a = 9;' "${RECOVERED:-/dev/null}" || true)" +assert_eq ".recovered has the surviving block expanded" "1" "$(grep -c 'const keepTwo = 43' "${RECOVERED:-/dev/null}" || true)" +assert_eq "Stop warns about the missing placeholder" "1" "$(printf '%s' "$stop_out" | grep -c 'placeholder is missing' || true)" + +# Same loss through a route that fires no Edit event (Bash, a formatter). +PROJ="$TMPDIR/rt17b"; mkdir -p "$PROJ" +RT="$PROJ/dropped.js" +cp "$TMPDIR/rt17/dropped.orig" "$RT" +rt_hook_event "$PROJ" "Read" "$RT" +awk 'f||!/BLOCK_/{print;next}{f=1}' "$RT" > "$PROJ/t" +cat "$PROJ/t" > "$RT" +rt_hook_event "$PROJ" "" "" 2>/dev/null || true +assert_eq "no-event removal: Stop puts the protected block back" "1" "$(grep -c 'const keepOne = 42' "$RT" || true)" + +else + printf '\nTests 14-17 skipped: jq not available (hook no-ops without it)\n' fi # ── Summary ────────────────────────────────────────────────────────────── diff --git a/hooks/simplify-ignore.sh b/hooks/simplify-ignore.sh index dec98220..5523ca5b 100755 --- a/hooks/simplify-ignore.sh +++ b/hooks/simplify-ignore.sh @@ -222,6 +222,27 @@ expand_file() { } # ── has_placeholders: is a cached BLOCK_ for $2 still present in file $1? ─ +# -- missing_placeholders: which cached BLOCK_ of $2 are gone from $1? -- +# Prints the missing placeholders, one per line, and returns 0 if any is gone. +# A placeholder stands for code the model never saw, so it cannot have removed +# it on purpose; whichever route removed it, the block must not be lost with it. +missing_placeholders() { + local f="$1" fid="$2" bf h bp bs br out="" + for bf in "$CACHE/${fid}".block.*; do + [ -f "$bf" ] || continue + h="${bf##*.}" + grep -qF "BLOCK_${h}" "$f" 2>/dev/null && continue + bp=""; bs=""; br="" + [ -f "$CACHE/${fid}.prefix.${h}" ] && bp=$(cat "$CACHE/${fid}.prefix.${h}") + [ -f "$CACHE/${fid}.suffix.${h}" ] && bs=$(cat "$CACHE/${fid}.suffix.${h}") + [ -f "$CACHE/${fid}.reason.${h}" ] && br=": $(cat "$CACHE/${fid}.reason.${h}")" + out="${out} ${bp}BLOCK_${h}${br}${bs} +" + done + [ -n "$out" ] || return 1 + printf '%s' "$out" +} + has_placeholders() { local f="$1" fid="$2" bf h for bf in "$CACHE/${fid}".block.*; do @@ -245,20 +266,25 @@ if [ -z "$TOOL_NAME" ]; then # A change can reach the file through a route that fires no Edit or Write # event (a Bash command, a formatter, an external editor), and the backup # is stale for all of them. Expand what is on disk rather than overwrite it. - if has_placeholders "$orig" "$fid"; then + if has_placeholders "$orig" "$fid" && ! missing_placeholders "$orig" "$fid" >/dev/null; then EXPANDED="$CACHE/${fid}.$$.expanded" expand_file "$orig" "$EXPANDED" "$fid" cat "$EXPANDED" > "$orig" rm -f "$EXPANDED" else - # Nothing left to expand — the file was rewritten wholesale, so the - # backup is the only remaining copy of the protected blocks. Keep the - # rewrite in the cache before the backup overwrites it, so the work is - # recoverable rather than lost. + # A placeholder is gone — the file was rewritten wholesale, or one + # block's line was deleted — so the backup is the only remaining copy + # of that protected block. Keep the rewrite in the cache (with any + # surviving placeholders expanded) before the backup overwrites it, so + # the work is recoverable rather than lost. rewritten="$CACHE/${fid}.recovered" - cat "$orig" > "$rewritten" + if has_placeholders "$orig" "$fid"; then + expand_file "$orig" "$rewritten" "$fid" 2>/dev/null + else + cat "$orig" > "$rewritten" + fi cat "$bak" > "$orig" - printf 'Warning: no BLOCK_ placeholder left in %s, restored from backup. Rewrite kept at %s\n' \ + printf 'Warning: a BLOCK_ placeholder is missing from %s, restored from backup. Rewrite kept at %s\n' \ "$orig" "$rewritten" >&2 fi rm -f "$bak" "$pathfile" "$CACHE/${fid}".block.* "$CACHE/${fid}".reason.* "$CACHE/${fid}".prefix.* "$CACHE/${fid}".suffix.* @@ -330,6 +356,19 @@ if [ "$TOOL_NAME" = "Edit" ] || [ "$TOOL_NAME" = "Write" ]; then [ -f "$CACHE/${ID}.bak" ] || exit 0 ls "$CACHE/${ID}".block.* >/dev/null 2>&1 || exit 0 + # The edit removed a placeholder. Expanding now would save a backup without + # that block, and Stop would then restore the loss. Keep the last good + # backup, leave the file as edited, and tell the model (exit 2 sends stderr + # back to it) so it can put the placeholder back; if it does not, Stop + # restores the backup and keeps this version as .recovered. + if dropped=$(missing_placeholders "$FILE_PATH" "$ID"); then + printf '%s\n%s\n%s\n' \ + "simplify-ignore: this $TOOL_NAME removed placeholder line(s) from $FILE_PATH. Each stands for protected code you cannot see:" \ + "$dropped" \ + "Put each one back exactly as shown. Until then the backup is not updated, and at session end the file is restored from it (this version is kept in the cache as .recovered)." >&2 + exit 2 + fi + # Expand placeholders, preserving any inline code the model added around them EXPANDED="$CACHE/${ID}.$$.expanded" expand_file "$FILE_PATH" "$EXPANDED" "$ID" From 066784e03fb9c872dc8f2494dc1a40e717ed5273 Mon Sep 17 00:00:00 2001 From: ayobamiseun Date: Tue, 6 Oct 2026 09:00:38 +0100 Subject: [PATCH 12/19] docs: list fx in other hosts The fx setup guide (#507) was closed under the new host-guide bar, which gives hosts no maintainer has run one line in docs/other-hosts.md. Add that line with the corrected install command from the guide: copy the skills into .fx/skills/, fx's native project root, discovered from 0.0.5 on. No affiliation with fx or Vercel Labs. Refs #504, #507 --- docs/other-hosts.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/other-hosts.md b/docs/other-hosts.md index c81195fc..8f482435 100644 --- a/docs/other-hosts.md +++ b/docs/other-hosts.md @@ -4,4 +4,5 @@ These hosts install the pack but are not in the README's install section, becaus Skills are plain `SKILL.md` folders, so any agent that reads the Agent Skills layout can load them. See [getting-started.md](getting-started.md). +- **fx**: `git clone https://github.com/addyosmani/agent-skills.git /tmp/agent-skills && mkdir -p .fx/skills && cp -R /tmp/agent-skills/skills/* .fx/skills/` (project scope; use `~/.fx/skills/` instead for every workspace; needs fx 0.0.5 or later) - **Oh My Pi**: `omp plugin marketplace add addyosmani/agent-skills`, then `omp plugin install agent-skills@addy-agent-skills` From 1ab25ea1c8a12f388906055ee133ec450b2d870e Mon Sep 17 00:00:00 2001 From: root Date: Wed, 7 Oct 2026 15:51:07 +0000 Subject: [PATCH 13/19] fix(linter): ignore comments and nested metadata keys in skill frontmatter --- scripts/lib/skill-lint-test.js | 49 +++++++++++++++++++++++++++++++++- scripts/lib/skill-lint.js | 13 ++++----- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/scripts/lib/skill-lint-test.js b/scripts/lib/skill-lint-test.js index ba08cb2a..f09fbacb 100644 --- a/scripts/lib/skill-lint-test.js +++ b/scripts/lib/skill-lint-test.js @@ -9,7 +9,7 @@ const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); -const { lintSkillContent, lintSkillLayout, topLevelFrontmatterKeys } = require('./skill-lint.js'); +const { lintSkillContent, lintSkillLayout, topLevelFrontmatterKeys, parseFrontmatter, frontmatterYamlErrors } = require('./skill-lint.js'); const KNOWN = new Set(['alpha', 'beta']); @@ -648,3 +648,50 @@ test('the spec-key check tolerates CRLF frontmatter', () => { const { errors } = lintSkillContent('alpha', content, KNOWN); assert.equal(errors.filter(e => /key 'temperature'/.test(e)).length, 1); }); + +test('parseFrontmatter ignores comments and nested metadata keys', () => { + const content = [ + '---', + 'name: alpha', + '# note: this is a comment', + 'description: Designs alphas. Use when building one.', + 'metadata:', + ' name: beta', + ' description: Nested details without trigger', + '---', + '', + ].join('\n'); + assert.deepEqual(parseFrontmatter(content), { + name: 'alpha', + description: 'Designs alphas. Use when building one.', + metadata: '', + }); +}); + +test('frontmatterYamlErrors ignores comment lines containing colons or quotes', () => { + const content = [ + '---', + 'name: alpha', + '# note: a comment: with a colon and "unclosed quote', + 'description: "Designs alphas. Use when building one."', + '---', + '', + ].join('\n'); + assert.deepEqual(frontmatterYamlErrors(content), []); +}); + +test('a skill with frontmatter comments and nested metadata passes validation', () => { + const content = withAllSections([ + '---', + 'name: alpha', + '# Note: keep description up to date: check docs', + 'description: Designs alphas. Use when building one.', + 'metadata:', + ' name: beta', + ' description: Internal notes', + '---', + ].join('\n')); + const { errors } = lintSkillContent('alpha', content, KNOWN); + assert.deepEqual(errors, []); +}); + diff --git a/scripts/lib/skill-lint.js b/scripts/lib/skill-lint.js index 8779c9c0..7d30feec 100644 --- a/scripts/lib/skill-lint.js +++ b/scripts/lib/skill-lint.js @@ -184,11 +184,12 @@ function parseFrontmatter(content) { const result = {}; for (const line of match[1].split(/\r?\n/)) { - const colonIdx = line.indexOf(':'); - if (colonIdx === -1) continue; - const key = line.slice(0, colonIdx).trim(); - const value = line.slice(colonIdx + 1).trim().replace(/^['"]|['"]$/g, ''); - if (key) result[key] = value; + if (/^\s*#/.test(line)) continue; + const m = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/); + if (!m) continue; + const key = m[1]; + const value = m[2].trim().replace(/^['"]|['"]$/g, ''); + result[key] = value; } return result; } @@ -234,7 +235,7 @@ function frontmatterYamlErrors(content) { const lines = match[1].split(/\r?\n/); lines.forEach((line, i) => { const lineNo = i + 2; // the opening `---` is line 1 - if (!line.trim()) return; + if (!line.trim() || /^\s*#/.test(line)) return; if (/^[ ]*\t/.test(line)) { errors.push( From 8fc42ebf266736192df3c02dd4402b0bffcb28ec Mon Sep 17 00:00:00 2001 From: 0xkatana Date: Wed, 7 Oct 2026 18:46:05 +0100 Subject: [PATCH 14/19] docs(other-hosts): add Pi install entry Pi implements the Agent Skills standard natively and discovers skills from ~/.agents/skills/ and .agents/skills/ with no plugin or config file, so its package manager installs the pack directly from git. Follows the Adding a Host Guide bar (CONTRIBUTING.md): one line, host name and install command only. Refs #355, #365 --- docs/other-hosts.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/other-hosts.md b/docs/other-hosts.md index c81195fc..1ef8d4e4 100644 --- a/docs/other-hosts.md +++ b/docs/other-hosts.md @@ -5,3 +5,4 @@ These hosts install the pack but are not in the README's install section, becaus Skills are plain `SKILL.md` folders, so any agent that reads the Agent Skills layout can load them. See [getting-started.md](getting-started.md). - **Oh My Pi**: `omp plugin marketplace add addyosmani/agent-skills`, then `omp plugin install agent-skills@addy-agent-skills` +- **Pi**: `pi install git:github.com/addyosmani/agent-skills` From fe9f1f26e0fee1c4e119adaa1e3ef0d3f2ae9e27 Mon Sep 17 00:00:00 2001 From: Prompeteer Date: Wed, 7 Oct 2026 12:15:24 -0700 Subject: [PATCH 15/19] docs: add source-preserving prose review to documentation skill Keep operational facts and caveats intact when editing documentation. Reference a pinned Zero Slop workflow only as an optional review aid, and add an execution fixture for the preservation checks. --- evals/cases/documentation-and-adrs.json | 17 ++++++++++ .../prose-review/README.md | 19 +++++++++++ .../prose-review/queue.js | 32 +++++++++++++++++++ skills/documentation-and-adrs/SKILL.md | 11 +++++++ 4 files changed, 79 insertions(+) create mode 100644 evals/fixtures/documentation-and-adrs/prose-review/README.md create mode 100644 evals/fixtures/documentation-and-adrs/prose-review/queue.js diff --git a/evals/cases/documentation-and-adrs.json b/evals/cases/documentation-and-adrs.json index 431074eb..8b96d30e 100644 --- a/evals/cases/documentation-and-adrs.json +++ b/evals/cases/documentation-and-adrs.json @@ -39,6 +39,23 @@ "Trade-offs and rejected options are recorded, not just the winning choice", "The ADR records a status from the skill's lifecycle (Proposed, Accepted, Superseded or Deprecated) and a date" ] + }, + { + "id": 4, + "kind": "execution", + "prompt": "Review README.md for operators onboarding to this batch queue. Make the prose clearer, check it against queue.js, and keep the operational details and caveats accurate. Do not change the implementation.", + "expected_output": "A concise operational README grounded in the implementation, with unsupported assurances removed or flagged and command examples, limits, and caveats preserved", + "files": [ + "documentation-and-adrs/prose-review" + ], + "expectations": [ + "The final README retains the documented queuectl submit jobs.json command and its --max-batch-size 50 option without inventing an alternative interface", + "The final README preserves the 50-job batch limit, 72-hour retention period, and at most three retries, distinguishing those units correctly", + "The final README preserves the operational caveat that failed jobs require operator review and queue state is not durable across a process restart", + "The unsupported claims of guaranteed zero job loss and no need for monitoring are removed or explicitly marked as unsubstantiated rather than restated as facts", + "The promotional opening is replaced with a concrete description of the queue without invented users, benchmarks, certifications, or capabilities", + "queue.js remains unchanged, and the agent does not install a tool or send the fixture to a hosted service" + ] } ] } diff --git a/evals/fixtures/documentation-and-adrs/prose-review/README.md b/evals/fixtures/documentation-and-adrs/prose-review/README.md new file mode 100644 index 00000000..e8b65f2c --- /dev/null +++ b/evals/fixtures/documentation-and-adrs/prose-review/README.md @@ -0,0 +1,19 @@ +# Batch Queue + +Batch Queue is an innovative, enterprise-grade solution that seamlessly unlocks +the power of batch processing. In today's fast-paced landscape, it is important +to note that this robust queue empowers operators to take work to the next level. + +## Submit a batch + +```sh +queuectl submit jobs.json --max-batch-size 50 +``` + +A batch contains at most 50 jobs. Queue entries expire after 72 hours. Failed +jobs are retried at most three times. + +Operators review jobs that still fail after retries. Queue state is held in +memory and is not durable across a process restart. + +This guarantees zero job loss and means operators never need to monitor the queue. diff --git a/evals/fixtures/documentation-and-adrs/prose-review/queue.js b/evals/fixtures/documentation-and-adrs/prose-review/queue.js new file mode 100644 index 00000000..7c6dda6b --- /dev/null +++ b/evals/fixtures/documentation-and-adrs/prose-review/queue.js @@ -0,0 +1,32 @@ +'use strict'; + +const MAX_BATCH_SIZE = 50; +const RETENTION_MS = 72 * 60 * 60 * 1000; +const MAX_RETRIES = 3; +const jobs = new Map(); + +function submit(batch, now = Date.now()) { + if (batch.length > MAX_BATCH_SIZE) throw new Error('Batch exceeds limit'); + for (const job of batch) { + jobs.set(job.id, { ...job, createdAt: now, retries: 0, state: 'queued' }); + } +} + +function recordFailure(id) { + const job = jobs.get(id); + if (!job) throw new Error('Unknown job'); + if (job.retries < MAX_RETRIES) { + job.retries += 1; + job.state = 'queued'; + } else { + job.state = 'operator-review'; + } +} + +function expire(now = Date.now()) { + for (const [id, job] of jobs) { + if (now - job.createdAt >= RETENTION_MS) jobs.delete(id); + } +} + +module.exports = { submit, recordFailure, expire, MAX_BATCH_SIZE, RETENTION_MS, MAX_RETRIES }; diff --git a/skills/documentation-and-adrs/SKILL.md b/skills/documentation-and-adrs/SKILL.md index e37c38f3..1ba9d75f 100644 --- a/skills/documentation-and-adrs/SKILL.md +++ b/skills/documentation-and-adrs/SKILL.md @@ -256,6 +256,16 @@ Special consideration for AI agent context: - **ADRs** — Help agents understand why past decisions were made (prevents re-deciding) - **Inline gotchas** — Prevent agents from falling into known traps +## Prose Review + +After checking the technical content, review the prose for the people who will use it. Clearer wording must not change the decision or its limits. + +1. Mark the details that must survive the edit: names, numbers and units, commands, paths, links, conditions, trade-offs, and uncertainty. Check them against the available code and decision context; flag anything you cannot substantiate. +2. Replace stock introductions, promotional adjectives, and repeated summaries with the concrete information the reader needs. Do not replace an unsupported claim with an invented example or a stronger guarantee. Keep necessary technical repetition and the project's established terminology. +3. Compare the edited document with the original and its sources. Restore any lost qualifier or caveat, verify command and code examples still work as documented, and explain any unresolved factual question rather than polishing it away. + +If the team already uses a reviewed version of [Zero Slop](https://github.com/manavmishra/ZeroSlop/tree/43b71836bf2feb8395a84aaeea809bd7ef65e385), its source-preserving editing workflow can assist this pass. This reference pins v2.12.22; do not install or update a tool just to complete the review. Its writing score is heuristic, not proof of factual accuracy or AI authorship. A lower score does not replace the source comparison, and this step does not require sending the document to a hosted editor. + ## Common Rationalizations | Rationalization | Reality | @@ -286,3 +296,4 @@ After documenting: - [ ] Known gotchas are documented inline where they matter - [ ] No commented-out code remains - [ ] Rules files (CLAUDE.md etc.) are current and accurate +- [ ] Prose edits preserve source details, decision trade-offs, and qualifiers without adding unsupported guarantees From 3acf1b8ac0cae5d0a11945832f5da55ef5065a64 Mon Sep 17 00:00:00 2001 From: Prompeteer Date: Wed, 7 Oct 2026 12:21:53 -0700 Subject: [PATCH 16/19] test: require source inspection and flag unverified queue command --- evals/cases/documentation-and-adrs.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/evals/cases/documentation-and-adrs.json b/evals/cases/documentation-and-adrs.json index 8b96d30e..1ea78028 100644 --- a/evals/cases/documentation-and-adrs.json +++ b/evals/cases/documentation-and-adrs.json @@ -44,16 +44,17 @@ "id": 4, "kind": "execution", "prompt": "Review README.md for operators onboarding to this batch queue. Make the prose clearer, check it against queue.js, and keep the operational details and caveats accurate. Do not change the implementation.", - "expected_output": "A concise operational README grounded in the implementation, with unsupported assurances removed or flagged and command examples, limits, and caveats preserved", + "expected_output": "A concise operational README grounded in the inspected implementation, with unsupported assurances removed or flagged, limits and caveats preserved, and the unverified command identified", "files": [ "documentation-and-adrs/prose-review" ], "expectations": [ - "The final README retains the documented queuectl submit jobs.json command and its --max-batch-size 50 option without inventing an alternative interface", + "The final README identifies queuectl submit jobs.json --max-batch-size 50 as an unverified documented example because the fixture contains neither the CLI nor jobs.json; it does not invent, silently certify, or replace that interface", "The final README preserves the 50-job batch limit, 72-hour retention period, and at most three retries, distinguishing those units correctly", "The final README preserves the operational caveat that failed jobs require operator review and queue state is not durable across a process restart", "The unsupported claims of guaranteed zero job loss and no need for monitoring are removed or explicitly marked as unsubstantiated rather than restated as facts", "The promotional opening is replaced with a concrete description of the queue without invented users, benchmarks, certifications, or capabilities", + "The execution trace shows the agent inspecting queue.js and comparing the README with its batch, retention, retry, and in-memory-state implementation before editing", "queue.js remains unchanged, and the agent does not install a tool or send the fixture to a hosted service" ] } From bd7309e9d190132eef6b612df56b71d18948da9b Mon Sep 17 00:00:00 2001 From: ayobamiseun Date: Thu, 8 Oct 2026 00:27:27 +0100 Subject: [PATCH 17/19] docs(copilot): say plainly that the lifecycle slash commands never appear in Copilot #542 reports following docs/copilot-setup.md and finding no /spec, /plan, /build or /test. The doc already says the wrappers are Claude Code commands, but in the middle of a paragraph that a reader scanning for install steps walks past; the reporter did. The maintainer's answer on the issue was that the doc should make this explicit. Add a callout right after the intro that names the commands, says they are expected to be absent, and points to the skill-name mapping and the optional aliases. Add a lead-in to the troubleshooting list that rules out the expected case before the real diagnostics. Add one sentence to the README's Copilot entry so the expectation is set before the doc is opened. Closes #542 --- README.md | 2 +- docs/copilot-setup.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c6039f4d..efd6fabb 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ See [docs/opencode-setup.md](docs/opencode-setup.md).
GitHub Copilot -Use agent definitions from `agents/` as Copilot personas and skill content in `.github/copilot-instructions.md`. See [docs/copilot-setup.md](docs/copilot-setup.md). +Use agent definitions from `agents/` as Copilot personas and skill content in `.github/copilot-instructions.md`. The lifecycle slash commands (`/spec`, `/plan`, `/build`, `/test`, `/review`, `/ship`) are Claude Code commands and do not appear in Copilot; invoke the skills by name (`/spec-driven-development`, …) instead. See [docs/copilot-setup.md](docs/copilot-setup.md). Using the standalone `copilot` CLI? Install it as a plugin — see [docs/copilot-cli-setup.md](docs/copilot-cli-setup.md). diff --git a/docs/copilot-setup.md b/docs/copilot-setup.md index f70aa3b1..b33d111c 100644 --- a/docs/copilot-setup.md +++ b/docs/copilot-setup.md @@ -4,6 +4,8 @@ This guide covers Copilot in VS Code. For the standalone `copilot` command-line **What an install actually gives you:** the skills. Each installed skill becomes a slash command named after its frontmatter `name` — `/spec-driven-development`, `/test-driven-development`, and so on. `npx skills add addyosmani/agent-skills` and the manual copy below both install skills only. Neither of those two routes copies this repo's short lifecycle wrappers (`/spec`, `/plan`, `/build`, `/test`, `/review`, `/ship`) — those are Claude Code commands living in `.claude/commands/`. Use the full skill names, or add your own aliases — see [Lifecycle workflows](#lifecycle-workflows). +> **`/spec`, `/plan`, `/build`, `/test`, `/review`, `/code-simplify`, `/webperf` and `/ship` never appear in Copilot.** They are Claude Code commands, and Copilot does not read `.claude/commands/`. Not seeing them after an install is expected, not a missing step. Invoke the skill by its own name instead — `/spec-driven-development` for `/spec`, `/planning-and-task-breakdown` for `/plan`, and so on; the full mapping is in [Lifecycle Workflows](#lifecycle-workflows). On VS Code you can add [short aliases](#optional-short-spec-style-aliases) yourself if you want the short forms. + ## Setup ### Copilot Instructions @@ -145,6 +147,8 @@ Write the body yourself, or take it from this table, rather than copying `.claud ### If the skill slash commands don't appear +First rule out the expected case: if the only commands you miss are the short lifecycle wrappers (`/spec`, `/plan`, `/build`, `/test`, `/review`, `/ship`), nothing is wrong — those are Claude Code commands and never surface in Copilot (see the note at the top). The steps below are for when the skills' own names, such as `/spec-driven-development`, are missing too. + Work through these in order — `/spec-driven-development` missing and `/spec` missing have different causes: 1. **Check where the skills landed.** Each one needs its own directory containing a `SKILL.md`. The project-level paths this guide uses are `.github/skills/`, `.claude/skills/` and `.agents/skills/` — but an installer run with a global/user flag writes outside the workspace instead (`~/.agents/skills/` and equivalents), so a skill that isn't in any of the three may simply be installed personally. Confirm the actual path against the [VS Code agent skills docs](https://code.visualstudio.com/docs/agent-customization/agent-skills), then re-install into the project if you wanted it project-scoped. From 15db73d8bd8f5d18531321b7a283eb5044808f1e Mon Sep 17 00:00:00 2001 From: ayobamiseun Date: Thu, 8 Oct 2026 00:30:21 +0100 Subject: [PATCH 18/19] docs(cursor): say where .cursor/ lives and note the attach-a-skill error Two open threads, one doc. #652 reads the Cursor guide as wrong because this repository has no .cursor/ directory; the guide means the user's project but never says so. #494 reports Cursor failing with "An unexpected error occurred on our servers" when a skill is attached to a prompt with /, after which every message in that chat fails; the maintainer confirmed it is Cursor-side, named referencing the skill by name as the workaround, and said the setup doc should carry a note. Add one sentence after the intro saying every .cursor/ path is in the reader's project and this repo ships none, linking to the install step. Add a troubleshooting row for the attach error with the workaround and a pointer to #494, which stays open as a known host issue. Add the project-not-repo clause to the README's Cursor entry. Closes #652. Refs #494. --- README.md | 2 +- docs/cursor-setup.md | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c6039f4d..2f27df8b 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ claude --plugin-dir /path/to/agent-skills
Cursor -Put workflow skills under `.cursor/skills/` (sync from `agent-skills/skills/`) and short policies in `.cursor/rules/*.mdc` — do not paste full skills into rules. See [docs/cursor-setup.md](docs/cursor-setup.md). +Put workflow skills under `.cursor/skills/` (sync from `agent-skills/skills/`) and short policies in `.cursor/rules/*.mdc` — do not paste full skills into rules. Both directories live in your project; this repo does not ship a `.cursor/` folder. See [docs/cursor-setup.md](docs/cursor-setup.md).
diff --git a/docs/cursor-setup.md b/docs/cursor-setup.md index 5fdde518..1b3c24fe 100644 --- a/docs/cursor-setup.md +++ b/docs/cursor-setup.md @@ -2,6 +2,8 @@ How to wire [agent-skills](../README.md) into **Cursor** using current, supported project context — not legacy monolith files or Kaizen-specific layouts. +Every `.cursor/` path in this guide lives in **your** project (or under `~/.cursor/` for user scope). This repository does not ship a `.cursor/` directory: you create it and sync the skills into it, which is what [step 1](#1-install-skills-into-cursorskills) does. + --- ## What Cursor supports today @@ -205,6 +207,7 @@ Files under `agent-skills/agents/` (e.g. code reviewer persona) are **not** load | Stale workflow | Re-`rsync` from `agent-skills/skills/` | | Duplicate instructions | Remove skill content from rules; keep one source | | Wrong skill picked | Narrow `description` in custom skills; nudge in chat | +| "An unexpected error occurred on our servers" right after attaching a skill with `/`, and every later message in that chat fails too | Known Cursor-side issue, tracked in [#494](https://github.com/addyosmani/agent-skills/issues/494): Cursor pulls the attached `SKILL.md` into the request and the failure happens on its side, not in the skill. Start a new chat and reference the skill by name in plain text ("Use the test-driven-development skill to …") instead of attaching it. | --- From 30f886278158cad70f6682dcaf7889338bf01444 Mon Sep 17 00:00:00 2001 From: Prompeteer Date: Thu, 8 Oct 2026 06:16:57 -0700 Subject: [PATCH 19/19] docs: keep prose review tool-neutral --- skills/documentation-and-adrs/SKILL.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/skills/documentation-and-adrs/SKILL.md b/skills/documentation-and-adrs/SKILL.md index 1ba9d75f..ecc28cef 100644 --- a/skills/documentation-and-adrs/SKILL.md +++ b/skills/documentation-and-adrs/SKILL.md @@ -264,8 +264,6 @@ After checking the technical content, review the prose for the people who will u 2. Replace stock introductions, promotional adjectives, and repeated summaries with the concrete information the reader needs. Do not replace an unsupported claim with an invented example or a stronger guarantee. Keep necessary technical repetition and the project's established terminology. 3. Compare the edited document with the original and its sources. Restore any lost qualifier or caveat, verify command and code examples still work as documented, and explain any unresolved factual question rather than polishing it away. -If the team already uses a reviewed version of [Zero Slop](https://github.com/manavmishra/ZeroSlop/tree/43b71836bf2feb8395a84aaeea809bd7ef65e385), its source-preserving editing workflow can assist this pass. This reference pins v2.12.22; do not install or update a tool just to complete the review. Its writing score is heuristic, not proof of factual accuracy or AI authorship. A lower score does not replace the source comparison, and this step does not require sending the document to a hosted editor. - ## Common Rationalizations | Rationalization | Reality |