From bce21620254bc7dd345c5675239282be7db348f5 Mon Sep 17 00:00:00 2001 From: DietrichGebert Date: Fri, 12 Jun 2026 17:39:11 +0200 Subject: [PATCH] chore: tighten rule-copy drift check; align Kiro copy (#9) Cover the Kiro steering file in the equality check (the stripper is now shared, renamed to stripFrontmatter), and align its body with AGENTS.md: the 'Not lazy about' and 'Non-trivial logic' sentences were split into two paragraphs where the canonical body and the other four copies keep them as one. The improved check caught this on the freshly merged #6. Also add a small invariant canary: four load-bearing rule phrases must appear verbatim in both SKILL.md (the runtime source of truth) and AGENTS.md, catching a rule that lands in one but not the other even though the two files are intentionally different lengths. Verified: passes on main, exits 1 when a copy diverges or an invariant goes missing. Co-authored-by: Claude Opus 4.8 --- .kiro/steering/ponytail.md | 4 +--- scripts/check-rule-copies.js | 33 +++++++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/.kiro/steering/ponytail.md b/.kiro/steering/ponytail.md index c3a09a1..2d4bf42 100644 --- a/.kiro/steering/ponytail.md +++ b/.kiro/steering/ponytail.md @@ -26,6 +26,4 @@ Rules: - Pick the edge-case-correct option when two stdlib approaches are the same size — lazy means less code, not the flimsier algorithm. - Mark intentional simplifications with a `ponytail:` comment. If the shortcut has a known ceiling (global lock, O(n²) scan, naive heuristic), the comment names the ceiling and the upgrade path. -Not lazy about: input validation at trust boundaries, error handling that prevents data loss, security, accessibility, anything explicitly requested. - -Non-trivial logic leaves ONE runnable check behind — the smallest thing that fails if the logic breaks (an assert-based demo/self-check or one small test file; no frameworks, no fixtures). Trivial one-liners need no test. +Not lazy about: input validation at trust boundaries, error handling that prevents data loss, security, accessibility, anything explicitly requested. Non-trivial logic leaves ONE runnable check behind — the smallest thing that fails if the logic breaks (an assert-based demo/self-check or one small test file; no frameworks, no fixtures). Trivial one-liners need no test. diff --git a/scripts/check-rule-copies.js b/scripts/check-rule-copies.js index 0a9a836..e37d747 100644 --- a/scripts/check-rule-copies.js +++ b/scripts/check-rule-copies.js @@ -8,18 +8,20 @@ function read(relPath) { return fs.readFileSync(path.join(root, relPath), 'utf8').replace(/\r\n/g, '\n').trim(); } -function stripCursorFrontmatter(text) { +function stripFrontmatter(text) { return text.replace(/^---\n[\s\S]*?\n---\n*/, '').trim(); } const agents = read('AGENTS.md'); const canonical = agents.replace(/\n\n\(Yes, this file also applies[\s\S]*?\)$/, '').trim(); +// Compact copies: same body as AGENTS.md, host-specific frontmatter stripped. const copies = [ - ['.cursor/rules/ponytail.mdc', stripCursorFrontmatter], + ['.cursor/rules/ponytail.mdc', stripFrontmatter], ['.windsurf/rules/ponytail.md', text => text.trim()], ['.clinerules/ponytail.md', text => text.trim()], ['.github/copilot-instructions.md', text => text.trim()], + ['.kiro/steering/ponytail.md', stripFrontmatter], ]; let failed = false; @@ -32,9 +34,32 @@ for (const [relPath, normalize] of copies) { } } +// SKILL.md is the runtime source of truth and is longer than the compact body, +// so it cannot be byte-compared. ponytail: canary, not full equality. Assert the +// load-bearing rules survive verbatim in both the source and AGENTS.md. Changing +// a rule's wording trips this, which is the reminder to propagate it everywhere. +// Upgrade path: generate the copies from SKILL.md if this ever misses a real drift. +const INVARIANTS = [ + 'naive heuristic', // ceiling-comment rule + 'ONE runnable check', // test reflex + 'flimsier algorithm', // robust-variant rule + 'input validation at trust boundaries', // the "not lazy about" clause +]; + +const skill = read('skills/ponytail/SKILL.md'); +const sources = [['skills/ponytail/SKILL.md', skill], ['AGENTS.md', agents]]; +for (const phrase of INVARIANTS) { + for (const [label, text] of sources) { + if (!text.includes(phrase)) { + console.error(`${label} is missing rule invariant: "${phrase}"`); + failed = true; + } + } +} + if (failed) { - console.error('Update the copied rule text or AGENTS.md so the shared body matches.'); + console.error('Update the copied rule text, AGENTS.md, or SKILL.md so the shared rules match.'); process.exit(1); } -console.log('Rule copies match AGENTS.md shared body.'); +console.log(`Rule copies match AGENTS.md; ${INVARIANTS.length} rule invariants present in SKILL.md and AGENTS.md.`);