From 471f5de524c4a35c21d6ebd66a98fa0d2f4065e9 Mon Sep 17 00:00:00 2001 From: Daniel McCoy Stephenson Date: Sun, 2 Aug 2026 21:17:44 -0600 Subject: [PATCH 1/2] Document install step and add ISSUE_TEMPLATE to doc sources table README never explained how to register create-dev-loop.md itself as a slash command, leaving first-time visitors with no way to actually invoke /create-dev-loop. CLAUDE.md's doc sources-of-truth table also omitted .github/ISSUE_TEMPLATE/*.md, which restates the same conventions as PULL_REQUEST_TEMPLATE.md and can drift the same way. Closes #96, closes #81 --- CLAUDE.md | 1 + README.md | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 66fd0b3..ff185b2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,3 +72,4 @@ Validate by running `/create-dev-loop` against any real repository you maintain | `CONTRIBUTING.md` | Restated conventions and the validation checklist still match this file (`CLAUDE.md`) | | `SECURITY.md` | The trust model still matches what Step 2 of `create-dev-loop.md` actually reads from the target repo | | `.github/PULL_REQUEST_TEMPLATE.md` | Doc-sync checkboxes and test-plan guidance match the current CI scope and this file (`CLAUDE.md`) | +| `.github/ISSUE_TEMPLATE/*.md` | Restated conventions and Step names still match this file (`CLAUDE.md`) | diff --git a/README.md b/README.md index b19fe44..537edc2 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,17 @@ Running `/create-dev-loop` in any repo runs seven Steps (mapped 1:1 to Steps 1 The generated skill drives a 10-phase loop: triage → work selection → implementation → PR → review → address comments → doc check → merge → self-audit → repeat. +## Installation + +Register `create-dev-loop.md` itself as a slash command by symlinking it into `~/.claude/commands/`: + +``` +mkdir -p ~/.claude/commands +ln -s "$(pwd)/create-dev-loop.md" ~/.claude/commands/create-dev-loop.md +``` + +This is a one-time setup step for this repo's own product, distinct from Step 5, which registers each *generated* skill the same way. + ## Usage ``` From 6037cf3675b51fc5589d2c551a03048422ddd793 Mon Sep 17 00:00:00 2001 From: Daniel McCoy Stephenson Date: Sun, 2 Aug 2026 21:24:43 -0600 Subject: [PATCH 2/2] Fold in nice-to-have readiness items: tests, TOC, pinned CI, scripts README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tests/test_check_docs.py: fixture-based unit tests for check_docs.py's three checks, wired into CI, so a silently-broken check doesn't read as "0 errors -> pass". Closes #97. - create-dev-loop.md: add a "Steps at a glance" anchor-link TOC — Step 3 and Step 4 sit ~450 lines apart with a lot of example content between. Closes #99. - .github/workflows/ci.yml: pin actions/checkout and actions/setup-python to commit SHAs instead of major-version tags. Closes #100. - scripts/README.md: one-line pointer to CLAUDE.md/CONTRIBUTING.md. Closes #101 (no CHANGELOG added — fine to skip pre-1.0, per the same issue). --- .github/workflows/ci.yml | 6 +- CLAUDE.md | 2 +- create-dev-loop.md | 8 +++ scripts/README.md | 3 + tests/test_check_docs.py | 130 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 scripts/README.md create mode 100644 tests/test_check_docs.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f011c7..3d4db91 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,9 +10,11 @@ jobs: doc-consistency: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.x" - name: Check doc consistency run: python3 scripts/check_docs.py + - name: Run tests + run: python3 -m unittest discover -s tests -v diff --git a/CLAUDE.md b/CLAUDE.md index ff185b2..58635ae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,7 +46,7 @@ When proposing a change to `create-dev-loop.md`, the generated template, or a ph ## Testing changes -CI (`.github/workflows/ci.yml`) runs `scripts/check_docs.py`, which mechanically enforces two of the rules above — every `{{placeholder}}` in the template has a Step 4 substitution-table row, and README's "What it does" list stays 1:1 with the Steps — plus checks that relative links between the repo's own docs resolve. It catches doc-drift, not behavior; there is no automated test of what `/create-dev-loop` actually generates. Validate behavioral changes by running `/create-dev-loop` against a real repo and confirming: +CI (`.github/workflows/ci.yml`) runs `scripts/check_docs.py`, which mechanically enforces two of the rules above — every `{{placeholder}}` in the template has a Step 4 substitution-table row, and README's "What it does" list stays 1:1 with the Steps — plus checks that relative links between the repo's own docs resolve. It catches doc-drift, not behavior; there is no automated test of what `/create-dev-loop` actually generates. CI also runs `tests/test_check_docs.py`, fixture-based unit tests of `check_docs.py` itself (so a silently-broken check doesn't read as "0 errors → pass"); add cases there when you change its logic. Validate behavioral changes by running `/create-dev-loop` against a real repo and confirming: 1. The generated skill file compiles (no unresolved `{{placeholders}}` remain) 2. The slash command link resolves correctly 3. The reported summary accurately reflects the target repo diff --git a/create-dev-loop.md b/create-dev-loop.md index 21e78dd..fb6eb72 100644 --- a/create-dev-loop.md +++ b/create-dev-loop.md @@ -8,6 +8,14 @@ Creates a tailored `dev-loop` Claude skill for the current repository by explori ## Steps +**Steps at a glance:** [1 — Identify](#1--identify-the-repository) · +[2 — Explore](#2--explore-the-repository) · +[3 — Write the skill file](#3--write-the-skill-file) · +[4 — Fill in the placeholders](#4--fill-in-the-placeholders) · +[5 — Register the skill](#5--register-the-skill) · +[6 — Create a private GitHub repo](#6--create-a-private-github-repo-for-the-skill) · +[7 — Record in a personal catalog](#7--record-the-skill-in-a-personal-catalog-optional) + ### 1 — Identify the repository Confirm the working directory is inside a git repository: diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..cc88e07 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,3 @@ +# scripts/ + +CI-support scripts. See [`CLAUDE.md`](../CLAUDE.md) and [`CONTRIBUTING.md`](../CONTRIBUTING.md) for conventions; `check_docs.py` is documented in `CLAUDE.md`'s "Testing changes" section. diff --git a/tests/test_check_docs.py b/tests/test_check_docs.py new file mode 100644 index 0000000..99840e8 --- /dev/null +++ b/tests/test_check_docs.py @@ -0,0 +1,130 @@ +"""Fixture-based smoke tests for scripts/check_docs.py. + +Each of check_docs.py's three checks appends to a module-level `errors` +list rather than returning a value, so these tests reset that list before +each call and assert on its contents afterward. +""" +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts")) +import check_docs # noqa: E402 + + +class ExtractTemplateBodyTests(unittest.TestCase): + def setUp(self): + check_docs.errors = [] + + def test_extracts_fenced_body(self): + text = "before\n```markdown\nline one\nline two\n```\nafter" + self.assertEqual(check_docs.extract_template_body(text), "line one\nline two") + self.assertEqual(check_docs.errors, []) + + def test_missing_opening_fence_errors(self): + body = check_docs.extract_template_body("no fence here at all") + self.assertEqual(body, "") + self.assertTrue(any("opens the generated-skill template" in e for e in check_docs.errors)) + + def test_missing_closing_fence_errors(self): + body = check_docs.extract_template_body("```markdown\nunterminated") + self.assertEqual(body, "") + self.assertTrue(any("closing" in e for e in check_docs.errors)) + + +class CheckPlaceholdersTests(unittest.TestCase): + def setUp(self): + check_docs.errors = [] + + def _table(self, *names): + rows = "\n".join(f"| `{n}` | ... |" for n in names) + return f"### 4 — Fill in the placeholders\n\n{rows}\n\n### 5 — Register\n" + + def test_all_placeholders_declared_passes(self): + body = "Hello {{NAME}}, {{#if FLAG}}shown{{/if}}" + text = self._table("NAME", "FLAG") + check_docs.check_placeholders(text, body) + self.assertEqual(check_docs.errors, []) + + def test_missing_placeholder_row_errors(self): + body = "Hello {{NAME}} and {{UNDECLARED}}" + text = self._table("NAME") + check_docs.check_placeholders(text, body) + self.assertEqual(len(check_docs.errors), 1) + self.assertIn("{{UNDECLARED}}", check_docs.errors[0]) + + def test_compound_row_covers_both_placeholders(self): + body = "{{GITHUB_OWNER}}/{{GITHUB_REPO}}" + text = self._table("GITHUB_OWNER/REPO") + check_docs.check_placeholders(text, body) + self.assertEqual(check_docs.errors, []) + + def test_missing_table_section_errors(self): + check_docs.check_placeholders("no such section here", "{{NAME}}") + self.assertTrue(any("Fill in the placeholders" in e for e in check_docs.errors)) + + +class CheckReadmeStepsSyncTests(unittest.TestCase): + def setUp(self): + check_docs.errors = [] + + def test_matching_steps_passes(self): + cdl_text = "### 1 — Identify\n...\n### 2 — Explore\n..." + readme_text = "## What it does\n1. **Identify**\n2. **Explore**\n\n## Usage\n" + check_docs.check_readme_steps_sync(cdl_text, readme_text) + self.assertEqual(check_docs.errors, []) + + def test_mismatched_steps_errors(self): + cdl_text = "### 1 — Identify\n...\n### 2 — Explore\n..." + readme_text = "## What it does\n1. **Identify**\n\n## Usage\n" + check_docs.check_readme_steps_sync(cdl_text, readme_text) + self.assertEqual(len(check_docs.errors), 1) + self.assertIn("out of sync", check_docs.errors[0]) + + def test_missing_what_it_does_section_errors(self): + check_docs.check_readme_steps_sync("### 1 — Identify\n...", "## Usage\nno such section\n") + self.assertTrue(any("What it does" in e for e in check_docs.errors)) + + +class CheckLocalLinksTests(unittest.TestCase): + def setUp(self): + check_docs.errors = [] + self._orig_root = check_docs.REPO_ROOT + + def tearDown(self): + check_docs.REPO_ROOT = self._orig_root + + def test_resolvable_link_passes(self): + import tempfile + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "TARGET.md").write_text("target") + (root / "SOURCE.md").write_text("[link](TARGET.md)") + check_docs.REPO_ROOT = root + check_docs.check_local_links() + self.assertEqual(check_docs.errors, []) + + def test_broken_link_errors(self): + import tempfile + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "SOURCE.md").write_text("[link](DOES_NOT_EXIST.md)") + check_docs.REPO_ROOT = root + check_docs.check_local_links() + self.assertEqual(len(check_docs.errors), 1) + self.assertIn("broken relative link", check_docs.errors[0]) + + def test_external_and_anchor_only_links_ignored(self): + import tempfile + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "SOURCE.md").write_text( + "[web](https://example.com)\n[mail](mailto:a@example.com)\n[anchor](#section)" + ) + check_docs.REPO_ROOT = root + check_docs.check_local_links() + self.assertEqual(check_docs.errors, []) + + +if __name__ == "__main__": + unittest.main()