/** * No prose skill under development/ may INSTRUCT a raw `git commit` / `git add` * in Bash: the deployed `git-guard` PreToolUse hook blocks that call by design, * so the mandatory commit step of `fix-bug` / `fix-build` could never complete * (audit 2026-09-24, D-CRIT-06). The sanctioned path is the gitflow commit CLI * (`skills/gitflow/cli/commit/index.ts --spec '{"message":…}' --workdir … --json`). * * A CLAUSE that MENTIONS the raw command to forbid it (git-guard, NEVER, blocks…) * or that names the sanctioned CLI is not an instruction. The exemption is * judged on the clause holding the match, never on the whole line (QC * 2026-09-24, lot 4): the original fix-bug:88 — "… never `git add -A`. Then * `git commit -m …`" — carried its `never` in one clause and the instruction in * the next, and a whole-line test exempted it. * * A clause is the text between sentence separators (`.` `;` `:` followed by * whitespace or the end of text, ` — `) or Markdown block boundaries (a blank * line, a fence line, a newline opening a list item / heading / table row / * quote). A soft line-wrap inside a paragraph does NOT end a clause: the real * `…hook BLOCKS a raw\n`git commit` typed in Bash` (fix-bug:88-89) is one clause. */ import { describe, it, expect } from 'vitest' import { readFileSync } from 'node:fs' import { dirname, relative, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { listFiles } from '../../lib/__tests__/helpers/cli-options.js' const DEV_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') // Built without the literal so a Bash grep / heredoc of this file never trips the hook itself. const GIT = 'git' const RAW = new RegExp(String.raw`\b` + GIT + String.raw`\s+(?:c` + String.raw`ommit|add)\b`, 'g') const EXEMPT = /git-guard|gitflow\/cli\/commit|NEVER|never|blocks|blocked|forbidden|refuse|instead of|not a raw|no raw|raw `git/i /** * Clause boundaries: sentence punctuation followed by whitespace or the end of * text (`index.ts`, `https://`, `"message":…` do not split), an em-dash break, * a whole fence line, or a newline that opens a new block (blank line, list * item, heading, table row, quote). A plain newline is a soft wrap — no boundary. */ const BOUNDARY = /[.;:](?=\s|$)|\s—\s|```[^\n]*|\n(?=[ \t]*(?:\n|$|[-*+]\s|\d+[.)]\s|#{1,6}\s|\||>))/g interface Offender { line: number; clause: string } /** Every raw `git commit` / `git add` whose clause carries no exemption, with its 1-based line. */ function rawGitOffenders(text: string): Offender[] { const src = text.replace(/\r\n/g, '\n') const cuts: Array<[number, number]> = [[0, 0]] for (const b of src.matchAll(BOUNDARY)) cuts.push([b.index!, b.index! + b[0].length]) cuts.push([src.length, src.length]) const out: Offender[] = [] for (const m of src.matchAll(RAW)) { const at = m.index! let start = 0 let end = src.length for (const [s, e] of cuts) { if (e <= at) start = e else if (s > at) { end = s; break } } const clause = src.slice(start, end) if (!EXEMPT.test(clause)) out.push({ line: src.slice(0, at).split('\n').length, clause: clause.replace(/\s+/g, ' ').trim() }) } return out } describe('development/**/*.md — no raw git commit instruction', () => { it('every clause naming the raw command either forbids it or points at the gitflow commit CLI', () => { const offenders: string[] = [] const files = listFiles(DEV_ROOT, (p) => p.endsWith('.md')) expect(files.length, 'an empty walk is red').toBeGreaterThanOrEqual(50) for (const file of files) { const path = relative(DEV_ROOT, file).replace(/\\/g, '/') for (const o of rawGitOffenders(readFileSync(file, 'utf-8'))) offenders.push(`${path}:${o.line}: ${o.clause}`) } expect(offenders, offenders.join('\n')).toEqual([]) }) }) describe('the predicate itself', () => { // fix-bug/SKILL.md:88 as it stood before lot 4 (00aeeecc^), verbatim — assembled at runtime, see GIT. const HISTORICAL_FIX_BUG_88 = ` Use \`${GIT} add \` listing only the files you actually changed — never \`${GIT} add -A\`. Then \`${GIT} commit -m "" -m ""\`. Do NOT push. Do NOT amend.` it('FIRES — the original fix-bug:88: a whole-line `never` no longer exempts the instruction clauses', () => { // The blind spot of the whole-line test: the line as a whole LOOKS exempt. expect(EXEMPT.test(HISTORICAL_FIX_BUG_88)).toBe(true) const offenders = rawGitOffenders(HISTORICAL_FIX_BUG_88) expect(offenders.map((o) => o.clause)).toEqual([ `Use \`${GIT} add \` listing only the files you actually changed`, `Then \`${GIT} commit -m "" -m ""\``, ]) expect(offenders.map((o) => o.line)).toEqual([1, 1]) }) it('SILENT — a clause that forbids the raw command and points at the gitflow CLI', () => { expect(rawGitOffenders(`never run \`${GIT} commit\` directly — use the gitflow CLI`)).toEqual([]) }) it('SILENT — the exemption survives a soft line-wrap inside the sentence (the real fix-bug:88-89 shape)', () => { const wrapped = [ ' Commit through the sanctioned CLI — the deployed `git-guard` PreToolUse hook BLOCKS a raw', ` \`${GIT} commit\` typed in Bash, by design; never bypass it — scoping the commit to the files you`, ' actually changed (`files[]`, never the whole tree):', ].join('\n') expect(rawGitOffenders(wrapped)).toEqual([]) }) it('SILENT — sentence punctuation inside a token (`index.ts`, `"message":…`) does not cut the clause', () => { const text = `the CLI (\`skills/gitflow/cli/commit/index.ts --spec '{"message":"…"}'\`) runs \`${GIT} commit\` for you` expect(rawGitOffenders(text)).toEqual([]) }) it('FIRES — a block boundary ends the clause: a previous list item’s `never` does not cover the next', () => { const items = `- never bypass the hook\n- run \`${GIT} commit -m "fix"\` in Bash\n` expect(rawGitOffenders(items).map((o) => o.line)).toEqual([2]) }) it('FIRES — inside a fenced block the command is an instruction, whatever the prose above says', () => { const fenced = `Never do this by hand\n\`\`\`bash\n${GIT} add -A && ${GIT} commit -m "x"\n\`\`\`\n` expect(rawGitOffenders(fenced).map((o) => o.line)).toEqual([3, 3]) }) it('FIRES — a blank line ends the clause: the paragraph above does not exempt the next', () => { const paragraphs = `The git-guard hook blocks the raw call.\n\nThen \`${GIT} commit -m ""\`\n` expect(rawGitOffenders(paragraphs).map((o) => o.line)).toEqual([3]) }) })