From 31ec51a8b4273945d562dc0034618523a44478d7 Mon Sep 17 00:00:00 2001 From: Edbert Chan Date: Fri, 11 Sep 2026 10:45:36 -0700 Subject: [PATCH 1/5] draft-pr: PR checker fails a Summary or Review Claim that uses code names Every pull request comes with a short written summary. Before review, a checker reads it and blocks text that is too hard to read. The problem: summaries full of code names still passed. The cause: the reading-grade scorer swapped every backticked name for "X", so each name scored as an easy word. The fix: summary-reading-grade.mjs gains findCodeNames(), which reads ## Summary and ## Review Claim and returns every backticked span, snake_case or camelCase word, file path, and word equal to a changed file or folder name. It has three outcomes: hard, clean, and unchecked (a section is missing). validate-pr-body.mjs passes --changed-files-file into it, fails with one error that lists each name, and prints a line when the check is unchecked. Later sections (Test Plan, Revert Plan, Architecture) are not read, so they may still hold names and output. SKILL.md's Summary guidance now says the reader is a busy director who has never seen the code, gives the what / problem / cause / fix shape, and adds a before-and-after worked example. Two old test fixtures used backticks in their Summary; they are updated to match the new rule. Co-Authored-By: Claude Opus 5 (1M context) --- engine/skills/draft-pr/SKILL.md | 36 +++++-- .../scripts/summary-reading-grade.mjs | 100 +++++++++++++++++- .../draft-pr/scripts/validate-pr-body.mjs | 7 +- .../draft-pr/tests/test_draft_pr_scripts.py | 98 ++++++++++++++++- 4 files changed, 225 insertions(+), 16 deletions(-) diff --git a/engine/skills/draft-pr/SKILL.md b/engine/skills/draft-pr/SKILL.md index 2ddc103e..3ccfeb3c 100644 --- a/engine/skills/draft-pr/SKILL.md +++ b/engine/skills/draft-pr/SKILL.md @@ -83,19 +83,37 @@ Default to this structure (validated by `validatePrBody()` in ```md ## Summary -Plain-English explanation of what changed and why. Paragraphs, not bullets, -under 30 words each (configurable via `drafter.config.json`'s -`prBody.summaryWordLimit`). One idea per paragraph. +The reader is a busy director who has never seen the code. Teach them from +zero. -Write it the way the `diu` skill says: for someone who never saw the code. -First sentence says what a person sees change. Short sentences, everyday -words. Explain or cut every term coined while working ("provenance gate", -"bare reference"). `scripts/validate-pr-body.mjs` blocks a Summary above -reading grade 11 or with more than 25% words of three or more syllables. +The first paragraph says what the part is and what it does for a person. +Then one short paragraph each for the problem, the cause, and the fix. +Paragraphs, not bullets, under 30 words each (configurable via +`drafter.config.json`'s `prBody.summaryWordLimit`). Short sentences, +everyday words; explain or cut every term coined while working. + +No code names in Summary or Review Claim: no backticked text, no snake_case +or camelCase words, no file paths, and no word that is the name of a changed +file or folder. Say what the part does instead. Names and output belong in +later sections: Test Plan, Revert Plan, Architecture. + +Before: "When diu-stop or prove-it-ship-gate block a reply and the agent +rewrites it, the rewrite is still checked for evidence. Before, both hooks +returned on `stop_hook_active` before running any check." + +After: "Before Claude sends a reply, small checker scripts read it. One +checks length. Others check that every claim comes with proof. If a check +fails, Claude must rewrite. The problem: when Claude rewrote, both checkers +stepped aside completely." + +`scripts/validate-pr-body.mjs` fails a Summary or Review Claim that holds a +code name and lists each one. It also blocks a Summary above reading grade 11 +or with more than 25% words of three or more syllables. ## Review Claim -State the one thing the reviewer is being asked to approve. +State the one thing the reviewer is being asked to approve, in everyday +words with no code names (same rule as Summary). ## Review Lane diff --git a/engine/skills/draft-pr/scripts/summary-reading-grade.mjs b/engine/skills/draft-pr/scripts/summary-reading-grade.mjs index 909144aa..9257fa49 100644 --- a/engine/skills/draft-pr/scripts/summary-reading-grade.mjs +++ b/engine/skills/draft-pr/scripts/summary-reading-grade.mjs @@ -10,10 +10,15 @@ function syllables(word) { return Math.max(1, (w.match(/[aeiouy]{1,2}/g) || []).length); } +function sectionText(body, heading) { + const match = new RegExp(`^## ${heading}[ \\t]*\\n([\\s\\S]*?)(?=^## |(?![\\s\\S]))`, 'm').exec(body || ''); + return match ? match[1] : null; +} + export function summaryText(body) { - const match = /^## Summary[ \t]*\n([\s\S]*?)(?=^## |(?![\s\S]))/m.exec(body || ''); - if (!match) return null; - return match[1] + const section = sectionText(body, 'Summary'); + if (section === null) return null; + return section .replace(/```[\s\S]*?```/g, ' ') .replace(/`[^`\n]*`/g, 'X') .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') @@ -51,3 +56,92 @@ export function readingGradeError(score) { 'and explain or cut every term coined while working (see the diu skill).' ); } + +export const CODE_NAME_SECTIONS = ['Summary', 'Review Claim']; + +const FILE_EXTENSIONS = new Set([ + 'py', 'mjs', 'cjs', 'js', 'jsx', 'ts', 'tsx', 'md', 'mdc', 'json', 'yml', 'yaml', 'toml', 'ini', 'cfg', + 'sh', 'bash', 'zsh', 'txt', 'tsv', 'csv', 'html', 'css', 'go', 'rs', 'rb', 'java', 'kt', 'swift', + 'c', 'h', 'cpp', 'sql', 'lock', 'xml', 'plist', +]); + +const TOKEN = + /```([\s\S]*?)```|`([^`\n]+)`||\]\([^)\s]*\)|https?:\/\/[^\s)>]+|<\/?[A-Za-z][^>]*>|([\w./~-]+)/g; + +function changedFileNames(changedFiles) { + const names = new Map(); + const add = (name, kind) => { + if (name && !names.has(name)) names.set(name, kind); + }; + for (const file of changedFiles || []) { + const parts = file.split('/').filter((part) => part && part !== '.' && part !== '..'); + const base = parts.pop(); + for (const folder of parts) add(folder, 'changed folder name'); + if (base) { + add(base, 'changed file name'); + add(base.replace(/\.[^.]+$/, ''), 'changed file name'); + } + } + return names; +} + +function wordKind(word, changedNames) { + if (changedNames.has(word)) return changedNames.get(word); + if (word.includes('/')) return 'file path'; + const extension = /\w\.([A-Za-z0-9]+)$/.exec(word); + if (extension && FILE_EXTENSIONS.has(extension[1])) return 'file path'; + if (/[A-Za-z0-9]_+[A-Za-z0-9]/.test(word)) return 'snake_case'; + if (/^[a-z][a-z0-9]*[A-Z]/.test(word)) return 'camelCase'; + return null; +} + +function sectionCodeNames(section, changedNames) { + const found = []; + const add = (name, kind) => { + if (name && !found.some((f) => f.name === name)) found.push({ name, kind }); + }; + for (const match of section.matchAll(TOKEN)) { + const [, fenced, inline, word] = match; + if (fenced !== undefined) { + const firstLine = fenced.replace(/^[\w-]*\n/, '').split('\n').map((l) => l.trim()).find(Boolean); + add((firstLine || 'code block').slice(0, 60), 'code block'); + } else if (inline !== undefined) { + add(inline.trim(), 'in backticks'); + } else if (word !== undefined) { + const cleaned = word.replace(/^(?:[-_]+|\.{2,})/, '').replace(/[._-]+$/, ''); + if (!/[A-Za-z]/.test(cleaned)) continue; + const kind = wordKind(cleaned, changedNames); + if (kind) add(cleaned, kind); + } + } + return found; +} + +export function findCodeNames(body, changedFiles = []) { + const changedNames = changedFileNames(changedFiles); + const found = []; + const missing = []; + for (const heading of CODE_NAME_SECTIONS) { + const section = sectionText(body, heading); + if (section === null) { + missing.push(`## ${heading}`); + continue; + } + const names = sectionCodeNames(section, changedNames); + if (names.length > 0) found.push({ section: heading, names }); + } + const reason = missing.length > 0 ? `no ${missing.join(' or ')} section to read` : undefined; + if (found.length > 0) return { status: 'hard', found, reason }; + if (reason) return { status: 'unchecked', found, reason }; + return { status: 'clean', found }; +} + +export function codeNameError(result) { + const where = result.found + .map(({ section, names }) => `${section}: ${names.map(({ name, kind }) => `"${name}" (${kind})`).join(', ')}`) + .join('; '); + return ( + `Summary and Review Claim must not use code names. Found in ${where}. ` + + 'Say what the part does in everyday words; put the name in a later section such as Test Plan.' + ); +} diff --git a/engine/skills/draft-pr/scripts/validate-pr-body.mjs b/engine/skills/draft-pr/scripts/validate-pr-body.mjs index db4999d3..c8f1eb6f 100644 --- a/engine/skills/draft-pr/scripts/validate-pr-body.mjs +++ b/engine/skills/draft-pr/scripts/validate-pr-body.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import { readFileSync } from 'node:fs'; import { loadDrafterConfig, validatePrBody, getPrBodyWarnings } from '@neko-catpital-labs/drafter-core'; -import { scoreSummary, readingGradeError } from './summary-reading-grade.mjs'; +import { scoreSummary, readingGradeError, findCodeNames, codeNameError } from './summary-reading-grade.mjs'; function usage() { console.error(`Usage: node scripts/validate-pr-body.mjs (--body-file | --body ) [--require-visual-proof] [--changed-files-file ] [--diff-file ] [--config ]`); @@ -48,6 +48,11 @@ async function main() { if (reading.status === 'hard') errors.push(readingGradeError(reading)); if (reading.status === 'unchecked') console.error(`Summary reading grade unchecked: ${reading.reason}.`); + const codeNames = findCodeNames(body, changedFiles); + if (codeNames.status === 'hard') errors.push(codeNameError(codeNames)); + if (codeNames.reason) console.error(`Code-name check unchecked: ${codeNames.reason}.`); + if (!changedFiles) console.error('Code-name check did not compare against changed file and folder names: no --changed-files-file given.'); + if (errors.length > 0) { console.error('PR body validation failed:'); for (const error of errors) console.error(`- ${error}`); diff --git a/engine/skills/draft-pr/tests/test_draft_pr_scripts.py b/engine/skills/draft-pr/tests/test_draft_pr_scripts.py index db6f8495..ba1a65ca 100644 --- a/engine/skills/draft-pr/tests/test_draft_pr_scripts.py +++ b/engine/skills/draft-pr/tests/test_draft_pr_scripts.py @@ -84,13 +84,13 @@ Regression tests cover rejected references, preserved external citations, and resolvable playbook names. A skill-usage-log test now checks its temporary state directory.""" -PLAIN_SUMMARY = """Rule files in this repo can no longer point at old PRs with a bare number like `#322`. A test now fails if they do. +PLAIN_SUMMARY = """Rule files in this repo can no longer point at old PRs with a bare number like "#322". A test now fails if they do. -"See `#322`" only tells a reader where to dig. Each rule should say what to do and why, in its own words. +"See #322" only tells a reader where to dig. Each rule should say what to do and why, in its own words. This PR also rewrites the old PR numbers in the detector playbook, so each rule there explains itself. -Numbers that name outside work, like "Cook `#3`", still pass.""" +Numbers that name outside work, like "Cook #3", still pass.""" def _with_summary(summary: str) -> str: @@ -99,6 +99,25 @@ def _with_summary(summary: str) -> str: ENGINE_BODY = VALID_BODY.replace("## Review Unit\n\nproduct", "## Review Unit\n\nengine-runtime") +BEFORE_SUMMARY = ( + "When diu-stop or prove-it-ship-gate block a reply and the agent rewrites it, " + "the rewrite is still checked for evidence. Before, both hooks returned on " + "`stop_hook_active` before running any check." +) + +AFTER_SUMMARY = ( + "Before Claude sends a reply, small checker scripts read it. One checks length. " + "Others check that every claim comes with proof. If a check fails, Claude must " + "rewrite. The problem: when Claude rewrote, both checkers stepped aside completely." +) + +HOOK_FILES = [ + "engine/hooks/diu-stop/claude_stop_check.py", + "engine/hooks/prove-it-ship-gate/detect.py", +] + +CODE_NAME_ERROR = "Summary and Review Claim must not use code names" + def _run_validator(body_text: str, changed_files: list[str] | None = None) -> subprocess.CompletedProcess: with tempfile.TemporaryDirectory() as tmp: @@ -158,10 +177,83 @@ def test_short_summary_is_reported_unchecked_not_passed(self): self.assertIn("reading grade unchecked", result.stderr) def test_code_spans_do_not_count_as_long_words(self): + """Code spans fail the code-name check, not the reading grade.""" spans = " ".join(f"`engine/skills/draft-pr/scripts/validate_{i}.mjs`" for i in range(12)) summary = PLAIN_SUMMARY + f"\n\nThe files are {spans}." result = _run_validator(_with_summary(summary)) + self.assertNotIn("too hard to read", result.stderr) + self.assertIn(CODE_NAME_ERROR, result.stderr) + + + +class TestSummaryCodeNames(unittest.TestCase): + def _engine_body(self, summary: str) -> str: + return ENGINE_BODY.replace(VALID_SUMMARY, summary) + + def test_code_names_in_summary_fail_and_are_each_named(self): + result = _run_validator(self._engine_body(BEFORE_SUMMARY), HOOK_FILES) + self.assertEqual(result.returncode, 1, result.stdout) + self.assertIn(CODE_NAME_ERROR, result.stderr) + for name in ("stop_hook_active", "diu-stop", "prove-it-ship-gate"): + self.assertIn(f'"{name}"', result.stderr) + self.assertIn("put the name in a later section such as Test Plan", result.stderr) + + def test_plain_summary_passes_with_the_same_changed_files(self): + result = _run_validator(self._engine_body(AFTER_SUMMARY), HOOK_FILES) + self.assertEqual(result.returncode, 0, result.stderr + result.stdout) + self.assertNotIn(CODE_NAME_ERROR, result.stderr) + + def test_hyphenated_english_words_are_not_code_names(self): + summary = ( + "This adds a brand-new word-count limit to the form. A person who types " + "too much now sees a short note that says how many words to cut." + ) + result = _run_validator(_with_summary(summary)) + self.assertEqual(result.returncode, 0, result.stderr + result.stdout) + self.assertNotIn(CODE_NAME_ERROR, result.stderr) + + def test_missing_summary_is_reported_unchecked_not_clean(self): + body = VALID_BODY.replace(f"## Summary\n\n{VALID_SUMMARY}\n\n", "") + self.assertNotIn("## Summary", body) + result = _run_validator(body) + self.assertIn("Code-name check unchecked: no ## Summary section to read", result.stderr) + + def test_code_names_in_later_sections_do_not_fail(self): + body = self._engine_body(AFTER_SUMMARY).replace( + "- [x] `pytest tests/test_widget_renderer.py`", + "- [x] `python3 engine/hooks/diu-stop/claude_stop_check.py` exits 0 once " + "`stop_hook_active` is set; prove-it-ship-gate/detect.py too", + ).replace( + "- Post-revert steps: None", + "- Post-revert steps: diu-stop and prove-it-ship-gate return on `stop_hook_active` again", + ).replace( + "## Test Plan", + """## Architecture + +diu-stop and prove-it-ship-gate both read `stop_hook_active`. + +### Before + +```mermaid +graph TD + A["claude_stop_check.py"] --> B["return on stop_hook_active"] +``` + +### After + +diu-stop/claude_stop_check.py keeps checking; detect.py too. + +```mermaid +graph TD + A["claude_stop_check.py"] --> B["run every check"] +``` + +## Test Plan""", + ) + result = _run_validator(body, HOOK_FILES) self.assertEqual(result.returncode, 0, result.stderr + result.stdout) + self.assertNotIn(CODE_NAME_ERROR, result.stderr) + self.assertNotIn("Code-name check unchecked", result.stderr) if __name__ == "__main__": From 740eb92bda06c2521e3aede2efa3802a9fb2702d Mon Sep 17 00:00:00 2001 From: Edbert Chan Date: Fri, 11 Sep 2026 10:46:11 -0700 Subject: [PATCH 2/5] =?UTF-8?q?invoker:=20wf-1789148090897-12/implement-pr?= =?UTF-8?q?-plain-english-rule=20=E2=80=94=20Make=20the=20PR=20checker=20f?= =?UTF-8?q?ail=20on=20code=20names=20in=20Summary=20and=20Review=20Claim,?= =?UTF-8?q?=20and=20rewrite=20the=20draft-pr=20writing=20rule=20for=20a=20?= =?UTF-8?q?reader=20who=20has=20never=20seen=20the=20code.=20Review=20clai?= =?UTF-8?q?m:=20A=20PR=20body=20whose=20Summary=20or=20Review=20Claim=20co?= =?UTF-8?q?ntains=20a=20code=20name=20now=20fails=20the=20PR=20body=20chec?= =?UTF-8?q?ker,=20and=20the=20error=20names=20each=20one;=20the=20draft-pr?= =?UTF-8?q?=20skill=20tells=20writers=20to=20teach=20the=20reader=20from?= =?UTF-8?q?=20zero.=20Review=20lane:=20behavior=20Safety=20invariant:=20A?= =?UTF-8?q?=20PR=20body=20that=20passes=20today=20still=20passes=20unless?= =?UTF-8?q?=20its=20Summary=20or=20Review=20Claim=20contains=20a=20code=20?= =?UTF-8?q?name;=20Test=20Plan,=20Revert=20Plan,=20Before=20and=20After,?= =?UTF-8?q?=20and=20Architecture=20sections=20may=20still=20hold=20code=20?= =?UTF-8?q?names=20and=20output.=20Effectiveness=20measurement:=20PR=20416?= =?UTF-8?q?'s=20current=20body=20(Summary=20contains=20stop=5Fhook=5Factiv?= =?UTF-8?q?e,=20diu-stop,=20prove-it-ship-gate)=20fails=20the=20checker,?= =?UTF-8?q?=20and=20its=20plain=20rewrite=20(no=20code=20names=20in=20Summ?= =?UTF-8?q?ary=20or=20Review=20Claim)=20passes;=20both=20are=20pinned=20as?= =?UTF-8?q?=20test=20fixtures.=20Slice=20rationale:=20The=20checker,=20its?= =?UTF-8?q?=20test,=20and=20the=20skill=20text=20that=20explains=20the=20c?= =?UTF-8?q?hecker=20are=20one=20claim;=20shipping=20them=20apart=20would?= =?UTF-8?q?=20leave=20a=20skill=20rule=20nothing=20enforces,=20or=20a=20ch?= =?UTF-8?q?eck=20the=20skill=20never=20explains.=20Architectural=20effect:?= =?UTF-8?q?=20summary-reading-grade.mjs=20gains=20a=20code-name=20check=20?= =?UTF-8?q?over=20Summary=20and=20Review=20Claim;=20the=20PR=20body=20chec?= =?UTF-8?q?ker=20script=20reports=20its=20findings=20as=20errors;=20the=20?= =?UTF-8?q?drafter-core=20npm=20package=20is=20untouched.=20Goal:=20A=20bu?= =?UTF-8?q?sy=20reader=20can=20read=20the=20Summary=20and=20Review=20Claim?= =?UTF-8?q?=20of=20any=20PR=20and=20understand=20what=20changed=20without?= =?UTF-8?q?=20knowing=20the=20code.=20Motivation:=20The=20user=20asked=20t?= =?UTF-8?q?hat=20all=20PRs=20be=20written=20in=20plain=20English=20for=20a?= =?UTF-8?q?=20director=20in=20a=20rush=20who=20does=20not=20know=20the=20a?= =?UTF-8?q?rchitecture.=20The=20current=20checker=20passed=20PR=20416's=20?= =?UTF-8?q?jargon-heavy=20Summary=20because=20engine/skills/draft-pr/scrip?= =?UTF-8?q?ts/summary-reading-grade.mjs=20line=2018=20replaces=20every=20b?= =?UTF-8?q?ackticked=20name=20with=20"X".=20Alternative=20considerations:?= =?UTF-8?q?=20Lowering=20the=20reading-grade=20limit=20was=20ruled=20out?= =?UTF-8?q?=20because=20code=20names=20already=20score=20as=20easy=20words?= =?UTF-8?q?,=20so=20no=20grade=20limit=20catches=20them.=20Renaming=20the?= =?UTF-8?q?=20schema=20headings=20(Review=20Claim,=20Safety=20Invariant)?= =?UTF-8?q?=20was=20ruled=20out=20for=20this=20slice=20because=20the=20hea?= =?UTF-8?q?ding=20set=20lives=20in=20the=20drafter-core=20npm=20package,?= =?UTF-8?q?=20a=20separate=20project.=20A=20word-list=20jargon=20detector?= =?UTF-8?q?=20was=20ruled=20out=20as=20noisier=20than=20a=20code-name=20ch?= =?UTF-8?q?eck.=20Implementation=20details:=20In=20summary-reading-grade.m?= =?UTF-8?q?js=20add=20an=20exported=20function=20that=20reads=20the=20Summ?= =?UTF-8?q?ary=20and=20Review=20Claim=20sections=20and=20returns=20every?= =?UTF-8?q?=20code=20name=20in=20them.=20A=20code=20name=20is=20any=20back?= =?UTF-8?q?ticked=20span,=20any=20snake=5Fcase=20or=20camelCase=20word,=20?= =?UTF-8?q?any=20file=20path=20(contains=20a=20slash=20or=20ends=20in=20a?= =?UTF-8?q?=20file=20extension),=20and=20any=20word=20equal=20to=20a=20fol?= =?UTF-8?q?der=20name=20or=20file=20base=20name=20from=20the=20changed=20f?= =?UTF-8?q?iles=20the=20checker=20receives=20through=20--changed-files-fil?= =?UTF-8?q?e.=20Keep=20three=20outcomes=20(hard,=20clean,=20unchecked);=20?= =?UTF-8?q?a=20missing=20section=20is=20unchecked,=20never=20clean.=20The?= =?UTF-8?q?=20PR=20body=20checker=20script=20pushes=20one=20error=20naming?= =?UTF-8?q?=20every=20code=20name=20found=20and=20the=20fix.=20In=20the=20?= =?UTF-8?q?draft-pr=20SKILL.md,=20the=20Summary=20guidance=20(current=20li?= =?UTF-8?q?nes=2086-94)=20becomes:=20the=20reader=20is=20a=20busy=20direct?= =?UTF-8?q?or=20who=20has=20never=20seen=20the=20code;=20the=20first=20par?= =?UTF-8?q?agraph=20says=20what=20the=20part=20is=20and=20what=20it=20does?= =?UTF-8?q?=20for=20a=20person;=20then=20the=20problem,=20the=20cause,=20a?= =?UTF-8?q?nd=20the=20fix,=20one=20short=20paragraph=20each;=20no=20code?= =?UTF-8?q?=20names=20in=20Summary=20or=20Review=20Claim;=20names=20and=20?= =?UTF-8?q?output=20go=20in=20later=20sections.=20PR=20416's=20before=20an?= =?UTF-8?q?d=20after=20Summary=20is=20the=20worked=20example.=20New=20case?= =?UTF-8?q?s=20go=20in=20the=20draft-pr=20test=20file.=20Non-goals:=20No?= =?UTF-8?q?=20change=20to=20the=20drafter-core=20npm=20package,=20to=20sec?= =?UTF-8?q?tion=20headings,=20to=20the=20reading-grade=20limits,=20to=20di?= =?UTF-8?q?ff-atomicity=20linting,=20or=20to=20the=20Invoker=20repo's=20ow?= =?UTF-8?q?n=20make-pr=20skill.=20Layer:=20domain=20Feature=20state:=20act?= =?UTF-8?q?ive=20Files:=20engine/skills/draft-pr/scripts/summary-reading-g?= =?UTF-8?q?rade.mjs,=20engine/skills/draft-pr/scripts/validate-pr-body.mjs?= =?UTF-8?q?,=20engine/skills/draft-pr/SKILL.md,=20engine/skills/draft-pr/t?= =?UTF-8?q?ests/test=5Fdraft=5Fpr=5Fscripts.py=20Change=20types:=20-=20eng?= =?UTF-8?q?ine/skills/draft-pr/scripts/summary-reading-grade.mjs:=20modify?= =?UTF-8?q?=20-=20engine/skills/draft-pr/scripts/validate-pr-body.mjs:=20m?= =?UTF-8?q?odify=20-=20engine/skills/draft-pr/SKILL.md:=20modify=20-=20eng?= =?UTF-8?q?ine/skills/draft-pr/tests/test=5Fdraft=5Fpr=5Fscripts.py:=20mod?= =?UTF-8?q?ify=20Acceptance=20criteria:=20-=20A=20Summary=20containing=20`?= =?UTF-8?q?stop=5Fhook=5Factive`,=20diu-stop,=20and=20prove-it-ship-gate?= =?UTF-8?q?=20(with=20those=20two=20directory=20names=20passed=20via=20--c?= =?UTF-8?q?hanged-files-file)=20makes=20validate-pr-body.mjs=20exit=201=20?= =?UTF-8?q?and=20print=20all=20three=20names.=20-=20A=20Summary=20written?= =?UTF-8?q?=20in=20everyday=20words,=20including=20ordinary=20hyphenated?= =?UTF-8?q?=20words=20like=20brand-new=20and=20word-count,=20exits=200.=20?= =?UTF-8?q?-=20A=20body=20with=20no=20Summary=20prints=20that=20the=20code?= =?UTF-8?q?-name=20check=20is=20unchecked=20and=20does=20not=20print=20a?= =?UTF-8?q?=20clean=20result.=20-=20Code=20names=20inside=20Test=20Plan,?= =?UTF-8?q?=20Revert=20Plan,=20Before=20and=20After,=20and=20Architecture?= =?UTF-8?q?=20do=20not=20cause=20a=20failure.=20-=20python3=20engine/skill?= =?UTF-8?q?s/draft-pr/tests/test=5Fdraft=5Fpr=5Fscripts.py=20exits=200.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exit code: 0 From be93ad1f576ab76e853c81bceca255c85dbde7e5 Mon Sep 17 00:00:00 2001 From: Edbert Chan Date: Fri, 11 Sep 2026 10:46:18 -0700 Subject: [PATCH 3/5] =?UTF-8?q?invoker:=20wf-1789148090897-12/verify-make-?= =?UTF-8?q?pr-preflight=20=E2=80=94=20Run=20the=20catstack=20PR=20prefligh?= =?UTF-8?q?t=20on=20the=20finished=20change.=20Review=20claim:=20The=20mak?= =?UTF-8?q?e-pr=20preflight=20passes=20on=20the=20change.=20Review=20lane:?= =?UTF-8?q?=20proof=20Safety=20invariant:=20Proof-only;=20adds=20no=20prod?= =?UTF-8?q?uct=20behavior.=20Effectiveness=20measurement:=20Preflight=20pr?= =?UTF-8?q?ints=20"ok=20preflight=20passed"=20and=20exits=200.=20Slice=20r?= =?UTF-8?q?ationale:=20One=20proof=20step=20for=20the=20repo's=20own=20pub?= =?UTF-8?q?lication=20gate.=20Architectural=20effect:=20None;=20verificati?= =?UTF-8?q?on=20only.=20Goal:=20Prove=20the=20change=20meets=20catstack's?= =?UTF-8?q?=20own=20PR=20gates.=20Motivation:=20The=20catstack=20make-pr?= =?UTF-8?q?=20overlay=20requires=20preflight=20to=20pass=20before=20publis?= =?UTF-8?q?hing.=20Alternative=20considerations:=20Running=20each=20gate?= =?UTF-8?q?=20by=20hand=20was=20ruled=20out;=20preflight=20runs=20the=20ga?= =?UTF-8?q?tes=20for=20the=20touched=20paths.=20Implementation=20details:?= =?UTF-8?q?=20Run=20make-pr=20preflight=20against=20origin/main.=20Non-goa?= =?UTF-8?q?ls:=20No=20product=20edits=20here;=20proof=20only.=20Layer:=20a?= =?UTF-8?q?pp=5Fregression=20Feature=20state:=20active?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exit code: 0 From 5b4a5ac22002c1109c82636b1361e912ea3180db Mon Sep 17 00:00:00 2001 From: Edbert Chan Date: Fri, 11 Sep 2026 10:46:24 -0700 Subject: [PATCH 4/5] =?UTF-8?q?invoker:=20wf-1789148090897-12/verify-draft?= =?UTF-8?q?-pr-tests=20=E2=80=94=20Run=20the=20draft-pr=20test=20file=20on?= =?UTF-8?q?=20the=20finished=20change.=20Review=20claim:=20The=20draft-pr?= =?UTF-8?q?=20test=20file=20passes=20on=20the=20change.=20Review=20lane:?= =?UTF-8?q?=20proof=20Safety=20invariant:=20Proof-only;=20adds=20no=20prod?= =?UTF-8?q?uct=20behavior.=20Effectiveness=20measurement:=20The=20test=20f?= =?UTF-8?q?ile=20includes=20the=20PR=20416=20before=20and=20after=20fixtur?= =?UTF-8?q?es=20and=20exits=200.=20Slice=20rationale:=20One=20proof=20step?= =?UTF-8?q?=20for=20the=20one=20review=20claim.=20Architectural=20effect:?= =?UTF-8?q?=20None;=20verification=20only.=20Goal:=20Prove=20the=20checker?= =?UTF-8?q?=20change=20passes=20its=20own=20tests.=20Motivation:=20A=20cha?= =?UTF-8?q?nged=20skill=20must=20pass=20its=20own=20tests=20before=20it=20?= =?UTF-8?q?ships.=20Alternative=20considerations:=20Running=20the=20full?= =?UTF-8?q?=2045-suite=20run=5Fall=5Ftests.sh=20was=20ruled=20out=20as=20s?= =?UTF-8?q?lower=20than=20needed=20for=20a=20change=20confined=20to=20draf?= =?UTF-8?q?t-pr.=20Implementation=20details:=20Install=20npm=20dev=20depen?= =?UTF-8?q?dencies,=20then=20run=20the=20draft-pr=20test=20file.=20Non-goa?= =?UTF-8?q?ls:=20No=20product=20edits=20here;=20proof=20only.=20Layer:=20a?= =?UTF-8?q?pp=5Fregression=20Feature=20state:=20active?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exit code: 0 From c64743adc957aea72e4e16439595cea2b76a774e Mon Sep 17 00:00:00 2001 From: Edbert Chan Date: Fri, 11 Sep 2026 10:46:29 -0700 Subject: [PATCH 5/5] =?UTF-8?q?invoker:=20wf-1789148090897-12/scrub-handof?= =?UTF-8?q?f-artifacts=20=E2=80=94=20Read-only=20check=20that=20no=20tempo?= =?UTF-8?q?rary=20handoff=20files=20are=20left=20on=20the=20branch=20befor?= =?UTF-8?q?e=20the=20PR=20merge=20gate.=20Review=20claim:=20The=20branch?= =?UTF-8?q?=20carries=20no=20leftover=20handoff=20files.=20Review=20lane:?= =?UTF-8?q?=20proof=20Safety=20invariant:=20Read-only;=20never=20deletes?= =?UTF-8?q?=20files=20or=20changes=20the=20index.=20Effectiveness=20measur?= =?UTF-8?q?ement:=20scripts/scrub-handoff-artifacts.sh=20exits=200=20on=20?= =?UTF-8?q?the=20finished=20branch.=20Slice=20rationale:=20Required=20term?= =?UTF-8?q?inal=20gate=20for=20implementation=20plans.=20Architectural=20e?= =?UTF-8?q?ffect:=20None;=20verification=20only.=20Goal:=20Keep=20temporar?= =?UTF-8?q?y=20files=20out=20of=20the=20PR.=20Motivation:=20Invoker=20task?= =?UTF-8?q?s=20can=20leave=20handoff=20files=20that=20must=20not=20ship.?= =?UTF-8?q?=20Alternative=20considerations:=20None;=20this=20is=20the=20st?= =?UTF-8?q?andard=20terminal=20gate.=20Implementation=20details:=20Run=20s?= =?UTF-8?q?cripts/scrub-handoff-artifacts.sh=20without=20--apply.=20Non-go?= =?UTF-8?q?als:=20No=20edits.=20Layer:=20app=5Fregression=20Feature=20stat?= =?UTF-8?q?e:=20active?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exit code: 0