diff --git a/template/.claude/skills/markdown/SKILL.md b/template/.claude/skills/markdown/SKILL.md
new file mode 100644
index 0000000..1dbb651
--- /dev/null
+++ b/template/.claude/skills/markdown/SKILL.md
@@ -0,0 +1,179 @@
+---
+name: markdown
+description: >-
+ Expert guidance for writing, editing, managing, and handling Markdown (.md) files
+ to a professional standard. Use this skill whenever the user wants to: create or
+ write a new Markdown file, edit or improve an existing .md file, review Markdown
+ for style/consistency, convert content into well-structured Markdown, build README
+ files, technical docs, changelogs, wikis, or any documentation in .md format.
+ Also trigger for requests involving front matter/metadata, table formatting, code
+ blocks, link hygiene, accessibility in Markdown, GitHub-flavored Markdown (GFM),
+ or best practices for Markdown documentation systems. If the user mentions .md,
+ README, CHANGELOG, docs, or wiki, use this skill.
+---
+
+# Markdown Skill
+
+A comprehensive skill for producing, editing, and managing Markdown files that are
+readable, portable, accessible, and maintainable over time.
+
+## Quick reference: where to go deeper
+
+| Topic | Reference file |
+|------------------------------------|-------------------------------------------------------------------------------|
+| Document structure and front matter | [references/document-structure.md](references/document-structure.md) |
+| Formatting syntax (emphasis, lists) | [references/formatting-syntax.md](references/formatting-syntax.md) |
+| Code blocks and links | [references/code-and-links.md](references/code-and-links.md) |
+| Tables, images, and HTML | [references/tables-images-html.md](references/tables-images-html.md) |
+| Extended syntax (GFM, callouts) | [references/extended-syntax.md](references/extended-syntax.md) |
+| File management and doc systems | [references/file-management.md](references/file-management.md) |
+| Anti-patterns and cheat sheet | [references/anti-patterns-cheatsheet.md](references/anti-patterns-cheatsheet.md) |
+
+Read the relevant reference file before working on a specific area. For a new README,
+skim `document-structure.md` first. For table formatting or GFM features, check the
+corresponding reference.
+
+---
+
+## Primary references
+
+This skill is grounded in the **Google Markdown Style Guide** and extended with best
+practices from CommonMark, Markdown Guide, and the GitHub Flavored Markdown Spec.
+
+---
+
+## Core philosophy
+
+Three goals govern every Markdown file:
+
+1. **Source text is readable and portable** — raw `.md` source should be legible as
+ plain text without rendering.
+2. **The corpus is maintainable over time** — consistent conventions let any author
+ pick up where another left off.
+3. **Syntax is simple and memorable** — favour standard Markdown over HTML hacks or
+ exotic extensions.
+
+---
+
+## Essential rules
+
+### Document structure
+
+Every well-formed Markdown document follows this skeleton:
+
+```markdown
+# Document Title
+
+Short introduction (1-3 sentences). Explain what this document is and who it is for.
+
+## First Topic
+
+Content.
+
+## Second Topic
+
+Content.
+
+## See Also
+
+- [Link to related resource](https://example.com)
+```
+
+- Exactly one `# H1` per document — the page title.
+- 1-3 sentence introduction before any sections.
+- Start subsequent headings at `## H2`; never skip levels.
+- Sentence-case headings (capitalise first word and proper nouns only).
+
+### Headings
+
+Use ATX-style (`#`, `##`, `###`). Never use Setext (underline) style. Add blank lines
+before and after every heading. Give headings unique, descriptive names to avoid anchor
+collisions.
+
+### Line length
+
+80-character limit for body text. Exceptions: links, table cells, headings, and code
+blocks. Use a blank line to separate paragraphs — do not rely on trailing whitespace.
+
+### Emphasis
+
+- `**bold**` for critical terms, UI labels, and warnings.
+- `*italic*` for introducing new terms and light emphasis.
+- Never bold or italicise entire paragraphs — overuse kills emphasis.
+- Use `*asterisks*` not `_underscores_` for mid-word emphasis.
+
+### Lists
+
+- Use `-` consistently for unordered lists.
+- Use explicit numbers (`1.`, `2.`, `3.`) for short stable lists.
+- Use lazy numbering (`1.`, `1.`, `1.`) for long or frequently changing lists.
+- 4-space indent for wrapped text and nested lists.
+
+### Code
+
+Always use fenced blocks with a language tag. Never use 4-space indented blocks:
+
+````markdown
+```python
+def greet(name: str) -> str:
+ return f"Hello, {name}!"
+```
+````
+
+Use backticks for inline code: commands, function names, file names, paths.
+
+### Links
+
+Write the sentence naturally, then wrap the most descriptive phrase as the link text.
+Never use "click here" or "here" as link text. Use relative paths for internal links:
+
+```markdown
+See the [contributing guide](docs/CONTRIBUTING.md) for details.
+```
+
+### Tables
+
+Use tables only when data is genuinely two-dimensional. Keep cells short. Use
+reference links for URLs in table cells. If a table has only 1-2 columns, prefer a
+list instead.
+
+### Images
+
+Always write descriptive alt text. Store images in a dedicated `images/` or `assets/`
+directory. Prefer SVG for diagrams and PNG for screenshots.
+
+### HTML
+
+Strongly prefer plain Markdown over HTML. Acceptable uses: `
`, ``,
+``/`` — only as a last resort.
+
+### Accessibility
+
+- Descriptive alt text on every meaningful image.
+- Meaningful link text (not "here" or bare URLs).
+- Logical heading hierarchy for assistive navigation.
+- Semantic emphasis (`**important**`) rather than formatting for decoration.
+
+---
+
+## Common anti-patterns
+
+| Anti-pattern | Fix |
+|----------------------------------|----------------------------------------|
+| Multiple H1s | Keep exactly one — the page title |
+| Skipping heading levels | Follow strict hierarchy |
+| `[click here](url)` | Write descriptive link text |
+| 4-space indented code blocks | Use fenced blocks with language tag |
+| Mixing `*` and `-` for bullets | Pick one style per file |
+| HTML ``, `` tags | Use `**` and `*` |
+| Tables for simple lists | Use a list instead |
+| Empty alt text on images | Always write descriptive alt text |
+
+---
+
+## See Also
+
+- [Google Markdown Style Guide](https://google.github.io/styleguide/docguide/style.html)
+- [CommonMark Specification](https://commonmark.org/)
+- [Markdown Guide](https://www.markdownguide.org/)
+- [GitHub Flavored Markdown Spec](https://github.github.com/gfm/)
diff --git a/template/.claude/skills/markdown/references/anti-patterns-cheatsheet.md b/template/.claude/skills/markdown/references/anti-patterns-cheatsheet.md
new file mode 100644
index 0000000..31b7146
--- /dev/null
+++ b/template/.claude/skills/markdown/references/anti-patterns-cheatsheet.md
@@ -0,0 +1,323 @@
+# Anti-patterns and cheat sheet
+
+Covers: comprehensive list of common Markdown mistakes and how to fix them,
+plus a complete syntax cheat sheet for quick reference.
+
+---
+
+## Anti-Patterns — Common Mistakes and Fixes
+
+### Structure Anti-Patterns
+
+| Anti-pattern | Why it's wrong | Fix |
+|---|---|---|
+| Multiple `# H1` headings | Only one H1 per doc (page title rule) | Use H2+ for all sections after the title |
+| Skipping heading levels (H2 → H4) | Breaks logical hierarchy; accessibility issue | Follow strict H1 → H2 → H3 → H4 order |
+| No introduction before first section | Reader has no context | Add 1–3 sentence overview before first H2 |
+| `[TOC]` at top before introduction | Screen readers miss the introduction | Place `[TOC]` after introduction, before H2 |
+| Generic headings: `### Summary` under multiple H2s | Anchor link collisions | Use unique, descriptive names: `### Foo Summary` |
+| Giant monolithic document (1000+ lines) | Hard to navigate and maintain | Split into focused sub-documents with an index |
+| No `## See Also` at the end | Reader has nowhere to go for related info | Add outbound links in a final `## See Also` section |
+
+### Formatting Anti-Patterns
+
+| Anti-pattern | Why it's wrong | Fix |
+|---|---|---|
+| Bolding entire paragraphs | Emphasis loses meaning | Bold key phrases only |
+| Mixing `*` and `-` and `+` for bullets | Inconsistent; confusing in source | Pick one style (`-`) and use it throughout |
+| Using `_underscores_` for bold/italic | Inconsistent parser behaviour mid-word | Always use `*asterisks*` and `**asterisks**` |
+| Setext-style headings (`===`, `---`) | Ambiguous (is `---` an H2 or a rule?); fragile | Use ATX `##` style only |
+| Trailing whitespace on lines | Hidden; unreliable line-break behaviour | Use `\` for intentional breaks; none otherwise |
+| 4-space indented code blocks | Cannot declare a language; ambiguous start/end | Use fenced ` ``` ` blocks with language tags |
+| Code block with no language declared | No syntax highlighting; reader/editor must guess | Always declare: ` ```python `, ` ```bash `, etc. |
+| HTML tags for bold (``) and italic (``) | Reduces portability | Use `**` and `*` |
+| Excessive horizontal rules between every section | Fragments the doc visually | Use headings to separate sections; use `---` sparingly |
+
+### Link Anti-Patterns
+
+| Anti-pattern | Why it's wrong | Fix |
+|---|---|---|
+| `[click here](url)` | Uninformative to scanners and screen readers | Write descriptive text: `[Installation guide](url)` |
+| `[here](url)` or `[this link](url)` | Same as above | Wrap the most descriptive phrase in the sentence |
+| Bare URL as link text: `[https://example.com](https://example.com)` | Wastes space; no meaningful label | Use `[Example Site](https://example.com)` |
+| `../../../relative/path.md` | Breaks on file restructuring | Use root-relative paths: `/docs/path.md` |
+| Full GitHub URL for an internal doc link | Breaks when repo is renamed/moved | Use root-relative path |
+| Long URL inline in a table cell | Makes table unreadable | Use reference-style links |
+| Reference links defined at bottom of long file (first use in section 2, definition in last section) | Hard to find source | Define reference links just before the next heading after first use |
+
+### Image Anti-Patterns
+
+| Anti-pattern | Why it's wrong | Fix |
+|---|---|---|
+| Empty or missing alt text on meaningful images | Inaccessible to screen readers | Write descriptive alt text: `` |
+| ``, `` | Meaningless alt text | Describe what the image shows and why it matters |
+| Storing all images in root directory | Clutters root; hard to manage | Use `images/` or `assets/images/` subdirectory |
+| Non-descriptive image filenames: `img1.png`, `screen.png` | Hard to find; no context | Use descriptive names: `auth-flow-diagram.svg` |
+| JPEG for screenshots | Lossy compression blurs text | Use PNG for screenshots |
+| PNG for photographs | Unnecessarily large file | Use JPEG for photographic images |
+| Relying solely on an image to convey content | Breaks for screen readers | Always accompany diagrams with prose or captioned explanation |
+
+### Table Anti-Patterns
+
+The Google style guide names three specific table problems to diagnose before writing:
+
+| Named problem | Signs | Fix |
+|---|---|---|
+| **Poor distribution** | Columns don't differ across rows; cells are empty | The data doesn't need a table — use a list |
+| **Unbalanced dimensions** | Very few rows vs. many columns (or vice versa) | Use a list with subheadings |
+| **Rambling prose in cells** | Long sentences or paragraphs inside cells | Move prose out of the table |
+
+General table anti-patterns:
+
+| Anti-pattern | Why it's wrong | Fix |
+|---|---|---|
+| Table where a list would be clearer | Hard to maintain; verbose source | Use lists and subheadings |
+| Mostly empty table cells | Data not appropriate for tabular form | Use lists or prose |
+| Long URLs inline in table cells | Makes table source unreadable | Use reference links in table cells |
+| Tables with a single column | Just use a list | Use `- item` list syntax |
+| Prose paragraphs inside table cells | Markdown has no cell line-wrap | Move verbose content to a list or subsection |
+| Inconsistent column alignment for numeric data | Hard to compare numbers | Right-align numeric columns with `---:` |
+
+### HTML Anti-Patterns
+
+| Anti-pattern | Why it's wrong | Fix |
+|---|---|---|
+| `
` for paragraph spacing | Not a paragraph break | Use a blank line |
+| ``, `` instead of `##`, `###` | Breaks portability and TOC generation | Use ATX heading syntax |
+| ``, `` instead of `**`, `*` | Reduces portability | Use Markdown emphasis |
+| ``, ``, `- ` for lists | Verbose; not Markdown | Use `-` and `1.` list syntax |
+| CSS styling on individual elements | Renders in very few environments | Use structural Markdown only |
+| HTML tables instead of pipe tables | Not necessary unless table is extremely complex | Use Markdown pipe-table syntax |
+
+### File Management Anti-Patterns
+
+| Anti-pattern | Why it's wrong | Fix |
+|---|---|---|
+| Spaces in filenames: `Getting Started.md` | Spaces break URLs | Use `getting-started.md` |
+| Uppercase in non-standard files: `API_Reference.md` | URL path is case-sensitive; inconsistent | Use `api-reference.md` |
+| All docs in the root directory | Clutters root; hard to navigate | Put docs in `docs/` subdirectory |
+| No `README.md` in subdirectories | Reader has no entry point | Add an index `README.md` to each docs subdirectory |
+| `CHANGELOG` without `.md` extension | May not render as Markdown on GitHub | Use `CHANGELOG.md` |
+| Stale docs left indefinitely | Worse than no docs; misleads readers | Delete or archive stale content regularly |
+
+---
+
+## Complete Syntax Cheat Sheet
+
+### Document Skeleton
+
+```markdown
+---
+title: "Document Title"
+date: "2024-01-15"
+---
+
+# Document Title
+
+Short introduction (1–3 sentences).
+
+[TOC]
+
+## Section One
+
+Content.
+
+## Section Two
+
+Content.
+
+## See Also
+
+- [Link one](https://example.com)
+```
+
+### Headings
+
+```markdown
+# H1 — one per document
+## H2
+### H3
+#### H4
+##### H5
+###### H6
+```
+
+### Emphasis
+
+```markdown
+**bold** Strong importance
+*italic* Stress emphasis
+***bold italic*** Both
+~~strikethrough~~ Deprecated / error
+`inline code` Code, commands, field names
+```
+
+### Lists
+
+```markdown
+- Unordered item Use - consistently
+- Another item
+ - Nested (2-space indent)
+
+1. Ordered item
+2. Second item
+1. Lazy numbering (also valid)
+
+- [x] Completed task GFM task list
+- [ ] Pending task
+```
+
+### Code
+
+````markdown
+`inline code`
+
+```python
+# Fenced block — always declare language
+def foo():
+ pass
+```
+````
+
+### Links
+
+```markdown
+[Link text](https://url.com)
+[Link text](/root/relative/path.md)
+[Link text](same-dir-file.md)
+[Link text][ref-id]
+
+[ref-id]: https://long-url.com
+```
+
+### Images
+
+```markdown
+
+
+[](https://link.com) ← clickable image
+```
+
+### Tables
+
+```markdown
+| Left | Centre | Right |
+|:-----|:------:|------:|
+| Text | Text | 12.50 |
+```
+
+### Blockquotes and Callouts
+
+```markdown
+> Standard blockquote
+
+> [!NOTE] GFM alert
+> [!TIP]
+> [!IMPORTANT]
+> [!WARNING]
+> [!CAUTION]
+```
+
+### Horizontal Rule
+
+```markdown
+---
+```
+
+### Footnotes (extended)
+
+```markdown
+Body text.[^1]
+
+[^1]: Footnote content.
+```
+
+### Math (extended)
+
+```markdown
+Inline: $E = mc^2$
+
+Block:
+$$
+\frac{d}{dx} f(x) = f'(x)
+$$
+```
+
+### Mermaid (extended)
+
+````markdown
+```mermaid
+flowchart LR
+ A --> B --> C
+```
+````
+
+### Escaping
+
+```markdown
+\* \# \` \_ \[ \] \( \) \{ \} \. \! \|
+```
+
+### HTML (last resort only)
+
+```markdown
+
Hard line break
+Ctrl Keyboard key
+subscript Subscript fallback
+superscript Superscript fallback
+Title
Body Collapsible
+ Invisible author note
+```
+
+---
+
+## Quick Review Checklist
+
+Use this when reviewing or finalising any Markdown file:
+
+**Structure**
+- [ ] Exactly one `# H1` heading
+- [ ] 1–3 sentence introduction before first section
+- [ ] No skipped heading levels
+- [ ] Heading names are unique and descriptive
+- [ ] `[TOC]` placed after introduction (if used)
+- [ ] `## See Also` at the end (if the doc has outbound links)
+
+**Formatting**
+- [ ] Asterisks used for emphasis (not underscores)
+- [ ] No bolded entire paragraphs
+- [ ] Blank line before and after every heading
+- [ ] Blank line between list and surrounding paragraphs
+- [ ] Consistent bullet style (`-` throughout)
+- [ ] No trailing whitespace
+
+**Code**
+- [ ] All fenced code blocks have a language tag
+- [ ] No 4-space indented code blocks
+- [ ] Long commands use trailing `\` for line continuation
+
+**Links**
+- [ ] All link text is descriptive (no "click here" / "here")
+- [ ] Internal links use root-relative paths (not full URLs)
+- [ ] No `../` cross-directory relative paths
+- [ ] Reference links defined near their first use
+
+**Images**
+- [ ] All meaningful images have descriptive alt text
+- [ ] No `` or `` alt text
+- [ ] Images stored in `images/` or `assets/` directory
+
+**Tables**
+- [ ] Tables only used where data is genuinely two-dimensional
+- [ ] Table cells are short (reference links for URLs)
+- [ ] Numeric columns right-aligned
+
+**HTML**
+- [ ] No unnecessary HTML tags
+- [ ] No HTML used for styling or layout
+
+**Files**
+- [ ] File name is lowercase-hyphenated (except standard UPPERCASE files)
+- [ ] No spaces in file names
+- [ ] Standard files (README, CHANGELOG) in root directory
+- [ ] Non-standard docs in `docs/` subdirectory
diff --git a/template/.claude/skills/markdown/references/code-and-links.md b/template/.claude/skills/markdown/references/code-and-links.md
new file mode 100644
index 0000000..0176919
--- /dev/null
+++ b/template/.claude/skills/markdown/references/code-and-links.md
@@ -0,0 +1,347 @@
+# Code and links
+
+Covers: inline code, fenced code blocks, language tags, escaping, all link types,
+reference links, and automatic links.
+
+**Primary source:** [Google Markdown Style Guide](https://google.github.io/styleguide/docguide/style.html)
+
+---
+
+## Code
+
+### Inline Code
+
+Use single backticks for short code references, command names, file names, field
+names, file extensions, paths, and environment variables:
+
+```markdown
+Run `really_cool_script.sh` before proceeding.
+Update your `README.md` to reflect the new API.
+Pay attention to the `user_id` field in the response.
+Set the `DATABASE_URL` environment variable.
+Files with `.env` extension are excluded by default.
+```
+
+#### Using Inline Code as an Escape Mechanism
+
+Wrap anything that should **not** be parsed as Markdown in backticks — fake paths,
+example URLs with query strings, template variables:
+
+```markdown
+An example path would be: `path/to/your/config.yaml`
+An example query: `https://api.example.com/search?q=$TERM&page=1`
+A template variable: `{{ user.name }}`
+```
+
+#### Escaping Backticks Inside Inline Code
+
+Use double backticks to wrap inline code that itself contains a backtick:
+
+```markdown
+`` Use `backticks` for inline code ``
+```
+
+---
+
+### Fenced Code Blocks
+
+For any multi-line code, **always use fenced blocks** (triple backticks).
+
+**Never use 4-space indented code blocks:**
+- Cannot declare a language for syntax highlighting
+- Beginning and end of the block are ambiguous
+- Harder to find in code search
+
+#### Always Declare the Language
+
+Declare the language immediately after the opening fence:
+
+````markdown
+```python
+def greet(name: str) -> str:
+ return f"Hello, {name}!"
+```
+````
+
+````markdown
+```javascript
+const greet = (name) => `Hello, ${name}!`;
+```
+````
+
+````markdown
+```bash
+git clone https://github.com/org/repo.git
+cd repo && npm install
+```
+````
+
+````markdown
+```json
+{
+ "name": "my-project",
+ "version": "1.0.0"
+}
+```
+````
+
+````markdown
+```yaml
+server:
+ host: 0.0.0.0
+ port: 8080
+```
+````
+
+````markdown
+```sql
+SELECT user_id, email
+FROM users
+WHERE created_at > '2024-01-01'
+ORDER BY created_at DESC;
+```
+````
+
+````markdown
+```diff
+- removed line
++ added line
+ unchanged line
+```
+````
+
+Specifying no language disables syntax highlighting — only do this for plain-text
+output or when no language applies:
+
+````markdown
+```
+Plain text output or logs with no language.
+```
+````
+
+#### Escape Long Command-Line Calls
+
+Use a trailing backslash to break long shell commands across lines so they can be
+copied and pasted directly into a terminal:
+
+````markdown
+```shell
+bazel run :target -- \
+ --flag \
+ --foo=longlonglonglonglongvalue \
+ --bar=anotherlonglonglonglonglongvalue
+```
+````
+
+#### Nesting Code Blocks Inside Lists
+
+Indent the fenced block by the same amount as the list item's text to avoid
+breaking the list:
+
+```markdown
+1. Clone the repo.
+
+ ```bash
+ git clone https://github.com/org/repo.git
+ ```
+
+2. Install dependencies.
+
+ ```bash
+ npm install
+ ```
+
+3. Start the server.
+
+ ```bash
+ npm run dev
+ ```
+```
+
+You can also nest a code block using **4 additional spaces** from the list
+indentation (no fences required, but no language tag either):
+
+```markdown
+* Bullet point.
+
+ int foo; ← indented 4 spaces beyond the bullet text
+
+* Next bullet.
+```
+
+Prefer the fenced approach (explicit language tag, unambiguous boundaries).
+Use the 4-space approach only when the platform does not support fenced blocks.
+
+---
+
+## Links
+
+> **General rule:** Long links make source Markdown difficult to read and break
+> 80-character line wrapping. **Wherever possible, shorten your links.**
+
+### Inline Links
+
+Write the sentence naturally first, then wrap the most descriptive phrase as the
+link text. Link text should describe the destination, not the act of clicking:
+
+```markdown
+# ✅ Good — descriptive link text
+See the [Markdown style guide](https://google.github.io/styleguide/docguide/style.html) for details.
+Read the [contributing guide](CONTRIBUTING.md) before opening a PR.
+
+# ❌ Bad — uninformative link text
+See the style guide [here](https://google.github.io/styleguide/docguide/style.html).
+For more info, click [this link](https://example.com).
+Check out [https://example.com/foo/bar](https://example.com/foo/bar).
+```
+
+Never use "click here", "here", "link", "this", or bare URLs as link text.
+
+### Links Within the Same Repository
+
+Use root-relative paths (not full URLs) for internal Markdown links:
+
+```markdown
+# ✅ Root-relative path
+[Contributing Guide](/docs/CONTRIBUTING.md)
+[API Reference](/docs/api/README.md)
+
+# ✅ Same-directory relative link
+[See also](other-page-in-same-dir.md)
+
+# ❌ Full URL for an internal doc
+[Contributing Guide](https://github.com/org/repo/blob/main/docs/CONTRIBUTING.md)
+```
+
+Avoid `../` relative paths across directory levels — they break when files are
+moved or the doc tree is restructured:
+
+```markdown
+# ❌ Fragile cross-directory relative path
+[Config Reference](../../bad/path/to/config.md)
+```
+
+### Link to a Heading Anchor
+
+```markdown
+[Jump to Installation](#installation)
+[See the Configuration section](#configuration-options)
+```
+
+Anchors are auto-generated: lowercase, spaces → hyphens, punctuation removed.
+
+### Reference-Style Links
+
+Use reference links when:
+- The URL is long enough to break the 80-character line limit.
+- The same URL is used more than once in the document.
+- The link appears inside a table cell.
+
+```markdown
+See the [Markdown style guide][google-style] for details.
+Also read the [CommonMark spec][commonmark].
+
+[google-style]: https://google.github.io/styleguide/docguide/style.html
+[commonmark]: https://commonmark.org/
+```
+
+#### When NOT to Use Reference Links
+
+Do not use reference links when the URL is short enough that inlining it does not
+disrupt the flow of the text. Adding reference syntax for a short URL adds noise
+without benefit:
+
+```markdown
+# ❌ Unnecessary — URL is short, inline is cleaner
+The [style guide][style_guide] says not to over-use reference links.
+
+[style_guide]: https://google.com/markdown-style
+
+# ✅ Just inline it
+The [style guide](https://google.com/markdown-style) says not to over-use reference links.
+```
+
+Only use reference links when the URL is genuinely long enough to hurt readability
+if inlined, when the URL is reused, or when inside a table cell.
+
+#### Where to Define Reference Links
+
+Define reference link definitions **just before the next heading** after their first
+use (treat them like footnotes anchored to the current section).
+
+> **Note:** If your editor has its own opinion about where reference links should go,
+> don't fight it — **the tools always win.** The goal is consistency; let your
+> tooling enforce placement automatically.
+
+```markdown
+## Authentication
+
+See the [OAuth 2.0 guide][oauth] for implementation details.
+The [JWT specification][jwt] covers token structure.
+
+[oauth]: https://oauth.net/2/
+[jwt]: https://datatracker.ietf.org/doc/html/rfc7519
+
+## Authorization
+
+...
+```
+
+**Exception:** Reference links used in **multiple sections** go at the end of the file
+to avoid dangling links when sections are moved or restructured.
+
+#### Reference Links in Tables
+
+Always use reference links inside table cells — inline URLs make tables unreadable:
+
+```markdown
+# ✅ Readable table with reference links
+| Site | Description |
+|---------|-------------------------|
+| [MDN] | Web platform reference |
+| [devdocs] | Unified API reference |
+
+[MDN]: https://developer.mozilla.org
+[devdocs]: https://devdocs.io
+
+# ❌ Unreadable table with inline links
+| Site | Description |
+|-----------------------------------------|-------------------------|
+| [MDN](https://developer.mozilla.org) | Web platform reference |
+```
+
+#### Reducing Duplication with Reference Links
+
+When the same URL appears three or more times in a document, define it once as a
+reference link and use the reference throughout.
+
+### Automatic Links
+
+Most modern Markdown processors auto-link bare URLs. Use angle brackets to force
+a URL to render as a clickable link when auto-linking is not enabled:
+
+```markdown
+
+
+```
+
+To **prevent** a URL from auto-linking, wrap it in backticks:
+
+```markdown
+`https://www.example.com/search?q=$TERM`
+```
+
+---
+
+## Link Quality Rules (Summary)
+
+| Rule | Rationale |
+|---|---|
+| Descriptive link text | Screen readers and scanners read link text in isolation |
+| Root-relative paths for internal links | Portable; survives file moves |
+| No bare-URL link text | Wastes space; conveys no information |
+| No `../` cross-directory paths | Breaks on restructuring |
+| Reference links for long URLs | Keeps body text within 80-char limit |
+| Reference links in tables | Keeps table cells short and readable |
+| Define reference links near first use | Easy to find; reduces "footnote overload" |
+| Audit links periodically | Broken links erode reader trust |
diff --git a/template/.claude/skills/markdown/references/document-structure.md b/template/.claude/skills/markdown/references/document-structure.md
new file mode 100644
index 0000000..cd94d83
--- /dev/null
+++ b/template/.claude/skills/markdown/references/document-structure.md
@@ -0,0 +1,274 @@
+# Document structure
+
+Covers: document layout, front matter / metadata, headings, table of contents,
+paragraphs, spacing, and capitalization.
+
+**Primary source:** [Google Markdown Style Guide](https://google.github.io/styleguide/docguide/style.html)
+
+---
+
+## The Better/Best Rule (Documentation Review Culture)
+
+The standards for a documentation review are different from code reviews. The
+Google Markdown Style Guide defines the "Better/Best Rule" — fast iteration
+produces better long-term quality than perfection on every change.
+
+> **"A small improvement shipped is better than a perfect doc never merged."**
+
+### As a reviewer
+
+1. When reasonable, **LGTM immediately** and trust that comments will be fixed appropriately.
+2. **Prefer to suggest an alternative** rather than leaving a vague comment.
+3. For substantial changes, **start your own follow-up CL** instead of blocking the author. Especially avoid comments of the form "You should *also*…".
+4. On rare occasions, **hold up submission only if the CL actually makes the docs worse**. It's okay to ask the author to revert.
+
+### As an author
+
+1. **Avoid wasting cycles with trivial argument.** Capitulate early and move on.
+2. **Cite the Better/Best Rule** as often as needed when reviewers ask for more than is necessary.
+
+Fast iteration is your friend. To get long-term improvement, **authors must stay
+productive** when making short-term improvements. Set lower standards for each
+change so that more such changes can happen.
+
+---
+
+## Standard Document Layout
+
+Every well-formed Markdown document follows this skeleton:
+
+```markdown
+# Document Title
+
+Short introduction (1–3 sentences). What is this? Who is it for?
+
+[TOC]
+
+## First Major Section
+
+Content.
+
+## Second Major Section
+
+Content.
+
+## See Also
+
+- [Related resource](https://example.com)
+- [Another link](https://example.com)
+```
+
+### Layout Rules
+
+| Element | Rule |
+|---|---|
+| `# H1` | Exactly **one** per document — the page title |
+| Introduction | 1–3 sentences high-level overview, before any TOC or section |
+| `[TOC]` | After introduction, before first H2 (accessibility: screen readers read it in DOM order) |
+| Subsequent headings | Start at H2; never skip levels (H2 → H4 is invalid) |
+| `## See Also` | Last section; collect miscellaneous outbound links here |
+| Author field | Optional, below the title — revision history usually suffices |
+
+> **TOC placement matters for accessibility.** The `[TOC]` directive inserts HTML into
+> the DOM exactly where it appears. Placing it at the bottom means screen readers won't
+> reach it until the end of the document. Always place it after the introduction.
+
+---
+
+## Front Matter (YAML Metadata)
+
+When the target platform supports it (Jekyll, Hugo, Docusaurus, GitHub Pages, MkDocs):
+
+```markdown
+---
+title: "Introduction to Python"
+author: "Jane Smith"
+date: "2024-08-03"
+description: "An introductory guide covering basic concepts and syntax."
+tags: ["python", "programming", "beginner"]
+category: "Programming Tutorials"
+version: "1.0.0"
+last_updated: "2024-08-03"
+---
+```
+
+### Front Matter Rules
+
+- Always place front matter at the **very top** of the file, before any Markdown content.
+- Use triple dashes (`---`) as opening and closing delimiters.
+- Keep keys **lowercase** and **snake_cased**.
+- Include `version` and `last_updated` for versioned API or library docs.
+- The `description` field feeds SEO meta tags on documentation sites.
+- `tags` and `category` improve discoverability in doc search systems.
+
+---
+
+## Headings
+
+### Use ATX-Style Only
+
+```markdown
+# H1 — Document Title
+## H2 — Major Section
+### H3 — Subsection
+#### H4 — Sub-subsection
+##### H5
+###### H6
+```
+
+**Never** use Setext (underline) style — it is ambiguous, fragile, and hard to maintain:
+
+```markdown
+Heading — DO NOT USE
+--------------------
+```
+
+An editor cannot tell at a glance whether `---` means H2 or a horizontal rule.
+
+### Heading Rules (Google Style Guide)
+
+1. **One H1 per document.** It becomes the page ``.
+2. **Never skip levels.** H2 → H3 is valid. H2 → H4 is not.
+3. **Unique, fully descriptive names.** Anchor links are auto-generated from heading text.
+ Generic repeated names (`### Summary` under multiple H2s) produce anchor collisions:
+
+ ```markdown
+ # ❌ BAD — duplicate anchors break navigation
+ ## Foo
+ ### Summary
+ ## Bar
+ ### Summary
+
+ # ✅ GOOD — unique, descriptive
+ ## Foo
+ ### Foo Summary
+ ### Foo Example
+ ## Bar
+ ### Bar Summary
+ ### Bar Example
+ ```
+
+4. **Add blank lines** before and after every heading — required for consistent parsing.
+
+ ```markdown
+ ...text before.
+
+ ## Heading 2
+
+ Text after...
+ ```
+
+5. **Sentence case** headings unless the target style guide mandates title case.
+ - Capitalise the first word and proper nouns only.
+ - Exception: product/tool names must preserve their exact capitalisation
+ (`GitHub`, `macOS`, `Node.js`, `TypeScript`).
+
+---
+
+## Table of Contents
+
+### When to Include a TOC
+
+Include a TOC whenever the document has content "below the fold" on a typical laptop
+screen (i.e., more than ~2 screens of content).
+
+### Manual TOC (universal Markdown)
+
+```markdown
+## Contents
+
+1. [Installation](#installation)
+2. [Configuration](#configuration)
+ - [Environment Variables](#environment-variables)
+ - [Config File](#config-file)
+3. [Usage](#usage)
+4. [API Reference](#api-reference)
+```
+
+Anchor links are generated by:
+- Converting heading text to lowercase
+- Replacing spaces with hyphens
+- Removing all punctuation except hyphens
+
+```markdown
+## My Section Title → #my-section-title
+## API: v2.0 → #api-v20
+```
+
+### Directive TOC (platform-specific)
+
+```markdown
+[TOC]
+[[toc]]
+{:toc}
+```
+
+---
+
+## Paragraphs and Line Length
+
+### 80-Character Line Limit
+
+Body text follows an 80-character line limit (matches code conventions; improves
+diff readability in version control).
+
+**Exceptions** that may exceed 80 characters:
+- URLs and hyperlinks
+- Table cells
+- Headings
+- Code blocks
+
+### Paragraph Rules
+
+- Separate paragraphs with **a single blank line**.
+- Do not use trailing spaces or `
` for paragraph breaks — use blank lines.
+- Each paragraph should focus on **one central idea**.
+- Aim for **3–5 sentences** (100–200 words) per paragraph. Technical docs may stretch
+ to 300 words maximum; longer than that, split the paragraph.
+- Use transitional sentences to connect consecutive paragraphs.
+
+### Line Breaks Within a Paragraph
+
+Avoid forced line breaks (`\` or two trailing spaces) within paragraphs. Rewrite
+the sentence instead. If a hard break is genuinely needed, use a trailing backslash
+(`\`) sparingly — it is more portable than two trailing spaces, which are invisible
+and may be silently stripped by editors.
+
+```markdown
+For some reason I really need a break here,\
+though it is probably not necessary.
+```
+
+---
+
+## Capitalization
+
+Use the **original capitalisation** of all products, tools, and binaries:
+
+```markdown
+# ✅ Correct
+`Markdown`, `GitHub`, `macOS`, `Node.js`, `npm`, `PostgreSQL`, `OAuth`
+
+# ❌ Wrong
+`markdown`, `Github`, `MacOS`, `node.js`, `NPM`, `Postgresql`, `oauth`
+```
+
+---
+
+## Document Hygiene
+
+- **Delete stale content frequently** in small batches — a small accurate doc beats a
+ large outdated one.
+- Identify what you truly need: release docs, API docs, testing guidelines.
+- Avoid "documentation" in disrepair. Own your docs the same way you own your tests.
+- When a document exceeds ~500 lines, split it into focused sub-documents with an index:
+
+ ```markdown
+ ## Documentation Index
+
+ 1. [Overview](./overview.md) — Concepts and introduction
+ 2. [Quick Start](./quickstart.md) — Up and running in 5 minutes
+ 3. [Tutorial](./tutorial.md) — Full walkthrough
+ 4. [API Reference](./api-reference.md) — Complete API reference
+ 5. [Troubleshooting](./troubleshooting.md) — Common issues and fixes
+ ```
diff --git a/template/.claude/skills/markdown/references/extended-syntax.md b/template/.claude/skills/markdown/references/extended-syntax.md
new file mode 100644
index 0000000..7b8de6e
--- /dev/null
+++ b/template/.claude/skills/markdown/references/extended-syntax.md
@@ -0,0 +1,325 @@
+# Extended syntax and platform flavours
+
+Covers: GitHub Flavored Markdown (GFM), GitLab Markdown, footnotes, task lists,
+definition lists, heading IDs, emoji, highlight, math (LaTeX), Mermaid diagrams,
+callout/alert blocks, automatic URLs, and platform compatibility notes.
+
+**Primary sources:** [GFM Spec](https://github.github.com/gfm/),
+[Markdown Guide — Extended Syntax](https://www.markdownguide.org/extended-syntax/),
+[GitLab Flavored Markdown](https://docs.gitlab.com/user/markdown/)
+
+> **Important:** Extended syntax is not universally supported. Always verify that
+> your target platform supports a feature before using it. When in doubt, use basic
+> syntax or an HTML fallback.
+
+---
+
+## GitHub Flavored Markdown (GFM)
+
+GFM is the most widely used Markdown dialect. It extends CommonMark with:
+
+| Feature | Syntax | Notes |
+|---|---|---|
+| Task lists | `- [x]` / `- [ ]` | Interactive checkboxes |
+| Tables | Pipe syntax | Native table support |
+| Strikethrough | `~~text~~` | Double tildes |
+| Autolinks | Bare URLs | Auto-converted to links |
+| Disallowed raw HTML | — | Some HTML tags are sanitised |
+| Emoji shortcodes | `:emoji:` | `:rocket:` → 🚀 |
+| Footnotes | `[^1]` | Supported in newer GFM |
+| Math | `$` and `$$` | LaTeX, added 2022 |
+| Mermaid diagrams | ` ```mermaid ` | Flowcharts, sequences, etc. |
+| Alerts / callouts | `> [!NOTE]` | 5 types, added 2023 |
+| Collapsed sections | `` | HTML, but rendered by GitHub |
+
+---
+
+## GFM Alert Blocks (Callouts)
+
+The most important GFM extension for documentation. Five alert types:
+
+```markdown
+> [!NOTE]
+> Useful information the reader should know, even if skimming.
+
+> [!TIP]
+> Optional advice that helps the reader succeed.
+
+> [!IMPORTANT]
+> Key information necessary for the reader to succeed.
+
+> [!WARNING]
+> Urgent information; the reader must take care.
+
+> [!CAUTION]
+> Advises about risks or negative outcomes of an action.
+```
+
+GitHub renders each with a distinct coloured icon. Use these instead of plain
+blockquotes whenever a semantic callout is needed.
+
+---
+
+## Footnotes
+
+Footnotes add references and notes without cluttering body text. A superscript
+link in the body jumps to the footnote definition at the bottom of the rendered page.
+
+```markdown
+Here is a claim that needs a citation.[^1]
+
+This statement has a named footnote.[^note]
+
+[^1]: The source for the first claim.
+[^note]: Named footnotes still render with sequential numbers,
+ but are easier to manage in source. Indent continuation lines
+ with 4 spaces to include multiple paragraphs.
+
+ Second paragraph of the footnote.
+```
+
+### Footnote Rules
+
+- Identifiers can be numbers or words; no spaces or tabs.
+- Identifiers are for source management only — output is always sequential numbers.
+- Place footnote definitions at the end of the document or at the end of the section.
+- Indent continuation lines with 4 spaces to include multi-paragraph footnotes.
+
+---
+
+## Heading IDs (Custom Anchors)
+
+Override the auto-generated anchor for a heading:
+
+```markdown
+## My Long Section Title {#short-id}
+
+Now link to it with:
+[Go to short section](#short-id)
+```
+
+Useful when:
+- The heading text is very long and produces an unwieldy anchor.
+- You want to rename a heading without breaking existing deep links.
+- You need stable anchor IDs that survive heading text changes.
+
+**Support:** Pandoc, Kramdown (Jekyll), MkDocs, many static site generators.
+Not supported in plain GitHub rendering.
+
+---
+
+## Task Lists
+
+See `02-formatting-syntax.md` for full task list syntax. Additional notes:
+
+- Task lists are **interactive** on GitHub (checkboxes are clickable in issues and PRs).
+- In rendered documentation, they are read-only visual checkboxes.
+- Nesting works:
+
+ ```markdown
+ - [ ] Parent task
+ - [x] Completed sub-task
+ - [ ] Pending sub-task
+ ```
+
+---
+
+## Definition Lists
+
+Supported by Pandoc, MkDocs (with extensions), Kramdown, and PHP Markdown Extra.
+**Not supported** in standard GFM.
+
+```markdown
+First Term
+: Definition of the first term.
+
+Second Term
+: First definition of the second term.
+: Second definition of the second term.
+
+*Markdown Term*
+: Definition can contain **Markdown** formatting.
+```
+
+Rendered output:
+> **First Term** — Definition of the first term.
+> **Second Term** — First definition of the second term. / Second definition of the second term.
+
+---
+
+## Math (LaTeX / KaTeX)
+
+Supported on GitHub (added 2022), GitLab, Jupyter, MkDocs with plugins, Pandoc.
+
+### Inline Math
+
+```markdown
+The formula is $E = mc^2$ where $m$ is mass and $c$ is the speed of light.
+```
+
+### Block Math
+
+```markdown
+$$
+\frac{d}{dx}\left( \int_{a}^{x} f(u)\,du\right)=f(x)
+$$
+```
+
+```markdown
+$$
+\begin{pmatrix}
+a & b \\
+c & d
+\end{pmatrix}
+$$
+```
+
+---
+
+## Mermaid Diagrams
+
+Supported on GitHub, GitLab, Notion, Obsidian, and many static site generators
+(MkDocs with `pymdownx.superfences`).
+
+### Flowchart
+
+````markdown
+```mermaid
+flowchart LR
+ A[User Request] --> B{Authenticated?}
+ B -- Yes --> C[Load Dashboard]
+ B -- No --> D[Redirect to Login]
+```
+````
+
+### Sequence Diagram
+
+````markdown
+```mermaid
+sequenceDiagram
+ participant Browser
+ participant Server
+ participant DB
+ Browser->>Server: GET /api/user
+ Server->>DB: SELECT * FROM users
+ DB-->>Server: user rows
+ Server-->>Browser: 200 OK + JSON
+```
+````
+
+### Gantt Chart
+
+````markdown
+```mermaid
+gantt
+ title Project Timeline
+ dateFormat YYYY-MM-DD
+ section Planning
+ Requirements :done, 2024-01-01, 2024-01-07
+ Design :active, 2024-01-08, 2024-01-14
+ section Development
+ Backend :2024-01-15, 2024-01-28
+ Frontend :2024-01-22, 2024-02-04
+```
+````
+
+### Entity Relationship Diagram
+
+````markdown
+```mermaid
+erDiagram
+ USER ||--o{ ORDER : places
+ ORDER ||--|{ LINE-ITEM : contains
+ PRODUCT ||--o{ LINE-ITEM : "included in"
+```
+````
+
+---
+
+## Emoji Shortcodes (GFM)
+
+```markdown
+:rocket: :white_check_mark: :warning: :x: :bulb: :books:
+```
+
+Renders as: 🚀 ✅ ⚠️ ❌ 💡 📚
+
+Use emoji sparingly in technical documentation — they improve scannability for
+status indicators but can appear unprofessional in formal docs. Never rely solely
+on emoji to convey meaning (accessibility concern).
+
+---
+
+## Strikethrough
+
+Standard GFM with double tildes:
+
+```markdown
+~~deleted text~~
+```
+
+**Do not** use single tildes (`~text~`) for strikethrough — single tildes are for
+subscript in some parsers and strikethrough in others, causing inconsistent rendering.
+
+---
+
+## Automatic URL Linking
+
+Most modern processors auto-link bare URLs:
+
+```markdown
+Visit https://example.com for more information.
+```
+
+To suppress auto-linking, wrap in backticks:
+
+```markdown
+`https://example.com/search?q=$TERM`
+```
+
+---
+
+## Inline Diff (GitLab Only)
+
+```markdown
+{+addition+}
+{-deletion-}
+```
+
+Renders with green/red highlighting in GitLab. Not supported elsewhere.
+
+---
+
+## Platform Compatibility Matrix
+
+| Feature | CommonMark | GFM (GitHub) | GitLab | Pandoc | Kramdown | MkDocs |
+|---|:---:|:---:|:---:|:---:|:---:|:---:|
+| Tables | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
+| Task lists | ❌ | ✅ | ✅ | ✅ | ✅ | ✅* |
+| Strikethrough | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
+| Footnotes | ❌ | ✅ | ✅ | ✅ | ✅ | ✅* |
+| Definition lists | ❌ | ❌ | ✅ | ✅ | ✅ | ✅* |
+| Math (LaTeX) | ❌ | ✅ | ✅ | ✅ | ✅* | ✅* |
+| Mermaid | ❌ | ✅ | ✅ | ❌ | ❌ | ✅* |
+| GFM Alerts | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
+| Custom heading IDs | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
+| Emoji shortcodes | ❌ | ✅ | ✅ | ✅* | ❌ | ✅* |
+| Highlight (`==`) | ❌ | ❌ | ❌ | ✅* | ✅* | ✅* |
+| Subscript (`~`) | ❌ | ❌ | ✅ | ✅* | ✅* | ✅* |
+| Superscript (`^`) | ❌ | ❌ | ✅ | ✅* | ✅* | ✅* |
+
+`*` = requires plugin/extension configuration
+
+---
+
+## Stack Overflow and Discord Notes
+
+**Stack Overflow** does not support: tables (use HTML ``), task lists, footnotes.
+Supports: headings, emphasis, lists, links, images, code blocks, blockquotes, horizontal rules.
+
+**Discord** supports: bold, italic, underline (`__text__`), strikethrough, inline code,
+code blocks with syntax highlighting, blockquotes, spoiler tags (`||text||`), H1–H3.
+Does not support: tables, images via Markdown, footnotes, task lists.
+Note: Discord uses `__text__` for **underline**, not bold — conflicts with standard Markdown.
+
+**Slack** uses its own "mrkdwn" format — significantly different from standard Markdown.
diff --git a/template/.claude/skills/markdown/references/file-management.md b/template/.claude/skills/markdown/references/file-management.md
new file mode 100644
index 0000000..cfeac51
--- /dev/null
+++ b/template/.claude/skills/markdown/references/file-management.md
@@ -0,0 +1,347 @@
+# File management and documentation systems
+
+Covers: file naming conventions, repository folder structure, standard Markdown
+files (README, CHANGELOG, CONTRIBUTING, etc.), versioning, commit conventions,
+documentation system design, and maintenance.
+
+**Sources:** [Google Markdown Style Guide](https://google.github.io/styleguide/docguide/style.html),
+[Folder Structure Conventions](https://github.com/kriasoft/Folder-Structure-Conventions),
+[README — Wikipedia](https://en.wikipedia.org/wiki/README),
+[Building a Markdown-Based Documentation System](https://medium.com/@rosgluk/building-a-markdown-based-documentation-system-72bef3cb1db3)
+
+---
+
+## File Naming Conventions
+
+### Standard Repository Files (UPPERCASE)
+
+The following well-known files use UPPERCASE names by convention — this ensures
+they sort near the top of ASCII-ordered directory listings and are immediately
+recognisable:
+
+| File | Purpose |
+|---|---|
+| `README.md` | Project overview, installation, usage, badges |
+| `CHANGELOG.md` | Version history and release notes |
+| `CONTRIBUTING.md` | How to contribute to the project |
+| `CODE_OF_CONDUCT.md` | Community behaviour standards |
+| `SECURITY.md` | Vulnerability reporting policy |
+| `LICENSE` | Licence text (plain text, no `.md` extension) |
+| `SUMMARY.md` | GitBook / documentation table of contents |
+| `SUPPORT.md` | Where to get help |
+
+These files should be placed in the **root directory** of the repository.
+
+### Non-Standard Documentation Files (lowercase-hyphenated)
+
+Any documentation that is not one of the standard files above should use
+**lowercase letters with hyphens** as word separators:
+
+```
+docs/getting-started.md ✅
+docs/api-reference.md ✅
+docs/configuration-guide.md ✅
+docs/myAdditionalDoc.md ✅ (camelCase also acceptable for non-standard)
+
+docs/Getting Started.md ❌ (spaces break URLs)
+docs/API_Reference.md ❌ (underscores less URL-friendly)
+docs/APIreference.md ❌ (hard to read)
+```
+
+### Naming Rules
+
+- Use only **lowercase letters, digits, and hyphens** in non-standard file names.
+- **No spaces** — spaces must be percent-encoded in URLs (`%20`) and break many tools.
+- **No special characters**: `! @ # $ % ^ & * ( ) = + [ ] { } | \ ; ' " , < > ?`
+- Use **printable ASCII only** — URI paths are case-sensitive; some file systems are not.
+- Keep names **short and descriptive**: `getting-started.md` not `a-guide-to-getting-started-with-this-project.md`.
+- Prefer **nouns** for reference docs and **verb phrases** for guides:
+ `configuration.md`, `deploy-to-production.md`.
+
+---
+
+## Repository Folder Structure
+
+### Minimal Structure (small project)
+
+```
+project/
+├── README.md
+├── CHANGELOG.md
+├── CONTRIBUTING.md
+├── LICENSE
+└── docs/
+ ├── getting-started.md
+ └── api-reference.md
+```
+
+### Standard Structure (medium project)
+
+```
+project/
+├── README.md
+├── CHANGELOG.md
+├── CONTRIBUTING.md
+├── CODE_OF_CONDUCT.md
+├── SECURITY.md
+├── LICENSE
+├── docs/
+│ ├── README.md ← docs index / overview
+│ ├── getting-started.md
+│ ├── configuration.md
+│ ├── api/
+│ │ ├── README.md
+│ │ └── endpoints.md
+│ └── guides/
+│ ├── deploy.md
+│ └── troubleshooting.md
+├── assets/
+│ └── images/
+└── src/
+```
+
+### Documentation-Only Repository or GitBook
+
+```
+docs/
+├── README.md ← introduction / overview
+├── SUMMARY.md ← table of contents (GitBook)
+├── chapter-1/
+│ ├── README.md ← chapter introduction
+│ └── subsection.md
+└── chapter-2/
+ ├── README.md
+ └── subsection.md
+```
+
+### Rules
+
+- Store all non-standard docs under a `docs/` folder to avoid cluttering the root namespace.
+- Each subdirectory in `docs/` should have its own `README.md` as an index/overview.
+- Use short lowercase names for top-level directories except the standard files.
+- `assets/` or `static/` for images, diagrams, and other media files.
+
+---
+
+## Standard File Templates
+
+### README.md
+
+A project README should answer four questions a new visitor has:
+
+```markdown
+# Project Name
+
+One-sentence description of what this project does and who it is for.
+
+## Features
+
+- Key feature one
+- Key feature two
+- Key feature three
+
+## Quick Start
+
+\```bash
+npm install my-project
+my-project --help
+\```
+
+## Documentation
+
+- [Getting Started](docs/getting-started.md)
+- [API Reference](docs/api-reference.md)
+- [Configuration](docs/configuration.md)
+
+## Contributing
+
+See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
+
+## License
+
+[MIT](LICENSE) © Your Name
+```
+
+**README sections to consider (include what is relevant):**
+
+- Project name and tagline
+- Badges (build status, coverage, version, licence)
+- Description / what it does
+- Features list
+- Quick start / installation
+- Usage examples (with code blocks)
+- Documentation index / links
+- Configuration reference (brief, or link out)
+- Contributing guide link
+- Acknowledgements / credits
+- Licence
+
+### CHANGELOG.md
+
+Follow [Keep a Changelog](https://keepachangelog.com/) conventions:
+
+```markdown
+# Changelog
+
+All notable changes to this project are documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/),
+and this project adheres to [Semantic Versioning](https://semver.org/).
+
+## [Unreleased]
+
+### Added
+- New feature description
+
+## [2.1.0] — 2024-01-15
+
+### Added
+- User group management API
+- Batch processing endpoint
+
+### Changed
+- Improved authentication flow performance
+
+### Fixed
+- Resolved race condition in session handling (#123)
+
+### Deprecated
+- `GET /api/v1/users` — use `GET /api/v2/users` instead
+
+### Removed
+- Legacy XML response format
+
+### Security
+- Updated dependency X to patch CVE-2024-XXXX
+
+## [2.0.0] — 2024-01-01
+
+### Changed
+- **Breaking:** Refactored authentication API (see migration guide)
+
+[Unreleased]: https://github.com/org/repo/compare/v2.1.0...HEAD
+[2.1.0]: https://github.com/org/repo/compare/v2.0.0...v2.1.0
+[2.0.0]: https://github.com/org/repo/releases/tag/v2.0.0
+```
+
+**Change types (use consistently):**
+`Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`
+
+### CONTRIBUTING.md
+
+```markdown
+# Contributing to Project Name
+
+Thank you for considering contributing!
+
+## Ways to Contribute
+
+- Report bugs via [GitHub Issues](https://github.com/org/repo/issues)
+- Suggest features or improvements
+- Submit pull requests
+
+## Development Setup
+
+\```bash
+git clone https://github.com/org/repo.git
+cd repo
+npm install
+npm test
+\```
+
+## Pull Request Process
+
+1. Fork the repository and create a feature branch.
+2. Write or update tests for your changes.
+3. Ensure all tests pass: `npm test`
+4. Update `CHANGELOG.md` under `[Unreleased]`.
+5. Submit a pull request with a clear description.
+
+## Code of Conduct
+
+This project follows our [Code of Conduct](CODE_OF_CONDUCT.md).
+```
+
+---
+
+## Versioning Blocks in Documentation
+
+For API and library documentation, embed version information clearly:
+
+```markdown
+---
+title: API Authentication Guide
+version: 2.1.0
+last_updated: 2024-01-15
+author: Documentation Team
+---
+
+> **Version note:** This document applies to API v2.1.0 and above.
+> For older versions, see the [archived documentation](./archive/).
+```
+
+---
+
+## Commit Message Conventions for Documentation Changes
+
+```
+docs: update API authentication guide
+
+- Add OAuth 2.0 example with PKCE flow
+- Fix broken code sample in section 3
+- Update related links
+
+Closes #123
+```
+
+**Commit type prefixes:**
+
+| Prefix | Use for |
+|---|---|
+| `docs` | Documentation changes |
+| `feat` | Documentation for a new feature |
+| `fix` | Fix incorrect or broken documentation |
+| `style` | Formatting-only changes (no content change) |
+| `refactor` | Restructure documentation without content change |
+| `chore` | Tooling, build, or configuration changes |
+
+---
+
+## Documentation Versioning Strategy
+
+### Inline Versioning (small projects)
+
+Mark version applicability inline in the document:
+
+```markdown
+> **Available since v1.4.0**
+
+> **Deprecated in v2.0.0** — use `newMethod()` instead.
+```
+
+### Changelog-Based (most projects)
+
+Maintain a single `CHANGELOG.md` as described above.
+
+### Versioned Subdirectories (large / enterprise projects)
+
+```
+docs/
+├── v2/ ← current version
+│ └── api-reference.md
+├── v1/ ← archived version
+│ └── api-reference.md
+└── README.md ← links to current version
+```
+
+---
+
+## Documentation Maintenance Rules
+
+- **Delete stale content frequently** and in small batches — stale docs are worse than no docs.
+- **Audit links periodically** — broken links erode reader trust.
+- **Keep docs close to the code** they describe — co-located docs get updated when code changes.
+- **Split documents** that exceed ~500 lines into focused sub-documents with an index.
+- **Review documentation** as part of every pull request that changes behaviour.
+- **Test code examples** in your docs (use CI where possible) — outdated examples mislead users.
+- For docs systems: use GitHub Actions or similar CI to build, lint, and check links automatically.
diff --git a/template/.claude/skills/markdown/references/formatting-syntax.md b/template/.claude/skills/markdown/references/formatting-syntax.md
new file mode 100644
index 0000000..e9d3ee5
--- /dev/null
+++ b/template/.claude/skills/markdown/references/formatting-syntax.md
@@ -0,0 +1,306 @@
+# Formatting syntax
+
+Covers: emphasis (bold, italic, strikethrough), lists (ordered, unordered, nested,
+task), blockquotes, horizontal rules, and line-break handling.
+
+**Primary sources:** [Google Markdown Style Guide](https://google.github.io/styleguide/docguide/style.html),
+[Markdown Guide — Basic Syntax](https://www.markdownguide.org/basic-syntax/)
+
+---
+
+## Emphasis
+
+### Bold
+
+Use `**double asterisks**` for bold (strong importance):
+
+```markdown
+**bold text**
+```
+
+Use bold for: critical terms on first introduction, UI element labels, warnings,
+key takeaways, and field/parameter names in documentation.
+
+### Italic
+
+Use `*single asterisks*` for italic (stress emphasis):
+
+```markdown
+*italic text*
+```
+
+Use italic for: introducing new terms, titles of books/products, technical terms
+used in a non-technical context, and light stress.
+
+### Bold + Italic
+
+```markdown
+***bold and italic***
+```
+
+Use only when both strong importance and stress emphasis are simultaneously needed.
+
+### Strikethrough
+
+```markdown
+~~strikethrough~~
+```
+
+Use for: text that is a deliberate error or has been superseded, deprecated
+features, or crossed-off items in prose.
+
+```markdown
+~~Old feature~~ replaced by new feature
+Price: ~~$99~~ $79
+```
+
+### Rules for Emphasis
+
+- **Always use asterisks**, not underscores. Underscores behave inconsistently inside
+ words across parsers (e.g., `some_variable_name` may render incorrectly):
+
+ ```markdown
+ # ✅ Use asterisks
+ *italic* **bold** ***bold italic***
+
+ # ❌ Avoid underscores (unreliable mid-word)
+ _italic_ __bold__
+ ```
+
+- **Never bold or italicise entire paragraphs.** Overuse destroys emphasis.
+- Use emphasis **sparingly** — every additional use dilutes the impact of all others.
+- Do not use emphasis for purely decorative formatting.
+
+---
+
+## Lists
+
+### When to Use Lists
+
+Use lists for genuinely enumerable or sequential items. Do **not** fragment flowing
+prose into bullets — write prose instead. If all list items are a single short
+phrase, a list is appropriate. If items need full sentences of context, consider
+prose with subheadings.
+
+### Unordered Lists
+
+```markdown
+- First item
+- Second item
+- Third item
+```
+
+Use `-` consistently throughout a file (do not mix `-`, `*`, and `+`).
+
+For single-line, non-nested lists, one space after the marker suffices:
+
+```markdown
+- Foo
+- Bar
+- Baz
+```
+
+For wrapped text, use a 4-space total indent (3 spaces after `-`):
+
+```markdown
+- Foo, which is a somewhat longer item that may wrap
+ to a second line and needs the 4-space indent.
+- Bar.
+```
+
+### Ordered Lists
+
+For **short, stable** lists, use explicit sequential numbers (more readable in source):
+
+```markdown
+1. First step
+2. Second step
+3. Third step
+```
+
+For **long or frequently changing** lists, use **lazy numbering** — Markdown renumbers
+automatically, so you never have to renumber after inserting or removing an item:
+
+```markdown
+1. First item
+1. Second item
+1. Third item
+1. Fourth item
+```
+
+For ordered list items with wrapped text, use 2 spaces after the number:
+
+```markdown
+1. First item — text starts at column 5 (4-space total indent).
+ Wrapped text also aligns here.
+2. Second item.
+```
+
+### Nested Lists
+
+Use a **4-space indent** for both bullet and numbered nested lists:
+
+```markdown
+1. Parent item (text at 4-space indent).
+ Wrapped text aligns here.
+ 1. Nested numbered item.
+ Wrapped text in nested list needs an 8-space indent.
+ 2. Another nested numbered item.
+2. Back to parent level.
+
+- Bullet parent (text at 4-space indent).
+ - Nested bullet.
+ - Another nested bullet.
+- Back to parent level.
+```
+
+**Do not mix inconsistent indentation** — it produces unpredictable rendering:
+
+```markdown
+# ❌ Messy — avoid
+* One space,
+with no indent for wrapped text.
+ 1. Irregular nesting.
+```
+
+### Task Lists (GFM)
+
+Supported on GitHub, GitLab, and most modern Markdown renderers:
+
+```markdown
+- [x] Complete project setup
+- [x] Write documentation
+- [ ] Add unit tests
+- [ ] Deploy to production
+```
+
+Nested task lists:
+
+```markdown
+- [ ] Top-level task
+ - [x] Sub-task one
+ - [ ] Sub-task two
+```
+
+---
+
+## Blockquotes
+
+Use `>` to call out quoted text, important notes, or external attributions:
+
+```markdown
+> This is a blockquote. It can span multiple lines.
+> All lines should be prefixed with `>`.
+
+> First paragraph of the quote.
+>
+> Second paragraph of the quote (blank `>` line separates paragraphs).
+```
+
+### Nested Blockquotes
+
+```markdown
+> Outer quote.
+>
+> > Nested quote inside the outer quote.
+>
+> Back to the outer quote.
+```
+
+### Blockquotes with Markdown Inside
+
+Blockquotes can contain any Markdown:
+
+```markdown
+> **Note:** This feature requires Node.js 18 or higher.
+>
+> - Supported platforms: Linux, macOS, Windows
+> - Minimum RAM: 512 MB
+```
+
+### Blockquote Usage Rules
+
+- Use to highlight important side-notes, warnings, and external quotations.
+- Use **moderately** — overuse dilutes their visual effect.
+- Do not use for generic indentation or visual styling.
+- For formal callouts (Notes, Warnings, Tips), prefer GFM alert syntax when available
+ (see `05-extended-syntax.md`).
+
+---
+
+## Horizontal Rules
+
+A horizontal rule is three or more hyphens, asterisks, or underscores on a line alone:
+
+```markdown
+---
+***
+___
+```
+
+### Usage Rules
+
+- Use `---` as the consistent choice (matches YAML front matter delimiters; distinctive).
+- Use **sparingly** — only at **major structural transitions** (e.g., between an
+ introduction block and the main body).
+- Do **not** use to separate every section — headings are the correct separator.
+- Ensure a blank line before and after to avoid the `---` being parsed as a Setext H2.
+
+---
+
+## Highlight, Subscript, Superscript
+
+These are extended syntax — not supported by all parsers. Always verify support
+before using them.
+
+### Highlight
+
+```markdown
+I need to highlight ==these very important words==.
+```
+
+### Subscript
+
+```markdown
+H~2~O
+CO~2~
+```
+
+### Superscript
+
+```markdown
+X^2^
+E = mc^2^
+```
+
+### Fallback HTML (when the above are unsupported)
+
+```markdown
+H2O
+X2
+```
+
+---
+
+## Escaping Characters
+
+To display a literal Markdown character that would otherwise be interpreted as syntax,
+prefix it with a backslash `\`:
+
+```markdown
+\*Not italic\*
+\# Not a heading
+\[Not a link\]
+\`Not code\`
+\> Not a blockquote
+```
+
+Characters that can be escaped:
+`\` `` ` `` `*` `_` `{}` `[]` `()` `#` `+` `-` `.` `!` `|`
+
+---
+
+## Inline HTML
+
+Plain Markdown handles nearly all formatting needs. Use HTML only as a last resort.
+See `04-tables-images-html.md` for the complete rules on HTML in Markdown.
diff --git a/template/.claude/skills/markdown/references/tables-images-html.md b/template/.claude/skills/markdown/references/tables-images-html.md
new file mode 100644
index 0000000..d507e19
--- /dev/null
+++ b/template/.claude/skills/markdown/references/tables-images-html.md
@@ -0,0 +1,277 @@
+# Tables, images, and HTML
+
+Covers: table syntax, alignment, best practices, when NOT to use tables; image
+syntax, alt text, accessibility; HTML-in-Markdown rules.
+
+**Primary source:** [Google Markdown Style Guide](https://google.github.io/styleguide/docguide/style.html)
+
+---
+
+## Tables
+
+### When to Use a Table
+
+Use tables **only** when data is genuinely two-dimensional with:
+- Relatively uniform data distribution across both dimensions
+- Many parallel items each with distinct, comparable attributes
+- Content that benefits from at-a-glance scanning
+
+Tables are NOT appropriate when:
+- Data could be a simple list (lists are easier to write and read)
+- Several columns have the same value across rows
+- Cells contain long prose
+- There are very few rows relative to columns (or vice versa)
+- The table has mostly empty cells
+
+**Example of data that should be a list, not a table (from the Google style guide):**
+
+```markdown
+# ❌ Bad — this table has three specific problems
+Fruit | Metrics | Grows on | Acute curvature | Attributes | Notes
+------ | ------------ | -------- | ------------------ | ------------------- | -----
+Apple | Very popular | Trees | | Juicy, Firm, Sweet | Apples keep doctors away.
+Banana | Very popular | Trees | 16 degrees average | Convenient, Soft | Most apes prefer mangoes.
+```
+
+The Google style guide names three specific table problems to watch for:
+
+1. **Poor distribution** — Several columns don't differ across rows, and some cells
+ are empty. This is usually a sign that the data may not benefit from tabular display.
+
+2. **Unbalanced dimensions** — There are very few rows relative to columns (or very
+ few columns relative to rows). When this ratio is unbalanced in either direction,
+ a table becomes little more than an inflexible format for text.
+
+3. **Rambling prose in cells** — Tables should tell a succinct story at a glance.
+ Long sentences or paragraphs inside cells defeat the purpose of a table.
+
+If any of these three problems apply, convert the table to a list with subheadings:
+
+```markdown
+# ✅ Good — list form is more readable here
+## Fruits
+
+Both are highly popular, sweet, and grow on trees.
+
+### Apple
+- Juicy, firm
+- Apples keep doctors away.
+
+### Banana
+- Convenient, soft
+- 16 degrees average acute curvature.
+- Contrary to popular belief, most apes prefer mangoes.
+```
+
+### Table Syntax
+
+Use pipes `|` to separate columns and hyphens `---` to create the header separator:
+
+```markdown
+| Column 1 | Column 2 | Column 3 |
+|----------|----------|----------|
+| Cell | Cell | Cell |
+| Cell | Cell | Cell |
+```
+
+The outer pipes on each row are optional but strongly recommended for consistency:
+
+```markdown
+Column 1 | Column 2 ← harder to read without outer pipes
+-------- | --------
+Cell | Cell
+```
+
+### Column Alignment
+
+Control alignment with colons in the separator row:
+
+```markdown
+| Left-aligned | Centred | Right-aligned |
+|:-------------|:-------:|--------------:|
+| Default | Centred | Numbers |
+| Text | Text | 12.50 |
+| More text | More | 100.00 |
+```
+
+- `:---` — left align (default if no colon)
+- `:---:` — centre align
+- `---:` — right align (use for numeric columns)
+
+### Keeping Tables Readable
+
+- **Keep cells short** — Markdown offers no line-break within table cells.
+- **Use reference links** for any URL inside a cell (see `03-code-and-links.md`).
+- **Align pipe characters vertically** in source for readability:
+
+ ```markdown
+ # ✅ Well-aligned source
+ | Transport | Favored by | Advantage |
+ |-----------|:-------------|:-------------------|
+ | Bicycle | Commuters | Zero emissions |
+ | Train | Travellers | High capacity |
+ | Bus | City riders | Frequent stops |
+ ```
+
+- Cell widths in source don't need to match — Markdown renders them uniformly regardless:
+
+ ```markdown
+ | Col | Col |
+ | --- | --- |
+ | Short | A longer cell that still renders fine |
+ ```
+
+### Inline Formatting in Tables
+
+Bold, italic, inline code, and links all work inside table cells:
+
+```markdown
+| Command | Description |
+|----------------|-----------------------------------|
+| `git status` | List **new or modified** files |
+| `git diff` | Show *unstaged* file differences |
+| `git commit` | [Commit staged changes][git-docs] |
+
+[git-docs]: https://git-scm.com/docs/git-commit
+```
+
+### Large Tables
+
+If a table is unavoidably wide, it is one of the few places where exceeding
+the 80-character line limit is acceptable. Even so, use reference links to
+minimise cell content.
+
+---
+
+## Images
+
+### Syntax
+
+```markdown
+
+
+
+
+
+
+![Alt text][dashboard]
+
+[dashboard]: images/dashboard.png "Main dashboard"
+```
+
+### Clickable Images
+
+Wrap an image in a link to make it clickable:
+
+```markdown
+[](https://docs.djangoproject.com/)
+```
+
+### Alt Text Rules
+
+Alt text is **mandatory for all meaningful images**. It is used by:
+- Screen readers for visually impaired users
+- Browsers when the image fails to load
+- Search engines for image indexing
+
+**Writing good alt text:**
+
+```markdown
+# ✅ Good — describes what the image shows and why it matters
+
+
+
+
+# ❌ Bad — uninformative
+
+
+
+```
+
+Guidelines for alt text:
+- Describe **what the image shows** and **why it is relevant** in that context.
+- Keep alt text concise — 1–2 sentences maximum.
+- Do not start with "Image of…" or "Picture of…" (screen readers already announce it's an image).
+- For **purely decorative** images (dividers, backgrounds), use empty alt text: ``
+ so screen readers skip them.
+- For **complex diagrams** (architecture diagrams, flowcharts), provide a caption or
+ prose description in the surrounding text in addition to alt text.
+- For **screenshots of UI**, describe the key element and what state it is in.
+
+### Image Storage
+
+- Store images in a dedicated `images/` or `assets/` subdirectory adjacent to the doc.
+- For large documentation sites, use a top-level `assets/` or `static/` folder.
+- **Prefer SVG** for diagrams and icons (scales without blurring, small file size).
+- **Use PNG** for screenshots (lossless, renders text crisply).
+- **Use JPEG** for photographs (efficient compression for photographic content).
+- Use descriptive file names: `auth-flow-diagram.svg`, not `img1.png`.
+
+### When to Use Images
+
+- When it is genuinely **easier to show than to describe** (UI navigation, visual layouts).
+- Architecture diagrams, flowcharts, and data visualisations that would be verbose as text.
+- **Use sparingly** — excessive images distract, slow page load, and are inaccessible
+ without good alt text. When in doubt, write prose.
+
+---
+
+## HTML in Markdown
+
+### Core Rule: Strongly Prefer Markdown
+
+Every HTML tag in a Markdown file reduces portability and readability. Some renderers
+(e.g., Gitiles) ignore HTML entirely. Markdown meets almost all formatting needs
+without HTML.
+
+> If you find yourself reaching for HTML, first ask: do I really need this?
+> Can I restructure the content to express it in plain Markdown?
+
+### Acceptable HTML Uses (as a last resort)
+
+These are the narrow cases where HTML is permitted:
+
+```markdown
+
+Line one
Line two
+
+
+H2O
+E = mc2
+
+
+
+Click to expand
+
+Collapsed content goes here. Full Markdown works inside.
+
+
+
+
+Press Ctrl + C to copy.
+
+
+Warning text
+```
+
+### What HTML Must Never Be Used For
+
+- Page layout or multi-column layouts
+- Font size, font family, or colour styling (except `` for one-off critical warnings)
+- `` or `` instead of `**` and `*`
+- ``–`` instead of `#` syntax
+- ``, ``, `- ` instead of Markdown list syntax
+- `` instead of Markdown link syntax
+- `
` instead of `![]()` syntax
+- `` instead of pipe-table syntax (except when the table is genuinely too complex)
+
+### HTML Commenting
+
+HTML comments are invisible in rendered output and valid in all processors:
+
+```markdown
+
+
+```
+
+Use sparingly for author notes, reminders, or section markers not meant for readers.
diff --git a/template/.claude/skills/pytest/SKILL.md b/template/.claude/skills/pytest/SKILL.md
index 347cb47..638d14d 100644
--- a/template/.claude/skills/pytest/SKILL.md
+++ b/template/.claude/skills/pytest/SKILL.md
@@ -10,11 +10,11 @@ description: >-
any request to add/improve/fix tests in a Python project.
---
-# Writing pytest tests
+# Pytest Skill
-This skill covers everything you need to write clear, maintainable, and thorough pytest
-test suites. It follows a progressive-disclosure structure: this file covers the
-essentials, and the `references/` directory has deep dives on each topic.
+A comprehensive skill for writing clear, maintainable, and thorough pytest test suites.
+This file covers the essentials, and the `references/` directory has deep dives on each
+topic.
## Quick reference: where to go deeper
@@ -29,6 +29,13 @@ essentials, and the `references/` directory has deep dives on each topic.
| Anti-patterns and fixes | [references/anti-patterns.md](references/anti-patterns.md) |
| CI, coverage, and plugins | [references/ci-and-plugins.md](references/ci-and-plugins.md) |
+**Bundled scripts** (in `scripts/` — run directly, no need to read them into context):
+
+| Script | What it does |
+|------------------------|----------------------------------------------------------|
+| `find_slow_tests.py` | Runs pytest, identifies tests exceeding a time threshold |
+| `mark_slow_tests.py` | Adds `@pytest.mark.slow` to the identified slow tests |
+
Read the relevant reference file before working on a specific area. For a new test file,
skim `test-organization.md` and `fixtures.md` first. For debugging flaky tests, start
with `anti-patterns.md`.
@@ -243,6 +250,73 @@ pytest --durations=10 # show 10 slowest tests
---
+## Finding and marking slow tests
+
+Long-running tests slow down the feedback loop. The skill bundles two scripts that
+work together to find slow tests and add `@pytest.mark.slow` so they can be skipped
+during fast development iterations.
+
+### Step 1: Find slow tests
+
+Run the finder script from your project root. It executes pytest with `--durations=0`
+and reports every test whose call phase exceeds the threshold (default: 1 second).
+
+```bash
+python /scripts/find_slow_tests.py --threshold 1.0
+```
+
+Options:
+
+- `--threshold 0.5` — flag tests slower than 500ms.
+- `--top 10` — only show the 10 slowest.
+- `--test-path tests/integration/` — limit to a specific directory.
+- `--json-only` — machine-readable JSON on stdout only.
+
+The output is a JSON array on stdout (for piping) plus a human summary on stderr.
+
+### Step 2: Mark them
+
+Pipe the output directly into the marker script:
+
+```bash
+python /scripts/find_slow_tests.py --threshold 1.0 \
+ | python /scripts/mark_slow_tests.py
+```
+
+This adds `@pytest.mark.slow` above each slow test function and inserts
+`import pytest` if the file does not already have it. Tests that already carry
+the marker are skipped.
+
+Use `--dry-run` to preview changes without modifying files:
+
+```bash
+python /scripts/find_slow_tests.py --threshold 1.0 \
+ | python /scripts/mark_slow_tests.py --dry-run
+```
+
+### Step 3: Run fast tests only
+
+Once slow tests are marked, skip them during normal development:
+
+```bash
+pytest -m "not slow"
+```
+
+Run the full suite (including slow tests) before opening a PR or in CI.
+
+### When to use this
+
+- After adding integration or E2E tests that touch real databases, networks, or
+ large datasets.
+- When `pytest --durations=10` shows tests taking more than a second.
+- During CI optimisation — split fast and slow test runs into separate jobs.
+
+Read [references/parametrize-and-markers.md](references/parametrize-and-markers.md)
+for more on custom markers and [references/ci-and-plugins.md](references/ci-and-plugins.md)
+for CI integration patterns.
+
+---
+
## Checklist before committing tests
- [ ] Each test has a clear, descriptive name.
@@ -251,5 +325,6 @@ pytest --durations=10 # show 10 slowest tests
- [ ] Fixtures handle setup and teardown (no leftover state).
- [ ] External I/O is mocked or uses `tmp_path`.
- [ ] Parametrize is used where multiple inputs test the same logic.
+- [ ] Slow tests (>1s) are marked with `@pytest.mark.slow`.
- [ ] Coverage threshold is met for new and modified modules.
- [ ] `just test` passes locally.
diff --git a/template/.claude/skills/pytest/references/ci-and-plugins.md b/template/.claude/skills/pytest/references/ci-and-plugins.md
index 05bfa96..293909f 100644
--- a/template/.claude/skills/pytest/references/ci-and-plugins.md
+++ b/template/.claude/skills/pytest/references/ci-and-plugins.md
@@ -184,6 +184,45 @@ pytest -n 4 # use exactly 4 workers
- **Use `pytest -k`** to run a subset matching a keyword.
- **Profile with `--durations=10`** to find the slowest tests and fixtures.
- **Use `monkeypatch` instead of real I/O** where possible.
+- **Mark and skip slow tests** during development — see below.
+
+### Automated slow-test detection
+
+The skill bundles scripts to automate slow-test management. Use them when
+`--durations` reveals tests that consistently take over a second.
+
+**Find slow tests:**
+
+```bash
+python /scripts/find_slow_tests.py --threshold 1.0
+```
+
+**Mark them automatically:**
+
+```bash
+python /scripts/find_slow_tests.py --threshold 1.0 \
+ | python /scripts/mark_slow_tests.py
+```
+
+This adds `@pytest.mark.slow` to each slow function and ensures `import pytest` is
+present. Use `--dry-run` on the marker script to preview changes first.
+
+**Split fast and slow runs in CI:**
+
+```yaml
+# GitHub Actions — two parallel jobs
+jobs:
+ fast-tests:
+ runs-on: ubuntu-latest
+ steps:
+ - run: pytest -m "not slow" -q
+ slow-tests:
+ runs-on: ubuntu-latest
+ steps:
+ - run: pytest -m "slow" -q
+```
+
+This keeps the fast feedback loop short while still running the full suite.
## Strict mode
diff --git a/template/.claude/skills/pytest/scripts/find_slow_tests.py b/template/.claude/skills/pytest/scripts/find_slow_tests.py
new file mode 100644
index 0000000..44b02f1
--- /dev/null
+++ b/template/.claude/skills/pytest/scripts/find_slow_tests.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python3
+"""Find slow-running test functions by analysing pytest duration output.
+
+Runs pytest with --durations=0 (all tests) and reports any test whose duration
+exceeds a configurable threshold. Output is a JSON array so other tools can
+consume it, plus a human-readable summary on stderr.
+
+Usage:
+ python find_slow_tests.py # default 1.0s threshold
+ python find_slow_tests.py --threshold 0.5 # 500ms threshold
+ python find_slow_tests.py --threshold 2.0 --top 5 # top 5 above 2s
+ python find_slow_tests.py --json-only # suppress human summary
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import re
+import subprocess
+import sys
+from dataclasses import asdict, dataclass
+
+
+@dataclass(frozen=True)
+class SlowTest:
+ """A test that exceeded the duration threshold."""
+
+ nodeid: str
+ duration: float
+ file_path: str
+ function_name: str
+ line_hint: str
+
+
+# pytest --durations output looks like:
+# 1.23s call tests/test_core.py::test_heavy_computation
+_DURATION_RE = re.compile(
+ r"^\s*(?P[\d.]+)s\s+(?Pcall|setup|teardown)\s+(?P.+)$"
+)
+
+
+def run_pytest_durations(test_path: str | None = None) -> str:
+ """Run pytest --durations=0 and return the raw stdout."""
+ cmd = [sys.executable, "-m", "pytest", "--durations=0", "-q", "--tb=no", "--no-header"]
+ if test_path:
+ cmd.append(test_path)
+
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
+ return result.stdout
+
+
+def parse_durations(output: str, threshold: float) -> list[SlowTest]:
+ """Parse pytest duration lines and return tests above the threshold."""
+ slow: list[SlowTest] = []
+
+ for line in output.splitlines():
+ match = _DURATION_RE.match(line)
+ if not match:
+ continue
+
+ # Only care about the 'call' phase — setup/teardown are fixture overhead
+ if match.group("phase") != "call":
+ continue
+
+ duration = float(match.group("duration"))
+ if duration < threshold:
+ continue
+
+ nodeid = match.group("nodeid").strip()
+ # nodeid format: tests/test_core.py::TestClass::test_method
+ # or: tests/test_core.py::test_function
+ parts = nodeid.split("::")
+ file_path = parts[0] if parts else nodeid
+ function_name = parts[-1] if len(parts) > 1 else ""
+
+ # Build a grep-friendly hint: "file_path:def function_name"
+ line_hint = f"{file_path}:def {function_name}"
+
+ slow.append(
+ SlowTest(
+ nodeid=nodeid,
+ duration=duration,
+ file_path=file_path,
+ function_name=function_name,
+ line_hint=line_hint,
+ )
+ )
+
+ # Sort slowest first
+ slow.sort(key=lambda t: t.duration, reverse=True)
+ return slow
+
+
+def main() -> None:
+ """Entry point for find_slow_tests."""
+ parser = argparse.ArgumentParser(
+ description="Find pytest tests that exceed a duration threshold."
+ )
+ parser.add_argument(
+ "--threshold",
+ type=float,
+ default=1.0,
+ help="Duration threshold in seconds (default: 1.0)",
+ )
+ parser.add_argument(
+ "--top",
+ type=int,
+ default=0,
+ help="Only show top N slowest tests (0 = all, default: 0)",
+ )
+ parser.add_argument(
+ "--test-path",
+ type=str,
+ default=None,
+ help="Path to a specific test file or directory (default: all tests)",
+ )
+ parser.add_argument(
+ "--json-only",
+ action="store_true",
+ help="Output JSON only, suppress human-readable summary",
+ )
+ args = parser.parse_args()
+
+ output = run_pytest_durations(args.test_path)
+ slow_tests = parse_durations(output, args.threshold)
+
+ if args.top > 0:
+ slow_tests = slow_tests[: args.top]
+
+ # JSON on stdout (avoid print — ruff T201 in repo template)
+ sys.stdout.write(json.dumps([asdict(t) for t in slow_tests], indent=2) + "\n")
+
+ # Human summary on stderr
+ if not args.json_only:
+ if not slow_tests:
+ sys.stderr.write(f"\n No tests exceeded {args.threshold}s threshold.\n")
+ else:
+ sys.stderr.write(
+ f"\n Found {len(slow_tests)} test(s) exceeding {args.threshold}s:\n\n"
+ )
+ for t in slow_tests:
+ marker = " SLOW " if t.duration >= args.threshold * 2 else " slow "
+ sys.stderr.write(f"{marker} {t.duration:6.2f}s {t.nodeid}\n")
+ sys.stderr.write("\n")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/template/.claude/skills/pytest/scripts/mark_slow_tests.py b/template/.claude/skills/pytest/scripts/mark_slow_tests.py
new file mode 100644
index 0000000..870da52
--- /dev/null
+++ b/template/.claude/skills/pytest/scripts/mark_slow_tests.py
@@ -0,0 +1,255 @@
+#!/usr/bin/env python3
+"""Add @pytest.mark.slow to test functions identified as slow.
+
+Reads a JSON array of slow tests (from find_slow_tests.py) and inserts the
+@pytest.mark.slow decorator on each function that does not already have it.
+Also ensures `import pytest` is present in modified files.
+
+Usage:
+ # Pipe from find_slow_tests.py
+ python find_slow_tests.py --threshold 1.0 | python mark_slow_tests.py
+
+ # Or from a saved JSON file
+ python mark_slow_tests.py --input slow_tests.json
+
+ # Dry-run to preview changes without writing
+ python mark_slow_tests.py --input slow_tests.json --dry-run
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import re
+import sys
+from dataclasses import dataclass
+from pathlib import Path
+
+
+@dataclass
+class PendingMark:
+ """A test function that needs the @pytest.mark.slow decorator."""
+
+ file_path: str
+ function_name: str
+ duration: float
+
+
+@dataclass
+class FileEdit:
+ """Tracks all edits to be made to a single file."""
+
+ path: Path
+ marks_to_add: list[PendingMark]
+
+
+def _stripped_lines(lines: list[str]) -> list[str]:
+ """Return lines without trailing newlines (for pattern matching)."""
+ return [ln.rstrip("\n") for ln in lines]
+
+
+def find_function_line(lines: list[str], function_name: str) -> int | None:
+ """Find the line index of a def statement matching function_name.
+
+ Returns the 0-based line index, or None if not found.
+ """
+ pattern = re.compile(rf"^\s*(?:async\s+)?def\s+{re.escape(function_name)}\s*\(")
+ for i, line in enumerate(lines):
+ if pattern.match(line):
+ return i
+ return None
+
+
+def already_has_slow_marker(lines: list[str], def_line: int) -> bool:
+ """Check whether the lines above a def already include @pytest.mark.slow."""
+ # Walk backwards from the def line looking for decorators
+ i = def_line - 1
+ while i >= 0:
+ stripped = lines[i].strip()
+ if stripped.startswith("@"):
+ if "pytest.mark.slow" in stripped:
+ return True
+ i -= 1
+ continue
+ # If we hit a non-decorator, non-blank line, stop
+ if stripped and not stripped.startswith("#"):
+ break
+ i -= 1
+ return False
+
+
+def has_pytest_import(lines: list[str]) -> bool:
+ """Check whether the file already imports pytest."""
+ for line in lines:
+ stripped = line.strip()
+ if stripped == "import pytest" or stripped.startswith("from pytest"):
+ return True
+ if re.match(r"^import\s+pytest\b", stripped):
+ return True
+ return False
+
+
+def find_import_insert_line(lines: list[str]) -> int:
+ """Find the best line to insert 'import pytest'.
+
+ Inserts after the last stdlib/third-party import block, or at the top of the
+ file (after docstrings and comments).
+ """
+ last_import_line = -1
+ for i, line in enumerate(lines):
+ stripped = line.strip()
+ if stripped.startswith(("import ", "from ")):
+ last_import_line = i
+
+ if last_import_line >= 0:
+ return last_import_line + 1
+
+ # No imports found — insert after initial comments/docstrings
+ for i, line in enumerate(lines):
+ stripped = line.strip()
+ if stripped and not stripped.startswith("#") and not stripped.startswith('"""'):
+ return i
+
+ return 0
+
+
+def _plan_decorator_insertions(
+ lines: list[str], stripped: list[str], marks_to_add: list[PendingMark], path: Path
+) -> tuple[dict[int, str], list[str]]:
+ """Compute line-index → decorator text and human-readable change lines."""
+ changes: list[str] = []
+ insertions: dict[int, str] = {}
+
+ for mark in marks_to_add:
+ def_line = find_function_line(stripped, mark.function_name)
+ if def_line is None:
+ changes.append(f" SKIP {mark.function_name} in {path} (def not found)")
+ continue
+
+ if already_has_slow_marker(stripped, def_line):
+ changes.append(f" SKIP {mark.function_name} in {path} (already marked slow)")
+ continue
+
+ indent = re.match(r"^(\s*)", lines[def_line]).group(1) # type: ignore[union-attr]
+ insertions[def_line] = f"{indent}@pytest.mark.slow\n"
+ changes.append(
+ f" ADD @pytest.mark.slow → {mark.function_name} ({mark.duration:.2f}s) in {path}"
+ )
+
+ return insertions, changes
+
+
+def _write_file_with_import_and_decorators(
+ path: Path,
+ lines: list[str],
+ stripped: list[str],
+ insertions: dict[int, str],
+ dry_run: bool,
+) -> list[str]:
+ """Build new file content with optional import and decorators; return extra changes."""
+ extra: list[str] = []
+ needs_import = not has_pytest_import(stripped)
+ import_line = find_import_insert_line(stripped) if needs_import else -1
+
+ new_lines: list[str] = []
+ import_inserted = False
+
+ for i, line in enumerate(lines):
+ if needs_import and i == import_line and not import_inserted:
+ new_lines.append("import pytest\n")
+ import_inserted = True
+ extra.append(f" ADD import pytest → {path}")
+
+ if i in insertions:
+ new_lines.append(insertions[i])
+
+ new_lines.append(line)
+
+ if not dry_run:
+ path.write_text("".join(new_lines), encoding="utf-8")
+
+ return extra
+
+
+def apply_marks(file_edit: FileEdit, dry_run: bool = False) -> list[str]:
+ """Apply @pytest.mark.slow to the identified functions in a file.
+
+ Returns a list of human-readable change descriptions.
+ """
+ path = file_edit.path
+ if not path.exists():
+ return [f" SKIP {path} (file not found)"]
+
+ lines = path.read_text(encoding="utf-8").splitlines(keepends=True)
+ stripped = _stripped_lines(lines)
+
+ insertions, changes = _plan_decorator_insertions(lines, stripped, file_edit.marks_to_add, path)
+
+ if not insertions:
+ return changes
+
+ extra = _write_file_with_import_and_decorators(path, lines, stripped, insertions, dry_run)
+ return changes + extra
+
+
+def main() -> None:
+ """Entry point for mark_slow_tests."""
+ parser = argparse.ArgumentParser(description="Add @pytest.mark.slow to slow test functions.")
+ parser.add_argument(
+ "--input",
+ type=str,
+ default=None,
+ help="Path to JSON file from find_slow_tests.py (default: read stdin)",
+ )
+ parser.add_argument(
+ "--dry-run",
+ action="store_true",
+ help="Show what would change without modifying files",
+ )
+ args = parser.parse_args()
+
+ raw = Path(args.input).read_text(encoding="utf-8") if args.input else sys.stdin.read()
+
+ try:
+ slow_tests = json.loads(raw)
+ except json.JSONDecodeError as e:
+ sys.stderr.write(f"Error: could not parse JSON input: {e}\n")
+ sys.exit(1)
+
+ if not slow_tests:
+ sys.stderr.write("No slow tests to mark.\n")
+ sys.exit(0)
+
+ # Group by file
+ file_edits: dict[str, FileEdit] = {}
+ for entry in slow_tests:
+ fp = entry["file_path"]
+ if fp not in file_edits:
+ file_edits[fp] = FileEdit(path=Path(fp), marks_to_add=[])
+ file_edits[fp].marks_to_add.append(
+ PendingMark(
+ file_path=fp,
+ function_name=entry["function_name"],
+ duration=entry["duration"],
+ )
+ )
+
+ # Apply marks
+ all_changes: list[str] = []
+ for file_edit in file_edits.values():
+ all_changes.extend(apply_marks(file_edit, dry_run=args.dry_run))
+
+ # Report
+ mode = "DRY RUN" if args.dry_run else "APPLIED"
+ sys.stderr.write(f"\n [{mode}] @pytest.mark.slow changes:\n\n")
+ for change in all_changes:
+ sys.stderr.write(f"{change}\n")
+ sys.stderr.write("\n")
+
+ added = sum(1 for c in all_changes if c.startswith(" ADD @pytest"))
+ skipped = sum(1 for c in all_changes if c.startswith(" SKIP"))
+ sys.stderr.write(f" Summary: {added} marker(s) added, {skipped} skipped\n")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/template/.claude/skills/python-code-quality/SKILL.md b/template/.claude/skills/python-code-quality/SKILL.md
new file mode 100644
index 0000000..023c24d
--- /dev/null
+++ b/template/.claude/skills/python-code-quality/SKILL.md
@@ -0,0 +1,113 @@
+---
+name: python-code-quality
+description: >-
+ Use this skill whenever the task involves Python code style, formatting, linting, type
+ checking, security scanning, or commit hygiene in this project. Triggers include:
+ configuring or running ruff (lint or format), setting up or modifying pre-commit hooks,
+ configuring or running basedpyright, running bandit or semgrep, fixing lint/type/security
+ errors, writing or updating pyproject.toml quality sections, CI pipeline steps for code
+ quality, or any question like "how do I enforce formatting", "why is ruff complaining",
+ "pre-commit isn't running", "type error I don't understand", or "security finding I need
+ to suppress". Also use proactively when writing new Python files that will need to pass
+ CI checks.
+---
+
+# Python Code Quality Skill
+
+This project enforces code quality through five tools:
+
+| Tool | Role | Config location |
+|---|---|---|
+| **ruff** | Lint + format (replaces flake8 / isort / black) | `[tool.ruff]` in `pyproject.toml` |
+| **pre-commit** | Git hook runner — gates every commit | `.pre-commit-config.yaml` |
+| **basedpyright** | Static type checker (strict pyright fork) | `[tool.basedpyright]` in `pyproject.toml` |
+| **bandit** | Security linter — fixed Python vulnerability patterns | `[tool.bandit]` in `pyproject.toml` |
+| **semgrep** | Pattern-based scanner + custom rules | `.semgrep.yml` |
+
+---
+
+## Quick reference
+
+### Run everything locally
+
+```bash
+pre-commit run --all-files # run all hooks on every file (recommended)
+
+# Or run each tool individually:
+ruff format . # auto-format
+ruff check --fix . # lint + auto-fix safe issues
+basedpyright # type-check
+bandit -c pyproject.toml -r src/ # security lint
+semgrep --config .semgrep.yml src/ # pattern scan
+```
+
+### Common failures and fast fixes
+
+| Symptom | Likely cause | Fix |
+|---|---|---|
+| `ruff` fails on unused import | `F401` rule | Remove import or `# noqa: F401` for intentional re-exports |
+| `ruff format` changes file | File not formatted | Run `ruff format .` and re-stage |
+| pre-commit hook not running | Hook not installed | Run `pre-commit install` |
+| pre-commit passes locally, fails in CI | Staged-only vs all-files mismatch | Run `pre-commit run --all-files` before pushing |
+| basedpyright `reportUnknownVariableType` | Missing annotation | Add type annotation; see `references/basedpyright.md` |
+| basedpyright `reportMissingImports` | Package not in venv | Install package or add stub; see `references/basedpyright.md` |
+| bandit `B[code]` finding | Security anti-pattern | Fix or add `# nosec B` with explanation |
+| semgrep finding | Code matches security pattern | Fix or add `# nosemgrep: ` inline |
+
+### CI ordering (fast-fail principle)
+
+Run cheapest checks first so failures surface quickly without wasting time on later stages:
+
+```
+ruff format --check → ruff check → bandit → semgrep → basedpyright → pytest
+```
+
+| Stage | Why here |
+|---|---|
+| `ruff format --check` | Instant — no point type-checking badly formatted code |
+| `ruff check` | Fast Rust linter — catches style and logic issues cheaply |
+| `bandit` | Fast fixed-rule security scan — pure AST analysis |
+| `semgrep` | Slower than bandit (fetches registry rules); runs after fast checks |
+| `basedpyright` | Needs full import graph resolved — slower, but before tests |
+| `pytest` | Most expensive — only run when everything above passes |
+
+---
+
+## Quick reference: where to go deeper
+
+Read the relevant reference file for configuration details, error code explanations,
+or CI integration specifics:
+
+| Topic | Reference file |
+|---------------------------------|----------------------------------------------------------------------|
+| ruff (lint + format) | [references/ruff.md](references/ruff.md) |
+| pre-commit hooks | [references/pre-commit.md](references/pre-commit.md) |
+| basedpyright (type checking) | [references/basedpyright.md](references/basedpyright.md) |
+| bandit (security linting) | [references/bandit.md](references/bandit.md) |
+| semgrep (pattern scanning) | [references/semgrep.md](references/semgrep.md) |
+| Complete config files | [references/complete-configs.md](references/complete-configs.md) |
+
+---
+
+## Adding a new tool
+
+1. Create `references/.md` using the template below.
+2. Add a row to the tools table at the top of this file.
+3. Add the tool's hook to `.pre-commit-config.yaml` (see `references/pre-commit.md`).
+4. Add the tool's `pyproject.toml` section to `references/complete-configs.md`.
+5. Insert the tool into the CI ordering table above with a rationale for placement.
+
+### New tool reference template
+
+```markdown
+#
+
+## What it does
+## Installation
+## pyproject.toml config (annotated)
+## Common error codes and fixes
+## Running
+## CI step
+## Pre-commit hook entry
+## Gotchas
+```
diff --git a/template/.claude/skills/python-code-quality/references/bandit.md b/template/.claude/skills/python-code-quality/references/bandit.md
new file mode 100644
index 0000000..a2df6a8
--- /dev/null
+++ b/template/.claude/skills/python-code-quality/references/bandit.md
@@ -0,0 +1,171 @@
+# Bandit
+
+Bandit is a security linter for Python. It scans source code for common security issues
+using a set of AST-based plugins, each targeting a specific vulnerability class.
+
+---
+
+## What it does
+
+- Statically analyses Python AST for security anti-patterns
+- Reports each finding with a severity (LOW / MEDIUM / HIGH) and a confidence
+ (LOW / MEDIUM / HIGH) so you can filter by signal strength
+- Does **not** execute code — pure static analysis
+
+---
+
+## Installation
+
+```bash
+pip install bandit # or: uv add --dev bandit
+```
+
+Verify:
+```bash
+bandit --version
+```
+
+---
+
+## pyproject.toml config (annotated)
+
+Bandit reads a limited set of keys from `[tool.bandit]`. Note that severity/confidence
+thresholds must be passed as **CLI flags** — they are not supported as pyproject.toml
+keys.
+
+```toml
+[tool.bandit]
+# Directories or files to scan (passed to -r).
+targets = ["src"]
+
+# Test IDs to skip project-wide. Use sparingly — prefer per-line nosec.
+# B101 = assert statement (valid in tests; exclude tests dir instead of skipping globally)
+# B104 = binding to 0.0.0.0 (acceptable in containerised services — document the reason)
+skips = ["B101"]
+
+# Paths to exclude from scanning (regex matched against file path).
+exclude_dirs = [".venv", "build", "dist", "tests"]
+```
+
+### Severity and confidence thresholds
+
+Thresholds are set via CLI flags, not pyproject.toml:
+
+| CLI flag | Meaning |
+|---|---|
+| `-l` | Report LOW+ severity (default: all) |
+| `-ll` | Report MEDIUM+ severity (recommended) |
+| `-lll` | Report HIGH severity only |
+| `-i` | Report LOW+ confidence |
+| `-ii` | Report MEDIUM+ confidence (recommended) |
+| `-iii` | Report HIGH confidence only |
+
+For CI, use `-ll -ii` (MEDIUM/MEDIUM) to filter noise without missing real issues:
+
+```bash
+bandit -c pyproject.toml -r src/ -ll -ii
+```
+
+---
+
+## Common issue codes and fixes
+
+| Code | Issue | Fix |
+|---|---|---|
+| `B101` | `assert` statement | Use explicit `if`/`raise` for runtime validation |
+| `B105` | Hardcoded password — string literal | Load from env var or secrets manager |
+| `B106` | Hardcoded password — function argument default | Use `None` default; load at runtime |
+| `B107` | Hardcoded password — function argument | Same as B106 |
+| `B108` | Probable insecure temp file | Use `tempfile.mkstemp()` or `tempfile.TemporaryFile()` |
+| `B110` | `try/except/pass` — silenced exception | Log or handle the exception explicitly |
+| `B201` | Flask debug mode in production | Never set `debug=True` outside local dev |
+| `B301` | `pickle` on untrusted data | Use `json` or `msgpack` for untrusted sources |
+| `B303` | MD5 / SHA1 for cryptography | Use `hashlib.sha256()` or better |
+| `B311` | `random` for security-sensitive use | Use the `secrets` module instead |
+| `B324` | Insecure hash function | Use SHA-256+ |
+| `B501` | SSL certificate verification disabled | Remove `verify=False` |
+| `B601` | `shell=True` in subprocess | Pass a list of args; never interpolate user input |
+| `B602` | `subprocess` with shell injection risk | Same as B601 |
+| `B608` | SQL string formatting — injection risk | Use parameterised queries |
+
+### Suppressing a finding inline
+
+```python
+result = subprocess.run(cmd, shell=True) # nosec B602
+```
+
+Always include the specific code in `# nosec`. A bare `# nosec` suppresses every finding
+on the line and makes the reasoning invisible to reviewers.
+
+To suppress a finding project-wide, add it to `skips` in `pyproject.toml` with a comment
+explaining why — e.g. `skips = ["B104"] # service runs in a container; 0.0.0.0 is correct`.
+
+---
+
+## Running bandit
+
+```bash
+# Basic scan using pyproject.toml config:
+bandit -c pyproject.toml -r src/
+
+# Recommended CI mode — MEDIUM severity and confidence minimum:
+bandit -c pyproject.toml -r src/ -ll -ii
+
+# JSON output for downstream tooling or artefact storage:
+bandit -c pyproject.toml -r src/ -f json -o bandit-report.json
+
+# Run only specific tests:
+bandit -r src/ -t B301,B303
+```
+
+Exit codes: `0` = no issues at or above threshold, `1` = issues found, `2` = bandit error.
+
+---
+
+## CI step (GitHub Actions)
+
+```yaml
+- name: Install bandit
+ run: pip install bandit
+
+- name: Security lint with bandit
+ run: bandit -c pyproject.toml -r src/ -ll -ii
+```
+
+See `SKILL.md` for the full CI ordering across all tools.
+
+---
+
+## Pre-commit hook entry
+
+```yaml
+- repo: https://github.com/PyCQA/bandit
+ rev: 1.8.3 # pin to a specific release; update with: pre-commit autoupdate
+ hooks:
+ - id: bandit
+ args: ["-c", "pyproject.toml", "-ll", "-ii"]
+ # Scope to src/ only — test code legitimately uses assert, subprocess, etc.
+ files: ^src/
+```
+
+---
+
+## Gotchas
+
+- **Exclude tests from scanning.** Test code legitimately uses `assert` (B101), raw
+ subprocess calls, and sometimes intentionally insecure patterns in fixtures. Add
+ `tests` to `exclude_dirs` in `[tool.bandit]` rather than globally skipping B101.
+- **Severity thresholds are CLI-only.** You cannot set `severity = "MEDIUM"` in
+ `[tool.bandit]` — bandit ignores unknown keys. Pass `-ll -ii` on the command line
+ (or in the pre-commit `args`).
+- **`# nosec` without a code is an anti-pattern.** Always write `# nosec B601` so
+ reviewers know exactly which finding is being suppressed and why.
+- **Bandit flags patterns, not intent.** A `random` call used for a non-security shuffle
+ will still trigger B311. Add `# nosec B311` with a comment explaining the non-security
+ use rather than globally skipping B311.
+- **Bandit is not a substitute for a full security audit.** It catches common
+ anti-patterns but misses logic-level vulnerabilities, dependency CVEs (use `pip-audit`
+ for those), and runtime issues.
+- **Overlap with semgrep.** Both tools flag issues like `eval`, insecure hashes, and SQL
+ injection. The overlap is intentional — each catches slightly different forms of the
+ same pattern. Do not remove bandit in favour of semgrep.
diff --git a/template/.claude/skills/python-code-quality/references/basedpyright.md b/template/.claude/skills/python-code-quality/references/basedpyright.md
new file mode 100644
index 0000000..e8fe363
--- /dev/null
+++ b/template/.claude/skills/python-code-quality/references/basedpyright.md
@@ -0,0 +1,247 @@
+# basedpyright
+
+basedpyright is a community fork of pyright (Microsoft's static type checker for Python).
+It adds stricter defaults, clearer error messages, and additional rules over vanilla pyright.
+
+---
+
+## What it does
+
+- Performs static type analysis — no code is executed
+- Checks annotations, inferred types, call signatures, narrowing, exhaustiveness, and more
+- Reads `[tool.basedpyright]` from `pyproject.toml`
+
+---
+
+## Installation
+
+```bash
+pip install basedpyright # or: uv add --dev basedpyright
+```
+
+Verify:
+```bash
+basedpyright --version
+```
+
+---
+
+## pyproject.toml config (annotated)
+
+```toml
+[tool.basedpyright]
+# Python version used for type checking (e.g. affects which stdlib types are available).
+pythonVersion = "3.11"
+
+# First-party source directories. Only these paths are type-checked.
+include = ["src"]
+
+# Paths excluded from analysis.
+exclude = ["**/__pycache__", ".venv", "build", "dist"]
+
+# Virtual environment resolution.
+# venvPath = the DIRECTORY that contains your venv folder (usually the project root).
+# venv = the NAME of the venv folder inside venvPath.
+# Together they tell basedpyright where to find installed third-party packages.
+# Example: venvPath = "." and venv = ".venv" resolves to ./.venv
+venvPath = "."
+venv = ".venv"
+
+# ── Strictness level ────────────────────────────────────────────────────────
+# Options: "off" | "basic" | "standard" | "strict" | "all"
+# Recommendation: start at "standard", move to "strict" once the codebase is annotated.
+typeCheckingMode = "standard"
+
+# ── Individual rule overrides ────────────────────────────────────────────────
+# Each rule can be set to "none" | "information" | "warning" | "error"
+# independently of typeCheckingMode. Uncomment to customise:
+
+# reportUnknownVariableType = "error" # flag x: Unknown
+# reportUnknownMemberType = "error" # flag unknown attribute types
+# reportMissingTypeArgument = "warning" # flag bare list, dict etc.
+# reportUnusedImport = "warning" # overlaps with ruff F401
+# reportUninitializedInstanceVariable = "warning"
+```
+
+### Strictness levels explained
+
+| Level | What it checks |
+|---|---|
+| `off` | Type checking disabled |
+| `basic` | Only obvious errors: undefined names, wrong argument counts |
+| `standard` | Full pyright checks — good default for most projects |
+| `strict` | All checks; full annotation coverage required |
+| `all` | basedpyright-specific extras on top of `strict` (may have false positives) |
+
+**Recommended progression:** start at `standard`, fix all errors, then move to `strict`.
+Use per-file overrides (below) to keep `strict` globally while granting exceptions for
+tests or legacy modules.
+
+---
+
+## Per-file overrides
+
+Relax or tighten type checking for specific directories:
+
+```toml
+[tool.basedpyright]
+typeCheckingMode = "strict"
+
+[[tool.basedpyright.executionEnvironments]]
+root = "tests"
+typeCheckingMode = "basic" # test fixtures are often untyped; relax here
+
+[[tool.basedpyright.executionEnvironments]]
+root = "scripts"
+typeCheckingMode = "standard"
+```
+
+For a single line, use an inline ignore (last resort — prefer fixing the root cause):
+
+```python
+result = some_untyped_library.call() # type: ignore[no-untyped-call]
+```
+
+Always include the specific error code in `type: ignore`. Bare `# type: ignore` silences
+all errors on the line and makes the intent invisible to reviewers.
+
+---
+
+## Common error codes and fixes
+
+| Code | Meaning | Fix |
+|---|---|---|
+| `reportMissingImports` | Package not installed in the venv | Install the package; or check `venvPath`/`venv` config |
+| `reportMissingTypeStubs` | Package has no type information | Install `types-` stub; see below |
+| `reportUnknownVariableType` | Type inferred as `Unknown` | Add an explicit annotation |
+| `reportUnknownMemberType` | Attribute type is `Unknown` | Annotate the class attribute |
+| `reportUnknownArgumentType` | Argument type cannot be inferred | Annotate the caller or the callee |
+| `reportReturnType` | Return value doesn't match declared return type | Fix annotation or return value |
+| `reportAttributeAccessIssue` | Attribute doesn't exist on the type | Check spelling; use `hasattr` guard |
+| `reportOperatorIssue` | Operator not defined for these types | Narrow the type before the operation |
+| `reportIndexIssue` | Index or key type is invalid | Ensure key type matches the container |
+| `reportCallIssue` | Object is not callable | Check the type; unwrap Optional before calling |
+
+### Handling third-party libraries without type stubs
+
+**Option 1 — Install community stubs** (preferred when stubs exist):
+
+```bash
+pip install types-requests types-PyYAML types-python-dateutil
+```
+
+**Option 2 — Suppress missing-stubs warnings project-wide** (when no stubs exist):
+
+```toml
+[tool.basedpyright]
+reportMissingTypeStubs = "none" # or "warning" to see it without failing CI
+```
+
+**Option 3 — Suppress per import** (for one-off cases):
+
+```python
+import untyped_lib # type: ignore[import-untyped]
+```
+
+**Option 4 — Generate a stub skeleton**:
+
+```bash
+basedpyright --createstub some_package # writes a stub to typestubs/some_package/
+```
+
+Commit the generated stubs to the repo and add `stubPath = "typestubs"` to
+`[tool.basedpyright]` so basedpyright picks them up automatically.
+
+---
+
+## Running basedpyright
+
+```bash
+# Check entire project (reads pyproject.toml automatically):
+basedpyright
+
+# Check a specific file:
+basedpyright src/mymodule.py
+
+# Verbose output — shows which config file was loaded, useful for debugging:
+basedpyright --verbose
+
+# JSON output — for tooling integration or counting errors:
+basedpyright --outputjson | python -c "import sys,json; d=json.load(sys.stdin); print(d['summary'])"
+
+# Count current errors (useful for tracking incremental adoption progress):
+basedpyright 2>&1 | grep " error" | wc -l
+```
+
+Exit codes: `0` = no errors, `1` = type errors found, `2` = fatal configuration error.
+
+---
+
+## Incremental adoption strategy
+
+For an existing codebase with many type errors, adopt in stages:
+
+1. **`typeCheckingMode = "basic"`** — fix the small set of obvious errors first.
+2. **`typeCheckingMode = "standard"`** — fix errors, use `executionEnvironments` to
+ relax specific directories (tests, scripts, legacy modules) temporarily.
+3. **`typeCheckingMode = "strict"`** — requires full annotation coverage across `src/`.
+ Tackle module by module. Add `# type: ignore[]` as a last resort.
+4. **`typeCheckingMode = "all"`** — evaluate locally; some rules may be too aggressive
+ for production CI. Keep at `"strict"` in CI unless all `"all"` rules are clean.
+
+---
+
+## CI step (GitHub Actions)
+
+```yaml
+- name: Install dependencies
+ run: pip install -r requirements.txt # basedpyright must be in here, or:
+ # run: uv sync
+
+- name: Type check with basedpyright
+ run: basedpyright
+```
+
+basedpyright needs the project's dependencies installed so it can resolve third-party
+imports. If it reports `reportMissingImports` on every import, the venv is not being
+found — check that `venvPath` and `venv` in `pyproject.toml` match the actual venv path,
+or that the CI runner has the packages on `PATH`.
+
+---
+
+## Pre-commit hook entry
+
+Run as a `local` hook to use the project's installed venv:
+
+```yaml
+- repo: local
+ hooks:
+ - id: basedpyright
+ name: basedpyright
+ entry: basedpyright
+ language: system # uses basedpyright from the active venv
+ types: [python]
+ pass_filenames: false # analyses the whole project, not individual files
+```
+
+Requires `basedpyright` to be installed in the active environment before committing.
+In CI, install dependencies before running pre-commit (see `references/pre-commit.md`).
+
+---
+
+## Gotchas
+
+- **`venvPath` vs `venv` — a common confusion.** `venvPath` is the *directory that
+ contains* the venv, and `venv` is the *name of the venv folder*. For a project at
+ `/my/project` with a venv at `/my/project/.venv`, set `venvPath = "."` and
+ `venv = ".venv"`. Setting `venvPath = ".venv"` is wrong.
+- **`reportMissingImports` on all third-party imports** usually means the venv isn't
+ found. Double-check `venvPath`/`venv` config or confirm packages are installed.
+- **basedpyright vs pyright.** A colleague using vanilla pyright will see nearly
+ identical errors. basedpyright adds rules like `reportUnreachable`. Both tools read
+ the same `[tool.basedpyright]` / `[tool.pyright]` config keys.
+- **`pass_filenames: false` is required in pre-commit.** basedpyright resolves the full
+ import graph across all files; giving it individual filenames breaks cross-module
+ type inference.
+- **`typeCheckingMode = "all"` may produce false positives.** Use `"strict"` in CI
+ and evaluate `"all"` locally before committing to it.
diff --git a/template/.claude/skills/python-code-quality/references/complete-configs.md b/template/.claude/skills/python-code-quality/references/complete-configs.md
new file mode 100644
index 0000000..bda78c2
--- /dev/null
+++ b/template/.claude/skills/python-code-quality/references/complete-configs.md
@@ -0,0 +1,257 @@
+# Complete configurations
+
+Ready-to-use configuration files for all tools in this project. Copy these into
+your project and adjust versions, paths, and rule selections as needed.
+
+---
+
+## pyproject.toml — all tool sections
+
+```toml
+# ── Ruff ────────────────────────────────────────────────────────────────────
+[tool.ruff]
+target-version = "py311"
+src = ["src"]
+exclude = [".git", ".venv", "__pycache__", "build", "dist"]
+
+[tool.ruff.lint]
+select = ["E", "W", "F", "I", "UP", "B", "SIM"]
+ignore = ["E501"]
+fixable = ["ALL"]
+
+[tool.ruff.lint.per-file-ignores]
+"tests/**/*.py" = ["PLR2004", "F401"]
+"**/__init__.py" = ["F401"]
+
+[tool.ruff.format]
+quote-style = "double"
+indent-style = "space"
+skip-magic-trailing-comma = false
+line-ending = "lf"
+line-length = 88
+
+# ── basedpyright ─────────────────────────────────────────────────────────────
+[tool.basedpyright]
+pythonVersion = "3.11"
+include = ["src"]
+exclude = ["**/__pycache__", ".venv", "build", "dist"]
+venvPath = "."
+venv = ".venv"
+typeCheckingMode = "standard"
+
+# ── Bandit ───────────────────────────────────────────────────────────────────
+[tool.bandit]
+targets = ["src"]
+skips = [] # add rule IDs here only with a comment explaining why
+exclude_dirs = [".venv", "build", "dist", "tests"]
+# Note: severity/confidence thresholds are CLI flags (-ll -ii), not pyproject keys.
+```
+
+---
+
+## .pre-commit-config.yaml — all hooks
+
+```yaml
+minimum_pre_commit_version: "3.0.0"
+
+default_language_version:
+ python: python3.11
+
+repos:
+ # ── Ruff: lint + format ──────────────────────────────────────────────────
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.9.0
+ hooks:
+ - id: ruff
+ args: ["--fix"]
+ - id: ruff-format
+
+ # ── Bandit: security lint ─────────────────────────────────────────────────
+ - repo: https://github.com/PyCQA/bandit
+ rev: 1.8.3
+ hooks:
+ - id: bandit
+ args: ["-c", "pyproject.toml", "-ll", "-ii"]
+ files: ^src/
+
+ # ── Semgrep: pattern-based security scan ─────────────────────────────────
+ - repo: https://github.com/semgrep/semgrep
+ rev: v1.60.0
+ hooks:
+ - id: semgrep
+ args: ["--config", "p/python", "--config", ".semgrep.yml", "--severity", "ERROR"]
+ files: ^src/
+
+ # ── basedpyright: type checking ──────────────────────────────────────────
+ # Requires basedpyright to be installed in the active venv.
+ - repo: local
+ hooks:
+ - id: basedpyright
+ name: basedpyright
+ entry: basedpyright
+ language: system
+ types: [python]
+ pass_filenames: false
+
+ # ── General hygiene ──────────────────────────────────────────────────────
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v5.0.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-yaml
+ - id: check-toml
+ - id: check-merge-conflict
+ - id: debug-statements
+```
+
+---
+
+## .semgrep.yml — local custom rules
+
+```yaml
+# .semgrep.yml
+# Local project-specific rules. Registry packs (p/python etc.) are passed
+# as --config flags on the CLI, not listed here.
+#
+# Usage:
+# semgrep --config p/python --config p/owasp-top-ten --config .semgrep.yml src/
+
+rules:
+ - id: no-eval
+ pattern: eval(...)
+ message: >
+ eval() executes arbitrary code. Use ast.literal_eval() for safe data
+ parsing, or refactor to avoid dynamic evaluation entirely.
+ languages: [python]
+ severity: ERROR
+
+ - id: no-hardcoded-secrets-in-jwt
+ patterns:
+ - pattern: jwt.encode(..., "$SECRET", ...)
+ - pattern-not: jwt.encode(..., os.environ.get(...), ...)
+ message: Hardcoded JWT secret. Load from environment variable or secrets manager.
+ languages: [python]
+ severity: ERROR
+```
+
+---
+
+## GitHub Actions workflow — full CI pipeline
+
+```yaml
+# .github/workflows/ci.yml
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+jobs:
+ # ── Fast checks: format + lint + security ───────────────────────────────
+ quality:
+ name: Code Quality
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+ cache: "pip"
+
+ - name: Install quality tools
+ run: pip install ruff bandit semgrep
+
+ - name: Ruff — format check
+ run: ruff format --check .
+
+ - name: Ruff — lint
+ run: ruff check .
+
+ - name: Bandit — security lint
+ run: bandit -c pyproject.toml -r src/ -ll -ii
+
+ - name: Cache semgrep rules
+ uses: actions/cache@v4
+ with:
+ path: ~/.semgrep/cache
+ key: semgrep-${{ hashFiles('.semgrep.yml') }}
+
+ - name: Semgrep — pattern scan
+ run: |
+ semgrep --config p/python \
+ --config p/owasp-top-ten \
+ --config .semgrep.yml \
+ --severity ERROR \
+ src/
+
+ # ── Type checking ────────────────────────────────────────────────────────
+ typecheck:
+ name: Type Check
+ runs-on: ubuntu-latest
+ needs: quality # only run if quality checks pass
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+ cache: "pip"
+
+ - name: Install dependencies
+ run: |
+ pip install -r requirements.txt # must include basedpyright
+
+ - name: basedpyright — type check
+ run: basedpyright
+
+ # ── Tests ────────────────────────────────────────────────────────────────
+ test:
+ name: Tests (Python ${{ matrix.python-version }})
+ runs-on: ubuntu-latest
+ needs: [quality, typecheck] # only run if both upstream jobs pass
+
+ strategy:
+ matrix:
+ python-version: ["3.11", "3.12"]
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ cache: "pip"
+
+ - name: Install dependencies
+ run: pip install -r requirements.txt
+
+ - name: Run pytest with coverage
+ run: pytest --maxfail=1 --disable-warnings --cov=src --cov-report=xml
+
+ - name: Upload coverage
+ uses: codecov/codecov-action@v4
+ with:
+ files: coverage.xml
+```
+
+---
+
+## Version update checklist
+
+When updating tool versions, change them in all three places:
+
+| Tool | pyproject.toml | .pre-commit-config.yaml | requirements.txt / pyproject deps |
+|---|---|---|---|
+| ruff | `target-version` (Python, not ruff) | `rev: v0.9.0` | `ruff>=0.9.0` |
+| bandit | — | `rev: 1.8.3` | `bandit>=1.8.3` |
+| semgrep | — | `rev: v1.60.0` | `semgrep>=1.60.0` |
+| basedpyright | `pythonVersion` (Python, not tool) | (local hook — no rev) | `basedpyright>=1.x` |
+| pre-commit-hooks | — | `rev: v5.0.0` | — |
diff --git a/template/.claude/skills/python-code-quality/references/pre-commit.md b/template/.claude/skills/python-code-quality/references/pre-commit.md
new file mode 100644
index 0000000..6eaee4f
--- /dev/null
+++ b/template/.claude/skills/python-code-quality/references/pre-commit.md
@@ -0,0 +1,221 @@
+# pre-commit
+
+pre-commit is a framework for managing Git hooks. It runs configured tools automatically
+before each commit, blocking the commit if any check fails.
+
+---
+
+## What it does
+
+- Installs hooks into `.git/hooks/` on `pre-commit install`
+- On `git commit`, runs each hook against staged files only (fast)
+- In CI, run against all files with `pre-commit run --all-files`
+
+---
+
+## Installation
+
+```bash
+pip install pre-commit # or: uv add --dev pre-commit
+pre-commit install # installs the git hook — run once per clone
+```
+
+Verify the hook was installed:
+```bash
+cat .git/hooks/pre-commit # should reference pre-commit
+```
+
+---
+
+## Complete .pre-commit-config.yaml
+
+This is the canonical config for all tools in this project. See
+`references/complete-configs.md` for a copy you can paste directly.
+
+```yaml
+# Minimum pre-commit version required.
+minimum_pre_commit_version: "3.0.0"
+
+# Default Python version used when a hook doesn't specify one.
+default_language_version:
+ python: python3.11
+
+repos:
+ # ── Ruff: lint + format ──────────────────────────────────────────────────
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.9.0 # pin to a specific tag
+ hooks:
+ - id: ruff
+ args: ["--fix"] # auto-fix safe issues on commit
+ - id: ruff-format # auto-format on commit
+
+ # ── Bandit: security lint ─────────────────────────────────────────────────
+ - repo: https://github.com/PyCQA/bandit
+ rev: 1.8.3
+ hooks:
+ - id: bandit
+ args: ["-c", "pyproject.toml"]
+ # Scope to src/ only — test code legitimately uses assert, subprocess, etc.
+ files: ^src/
+
+ # ── Semgrep: pattern-based security scan ─────────────────────────────────
+ - repo: https://github.com/semgrep/semgrep
+ rev: v1.60.0 # pin to a specific release
+ hooks:
+ - id: semgrep
+ args: ["--config", ".semgrep.yml", "--error"]
+ files: ^src/
+ # pass_filenames: true is correct for per-file analysis.
+ # Cross-file taint rules require running semgrep outside pre-commit (in CI).
+
+ # ── basedpyright: type checking ──────────────────────────────────────────
+ # Runs as a `local` hook so it uses the project's installed venv.
+ # Requires basedpyright to be installed: pip install basedpyright
+ - repo: local
+ hooks:
+ - id: basedpyright
+ name: basedpyright
+ entry: basedpyright
+ language: system # uses whatever basedpyright is on PATH
+ types: [python]
+ pass_filenames: false # basedpyright analyses the whole project graph
+
+ # ── General hygiene ──────────────────────────────────────────────────────
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v5.0.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-yaml
+ - id: check-toml
+ - id: check-merge-conflict
+ - id: debug-statements # catches leftover breakpoint() / pdb.set_trace()
+```
+
+### Hook ordering rationale
+
+Hooks run in the order listed. Fast, auto-fixing hooks go first so the developer sees
+clean output; slow analysis hooks (basedpyright) go last.
+
+```
+ruff (fix + format) → bandit → semgrep → basedpyright → hygiene
+```
+
+---
+
+## Running pre-commit
+
+```bash
+# Run all hooks against all files (use after install or config changes):
+pre-commit run --all-files
+
+# Run a specific hook only:
+pre-commit run ruff --all-files
+pre-commit run bandit --all-files
+pre-commit run semgrep --all-files
+pre-commit run basedpyright --all-files
+
+# Run against staged files only (mirrors what git commit does):
+pre-commit run
+
+# Run against a specific file:
+pre-commit run --files src/mymodule.py
+```
+
+---
+
+## Skipping hooks
+
+**Skip all hooks for one commit** (use very rarely — e.g. emergency hotfix):
+```bash
+git commit --no-verify -m "emergency: revert broken deploy"
+```
+
+**Skip a specific hook for one commit:**
+```bash
+SKIP=basedpyright git commit -m "wip: partially typed module"
+SKIP=semgrep,bandit git commit -m "temp: adding fixture with insecure pattern"
+```
+
+**Exclude files permanently** in the hook config:
+```yaml
+- id: ruff
+ exclude: "^tests/fixtures/" # regex matched against the file path
+```
+
+---
+
+## Updating hook versions
+
+Always pin `rev:` to a tagged release. Update intentionally:
+
+```bash
+# Update all hooks to latest tagged release:
+pre-commit autoupdate
+
+# Update a single repo:
+pre-commit autoupdate --repo https://github.com/astral-sh/ruff-pre-commit
+```
+
+After updating, run `pre-commit run --all-files` to confirm nothing broke.
+
+---
+
+## CI integration (GitHub Actions)
+
+Use the dedicated action — it caches hook environments automatically:
+
+```yaml
+- name: Run pre-commit
+ uses: pre-commit/action@v3.0.1
+```
+
+This caches `~/.cache/pre-commit` keyed on the hash of `.pre-commit-config.yaml`.
+
+**Important for `language: system` hooks** (basedpyright): the hook runs whatever is on
+`PATH`, so project dependencies must be installed *before* pre-commit runs:
+
+```yaml
+- name: Install Python dependencies
+ run: pip install -r requirements.txt # or: uv sync — must include basedpyright
+
+- name: Run pre-commit
+ uses: pre-commit/action@v3.0.1
+```
+
+If you run individual tool steps (ruff, basedpyright, etc.) as separate CI jobs, the
+pre-commit job is optional but still useful as a final gate — it catches hooks that
+aren't individually tested.
+
+---
+
+## Adding a new hook
+
+1. Find the hook repo at [pre-commit.com/hooks](https://pre-commit.com/hooks.html) or
+ the tool's own docs.
+2. Add a `- repo:` block to `.pre-commit-config.yaml` in the appropriate position
+ (fast/auto-fixing hooks first, slow analysis hooks last).
+3. Pin `rev:` to a tagged release — never `HEAD` or a branch name.
+4. Run `pre-commit run --all-files` to validate.
+5. Add the tool to the table in `SKILL.md` and create `references/.md`.
+6. Copy the updated full config to `references/complete-configs.md`.
+
+---
+
+## Gotchas
+
+- **Staged-only mode can hide issues.** On commit, pre-commit checks staged files only.
+ Unstaged changes interacting with staged ones can produce a passing local commit that
+ fails CI (which runs `--all-files`). Always run `pre-commit run --all-files` before
+ pushing to a shared branch.
+- **`language: system` hooks depend on PATH.** The basedpyright hook uses whatever
+ `basedpyright` is on `PATH`. If your venv isn't activated, it either uses a wrong
+ version or fails entirely. In CI, install dependencies before running pre-commit.
+- **Cache in `~/.cache/pre-commit`.** If a hook behaves strangely after a version bump,
+ clear it: `pre-commit clean`. Then re-run `pre-commit install`.
+- **`pass_filenames: false` is required for whole-project tools.** basedpyright resolves
+ the full import graph; passing individual filenames breaks cross-module type inference.
+ Semgrep's per-file mode (`pass_filenames: true`) is fine for single-file rules but
+ won't catch cross-file taint flows — run those in CI without pre-commit.
+- **`rev` must be a tag.** pre-commit warns if you use a branch; tags guarantee
+ reproducibility across machines and CI runs.
diff --git a/template/.claude/skills/python-code-quality/references/ruff.md b/template/.claude/skills/python-code-quality/references/ruff.md
new file mode 100644
index 0000000..0723c73
--- /dev/null
+++ b/template/.claude/skills/python-code-quality/references/ruff.md
@@ -0,0 +1,203 @@
+# Ruff
+
+Ruff is a fast Python linter and formatter written in Rust. It replaces flake8, isort,
+and black in a single tool.
+
+---
+
+## What it does
+
+- **`ruff check`** — lints: enforces rules (unused imports, style, complexity, bugs, etc.)
+- **`ruff format`** — formats: opinionated code formatter (Black-compatible output)
+
+Both read from `[tool.ruff]` in `pyproject.toml`.
+
+---
+
+## Installation
+
+```bash
+pip install ruff # or: uv add --dev ruff
+```
+
+Verify:
+```bash
+ruff --version
+```
+
+---
+
+## pyproject.toml config (annotated)
+
+```toml
+[tool.ruff]
+# Target Python version — affects which syntax is valid and which rules apply.
+# Keep in sync with the minimum Python version the project supports.
+target-version = "py311"
+
+# Directories ruff will scan. Excludes generated, vendored, or build dirs.
+src = ["src"]
+exclude = [
+ ".git",
+ ".venv",
+ "__pycache__",
+ "build",
+ "dist",
+]
+
+[tool.ruff.lint]
+# Rule sets to enable. Add a prefix letter to enable the entire ruleset.
+# E/W = pycodestyle errors/warnings
+# F = pyflakes (undefined names, unused imports)
+# I = isort (import order) — replaces standalone isort
+# UP = pyupgrade (modernise syntax for target Python version)
+# B = flake8-bugbear (likely bugs and design issues)
+# SIM = flake8-simplify (suggest simpler expressions)
+# S = flake8-bandit (security — lightweight overlap with bandit)
+select = ["E", "W", "F", "I", "UP", "B", "SIM"]
+
+# Rules to ignore project-wide.
+# E501 = line too long — controlled by the formatter, not the linter.
+ignore = ["E501"]
+
+# Allow autofix for all enabled rules when running `ruff check --fix`.
+fixable = ["ALL"]
+unfixable = []
+
+# Per-file rule overrides. Use sparingly — prefer fixing the root cause.
+[tool.ruff.lint.per-file-ignores]
+# Test files: assert is valid (pytest), magic values are fine, fixtures look unused.
+# S101 requires the S ruleset to be in `select` above — add "S" if you want it.
+"tests/**/*.py" = ["PLR2004", "F401"]
+# __init__.py: re-exports don't need to be explicitly used.
+"**/__init__.py" = ["F401"]
+
+[tool.ruff.format]
+# Double quotes matches Black's default.
+quote-style = "double"
+# Spaces, not tabs.
+indent-style = "space"
+# Preserve magic trailing commas (they affect how multi-line structures are formatted).
+skip-magic-trailing-comma = false
+# Normalise line endings to LF on all platforms.
+line-ending = "lf"
+# Line length for the formatter. Default is 88 (same as Black).
+# If you change this, also set line-length under [tool.ruff.lint] for E501.
+line-length = 88
+```
+
+---
+
+## Common rule codes and fixes
+
+| Code | Meaning | Fix |
+|---|---|---|
+| `F401` | Unused import | Remove import; `# noqa: F401` only for intentional re-exports |
+| `F811` | Redefinition of unused name | Remove or rename the duplicate |
+| `F841` | Local variable assigned but never used | Remove assignment or rename to `_` |
+| `E711` | `== None` instead of `is None` | Use `is None` / `is not None` |
+| `E712` | `== True` instead of `is True` | Use `is True` or just the boolean directly |
+| `I001` | Import block unsorted | Run `ruff check --fix` to auto-sort |
+| `UP006` | Use `list` instead of `List` (3.9+) | Update type annotation |
+| `UP007` | Use `X \| Y` instead of `Optional[X]` (3.10+) | Update union syntax |
+| `B006` | Mutable default argument | Use `None` default + guard in body |
+| `B007` | Loop variable unused | Rename to `_` |
+| `B008` | Function call as default argument | Move call inside the function body |
+| `SIM108` | Ternary can replace if/else | Simplify to `x = a if cond else b` |
+| `SIM117` | Nested `with` can be merged | Use `with a(), b():` |
+
+### Looking up an unfamiliar rule
+
+```bash
+ruff rule F401 # prints full explanation of the rule
+ruff rule --all # lists every available rule with description
+```
+
+### Suppressing a rule inline
+
+```python
+import os # noqa: F401 ← suppress one specific rule (preferred)
+import os # noqa ← suppress all rules on this line (avoid)
+```
+
+Prefer adding an entry to `per-file-ignores` in `pyproject.toml` over scattering
+`# noqa` across files — it keeps suppression decisions visible and reviewable.
+
+---
+
+## Running ruff
+
+```bash
+# Check (no changes, exits non-zero on violations):
+ruff check .
+
+# Check and auto-fix safe issues:
+ruff check --fix .
+
+# Show a summary of which rules fired most (useful for tuning config):
+ruff check --statistics .
+
+# Format (rewrites files in place):
+ruff format .
+
+# Format check — CI mode: no changes, exits non-zero if formatting needed:
+ruff format --check .
+
+# Show exactly what the formatter would change without writing:
+ruff format --diff .
+
+# Check a single file:
+ruff check src/mymodule.py
+```
+
+---
+
+## CI step (GitHub Actions)
+
+```yaml
+- name: Install ruff
+ run: pip install ruff
+
+- name: Ruff — format check
+ run: ruff format --check .
+
+- name: Ruff — lint
+ run: ruff check .
+```
+
+Always run `ruff format --check` before `ruff check` — formatting failures surface
+first without wasting time on lint output.
+
+With `cache: 'pip'` on `actions/setup-python`, repeated installs are fast.
+
+---
+
+## Pre-commit hook entry
+
+```yaml
+- repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.9.0 # pin to a specific release; update with: pre-commit autoupdate
+ hooks:
+ - id: ruff
+ args: ["--fix"] # auto-fix on commit so the developer sees clean diffs
+ - id: ruff-format # auto-format on commit
+```
+
+---
+
+## Gotchas
+
+- **`ruff format` and `ruff check` are separate concerns.** A file can pass linting but
+ still need formatting, and vice versa. Always run both.
+- **`--fix` in local dev, `--check` in CI.** Using `--fix` in CI silently rewrites files;
+ the pipeline appears to pass but the rewrite never gets committed. Use check mode in CI.
+- **`target-version` drives UP rules.** `UP006` (drop `List`/`Dict`) only fires at
+ `py39+`. Keep `target-version` in sync with your actual minimum Python.
+- **isort is built in via the `I` ruleset.** Do not also run standalone isort — they will
+ conflict on import ordering decisions.
+- **`S` ruleset (flake8-bandit) overlaps with bandit.** If you add `"S"` to `select`,
+ expect duplicate findings. Either add it and accept overlap, or omit it and rely on
+ bandit for security rules. If you add `"S"`, also update `per-file-ignores` for tests
+ to include `"S101"` (assert statements).
+- **Line length must be set consistently.** If you change `line-length` from 88, set it
+ in *both* `[tool.ruff.format]` and `[tool.ruff.lint]` (for `E501` if you enable it).
diff --git a/template/.claude/skills/python-code-quality/references/semgrep.md b/template/.claude/skills/python-code-quality/references/semgrep.md
new file mode 100644
index 0000000..13cafed
--- /dev/null
+++ b/template/.claude/skills/python-code-quality/references/semgrep.md
@@ -0,0 +1,226 @@
+# Semgrep
+
+Semgrep is a fast, multi-language static analysis tool that matches AST-aware code
+patterns. It complements bandit: bandit targets a fixed catalogue of Python security
+rules, while semgrep lets you use curated registries and write project-specific custom
+rules.
+
+---
+
+## What it does
+
+- Matches structural code patterns without executing code
+- Ships with curated rule registries (`p/python`, `p/owasp-top-ten`, `p/security-audit`)
+- Supports custom rules written in YAML — the main value-add over bandit
+- Reports findings with severity, rule ID, file, and line number
+
+---
+
+## Installation
+
+```bash
+pip install semgrep # or: uv add --dev semgrep
+```
+
+The PyPI package bundles the semgrep engine — no separate binary needed.
+
+Verify:
+```bash
+semgrep --version
+```
+
+---
+
+## Configuration
+
+Semgrep does **not** read `pyproject.toml`. All configuration lives in `.semgrep.yml`
+(or a `.semgrep/` directory) at the project root and is passed via `--config`.
+
+---
+
+## .semgrep.yml (annotated)
+
+```yaml
+# .semgrep.yml — rule configuration file at the project root.
+#
+# Registry packs (p/python etc.) cannot be embedded directly in this file —
+# they must be passed via --config on the CLI or in pre-commit args.
+# This file is for LOCAL custom rules only.
+#
+# To use registry packs alongside local rules, pass multiple --config flags:
+# semgrep --config p/python --config p/owasp-top-ten --config .semgrep.yml src/
+
+rules:
+ # ── Example custom rule ──────────────────────────────────────────────────
+ # Custom rules give semgrep its real advantage over bandit.
+ # Save additional rules in .semgrep/rules/.yml and reference them
+ # with: semgrep --config .semgrep/rules/ src/
+
+ - id: no-hardcoded-jwt-secret
+ patterns:
+ - pattern: jwt.encode(..., "$SECRET", ...)
+ - pattern-not: jwt.encode(..., os.environ.get(...), ...)
+ message: >
+ Hardcoded JWT secret. Load the secret from an environment variable
+ or secrets manager instead.
+ languages: [python]
+ severity: ERROR
+
+ - id: no-eval
+ pattern: eval(...)
+ message: >
+ eval() executes arbitrary code. Use ast.literal_eval() for safe
+ data parsing, or refactor to avoid dynamic evaluation.
+ languages: [python]
+ severity: ERROR
+
+ - id: no-print-statements
+ pattern: print(...)
+ message: Use the logging module instead of print() in production code.
+ languages: [python]
+ severity: WARNING
+ # Only enforce this in src/, not tests — pass `files: ^src/` in pre-commit.
+```
+
+### Structuring custom rules
+
+For more than a handful of rules, organise under `.semgrep/rules/`:
+
+```
+.semgrep/
+├── rules/
+│ ├── security.yml # project-specific security rules
+│ ├── style.yml # project-specific style rules
+│ └── tests/ # test fixtures for rules (semgrep --test)
+│ └── security_test.py
+```
+
+Reference the whole directory: `semgrep --config .semgrep/rules/ src/`
+
+---
+
+## Common registry rule IDs and fixes
+
+These are available via `--config p/python` or `--config p/security-audit`:
+
+| Rule ID | Issue | Fix |
+|---|---|---|
+| `python.lang.security.audit.exec-detected` | `exec()` call | Refactor to explicit function calls |
+| `python.lang.security.audit.eval-detected` | `eval()` call | Use `ast.literal_eval()` for data |
+| `python.lang.security.audit.dangerous-system-call` | `os.system()` | Use `subprocess.run(["cmd"], check=True)` |
+| `python.lang.security.audit.formatted-sql-query` | SQL via f-string / `%` | Use parameterised queries |
+| `python.lang.security.audit.jinja2.autoescape-disabled` | XSS via Jinja2 | Set `autoescape=True` |
+| `python.django.security.audit.raw-query` | Django raw SQL | Use the ORM or parameterised `raw()` |
+| `python.requests.security.no-auth-over-http` | Credentials over HTTP | Enforce HTTPS |
+| `python.lang.security.insecure-hash-use` | MD5 / SHA1 for security | Use SHA-256 or better |
+
+---
+
+## Suppressing a finding
+
+**Inline (single line):**
+```python
+result = eval(user_input) # nosemgrep: python.lang.security.audit.eval-detected
+```
+
+**Whole file** (add at top of file):
+```python
+# nosemgrep: python.lang.security.audit.eval-detected
+```
+
+Always include the specific rule ID. A bare `# nosemgrep` suppresses all findings on
+the line and makes the intent invisible to reviewers.
+
+---
+
+## Running semgrep
+
+```bash
+# Run local rules in .semgrep.yml:
+semgrep --config .semgrep.yml src/
+
+# Run a registry pack (fetches rules from semgrep.dev on first run, then caches):
+semgrep --config p/python src/
+
+# Run multiple sources together:
+semgrep --config p/python --config p/owasp-top-ten --config .semgrep.yml src/
+
+# Restrict to ERROR severity only (suppress WARNING / INFO):
+semgrep --config p/python --severity ERROR src/
+
+# JSON output for CI artefacts or downstream tooling:
+semgrep --config .semgrep.yml --json src/ -o semgrep-report.json
+
+# Test custom rules against fixtures in .semgrep/rules/tests/:
+semgrep --test .semgrep/rules/
+```
+
+Exit codes: `0` = no findings, `1` = findings found.
+
+---
+
+## CI step (GitHub Actions)
+
+```yaml
+- name: Install semgrep
+ run: pip install semgrep
+
+# Cache registry rules to avoid re-fetching on every run.
+- name: Cache semgrep rules
+ uses: actions/cache@v4
+ with:
+ path: ~/.semgrep/cache
+ key: semgrep-${{ hashFiles('.semgrep.yml') }}
+
+# Run registry packs + local rules together.
+- name: Semgrep scan
+ run: |
+ semgrep --config p/python \
+ --config p/owasp-top-ten \
+ --config .semgrep.yml \
+ --severity ERROR \
+ src/
+```
+
+See `SKILL.md` for the full CI ordering across all tools.
+
+---
+
+## Pre-commit hook entry
+
+```yaml
+- repo: https://github.com/semgrep/semgrep
+ rev: v1.60.0 # pin to a specific release; update with: pre-commit autoupdate
+ hooks:
+ - id: semgrep
+ # Registry packs can be passed here too.
+ args: ["--config", "p/python", "--config", ".semgrep.yml", "--severity", "ERROR"]
+ # Scope to src/ only.
+ files: ^src/
+ # pass_filenames: true is correct for per-file pattern matching.
+ # Cross-file taint analysis is not supported in pre-commit mode.
+```
+
+---
+
+## Gotchas
+
+- **Registry packs are CLI flags, not YAML rule entries.** You cannot write `- p/python`
+ inside a `rules:` block in `.semgrep.yml` — that syntax is invalid. Pass registries
+ with `--config p/python` on the command line or in the pre-commit `args`.
+- **Registry rules require internet access on first run.** Rules are cached in
+ `~/.semgrep/cache`. In an air-gapped CI environment, vendor the rules locally by
+ downloading them and adding as local rule files.
+- **`p/security-audit` is noisy.** It includes WARNING and INFO findings on top of
+ ERROR. Use `--severity ERROR` in CI until the baseline is clean, then broaden.
+- **Semgrep is slower than bandit on first run** because it fetches and compiles registry
+ rules. Cache `~/.semgrep/cache` in CI to keep subsequent runs fast.
+- **Custom rules are the real value-add.** Registry rules catch generic issues; custom
+ rules catch your project's specific anti-patterns. Even two or three custom rules
+ tailored to your stack are worth writing.
+- **`--config auto` changes silently over time.** Auto mode infers rulesets from your
+ codebase and will shift as semgrep's auto-detection improves. Use explicit `--config`
+ flags in CI for reproducible results.
+- **Overlap with bandit is intentional.** Both tools flag `eval`, insecure hashes, and
+ SQL injection but via different matching mechanisms. The overlap means issues are
+ caught by at least one tool even if the other misses a variant.
diff --git a/template/.claude/skills/python-code-reviewer/SKILL.md b/template/.claude/skills/python-code-reviewer/SKILL.md
new file mode 100644
index 0000000..fddb85c
--- /dev/null
+++ b/template/.claude/skills/python-code-reviewer/SKILL.md
@@ -0,0 +1,204 @@
+---
+name: python-code-reviewer
+description: >-
+ Performs structured, multi-phase Python code review covering security, correctness,
+ Pythonic style, type safety, performance, tests, and documentation. Targets Python 3.11+.
+ ALWAYS use this skill when Python code appears anywhere in the conversation — even if the
+ user has not explicitly asked for a review. Trigger on: "review", "check", "audit",
+ "critique", "look at this", "any issues?", "is this good?", "thoughts?", "feedback",
+ "what do you think?", or whenever a Python snippet, file, class, function, or PR diff
+ is pasted. Do NOT rely on memory for review patterns — always load this skill.
+ Covers Django, Flask, FastAPI, async, data science (pandas/numpy/torch), and general Python.
+---
+
+# Python Code Reviewer Skill
+
+You are a senior Python code reviewer. Every finding must be **confident** (>80% sure it is
+a real issue), **specific** (cite file + line when possible), and **fixable** (include a
+concrete corrected snippet). Never flood the review with noise — consolidate similar issues.
+
+**Assumed Python version: 3.11** unless overridden by `python_requires` in `pyproject.toml`
+or `setup.cfg`. Note version-conditional advice where relevant.
+
+---
+
+## Phase 0 — Gather Context
+
+Determine before reviewing:
+
+1. **Scope** — snippet / single function / module / PR diff / full repo?
+2. **Purpose** — what does this code do? Read docstrings, imports, READMEs.
+3. **Python version** — check `pyproject.toml`, `setup.cfg`, `.python-version`. Default: 3.11.
+4. **Conventions in use** — test framework, formatter, existing style, annotation density.
+5. **What NOT to flag** — do not report issues in *unchanged* code unless CRITICAL security.
+
+**Decision gate — when context is insufficient:**
+- Code **< 30 lines**: proceed directly; state assumptions in the review header.
+- Code **≥ 30 lines** with unclear purpose and no context: ask **one** clarifying question
+ (e.g. "Is this a web handler or a background job?"). Make assumptions for everything else.
+- **Never ask more than one question** per iteration.
+
+---
+
+## Phase 1 — Automated Tools
+
+**If a bash tool or terminal is available**, run these and cite exact output in findings:
+
+ mypy --strict .
+ ruff check .
+ black --check .
+ bandit -r . -ll
+ pytest --tb=short --cov=. --cov-report=term-missing
+
+Surface any ERROR / WARNING lines as findings at the appropriate severity.
+
+**If no tools are available**: skip Phase 1 entirely. Add to the review header:
+ ⚠️ Automated tools not run — manual review only.
+
+Do NOT write "would fail mypy" as mock output. If you cannot run the tool, flag the issue
+from first principles in Phase 2 with a specific line citation instead.
+
+---
+
+## Phase 2 — Structured Review
+
+### Review Mode
+
+Select before starting:
+
+| Mode | When to use | Categories | Max findings |
+|------|-------------|------------|--------------|
+| **Quick** | < 30 lines, or user says "quick look" / "just check X" | Security + Correctness only | 3 |
+| **Standard** | Default | All 8 categories below | Unlimited |
+| **Deep** | Full module, audit, "thorough review" | All 13 in `checklist.md` | Unlimited |
+
+For **Deep** mode: load `references/checklist.md` now and work through all 13 sections.
+
+---
+
+### Severity Definitions
+
+| Severity | Meaning | Block merge? |
+|----------|---------|--------------|
+| CRITICAL | Security vuln, data loss, auth bypass, hardcoded secret | Yes — always |
+| HIGH | Bug in normal use path; missing I/O error handling | Yes |
+| MEDIUM | Maintenance debt, test gap, type error, bad pattern | Recommended |
+| LOW | Naming, minor style, optional improvement | No (nit) |
+
+Report CRITICAL issues at the top — never bury them.
+
+---
+
+### Category Prompts (Standard mode)
+
+These are decision prompts — they tell you *whether* to flag an issue in each area.
+For the sub-items, load `references/checklist.md`. Do not duplicate checklist content here.
+
+**1. Security** (always check, all modes)
+- Hardcoded credentials / tokens? SQL string concatenation? `eval()`/`exec()` on input?
+- Unsafe deserialization (`pickle`, `yaml.load` without SafeLoader)?
+- Path traversal? Weak crypto (`MD5`/`SHA1`)? Insecure `random` for tokens? JWT misconfig?
+- See `references/python-patterns.md` Section 1 for before/after fixes.
+
+**2. Correctness & Bugs**
+- Mutable defaults? Late-binding closures? Bare `except`? Missing context managers?
+- Mutating a collection while iterating? Integer vs float division?
+
+**3. Type Safety** (Python 3.11 idioms)
+- Use `X | Y` unions (not `Optional[X]` or `Union[X, Y]`).
+- Use `Self` for methods returning the same class. Use `Never` for functions that always raise.
+- Missing annotations on public APIs? `Any` overused? `# type: ignore` without comment?
+- `TypeVarTuple` / `Unpack` for variadic generics where applicable.
+
+**4. Pythonic Style & PEP 8**
+- `is`/`is not` for None? `match`/`case` for structural dispatch over long `if`/`elif` chains?
+- f-strings over `.format()`? `pathlib.Path` over `os.path`? `enumerate` over `range(len)`?
+- `tomllib` (stdlib in 3.11) instead of third-party toml packages?
+
+**5. Design & Architecture**
+- Single responsibility? Functions > 30 lines? Deep nesting (> 3 levels)?
+- Boolean flag parameters that switch behaviour — consider two functions instead.
+- Magic numbers as named constants? Hard-coded config mixed with logic?
+
+**6. Error Handling**
+- `except Exception: pass` swallowing errors? Exception chain lost?
+ (`raise X` loses original; use `raise X from e`)
+- Resource leaks not covered by a `with` block?
+- `ExceptionGroup` / `except*` available in 3.11 — use for concurrent exception handling.
+
+**7. Tests**
+- New logic without tests? Tests asserting real behaviour, not just "no exception"?
+- Edge cases: empty input, None, zero, boundary values?
+- Mock targets at the right import path? No real I/O in unit tests?
+
+**8. Documentation**
+- Public functions/classes missing docstrings?
+- Comments explain *why*, not just *what*?
+- README / CHANGELOG stale after interface change?
+
+---
+
+## Phase 3 — Output the Review
+
+Load `references/output-format.md` and select the matching template:
+- **Template A** — snippet / function / module (most common)
+- **Template B** — PR / diff review
+- **Template C** — full module audit (Deep mode only)
+
+Always include at least one "What's Good" item — even for heavily flawed code.
+
+---
+
+## Phase 4 — Calibrate Feedback Tone
+
+- **Consolidate**: "5 functions missing type annotations" not 5 separate items.
+- **Label nits**: prefix LOW items with `[Nit]` so the author knows they are optional.
+- **Praise specifically**: name the exact pattern or decision done well.
+- **Don't police formatting** that `black`/`ruff` auto-fix.
+- **Design opinions → questions**: "Could this be split into two functions?" not "Split this."
+- **Never** tie findings to developer skill. Critique the code, not the author.
+
+---
+
+## Phase 5 — Self-Check (run on draft before sending)
+
+- [ ] Every CRITICAL/HIGH finding has a specific line citation and a concrete fix snippet
+- [ ] Similar issues are consolidated, not listed individually
+- [ ] At least one "What's Good" item is present
+- [ ] Verdict is consistent with the findings (no CRITICAL → APPROVE is wrong)
+- [ ] No formatter-fixable style issues are reported as findings
+- [ ] Tone is constructive throughout — no statements about the author's ability
+
+---
+
+## Phase 6 — Follow-Up / Iterative Reviews
+
+When the author submits a revision:
+
+1. **Incremental scope** — only look at changed lines. Do not re-raise already-fixed issues.
+ Do not introduce new findings on unchanged code (except CRITICAL security).
+2. **Author disagreement** — acknowledge their argument. Update verdict if technically valid.
+ If not valid, explain once clearly and do not repeat the point in subsequent rounds.
+3. **Closing out** — once all CRITICAL and HIGH issues are resolved, approve.
+ Open MEDIUMs and LOWs are not a blocking reason — note them as follow-up candidates.
+4. **Post-merge** — treat as a standard review; open issues for CRITICAL/HIGH rather than blocking.
+
+---
+
+## Quick reference: where to go deeper
+
+| Topic | Reference file |
+|------------------------------------|--------------------------------------------------------------------------|
+| Full 13-category review checklist | [references/checklist.md](references/checklist.md) |
+| Before/after code pattern fixes | [references/python-patterns.md](references/python-patterns.md) |
+| Output format templates (A/B/C) | [references/output-format.md](references/output-format.md) |
+
+---
+
+## Noise Filters (always apply)
+
+- Do NOT report: unused imports, trailing whitespace, import order — ruff/black handles these
+- Do NOT report: line-length violations if the file has a non-default `line-length` config
+- Do NOT report: style opinions not in PEP 8 or the project's established conventions
+- Do NOT report: issues in third-party code, `migrations/`, or generated files
+- DO report even in unchanged code: any CRITICAL security vulnerability noticed in passing
diff --git a/template/.claude/skills/python-code-reviewer/references/checklist.md b/template/.claude/skills/python-code-reviewer/references/checklist.md
new file mode 100644
index 0000000..c0afc87
--- /dev/null
+++ b/template/.claude/skills/python-code-reviewer/references/checklist.md
@@ -0,0 +1,291 @@
+# Review checklist
+
+> Load this file for Deep mode reviews, thorough audits, or when uncertain about
+> specific sub-checks in any category. All checklist items assume Python 3.11
+> unless marked with a version note.
+
+---
+
+## Table of Contents
+1. Security
+2. Correctness & Bugs
+3. Type Safety
+4. Pythonic Style & PEP 8
+5. Design & Architecture
+6. Error Handling
+7. Testing
+8. Performance
+9. Concurrency & Async
+10. Dependencies
+11. Documentation
+12. Framework Specifics (Django / Flask / FastAPI)
+13. Data Science (Pandas / NumPy / PyTorch)
+
+---
+
+## 1. Security
+
+### Injection
+- [ ] SQL built with string formatting or concatenation instead of parameterised queries?
+- [ ] `subprocess` called with `shell=True` and user-controlled input?
+- [ ] Template injection: `render_template_string` or `jinja2.Template(user_input)`?
+- [ ] `eval()`, `exec()`, or `compile()` called on user-controlled strings?
+
+### Deserialization
+- [ ] `pickle.loads()` on untrusted data? (Use JSON or sign the payload)
+- [ ] `yaml.load()` without `Loader=yaml.SafeLoader`?
+- [ ] `marshal`, `shelve`, or `__reduce__` used with untrusted data?
+
+### Path Traversal
+- [ ] User-controlled path not validated with `os.path.normpath()` + prefix check?
+- [ ] `open(user_input)` without sanitisation?
+- [ ] `send_file(user_input)` in Flask/FastAPI without `safe_join`?
+
+### Secrets & Auth
+- [ ] API keys, passwords, or tokens hardcoded in source?
+- [ ] Secrets in committed config files instead of `.env` or a secrets manager?
+- [ ] `MD5` or `SHA1` used for security (passwords, tokens) — not just checksums?
+- [ ] `random` module used for security tokens? (Use `secrets` module)
+- [ ] JWT decoded with `algorithm=None` or `options={"verify_signature": False}`?
+- [ ] `hashlib.pbkdf2_hmac` with too few iterations (< 600 000 for PBKDF2-SHA256)?
+
+### Web Security
+- [ ] CSRF protection missing on state-changing endpoints?
+- [ ] Cookies missing `HttpOnly` / `Secure` / `SameSite` flags?
+- [ ] User input rendered in HTML without escaping?
+- [ ] CORS configured as `allow_origins=["*"]` with `allow_credentials=True`?
+- [ ] Rate limiting absent on authentication endpoints?
+
+---
+
+## 2. Correctness & Bugs
+
+### Python-Specific Traps
+- [ ] Mutable default argument: `def f(items=[])` — use `None` + guard
+- [ ] Late-binding closure: `lambda: x` inside a loop captures loop variable by reference
+- [ ] Integer vs float: `5 / 2 == 2.5`; use `//` if integer result intended
+- [ ] `is` vs `==`: `is` for identity (None, True, False only); `==` for value equality
+- [ ] String concatenation in a loop (`s += chunk`) — O(n²); use `"".join()`
+- [ ] `dict.keys()` iterated while the dict is mutated
+- [ ] `datetime.datetime.utcnow()` deprecated in 3.12 — prefer `datetime.now(UTC)` now
+
+### Logic
+- [ ] Off-by-one errors in slices (`[:n]` vs `[:n+1]`) or `range()`
+- [ ] Incorrect boolean short-circuit assumptions
+- [ ] Function that should return a value has a path that falls through to implicit `None`
+- [ ] Unreachable code after `return` / `raise` / `continue` / `break`
+
+### Data Structures
+- [ ] List used for O(n) membership tests that should be a `set` (O(1))
+- [ ] Modifying a list or dict while iterating it
+- [ ] Shallow copy (`list.copy()`, `dict.copy()`) where deep copy is needed
+- [ ] `collections.defaultdict` or `Counter` would simplify the pattern
+
+---
+
+## 3. Type Safety (Python 3.11)
+
+- [ ] Use `X | Y` union syntax (3.10+) — not `Union[X, Y]` or `Optional[X]`
+- [ ] Use `Self` (from `typing`) for methods that return the instance type
+- [ ] Use `Never` for functions that always raise or never return
+- [ ] Use `TypeVarTuple` / `Unpack` for variadic generics
+- [ ] `LiteralString` used for SQL/shell strings built from user input (3.11)
+- [ ] Public functions missing parameter and return type annotations
+- [ ] `Any` used where a more specific type is knowable
+- [ ] `Optional` result used at callsite without a `None` check
+- [ ] `cast()` used to silence mypy rather than fix a real type inconsistency
+- [ ] `# type: ignore` without an explanatory comment — is it justified?
+- [ ] `isinstance()` used for runtime checks (correct); `type(x) ==` avoided
+- [ ] `dataclass`, `TypedDict`, or Pydantic `BaseModel` used for structured data
+- [ ] `@overload` used where the return type depends on argument type
+
+---
+
+## 4. Pythonic Style & PEP 8
+
+### Naming
+- [ ] `snake_case` for functions and variables
+- [ ] `PascalCase` for classes
+- [ ] `UPPER_CASE` for module-level constants
+- [ ] No single-letter names outside comprehensions, lambdas, or mathematical code
+- [ ] No `l`, `O`, or `I` as variable names (ambiguous with `1`, `0`, `1`)
+
+### Modern Python 3.11 Idioms
+- [ ] `match`/`case` used for structural dispatch instead of long `if`/`elif` chains (3.10+)
+- [ ] `tomllib` (stdlib 3.11) instead of third-party `toml` / `tomli` packages
+- [ ] `datetime.now(timezone.utc)` instead of deprecated `utcnow()`
+- [ ] `StrEnum` (3.11) for string-valued enums instead of `(str, Enum)`
+- [ ] `ExceptionGroup` and `except*` for concurrent/multi-error handling (3.11)
+- [ ] `typing.assert_never()` in exhaustive `match` branches to catch missing cases
+
+### General Idioms
+- [ ] `if x is None:` not `if x == None:`
+- [ ] `if not items:` not `if len(items) == 0:`
+- [ ] f-strings for interpolation (not `.format()` or `%`)
+- [ ] `with open(...) as f:` — not manual `f.open()` / `f.close()`
+- [ ] `enumerate()` instead of `range(len(...))`
+- [ ] `zip()` to iterate parallel sequences; `zip(..., strict=True)` (3.10+) to catch length mismatches
+- [ ] `pathlib.Path` over `os.path` string manipulation
+- [ ] `_` for unused loop variables: `for _ in range(n):`
+- [ ] `dict | other` for merging dicts (3.9+) over `{**a, **b}`
+
+### Code Style
+- [ ] 2 blank lines around top-level definitions; 1 blank line between methods
+- [ ] Imports: stdlib → third-party → local; one import per line
+- [ ] No wildcard imports (`from module import *`) except deliberate `__init__.py` re-exports
+- [ ] No semicolons combining multiple statements on one line
+
+---
+
+## 5. Design & Architecture
+
+### Functions
+- [ ] Single responsibility — does each function do exactly one thing?
+- [ ] Functions > 30 lines — consider splitting
+- [ ] > 3–4 parameters without a dataclass/config object grouping them
+- [ ] Boolean flag parameter that switches behaviour — consider two separate functions
+- [ ] Deep nesting (> 3 levels) — can guard clauses / early returns flatten this?
+- [ ] Side effects (I/O, mutation) mixed with pure computation in the same function
+
+### Classes
+- [ ] Is a class actually needed, or would module-level functions suffice?
+- [ ] `__init__` doing expensive work (I/O, network) — consider a factory `@classmethod`
+- [ ] Missing `__repr__` on data-holding classes
+- [ ] Inheritance used where composition would be simpler and more explicit
+- [ ] `@dataclass` / `NamedTuple` / Pydantic model for data-only classes
+- [ ] `__slots__` considered for high-volume small objects
+
+### Modules
+- [ ] Circular imports (A imports B imports A)?
+- [ ] Magic numbers that should be named constants
+- [ ] Configuration logic mixed with business logic — separate them
+- [ ] God module (> 500 lines doing several unrelated things)?
+- [ ] Missing `__all__` on a public module (controls what `import *` exposes)
+
+---
+
+## 6. Error Handling
+
+- [ ] Bare `except:` catches `KeyboardInterrupt`, `SystemExit` — always name the exception
+- [ ] `except Exception: pass` silently swallowing errors
+- [ ] Original exception lost: `raise RuntimeError(...)` inside except — use `raise X from e`
+- [ ] `try` block too wide — wrapping code that cannot realistically raise the caught exception
+- [ ] Resource leak: `open()`, DB connection, socket not in a `with` block
+- [ ] Custom exceptions inheriting from a semantically appropriate base
+ (`ValueError`, `RuntimeError`, `OSError`, etc.)
+- [ ] `logger.exception()` used (not `logger.error()`) to preserve traceback in except blocks
+- [ ] `ExceptionGroup` used where multiple concurrent errors can occur together (3.11)
+- [ ] `except*` used to handle specific exception types within an `ExceptionGroup` (3.11)
+
+---
+
+## 7. Testing
+
+- [ ] New public functions / classes covered by at least one test?
+- [ ] Edge cases: empty input, `None`, zero, negative, max/min values, empty collections
+- [ ] Tests assert behaviour (not just "no exception raised")
+- [ ] Mock / patch targets the right import path:
+ `mymodule.requests.get`, not `requests.get`
+- [ ] No real I/O (filesystem, network, DB) in unit tests — mock or use fixtures
+- [ ] Tests are isolated: no shared mutable state, no execution-order dependency
+- [ ] `@pytest.mark.parametrize` used for repeated cases instead of copy-paste tests
+- [ ] Fixtures reused via `conftest.py` instead of duplicated `setUp`-style code
+- [ ] Line coverage ≠ branch coverage ≠ behaviour coverage — check what is actually asserted
+- [ ] `pytest-asyncio` used correctly for async tests
+
+---
+
+## 8. Performance
+
+> Flag only for hot paths, loops over large data, or when explicitly requested.
+
+- [ ] N+1 query: related objects fetched inside a loop — use `select_related` / `prefetch_related`
+- [ ] Expensive value re-computed inside a loop that could be hoisted before the loop
+- [ ] String concatenation in a loop — use `"".join()`
+- [ ] Reading an entire large file into memory — consider streaming / chunking
+- [ ] `for i in range(len(items)):` — use `enumerate` or iterate directly
+- [ ] Unnecessary object creation in a tight inner loop
+
+---
+
+## 9. Concurrency & Async
+
+- [ ] Blocking I/O (`requests.get`, `time.sleep`) inside `async def` — use async alternatives
+- [ ] `asyncio.create_task()` result not stored — uncaught exceptions silently discarded
+- [ ] `asyncio.sleep(0)` missing to yield control in CPU-bound `async` loops
+- [ ] Shared mutable state accessed from multiple threads without a lock
+- [ ] `threading.Thread` missing `daemon=True` or `join()` — may hang on exit
+- [ ] Race condition in `if not exists: create` — use atomic DB operations or a lock
+- [ ] `asyncio.TaskGroup` (3.11) preferred over `asyncio.gather()` for structured concurrency
+- [ ] `timeout` parameter on `asyncio.wait_for()` to prevent hung tasks
+
+---
+
+## 10. Dependencies
+
+- [ ] New dependency not added to `pyproject.toml` / `requirements.txt`?
+- [ ] Production deps pinned to an exact version or a narrow range?
+- [ ] Known vulnerability in the pinned version (check PyPI Security Advisories)?
+- [ ] Heavy library imported for a trivial use that stdlib could handle?
+- [ ] `tomllib` (stdlib 3.11) replaces the need for `tomli` or `toml` packages
+
+---
+
+## 11. Documentation
+
+- [ ] Public functions and classes have docstrings?
+- [ ] Docstring style consistent with the project (Google / NumPy / reStructuredText)?
+- [ ] Non-obvious logic has a comment explaining *why*, not just *what*
+- [ ] Module-level docstring states the module's purpose?
+- [ ] TODOs tracked in an issue, not left indefinitely in code
+- [ ] `CHANGELOG.md` / `README.md` updated for user-visible interface changes?
+- [ ] Deprecation warnings (`warnings.warn(..., DeprecationWarning)`) added for removed APIs?
+
+---
+
+## 12. Framework Specifics
+
+### Django
+- [ ] Raw SQL in `cursor.execute()` using string formatting?
+- [ ] `request.POST` / `request.GET` used without form or serializer validation?
+- [ ] Views missing `@login_required` or `permission_classes`?
+- [ ] Queryset accesses related object in a loop without `select_related`/`prefetch_related`?
+- [ ] `null=True` on a `CharField` — use `blank=True`; empty string is the Django convention
+- [ ] `SECRET_KEY` or `DEBUG=True` committed to source?
+- [ ] Django signals used where an explicit service-layer call would be clearer?
+
+### Flask
+- [ ] `app.secret_key` hardcoded in source?
+- [ ] `render_template_string` called with unsanitised user input?
+- [ ] `DEBUG=True` in a production config file?
+- [ ] Missing `abort(403)` / `abort(404)` where appropriate?
+
+### FastAPI
+- [ ] Request/response bodies use Pydantic `BaseModel`, not raw `dict`?
+- [ ] `response_model` specified on all endpoints?
+- [ ] Blocking code inside `async def` endpoints?
+ (Use `def` route handlers or `run_in_executor` for sync I/O)
+- [ ] Dependencies injected via `Depends()`, not via global state?
+- [ ] `Annotated[X, Depends(...)]` pattern (modern FastAPI style) used?
+
+---
+
+## 13. Data Science (Pandas / NumPy / PyTorch)
+
+### Pandas
+- [ ] Chained indexing `df[col][row]` — use `df.loc[row, col]`
+- [ ] `.iterrows()` for something that can be vectorised?
+- [ ] `inplace=True` — prefer explicit reassignment (`df = df.dropna()`)
+- [ ] Reading a large CSV fully into memory — consider chunking or Polars
+
+### NumPy
+- [ ] Python loop where vectorised NumPy operations would work?
+- [ ] Unnecessary copy (`np.copy()`) where a view suffices?
+- [ ] Broadcasting assumptions not documented with a comment?
+
+### PyTorch
+- [ ] `model.eval()` and `torch.no_grad()` missing during inference?
+- [ ] Tensors not moved to the same device before operations?
+- [ ] No random seed set for reproducibility (`torch.manual_seed`, `numpy.random.seed`)?
+- [ ] `print()` debugging statements left in notebook or training script?
+- [ ] Data loading/preprocessing logic mixed with model logic — consider a `Dataset` class?
diff --git a/template/.claude/skills/python-code-reviewer/references/output-format.md b/template/.claude/skills/python-code-reviewer/references/output-format.md
new file mode 100644
index 0000000..142d835
--- /dev/null
+++ b/template/.claude/skills/python-code-reviewer/references/output-format.md
@@ -0,0 +1,270 @@
+# Output format templates
+
+> Always load this file in Phase 3. Choose the template that matches the review scope.
+> Templates are shown as live Markdown — copy and fill in the placeholders.
+
+---
+
+## Verdict Decision Guide (read before selecting template)
+
+| Situation | Verdict |
+|-----------|---------|
+| No CRITICAL or HIGH issues | **APPROVE** |
+| 1–2 HIGH issues, no CRITICAL | **REQUEST CHANGES** |
+| Any CRITICAL issue | **BLOCK** |
+| New logic has no tests and project requires them | **REQUEST CHANGES** |
+| Only LOW / nit issues remain | **APPROVE** (list nits as optional) |
+| Hotfix under time pressure, 1 known HIGH issue | **APPROVE** + open follow-up ticket |
+
+---
+
+## Template A — Snippet / Function / Module Review
+
+Use for: a pasted function, a single file, or a module-level review.
+
+---
+
+## Python Code Review
+
+**Mode**: Quick / Standard / Deep
+**Python version**: 3.11
+**Tools run**: mypy, ruff, black, bandit, pytest — OR — Manual review only (tools not available)
+
+> *Assumptions (fill in if context was absent):*
+> e.g. "Treating this as a utility module, not a web handler."
+
+---
+
+### Summary
+
+| Severity | Count | Status |
+|----------|-------|--------|
+| CRITICAL | N | Block |
+| HIGH | N | Warn |
+| MEDIUM | N | Info |
+| LOW | N | Nit |
+
+**Verdict**: APPROVE / REQUEST CHANGES / BLOCK
+
+---
+
+### Findings
+
+#### [CRITICAL] Title of the issue
+
+**Location**: `path/to/file.py:42`
+**Issue**: Clear description of the problem and why it matters (what can go wrong).
+
+**Before:**
+
+ # The problematic code
+ eval(user_input)
+
+**After:**
+
+ # The corrected code
+ import ast
+ ast.literal_eval(user_input)
+
+---
+
+#### [HIGH] Title
+
+**Location**: `path/to/file.py:17`
+**Issue**: Description.
+
+**Fix**: Description of change, plus a snippet if helpful.
+
+ # corrected snippet here
+
+---
+
+#### [MEDIUM] Title
+
+**Location**: `path/to/file.py:88`
+**Issue**: Description.
+
+**Fix**: Description or snippet.
+
+---
+
+#### [Nit] Title
+
+**Location**: `path/to/file.py:5`
+**Note**: Optional — not required to fix. e.g. "Could rename `d` to `user_data` for clarity."
+
+---
+
+### What's Working Well
+
+- **[Specific pattern]**: Brief specific praise. Name the exact decision done well.
+- **[Another item]**: ...
+
+---
+
+### Automated Tool Results
+
+| Tool | Status | Notes |
+|------|--------|-------|
+| `mypy --strict` | Pass / Fail / Not run | ... |
+| `ruff check` | Pass / Fail / Not run | ... |
+| `black --check` | Pass / Fail / Not run | ... |
+| `bandit -r -ll` | Pass / Fail / Not run | ... |
+| `pytest --cov` | Pass / Fail / Not run | ... |
+
+---
+
+## Template B — PR / Diff Review
+
+Use for: a GitHub/GitLab PR, a git diff, or a branch comparison.
+
+---
+
+## PR Review: #NUMBER — TITLE
+
+**Reviewed**: DATE
+**Author**: AUTHOR
+**Branch**: `head-branch` → `base-branch`
+**Scope**: +N / -N lines across N files
+**Tools run**: mypy, ruff, black, bandit, pytest — OR — Manual only
+
+---
+
+### Decision: APPROVE / REQUEST CHANGES / BLOCK
+
+**Summary**: One sentence verdict.
+e.g. "Two HIGH issues in error handling should be resolved before merge."
+
+---
+
+### Summary Table
+
+| Severity | Count | Status |
+|----------|-------|--------|
+| CRITICAL | 0 | Pass |
+| HIGH | 2 | Warn |
+| MEDIUM | 3 | Info |
+| LOW | 1 | Nit |
+
+---
+
+### Findings
+
+(Use the same finding format as Template A — Location / Issue / Before+After)
+
+---
+
+### Validation Results
+
+| Check | Result | Notes |
+|-------|--------|-------|
+| `mypy --strict` | Pass / Fail / Skipped | ... |
+| `ruff check` | Pass / Fail / Skipped | ... |
+| `black --check` | Pass / Fail / Skipped | ... |
+| `bandit -r -ll` | Pass / Fail / Skipped | ... |
+| Tests | Pass / Fail / Skipped | coverage: N% |
+
+---
+
+### Files Reviewed
+
+| File | Change | Notes |
+|------|--------|-------|
+| `src/auth.py` | Modified | Main logic change |
+| `tests/test_auth.py` | Added | New tests |
+| `requirements.txt` | Modified | New dep added |
+
+---
+
+### What's Working Well
+
+...
+
+---
+
+### Discussion Points (optional)
+
+Design observations that do not block merge but are worth discussing async:
+
+- ...
+
+---
+
+## Template C — Module / Codebase Audit
+
+Use for: Deep mode reviews of an entire module or unfamiliar codebase.
+
+---
+
+## Python Module Audit: `module_name`
+
+**Date**: DATE
+**Scope**: N files, ~N lines of code
+**Python version**: 3.11
+**Purpose**: What this module does
+
+---
+
+### Executive Summary
+
+2–3 sentences: overall health, most critical areas, and the top recommendation.
+
+---
+
+### Risk Matrix
+
+| Category | Risk | Key Issues |
+|----------|------|------------|
+| Security | High / Medium / Low | ... |
+| Correctness | ... | ... |
+| Test Coverage | ... | ... |
+| Type Safety | ... | ... |
+| Documentation | ... | ... |
+| Performance | ... | ... |
+
+---
+
+### Critical Findings (must fix immediately)
+
+...
+
+### High Priority
+
+...
+
+### Recommended Improvements (medium / low — prioritised)
+
+1. ...
+2. ...
+
+---
+
+### Strengths
+
+...
+
+### Suggested Next Steps
+
+1. Run `bandit -r . -ll` and address all HIGH findings
+2. Enable `mypy --strict` incrementally, module by module
+3. ...
+
+---
+
+## Tone Guidelines
+
+**Use:**
+- "This could be simplified by..."
+- "Consider using X here — it makes the intent clearer."
+- "Nice use of dataclasses for the config object — exactly the right pattern."
+- "[Nit] Could rename `d` to `user_data` for clarity."
+- "Would it make sense to extract this into a helper to separate concerns?"
+
+**Avoid:**
+- "This is wrong / bad / amateur."
+- "You should know not to do this."
+- "Obviously this needs to be..."
+- Any language that judges the author rather than the code.
+
+**For optional suggestions**: prefix with `[Nit]` or `[Suggestion]`.
+**For design questions**: phrase as open questions, not commands.
diff --git a/template/.claude/skills/python-code-reviewer/references/python-patterns.md b/template/.claude/skills/python-code-reviewer/references/python-patterns.md
new file mode 100644
index 0000000..7de31cd
--- /dev/null
+++ b/template/.claude/skills/python-code-reviewer/references/python-patterns.md
@@ -0,0 +1,502 @@
+# Python patterns
+
+> Load this file when you need concrete before/after code examples to include
+> in a finding's Fix section. All examples target Python 3.11 unless noted.
+
+---
+
+## Table of Contents
+1. Security Anti-Patterns
+2. Common Bug Patterns
+3. Pythonic Idioms (3.11)
+4. Type Annotation Patterns (3.11)
+5. Error Handling Patterns
+6. Testing Patterns
+7. Async Patterns
+8. Python 3.11 New Patterns
+
+---
+
+## 1. Security Anti-Patterns
+
+### SQL Injection
+ # CRITICAL
+ query = f"SELECT * FROM users WHERE name = '{username}'"
+ cursor.execute(query)
+
+ # Fix — parameterised query
+ cursor.execute("SELECT * FROM users WHERE name = %s", (username,))
+ # ORM (Django) — always safe
+ User.objects.filter(name=username)
+
+### Unsafe YAML Loading
+ # CRITICAL
+ import yaml
+ data = yaml.load(user_input)
+
+ # Fix
+ data = yaml.safe_load(user_input)
+
+### eval on User Input
+ # CRITICAL
+ result = eval(user_expression)
+
+ # Fix — safe literal parsing only
+ import ast
+ result = ast.literal_eval(user_expression)
+
+### Path Traversal
+ # HIGH
+ def serve_file(filename):
+ return open(f"/var/www/files/{filename}").read() # ../../../etc/passwd works
+
+ # Fix
+ import os
+ BASE_DIR = "/var/www/files"
+
+ def serve_file(filename: str) -> str:
+ path = os.path.normpath(os.path.join(BASE_DIR, filename))
+ if not path.startswith(BASE_DIR + os.sep):
+ raise PermissionError("Path traversal attempt blocked")
+ return open(path).read()
+
+### Hardcoded Secrets
+ # CRITICAL
+ API_KEY = "sk-abc123" # pragma: allowlist secret
+ DB_PASSWORD = "hunter2" # pragma: allowlist secret
+
+ # Fix
+ import os
+ API_KEY = os.environ["API_KEY"] # crash-fast if missing
+ DB_PASSWORD = os.environ["DB_PASSWORD"]
+
+### Weak Crypto / Insecure Random
+ # HIGH — MD5 for passwords, predictable token generation
+ import hashlib, random
+ token = random.randint(100000, 999999)
+ pw_hash = hashlib.md5(password.encode()).hexdigest()
+
+ # Fix
+ import secrets, bcrypt
+ token = secrets.token_urlsafe(32)
+ pw_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
+
+### Unsafe Pickle
+ # CRITICAL — arbitrary code execution
+ import pickle
+ obj = pickle.loads(user_bytes)
+
+ # Fix — use JSON for data exchange
+ import json
+ obj = json.loads(user_bytes)
+
+---
+
+## 2. Common Bug Patterns
+
+### Mutable Default Argument
+ # HIGH
+ def add_item(item, container=[]):
+ container.append(item)
+ return container
+
+ # Fix
+ def add_item(item: object, container: list | None = None) -> list:
+ if container is None:
+ container = []
+ container.append(item)
+ return container
+
+### Late-Binding Closure
+ # HIGH — all lambdas return 4, not 0..4
+ functions = [lambda: i for i in range(5)]
+
+ # Fix — capture value at creation time
+ functions = [lambda i=i: i for i in range(5)]
+
+### Swallowing Exceptions
+ # HIGH
+ try:
+ risky()
+ except: # catches KeyboardInterrupt, SystemExit
+ pass
+
+ # Fix
+ import logging
+ try:
+ risky()
+ except ValueError as e:
+ logging.warning("Validation failed: %s", e)
+ raise
+
+### Missing Context Manager
+ # MEDIUM
+ f = open("data.txt")
+ data = f.read()
+ f.close() # skipped if read() raises
+
+ # Fix
+ with open("data.txt") as f:
+ data = f.read()
+
+### String Concatenation in Loop
+ # MEDIUM — O(n²)
+ result = ""
+ for chunk in chunks:
+ result += chunk
+
+ # Fix
+ result = "".join(chunks)
+
+### Iterating While Mutating
+ # HIGH
+ for key in my_dict:
+ if condition(key):
+ del my_dict[key] # RuntimeError
+
+ # Fix
+ my_dict = {k: v for k, v in my_dict.items() if not condition(k)}
+
+---
+
+## 3. Pythonic Idioms (3.11)
+
+### None Comparisons
+ # Wrong
+ if x == None: ...
+ if x != None: ...
+
+ # Right
+ if x is None: ...
+ if x is not None: ...
+
+### Empty Container Check
+ # Verbose
+ if len(items) == 0: ...
+
+ # Pythonic
+ if not items: ...
+
+### Enumerate / Zip
+ # Old style
+ for i in range(len(items)):
+ print(i, items[i])
+
+ # Pythonic
+ for i, item in enumerate(items):
+ print(i, item)
+
+ # Zip with length safety check (3.10+)
+ for a, b in zip(list_a, list_b, strict=True):
+ ...
+
+### Dict Merge (3.9+)
+ # Old
+ merged = {**defaults, **overrides}
+
+ # Modern
+ merged = defaults | overrides
+
+### Structural Pattern Matching (3.10+)
+ # Verbose if/elif chain
+ if command == "quit":
+ quit_game()
+ elif command == "go" and direction in ("north", "south"):
+ go(direction)
+ elif command == "pick" and item:
+ pick(item)
+
+ # match/case — clearer intent, exhaustiveness checkable
+ match command.split():
+ case ["quit"]:
+ quit_game()
+ case ["go", ("north" | "south") as direction]:
+ go(direction)
+ case ["pick", item]:
+ pick(item)
+ case _:
+ print(f"Unknown command: {command}")
+
+### tomllib for TOML (3.11 stdlib)
+ # Old — required third-party package
+ import toml
+ config = toml.load("config.toml")
+
+ # Modern — stdlib, no dependency needed
+ import tomllib
+ with open("config.toml", "rb") as f:
+ config = tomllib.load(f)
+
+### StrEnum (3.11)
+ # Old verbose pattern
+ from enum import Enum
+ class Color(str, Enum):
+ RED = "red"
+ GREEN = "green"
+
+ # Modern
+ from enum import StrEnum
+ class Color(StrEnum):
+ RED = "red"
+ GREEN = "green"
+
+### Datetime UTC (deprecation in 3.12)
+ # Deprecated
+ from datetime import datetime
+ now = datetime.utcnow()
+
+ # Correct — timezone-aware
+ from datetime import datetime, timezone
+ now = datetime.now(timezone.utc)
+
+### Pathlib
+ # String-based — fragile
+ import os
+ path = os.path.join(base, "subdir", "file.txt")
+ if os.path.exists(path):
+ with open(path) as f:
+ data = f.read()
+
+ # Pathlib — composable and readable
+ from pathlib import Path
+ path = Path(base) / "subdir" / "file.txt"
+ if path.exists():
+ data = path.read_text()
+
+---
+
+## 4. Type Annotation Patterns (3.11)
+
+### Union Syntax
+ # Old (still valid, but verbose)
+ from typing import Optional, Union
+ def find(user_id: int) -> Optional[User]: ...
+ def process(value: Union[int, str]) -> None: ...
+
+ # Modern (3.10+)
+ def find(user_id: int) -> User | None: ...
+ def process(value: int | str) -> None: ...
+
+### Self Return Type
+ # Old — fragile with subclassing
+ from typing import TypeVar
+ T = TypeVar("T", bound="Builder")
+ class Builder:
+ def set_name(self: T, name: str) -> T: ...
+
+ # Modern (3.11)
+ from typing import Self
+ class Builder:
+ def set_name(self, name: str) -> Self: ...
+
+### Never for Unreachable Code
+ from typing import Never
+ def assert_never(x: Never) -> Never:
+ raise AssertionError(f"Unhandled value: {x}")
+
+ # Use in exhaustive match
+ match status:
+ case "ok": handle_ok()
+ case "error": handle_error()
+ case _ as unreachable:
+ assert_never(unreachable) # mypy confirms exhaustiveness
+
+### LiteralString for SQL Safety (3.11)
+ from typing import LiteralString
+
+ def execute(query: LiteralString) -> None:
+ cursor.execute(query)
+
+ # mypy now rejects: execute(f"SELECT * FROM {user_input}")
+ # mypy accepts: execute("SELECT * FROM users WHERE id = %s")
+
+### TypedDict
+ # Untyped
+ def create_response(status: int, body: str) -> dict: ...
+
+ # Typed
+ from typing import TypedDict
+ class Response(TypedDict):
+ status: int
+ body: str
+ headers: dict[str, str]
+
+ def create_response(status: int, body: str) -> Response: ...
+
+---
+
+## 5. Error Handling Patterns
+
+### Exception Chaining
+ # MEDIUM — original traceback lost
+ try:
+ db.connect()
+ except ConnectionError:
+ raise RuntimeError("Database unavailable")
+
+ # Fix
+ try:
+ db.connect()
+ except ConnectionError as e:
+ raise RuntimeError("Database unavailable") from e
+
+### ExceptionGroup (3.11) — Concurrent Errors
+ # Multiple errors from concurrent tasks
+ errors = []
+ for task in tasks:
+ try:
+ task.run()
+ except ValueError as e:
+ errors.append(e)
+ if errors:
+ raise ExceptionGroup("task failures", errors)
+
+ # Handle specific types in the group
+ try:
+ run_all_tasks()
+ except* ValueError as eg:
+ for exc in eg.exceptions:
+ log.warning("Validation error: %s", exc)
+ except* IOError as eg:
+ for exc in eg.exceptions:
+ log.error("I/O error: %s", exc)
+
+### Custom Exception Hierarchy
+ # Too generic
+ raise Exception("User not found")
+
+ # Better — specific, catchable by callers
+ class AppError(Exception):
+ """Base for all application errors."""
+
+ class NotFoundError(AppError):
+ """Resource does not exist."""
+
+ class ConfigError(AppError):
+ """Invalid or missing configuration."""
+
+ raise NotFoundError(f"User {user_id} not found")
+
+---
+
+## 6. Testing Patterns
+
+### Correct Mock Target
+ # Wrong — mocking where requests is defined
+ from unittest.mock import patch
+ @patch("requests.get")
+ def test_fetch(mock_get): ...
+
+ # Right — mock where it is used
+ @patch("mymodule.requests.get")
+ def test_fetch(mock_get): ...
+
+### Parametrize
+ # Repetitive
+ def test_valid_1(): assert validate("a@b.com")
+ def test_valid_2(): assert validate("x@y.org")
+ def test_invalid(): assert not validate("bad")
+
+ # Concise
+ import pytest
+ @pytest.mark.parametrize("email,expected", [
+ ("a@b.com", True),
+ ("x@y.org", True),
+ ("bad", False),
+ ])
+ def test_validate(email, expected):
+ assert validate(email) == expected
+
+### Async Tests
+ import pytest, pytest_asyncio
+
+ @pytest.mark.asyncio
+ async def test_fetch():
+ result = await fetch_data()
+ assert result["status"] == "ok"
+
+---
+
+## 7. Async Patterns
+
+### Blocking I/O in Async
+ # HIGH — blocks the event loop
+ async def fetch():
+ response = requests.get(url) # sync, blocking
+
+ # Fix
+ import httpx
+ async def fetch():
+ async with httpx.AsyncClient() as client:
+ response = await client.get(url)
+ return response.json()
+
+### Fire-and-Forget Bug
+ # HIGH — exception from background_job() is silently discarded
+ async def handler():
+ asyncio.create_task(background_job())
+
+ # Fix — store the task reference
+ _background_tasks: set[asyncio.Task] = set()
+
+ async def handler():
+ task = asyncio.create_task(background_job())
+ _background_tasks.add(task)
+ task.add_done_callback(_background_tasks.discard)
+
+### TaskGroup (3.11) — Structured Concurrency
+ # Old — gather swallows some exceptions
+ results = await asyncio.gather(task1(), task2(), task3())
+
+ # Modern — all tasks cancelled if any fails, errors surface cleanly
+ async with asyncio.TaskGroup() as tg:
+ t1 = tg.create_task(task1())
+ t2 = tg.create_task(task2())
+ t3 = tg.create_task(task3())
+ results = [t1.result(), t2.result(), t3.result()]
+
+### Run Blocking Sync Code in Async
+ # MEDIUM — CPU-bound work blocks the event loop
+ async def process():
+ result = heavy_computation(data)
+
+ # Fix
+ import asyncio
+ async def process():
+ loop = asyncio.get_event_loop()
+ result = await loop.run_in_executor(None, heavy_computation, data)
+
+---
+
+## 8. Python 3.11 New Patterns
+
+### Fine-Grained Error Locations
+ # Python 3.11 tracebacks now show the exact expression that raised.
+ # This means multi-expression lines like:
+ result = obj.method().attr["key"]
+ # will point to the specific sub-expression (e.g. .attr) not just the line.
+ # Implication for review: split complex chains for better production debugging.
+
+### tomllib (No External Dependency)
+ # 3.11 ships tomllib in stdlib — remove tomli / toml from dependencies
+ import tomllib
+ with open("pyproject.toml", "rb") as f:
+ config = tomllib.load(f)
+
+### StrEnum
+ from enum import StrEnum, auto
+ class Direction(StrEnum):
+ NORTH = auto() # value becomes "north" (lowercased name)
+ SOUTH = auto()
+
+ assert Direction.NORTH == "north" # True — no .value needed
+
+### TaskGroup (see Async section above)
+
+### exception.__notes__ (3.11)
+ # Attach context notes to an exception without subclassing
+ try:
+ process(record)
+ except ValueError as e:
+ e.add_note(f"Occurred while processing record id={record.id}")
+ raise
diff --git a/template/.claude/skills/skill-maintainer/SKILL.md b/template/.claude/skills/skill-maintainer/SKILL.md
new file mode 100644
index 0000000..e340260
--- /dev/null
+++ b/template/.claude/skills/skill-maintainer/SKILL.md
@@ -0,0 +1,419 @@
+---
+name: skill-maintainer
+description: >-
+ Audit, standardize, and improve existing skills in template/.claude/skills/. Use this
+ skill when the user wants to: review or audit one or more skills, do a repo-wide health
+ check, standardize skills to a consistent format, fix outdated references, or improve
+ skill descriptions for better triggering. Triggers: 'audit this skill', 'review skill
+ quality', 'check skill health', 'standardize skills', 'skill repo maintenance', or any
+ request to systematically review or improve a SKILL.md file. Do NOT use for creating
+ brand-new skills from scratch — use the skill-creator skill instead.
+---
+
+# Skill Maintainer Skill
+
+Audit, standardize, and improve existing skills. This skill provides exact rules and
+commands so any model — including haiku — can maintain skills mechanically.
+
+Skills live in `template/.claude/skills/`. Each skill is a directory:
+
+```
+template/.claude/skills//
+├── SKILL.md # Required — frontmatter + instructions (< 400 lines)
+├── references/ # Optional — deep-dive documents (one per topic)
+│ └── .md # Descriptive kebab-case names, 200-270 lines each
+└── scripts/ # Optional — executable tools
+ └── *.py or *.sh
+```
+
+---
+
+## The six consistency dimensions
+
+Every skill MUST pass all six checks. These are the primary audit criteria.
+
+### Dimension 1 — Frontmatter block scalar
+
+The `description` field MUST use the `>-` block scalar (folds lines, strips trailing
+newline). Not `>`, not a quoted string, not a plain string.
+
+```yaml
+# CORRECT
+description: >-
+ Comprehensive guide for writing pytest test cases...
+
+# WRONG — quoted string
+description: "Comprehensive guide for writing pytest test cases..."
+
+# WRONG — plain > (keeps trailing newline)
+description: >
+ Comprehensive guide for writing pytest test cases...
+```
+
+**Check:**
+```bash
+grep 'description:' template/.claude/skills/*/SKILL.md
+```
+Every line must show `description: >-`.
+
+### Dimension 2 — Title format
+
+The first H1 heading MUST be `# Skill` where `` matches the skill's
+identity in sentence case.
+
+```markdown
+# Pytest Skill ← CORRECT
+# Python Code Reviewer Skill ← CORRECT
+
+# pytest skill ← WRONG (not sentence case)
+# Pytest ← WRONG (missing "Skill")
+# Pytest Skill Guide ← WRONG (extra word)
+```
+
+**Check:**
+```bash
+grep '^# ' template/.claude/skills/*/SKILL.md | head -1
+```
+Every line must end with ` Skill`.
+
+### Dimension 3 — Reference table header
+
+Every SKILL.md MUST contain a section with the exact header:
+
+```markdown
+## Quick reference: where to go deeper
+```
+
+This section MUST contain a pipe-delimited table with two columns:
+
+```markdown
+| Topic | Reference file |
+|------------------------|------------------------------------------------------|
+| Descriptive topic name | [references/filename.md](references/filename.md) |
+```
+
+Column 1 is a human-readable topic description. Column 2 is a Markdown link using
+relative path `references/.md`. Every file in `references/` MUST appear
+in this table. No orphaned reference files.
+
+**Check:**
+```bash
+grep 'where to go deeper' template/.claude/skills/*/SKILL.md
+```
+Every skill must have exactly one match.
+
+### Dimension 4 — Reference file naming
+
+All files in `references/` MUST use descriptive kebab-case names. No numbered prefixes.
+
+```
+fixtures.md ← CORRECT
+anti-patterns.md ← CORRECT
+document-structure.md ← CORRECT
+
+01-fixtures.md ← WRONG (numbered prefix)
+Fixtures.md ← WRONG (uppercase)
+fixtures_and_more.md ← WRONG (underscores)
+```
+
+**Check:**
+```bash
+find template/.claude/skills/*/references/ -name '[0-9]*' 2>/dev/null
+```
+Must return zero results.
+
+### Dimension 5 — Reference H1 headings
+
+Every reference file MUST start with a sentence-case H1 heading. Capitalize the first
+word and proper nouns only.
+
+```markdown
+# Fixtures ← CORRECT
+# Anti-patterns and fixes ← CORRECT
+# CI, coverage, and plugins ← CORRECT
+
+# All About Fixtures ← WRONG (title case)
+# FIXTURES ← WRONG (all caps)
+# Python Patterns Reference (3.11+) ← WRONG (subtitle, version info)
+```
+
+**Check:**
+```bash
+for f in template/.claude/skills/*/references/*.md; do
+ echo "$(basename "$f"): $(head -1 "$f")"
+done
+```
+Inspect each heading for sentence case.
+
+### Dimension 6 — SKILL.md line count
+
+SKILL.md MUST be under 400 lines. Target range: 150-330 lines. If content exceeds
+400 lines, move sections to `references/`.
+
+**Check:**
+```bash
+wc -l template/.claude/skills/*/SKILL.md
+```
+Every file must show < 400.
+
+---
+
+## Full verification script
+
+Run this after ANY skill edit. All six dimensions in one pass:
+
+```bash
+SKILLS_DIR="template/.claude/skills"
+
+echo "=== 1. Frontmatter block scalar ==="
+for f in "$SKILLS_DIR"/*/SKILL.md; do
+ skill=$(basename "$(dirname "$f")")
+ scalar=$(grep 'description:' "$f" | head -1 | sed 's/.*description: *//')
+ echo " $skill: $scalar"
+done
+
+echo ""
+echo "=== 2. Title format ==="
+for f in "$SKILLS_DIR"/*/SKILL.md; do
+ skill=$(basename "$(dirname "$f")")
+ echo " $skill: $(grep '^# ' "$f" | head -1)"
+done
+
+echo ""
+echo "=== 3. Reference table header ==="
+for f in "$SKILLS_DIR"/*/SKILL.md; do
+ skill=$(basename "$(dirname "$f")")
+ header=$(grep 'where to go deeper' "$f" || echo "(NOT FOUND)")
+ echo " $skill: $header"
+done
+
+echo ""
+echo "=== 4. Reference file naming (any numbered?) ==="
+found=$(find "$SKILLS_DIR"/*/references/ -name '[0-9]*' 2>/dev/null)
+[ -z "$found" ] && echo " None found ✓" || echo " $found"
+
+echo ""
+echo "=== 5. Reference H1 headings ==="
+for f in "$SKILLS_DIR"/*/references/*.md; do
+ skill=$(basename "$(dirname "$(dirname "$f")")")
+ echo " $skill/$(basename "$f"): $(head -1 "$f")"
+done
+
+echo ""
+echo "=== 6. SKILL.md line counts ==="
+wc -l "$SKILLS_DIR"/*/SKILL.md
+```
+
+**Pass criteria:** ALL of these must be true:
+
+- Dimension 1: every skill shows `>-`
+- Dimension 2: every title ends with ` Skill`
+- Dimension 3: every skill has exactly one `where to go deeper` match
+- Dimension 4: zero numbered files found
+- Dimension 5: every H1 heading is sentence case
+- Dimension 6: every SKILL.md is under 400 lines
+
+---
+
+## Step-by-step: standardize a single skill
+
+Follow these steps exactly. Do not skip any step.
+
+### Step 1 — Inventory
+
+```bash
+SKILL=""
+SKILL_DIR="template/.claude/skills/$SKILL"
+
+# Check what exists
+ls -la "$SKILL_DIR"/
+wc -l "$SKILL_DIR/SKILL.md"
+ls "$SKILL_DIR/references/" 2>/dev/null || echo "No references/ directory"
+ls "$SKILL_DIR/scripts/" 2>/dev/null || echo "No scripts/ directory"
+```
+
+### Step 2 — Read SKILL.md and note violations
+
+Read the full SKILL.md. Check each dimension against the rules above. Write down
+every violation found.
+
+### Step 3 — Fix frontmatter (dimension 1)
+
+If the description uses a quoted string or `>` instead of `>-`, replace it.
+
+**Before (quoted string):**
+```yaml
+description: "Audit and improve skills..."
+```
+
+**After:**
+```yaml
+description: >-
+ Audit and improve skills...
+```
+
+**Before (plain >):**
+```yaml
+description: >
+ Audit and improve skills...
+```
+
+**After (add the dash):**
+```yaml
+description: >-
+ Audit and improve skills...
+```
+
+### Step 4 — Fix title (dimension 2)
+
+If the H1 heading does not end with ` Skill`, fix it.
+
+**Before:**
+```markdown
+# Python Code Reviewer (Python 3.11+)
+```
+
+**After:**
+```markdown
+# Python Code Reviewer Skill
+```
+
+Remove version numbers, subtitles, and parenthetical notes from the title.
+Move that information into the intro paragraph below the title.
+
+### Step 5 — Fix reference table (dimension 3)
+
+If the reference section uses a different header or format, replace it.
+
+**Before (wrong header, wrong format):**
+```markdown
+## Reference Files (load on-demand)
+
+| File | When to load |
+|------|-------------|
+| `references/checklist.md` | Deep mode reviews |
+```
+
+**After (correct header, correct format):**
+```markdown
+## Quick reference: where to go deeper
+
+| Topic | Reference file |
+|------------------------------------|--------------------------------------------------------|
+| Full 13-category review checklist | [references/checklist.md](references/checklist.md) |
+```
+
+If other sections use the word "Quick reference" (e.g., a dispatch table), rename
+those to avoid collision. Example: rename `## Quick Reference` to `## Scope dispatch`.
+
+### Step 6 — Fix reference file names (dimension 4)
+
+If any reference files have numbered prefixes, rename them.
+
+```bash
+# Rename numbered files
+cd "$SKILL_DIR/references/"
+mv 01-document-structure.md document-structure.md
+mv 02-formatting-syntax.md formatting-syntax.md
+```
+
+After renaming, update ALL links in SKILL.md that reference the old filenames.
+
+### Step 7 — Fix reference H1 headings (dimension 5)
+
+Read line 1 of each reference file. If not sentence case, fix it.
+
+**Before:**
+```markdown
+# Python Review Checklist — Full Detail (Python 3.11+)
+```
+
+**After:**
+```markdown
+# Review checklist
+```
+
+Rules for sentence case: capitalize first word and proper nouns (Python, Django,
+FastAPI, GitHub, etc.) only. Everything else is lowercase.
+
+### Step 8 — Fix line count (dimension 6)
+
+If SKILL.md exceeds 400 lines, identify sections that are:
+- Detailed reference material (not needed on every invocation)
+- Long code example blocks
+- Extended checklists or tables
+
+Move those sections to new files in `references/`. Add a row to the quick-reference
+table for each new file.
+
+### Step 9 — Run verification
+
+Run the full verification script from above. ALL six dimensions must pass.
+
+---
+
+## Step-by-step: repo-wide audit
+
+```bash
+# List all skills
+ls template/.claude/skills/
+
+# Run full verification (all six dimensions)
+# Copy and run the verification script above
+
+# Check for broken reference links
+for f in template/.claude/skills/*/SKILL.md; do
+ skill_dir=$(dirname "$f")
+ grep -oP 'references/[a-z0-9-]+\.md' "$f" | while read ref; do
+ [ -f "$skill_dir/$ref" ] || echo "BROKEN: $skill_dir/$ref"
+ done
+done
+
+# Check for orphaned reference files (not linked from SKILL.md)
+for dir in template/.claude/skills/*/references; do
+ skill_md="$(dirname "$dir")/SKILL.md"
+ for ref in "$dir"/*.md; do
+ basename_ref=$(basename "$ref")
+ grep -q "$basename_ref" "$skill_md" || echo "ORPHANED: $ref"
+ done
+done
+```
+
+Fix issues in priority order: broken links first, then dimension violations,
+then orphaned files.
+
+---
+
+## SKILL.md section order
+
+When restructuring a SKILL.md, use this standard order:
+
+1. YAML frontmatter (`---` block with `name` and `description: >-`)
+2. `# Skill` title
+3. 1-3 line intro paragraph
+4. Core content sections (main workflows, principles, checklists)
+5. `## Quick reference: where to go deeper` (reference table)
+6. Optional: `## Bundled scripts` (if `scripts/` exists)
+
+---
+
+## Writing style rules
+
+Apply these when editing any skill content:
+
+- **Sentence case headings** — capitalize first word and proper nouns only
+- **Imperative voice** — "Run this command" not "This command can be run"
+- **Short paragraphs** — 3-5 lines max; use tables for parallel items
+- **100-character line width** — wrap prose, not code blocks or tables
+- **No emoji in headings** — emoji are allowed in table cells for quick scanning
+- **Relative links** — use `[references/file.md](references/file.md)`, not absolute paths
+
+---
+
+## Quick reference: where to go deeper
+
+| Topic | Reference file |
+|--------------------------------------|----------------------------------------------------------------------------|
+| Full 9-area deep audit checklist | [references/audit-checklist.md](references/audit-checklist.md) |
+| Annotated before/after fix examples | [references/audit-examples.md](references/audit-examples.md) |
+| Running log of all audit sessions | [references/maintenance-log.md](references/maintenance-log.md) |
diff --git a/template/.claude/skills/skill-maintainer/references/audit-checklist.md b/template/.claude/skills/skill-maintainer/references/audit-checklist.md
new file mode 100644
index 0000000..e8535db
--- /dev/null
+++ b/template/.claude/skills/skill-maintainer/references/audit-checklist.md
@@ -0,0 +1,234 @@
+# Audit checklist
+
+Full 9-area deep audit. Run all areas for each skill, top to bottom. Use for
+thorough reviews, repo-wide sweeps, or when unsure about a specific quality area.
+
+The six consistency dimensions (in SKILL.md) are the minimum bar. This checklist
+goes deeper into content quality, technical accuracy, and cross-skill health.
+
+---
+
+## Severity rubric
+
+Use consistently when labelling issues:
+
+| Severity | Definition | Examples |
+|----------|------------|---------|
+| **HIGH** | Skill won't trigger, produces broken output, or has broken links | Missing/vague description, dangling reference, corrupt script |
+| **MED** | Degrades quality, causes confusion, or routes to wrong skill | Stale examples, missing NOT triggers, passive writing style |
+| **LOW** | Cosmetic, organizational, or future-proofing | Line count creep, minor heading case error, hypothetical examples |
+
+---
+
+## Area 1 — Frontmatter quality
+
+Read the YAML frontmatter at the top of SKILL.md.
+
+**Required fields:** `name` (lowercase, hyphenated) and `description` (using `>-`).
+
+**Description quality checklist:**
+
+| Criterion | Good | Bad |
+|-----------|------|-----|
+| Specific trigger phrases | "triggers on 'Word doc', '.docx'" | "use for document tasks" |
+| Explicit NOT/DO NOT list | "Do NOT use for PDFs" | no exclusions at all |
+| Covers implicit requests | "If user asks for a 'report' as a Word file" | only explicit mentions |
+| Imperative phrasing | "Use this skill whenever..." | "This skill can be used..." |
+| Word count | 80-150 words | < 30 or > 200 |
+
+**Collision check** — compare descriptions across skills for overlapping triggers:
+
+```bash
+grep -A5 "^description:" template/.claude/skills/*/SKILL.md
+```
+
+If two skills claim the same trigger, both need NOT clauses naming the other.
+
+---
+
+## Area 2 — Structure and navigation
+
+- **Clear intro** — 1-3 lines after the title explaining what the skill does
+- **Logical headers** — H2 for major sections, H3 for subsections, no skipped levels
+- **Under 400 lines** — if longer, move content to `references/`
+- **Reference table present** — `## Quick reference: where to go deeper` with table
+- **No dangling references** — every linked file actually exists
+
+```bash
+SKILL_DIR="template/.claude/skills/"
+
+# Line count
+wc -l "$SKILL_DIR/SKILL.md"
+
+# Check all reference links resolve
+grep -oP 'references/[a-z0-9-]+\.md' "$SKILL_DIR/SKILL.md" | while read ref; do
+ [ -f "$SKILL_DIR/$ref" ] || echo "BROKEN: $ref"
+done
+```
+
+**Writing style** — instructions use imperative form ("Run this command", "Set the
+page size"), not passive ("This command can be run", "The page size should be set").
+Flag skills that mix tenses or are mostly passive.
+
+---
+
+## Area 3 — Technical accuracy
+
+The most perishable part of any skill.
+
+**Package versions and install commands:**
+```bash
+# pip packages
+pip index versions --break-system-packages 2>/dev/null | head -3
+
+# npm packages
+npm view version
+```
+
+Flag when a skill pins an old major version and the new version has breaking changes.
+
+**Script paths** — verify every script reference exists:
+```bash
+ls template/.claude/skills//scripts/ 2>/dev/null
+```
+
+**API/syntax accuracy** — spot-check 2-3 code examples against the current library
+API. Flag method renames, removed parameters, or changed import paths.
+
+---
+
+## Area 4 — Script quality
+
+For skills with a `scripts/` directory:
+
+**Runnability:**
+```bash
+python template/.claude/skills//scripts/