From 1aafb0efb7c530c7a35850237df2e66e5aabab58 Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Wed, 5 Aug 2026 13:22:13 +0200 Subject: [PATCH 01/20] Block edits to generated cute_version files and add hook tests --- .claude/hooks/block-generated-files.py | 19 +++++++- .claude/hooks/tests/test_hooks.py | 66 ++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 .claude/hooks/tests/test_hooks.py diff --git a/.claude/hooks/block-generated-files.py b/.claude/hooks/block-generated-files.py index 614e2c658..1ee047a06 100644 --- a/.claude/hooks/block-generated-files.py +++ b/.claude/hooks/block-generated-files.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 -"""PreToolUse hook: blocks edits to generated shader headers (*_shd.h). +"""PreToolUse hook: blocks edits to generated files. -Only files ending in _shd.h are generated by build/cute-shaderc. +Blocks shader headers (*_shd.h) generated by build/cute-shaderc and +version files (cute_version.h, cute_version.cpp) generated by CMake. cute_shader_bytecode.h is hand-written and NOT blocked. """ import sys @@ -14,6 +15,8 @@ if file_path: name = os.path.basename(file_path) + norm = os.path.normpath(file_path).replace(os.sep, "/") + if name.endswith("_shd.h"): print( f"Blocked: '{name}' is a generated file. " @@ -21,3 +24,15 @@ file=sys.stderr, ) sys.exit(2) + + for suffix, template in ( + ("include/cute_version.h", "include/cute_version.h.in"), + ("src/cute_version.cpp", "src/cute_version.cpp.in"), + ): + if norm.endswith(suffix): + print( + f"Blocked: '{name}' is generated by CMake configure_file. " + f"Edit {template} instead.", + file=sys.stderr, + ) + sys.exit(2) diff --git a/.claude/hooks/tests/test_hooks.py b/.claude/hooks/tests/test_hooks.py new file mode 100644 index 000000000..934791cd2 --- /dev/null +++ b/.claude/hooks/tests/test_hooks.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Tests for the .claude/hooks scripts. Run from the repo root: + + python3 .claude/hooks/tests/test_hooks.py -v + +Each hook reads Claude Code hook JSON on stdin and communicates through its +exit code: 0 = silent pass, 2 = block (PreToolUse) or warn-to-Claude +(PostToolUse), with the message on stderr. +""" +import json +import pathlib +import subprocess +import sys +import unittest + +HOOKS_DIR = pathlib.Path(__file__).resolve().parents[1] +REPO_ROOT = HOOKS_DIR.parents[1] +FIXTURES = pathlib.Path(__file__).resolve().parent / "fixtures" + + +def run_hook(script_name, file_path): + payload = json.dumps({"tool_input": {"file_path": str(file_path)}}) + return subprocess.run( + [sys.executable, str(HOOKS_DIR / script_name)], + input=payload, + capture_output=True, + text=True, + cwd=REPO_ROOT, + ) + + +class TestBlockGeneratedFiles(unittest.TestCase): + SCRIPT = "block-generated-files.py" + + def test_blocks_shd_header(self): + r = run_hook(self.SCRIPT, "include/blit_shd.h") + self.assertEqual(r.returncode, 2) + self.assertIn("generated", r.stderr) + + def test_blocks_generated_version_header(self): + r = run_hook(self.SCRIPT, "include/cute_version.h") + self.assertEqual(r.returncode, 2) + self.assertIn("cute_version.h.in", r.stderr) + + def test_blocks_generated_version_source(self): + r = run_hook(self.SCRIPT, "src/cute_version.cpp") + self.assertEqual(r.returncode, 2) + self.assertIn("cute_version.cpp.in", r.stderr) + + def test_allows_version_templates(self): + for p in ("include/cute_version.h.in", "src/cute_version.cpp.in"): + r = run_hook(self.SCRIPT, p) + self.assertEqual(r.returncode, 0, msg=p) + + def test_allows_normal_source(self): + r = run_hook(self.SCRIPT, "src/cute_draw.cpp") + self.assertEqual(r.returncode, 0) + + def test_allows_shader_bytecode_header(self): + # cute_shader_bytecode.h is hand-written, not generated. + r = run_hook(self.SCRIPT, "include/cute_shader_bytecode.h") + self.assertEqual(r.returncode, 0) + + +if __name__ == "__main__": + unittest.main() From ba9d153421006c6b1a7eb857474c3d88261349ac Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Wed, 5 Aug 2026 13:27:07 +0200 Subject: [PATCH 02/20] Warn on missing copyright block and surface guard warnings to Claude --- .claude/hooks/check-include-guard.py | 34 +++++++++++++------ .../tests/fixtures/include/cute_goodfixture.h | 20 +++++++++++ .../tests/fixtures/include/cute_nocopyright.h | 6 ++++ .../tests/fixtures/include/cute_wrongguard.h | 13 +++++++ .claude/hooks/tests/test_hooks.py | 23 +++++++++++++ 5 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 .claude/hooks/tests/fixtures/include/cute_goodfixture.h create mode 100644 .claude/hooks/tests/fixtures/include/cute_nocopyright.h create mode 100644 .claude/hooks/tests/fixtures/include/cute_wrongguard.h diff --git a/.claude/hooks/check-include-guard.py b/.claude/hooks/check-include-guard.py index 6589d1d2c..74d307b33 100644 --- a/.claude/hooks/check-include-guard.py +++ b/.claude/hooks/check-include-guard.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 -"""PostToolUse hook: warns when an include/ header is missing its expected CF_*_H guard.""" +"""PostToolUse hook: warns when an include/ header is missing its expected CF_*_H guard +or copyright block. Warnings exit with code 2 so they reach Claude.""" import sys import json import os +import re data = json.load(sys.stdin) tool_input = data.get("tool_input", {}) @@ -10,12 +12,14 @@ norm = os.path.normpath(file_path) parts = norm.split(os.sep) +problems = [] + if "include" in parts and file_path.endswith(".h"): - name = os.path.basename(file_path) # e.g. "cute_graphics.h" - base = name[:-2] # strip ".h" + name = os.path.basename(file_path) + base = name[:-2] if base.startswith("cute_"): - rest = base[5:] # "cute_graphics" -> "graphics" + rest = base[5:] elif base == "cute": rest = "" else: @@ -26,10 +30,20 @@ try: with open(file_path) as f: content = f.read() - if expected not in content: - print( - f"WARNING: {name} is missing expected include guard '{expected}'.", - file=sys.stderr, - ) except OSError: - pass + sys.exit(0) + + if expected not in content: + problems.append(f"{name} is missing expected include guard '{expected}'.") + + copyright_re = r"Copyright \(C\) 20\d\d Randy Gaul https://randygaul\.github\.io/" + if not re.search(copyright_re, content): + problems.append( + f"{name} is missing the standard Cute Framework copyright block " + "(see any header in include/ for the exact text)." + ) + +if problems: + for p in problems: + print(f"WARNING: {p}", file=sys.stderr) + sys.exit(2) diff --git a/.claude/hooks/tests/fixtures/include/cute_goodfixture.h b/.claude/hooks/tests/fixtures/include/cute_goodfixture.h new file mode 100644 index 000000000..ff7834add --- /dev/null +++ b/.claude/hooks/tests/fixtures/include/cute_goodfixture.h @@ -0,0 +1,20 @@ +/* + Cute Framework + Copyright (C) 2024 Randy Gaul https://randygaul.github.io/ + + This software is dual-licensed with zlib or Unlicense, check LICENSE.txt for more info +*/ + +#ifndef CF_GOODFIXTURE_H +#define CF_GOODFIXTURE_H + +/** + * @function cf_goodfixture_noop + * @category test + * @brief Fixture symbol for hook tests. + * @return Always returns 0. + * @related cf_goodfixture_noop + */ +int cf_goodfixture_noop(void); + +#endif // CF_GOODFIXTURE_H diff --git a/.claude/hooks/tests/fixtures/include/cute_nocopyright.h b/.claude/hooks/tests/fixtures/include/cute_nocopyright.h new file mode 100644 index 000000000..07e5cd40e --- /dev/null +++ b/.claude/hooks/tests/fixtures/include/cute_nocopyright.h @@ -0,0 +1,6 @@ +#ifndef CF_NOCOPYRIGHT_H +#define CF_NOCOPYRIGHT_H + +int cf_nocopyright_noop(void); + +#endif diff --git a/.claude/hooks/tests/fixtures/include/cute_wrongguard.h b/.claude/hooks/tests/fixtures/include/cute_wrongguard.h new file mode 100644 index 000000000..4ba74c32c --- /dev/null +++ b/.claude/hooks/tests/fixtures/include/cute_wrongguard.h @@ -0,0 +1,13 @@ +/* + Cute Framework + Copyright (C) 2024 Randy Gaul https://randygaul.github.io/ + + This software is dual-licensed with zlib or Unlicense, check LICENSE.txt for more info +*/ + +#ifndef CF_TOTALLY_WRONG_H +#define CF_TOTALLY_WRONG_H + +int cf_wrongguard_noop(void); + +#endif diff --git a/.claude/hooks/tests/test_hooks.py b/.claude/hooks/tests/test_hooks.py index 934791cd2..b5539e92e 100644 --- a/.claude/hooks/tests/test_hooks.py +++ b/.claude/hooks/tests/test_hooks.py @@ -62,5 +62,28 @@ def test_allows_shader_bytecode_header(self): self.assertEqual(r.returncode, 0) +class TestCheckIncludeGuard(unittest.TestCase): + SCRIPT = "check-include-guard.py" + + def test_silent_on_clean_header(self): + r = run_hook(self.SCRIPT, FIXTURES / "include" / "cute_goodfixture.h") + self.assertEqual(r.returncode, 0) + self.assertEqual(r.stderr, "") + + def test_warns_on_wrong_guard(self): + r = run_hook(self.SCRIPT, FIXTURES / "include" / "cute_wrongguard.h") + self.assertEqual(r.returncode, 2) + self.assertIn("CF_WRONGGUARD_H", r.stderr) + + def test_warns_on_missing_copyright(self): + r = run_hook(self.SCRIPT, FIXTURES / "include" / "cute_nocopyright.h") + self.assertEqual(r.returncode, 2) + self.assertIn("copyright", r.stderr.lower()) + + def test_ignores_non_include_paths(self): + r = run_hook(self.SCRIPT, "src/cute_draw.cpp") + self.assertEqual(r.returncode, 0) + + if __name__ == "__main__": unittest.main() From 230550c776e24ce9225430fb7ef2e7c0659095d7 Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Wed, 5 Aug 2026 13:32:08 +0200 Subject: [PATCH 03/20] Add hook warning about doc tags that panic the docs parser --- .claude/hooks/check-docs-tags.py | 54 +++++++++++++++++++ .../tests/fixtures/include/cute_badtag.h | 21 ++++++++ .claude/hooks/tests/test_hooks.py | 30 +++++++++++ 3 files changed, 105 insertions(+) create mode 100755 .claude/hooks/check-docs-tags.py create mode 100644 .claude/hooks/tests/fixtures/include/cute_badtag.h diff --git a/.claude/hooks/check-docs-tags.py b/.claude/hooks/check-docs-tags.py new file mode 100755 index 000000000..42eeb803e --- /dev/null +++ b/.claude/hooks/check-docs-tags.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""PostToolUse hook: warns when an include/ header contains an @tag the docs +parser does not recognize. + +tools/docs_parser.c tokenizes ENTIRE headers (not just doc blocks) and panics +on any unknown @word — even inside a plain // comment. That kills the docs +build in CI. This hook catches it at edit time. Warn-only (exit 2 so the +message reaches Claude). +""" +import json +import os +import re +import sys + +ALLOWED = { + "function", "struct", "enum", "category", "brief", "param", "return", + "remarks", "example", "related", "member", "entry", "end", +} + +data = json.load(sys.stdin) +file_path = data.get("tool_input", {}).get("file_path", "") + +norm = os.path.normpath(file_path) +parts = norm.split(os.sep) +if "include" not in parts or not file_path.endswith(".h"): + sys.exit(0) + +try: + with open(file_path) as f: + lines = f.readlines() +except OSError: + sys.exit(0) + +bad = [] +for lineno, line in enumerate(lines, 1): + for m in re.finditer(r"@([A-Za-z_]\w*)", line): + if m.group(1) not in ALLOWED: + bad.append((lineno, "@" + m.group(1))) + +if bad: + name = os.path.basename(file_path) + print( + f"WARNING: {name} contains @tags the docs parser rejects — " + "these will PANIC the docs build in CI:", + file=sys.stderr, + ) + for lineno, tag in bad: + print(f" line {lineno}: {tag}", file=sys.stderr) + print( + "Allowed tags: " + " ".join(sorted("@" + t for t in ALLOWED)) + ". " + "Mark deprecations in prose inside @brief/@remarks instead of @deprecated.", + file=sys.stderr, + ) + sys.exit(2) diff --git a/.claude/hooks/tests/fixtures/include/cute_badtag.h b/.claude/hooks/tests/fixtures/include/cute_badtag.h new file mode 100644 index 000000000..f89a9606d --- /dev/null +++ b/.claude/hooks/tests/fixtures/include/cute_badtag.h @@ -0,0 +1,21 @@ +/* + Cute Framework + Copyright (C) 2024 Randy Gaul https://randygaul.github.io/ + + This software is dual-licensed with zlib or Unlicense, check LICENSE.txt for more info +*/ + +#ifndef CF_BADTAG_H +#define CF_BADTAG_H + +/** + * @function cf_badtag_noop + * @category test + * @brief Fixture with tags the docs parser rejects. + * @deprecated Use something else instead. + * @return Always returns 0. + */ +// @todo remove this someday +int cf_badtag_noop(void); + +#endif // CF_BADTAG_H diff --git a/.claude/hooks/tests/test_hooks.py b/.claude/hooks/tests/test_hooks.py index b5539e92e..db4118535 100644 --- a/.claude/hooks/tests/test_hooks.py +++ b/.claude/hooks/tests/test_hooks.py @@ -85,5 +85,35 @@ def test_ignores_non_include_paths(self): self.assertEqual(r.returncode, 0) +class TestCheckDocsTags(unittest.TestCase): + SCRIPT = "check-docs-tags.py" + + def test_silent_on_allowed_tags(self): + r = run_hook(self.SCRIPT, FIXTURES / "include" / "cute_goodfixture.h") + self.assertEqual(r.returncode, 0) + self.assertEqual(r.stderr, "") + + def test_warns_on_unknown_tags_with_line_numbers(self): + r = run_hook(self.SCRIPT, FIXTURES / "include" / "cute_badtag.h") + self.assertEqual(r.returncode, 2) + self.assertIn("@deprecated", r.stderr) + self.assertIn("@todo", r.stderr) + self.assertIn("docs build", r.stderr) + + def test_ignores_non_include_files(self): + r = run_hook(self.SCRIPT, "src/cute_draw.cpp") + self.assertEqual(r.returncode, 0) + + def test_all_real_headers_are_clean(self): + # Regression guard: every current public header must pass, or the + # hook would nag on every edit. (docs CI is green, so they must.) + import glob + for h in glob.glob(str(REPO_ROOT / "include" / "cute_*.h")): + if h.endswith("_shd.h"): + continue # generated shader headers, not doc-parsed prose + r = run_hook(self.SCRIPT, h) + self.assertEqual(r.returncode, 0, msg=f"{h}: {r.stderr}") + + if __name__ == "__main__": unittest.main() From 68a797714ce3e4f4a1935132ea15b1f637eb3f54 Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Wed, 5 Aug 2026 18:39:11 +0200 Subject: [PATCH 04/20] Anchor docs-tag detection to token starts like the real parser --- .claude/hooks/check-docs-tags.py | 2 +- .../tests/fixtures/include/cute_midtoken.h | 20 +++++++++++++++++++ .claude/hooks/tests/test_hooks.py | 7 +++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 .claude/hooks/tests/fixtures/include/cute_midtoken.h diff --git a/.claude/hooks/check-docs-tags.py b/.claude/hooks/check-docs-tags.py index 42eeb803e..3c48931e2 100755 --- a/.claude/hooks/check-docs-tags.py +++ b/.claude/hooks/check-docs-tags.py @@ -33,7 +33,7 @@ bad = [] for lineno, line in enumerate(lines, 1): - for m in re.finditer(r"@([A-Za-z_]\w*)", line): + for m in re.finditer(r"(?:^|\s)@([A-Za-z_]\w*)", line): if m.group(1) not in ALLOWED: bad.append((lineno, "@" + m.group(1))) diff --git a/.claude/hooks/tests/fixtures/include/cute_midtoken.h b/.claude/hooks/tests/fixtures/include/cute_midtoken.h new file mode 100644 index 000000000..008af82a0 --- /dev/null +++ b/.claude/hooks/tests/fixtures/include/cute_midtoken.h @@ -0,0 +1,20 @@ +/* + Cute Framework + Copyright (C) 2024 Randy Gaul https://randygaul.github.io/ + + This software is dual-licensed with zlib or Unlicense, check LICENSE.txt for more info +*/ + +#ifndef CF_MIDTOKEN_H +#define CF_MIDTOKEN_H + +/** + * @function cf_midtoken_noop + * @category test + * @brief Fixture with @ symbols that are not doc tags (mid-token). + * @return Always returns 0. + */ +// see http://example.com/page@fragment and array@index +int cf_midtoken_noop(void); + +#endif // CF_MIDTOKEN_H diff --git a/.claude/hooks/tests/test_hooks.py b/.claude/hooks/tests/test_hooks.py index db4118535..08f3bbf62 100644 --- a/.claude/hooks/tests/test_hooks.py +++ b/.claude/hooks/tests/test_hooks.py @@ -104,6 +104,13 @@ def test_ignores_non_include_files(self): r = run_hook(self.SCRIPT, "src/cute_draw.cpp") self.assertEqual(r.returncode, 0) + def test_ignores_at_symbols_mid_token(self): + # The real docs parser only reacts to whitespace-delimited @tokens, + # so @ symbols in URLs or other mid-token positions should not trigger. + r = run_hook(self.SCRIPT, FIXTURES / "include" / "cute_midtoken.h") + self.assertEqual(r.returncode, 0) + self.assertEqual(r.stderr, "") + def test_all_real_headers_are_clean(self): # Regression guard: every current public header must pass, or the # hook would nag on every edit. (docs CI is green, so they must.) From 969063cc48a829238cd1e8365aacab92724afc27 Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Wed, 5 Aug 2026 18:43:36 +0200 Subject: [PATCH 05/20] Add hook warning about unregistered source, header, test, and sample files --- .claude/hooks/check-registration.py | 76 +++++++++++++++++++++++++++++ .claude/hooks/tests/test_hooks.py | 51 +++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 .claude/hooks/check-registration.py diff --git a/.claude/hooks/check-registration.py b/.claude/hooks/check-registration.py new file mode 100644 index 000000000..6dc9e0c56 --- /dev/null +++ b/.claude/hooks/check-registration.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""PostToolUse hook: warns when a new file is not registered where the build +expects it. Catches "wrote the file, forgot to register it, build still green +because nothing references it yet." + + src/*.cpp -> CF_SRCS in CMakeLists.txt + include/cute_*.h -> #include in include/cute.h (with whitelist) + test/test_*.cpp -> CF_TEST_SRCS in test/CMakeLists.txt + AND TEST_SUITE/RUN_TRACED in test/main.cpp + samples/*.c|*.cpp -> add_sample(...) in samples/CMakeLists.txt + +Warn-only (exit 2 so the message reaches Claude). Runs from the repo root. +""" +import json +import os +import sys + +# Headers deliberately not in the cute.h umbrella. +UMBRELLA_WHITELIST = { + "cute_result.h", "cute_shader_bytecode.h", "cute_user_config.h", + "cute_priority_queue.h", "cute_debug_printf.h", "cute_c_runtime.h", + "cute_defines.h", +} + + +def read(path): + try: + with open(path) as f: + return f.read() + except OSError: + return "" + + +data = json.load(sys.stdin) +file_path = data.get("tool_input", {}).get("file_path", "") +if not file_path: + sys.exit(0) + +rel = os.path.relpath(os.path.abspath(file_path), os.getcwd()) +rel = rel.replace(os.sep, "/") +name = os.path.basename(rel) +problems = [] + +if rel.startswith("src/") and rel.endswith(".cpp") and rel.count("/") == 1: + if name not in read("CMakeLists.txt"): + problems.append(f"{name} is not listed in CF_SRCS in CMakeLists.txt.") + +elif (rel.startswith("include/") and rel.endswith(".h") + and rel.count("/") == 1 and name.startswith("cute_") + and not name.endswith("_shd.h") and name not in UMBRELLA_WHITELIST): + if name not in read("include/cute.h"): + problems.append(f"{name} is not included by the umbrella header include/cute.h.") + +elif (rel.startswith("test/") and rel.endswith(".cpp") + and rel.count("/") == 1 and name.startswith("test_")): + stem = name[:-len(".cpp")] + if name not in read("test/CMakeLists.txt"): + problems.append(f"{name} is not listed in CF_TEST_SRCS in test/CMakeLists.txt.") + main_cpp = read("test/main.cpp") + if stem not in main_cpp: + problems.append( + f"suite '{stem}' is not registered in test/main.cpp " + f"(needs TEST_SUITE({stem}); and RUN_TRACED({stem});)." + ) + +elif (rel.startswith("samples/") and rel.count("/") == 1 + and (rel.endswith(".c") or rel.endswith(".cpp"))): + if name not in read("samples/CMakeLists.txt"): + problems.append( + f"{name} has no add_sample(...) entry in samples/CMakeLists.txt." + ) + +if problems: + for p in problems: + print(f"WARNING: {p}", file=sys.stderr) + sys.exit(2) diff --git a/.claude/hooks/tests/test_hooks.py b/.claude/hooks/tests/test_hooks.py index 08f3bbf62..021c2f87f 100644 --- a/.claude/hooks/tests/test_hooks.py +++ b/.claude/hooks/tests/test_hooks.py @@ -122,5 +122,56 @@ def test_all_real_headers_are_clean(self): self.assertEqual(r.returncode, 0, msg=f"{h}: {r.stderr}") +class TestCheckRegistration(unittest.TestCase): + SCRIPT = "check-registration.py" + + def test_registered_source_is_silent(self): + r = run_hook(self.SCRIPT, "src/cute_draw.cpp") + self.assertEqual(r.returncode, 0) + + def test_unregistered_source_warns(self): + r = run_hook(self.SCRIPT, "src/cute_notreal.cpp") + self.assertEqual(r.returncode, 2) + self.assertIn("CF_SRCS", r.stderr) + + def test_registered_header_is_silent(self): + r = run_hook(self.SCRIPT, "include/cute_draw.h") + self.assertEqual(r.returncode, 0) + + def test_unregistered_header_warns(self): + r = run_hook(self.SCRIPT, "include/cute_notreal.h") + self.assertEqual(r.returncode, 2) + self.assertIn("cute.h", r.stderr) + + def test_whitelisted_header_is_silent(self): + r = run_hook(self.SCRIPT, "include/cute_defines.h") + self.assertEqual(r.returncode, 0) + + def test_registered_test_is_silent(self): + r = run_hook(self.SCRIPT, "test/test_math.cpp") + self.assertEqual(r.returncode, 0) + + def test_unregistered_test_warns_both_registries(self): + r = run_hook(self.SCRIPT, "test/test_notreal.cpp") + self.assertEqual(r.returncode, 2) + self.assertIn("CF_TEST_SRCS", r.stderr) + self.assertIn("main.cpp", r.stderr) + + def test_registered_sample_is_silent(self): + r = run_hook(self.SCRIPT, "samples/easy_sprite.c") + self.assertEqual(r.returncode, 0) + + def test_unregistered_sample_warns(self): + r = run_hook(self.SCRIPT, "samples/notreal.cpp") + self.assertEqual(r.returncode, 2) + self.assertIn("add_sample", r.stderr) + + def test_non_registerable_paths_are_silent(self): + for p in ("docs/foo.md", "test/test_harness.h", "include/cute_version.h.in", + ".claude/hooks/tests/fixtures/include/cute_goodfixture.h"): + r = run_hook(self.SCRIPT, p) + self.assertEqual(r.returncode, 0, msg=p) + + if __name__ == "__main__": unittest.main() From c56e5d8a3628d669a423b68c8784a514af0afb09 Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Wed, 5 Aug 2026 23:03:17 +0200 Subject: [PATCH 06/20] Bound registry matching to avoid false silent passes --- .claude/hooks/check-registration.py | 16 +++++++++++----- .claude/hooks/tests/test_hooks.py | 6 ++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.claude/hooks/check-registration.py b/.claude/hooks/check-registration.py index 6dc9e0c56..cc9953410 100644 --- a/.claude/hooks/check-registration.py +++ b/.claude/hooks/check-registration.py @@ -13,6 +13,7 @@ """ import json import os +import re import sys # Headers deliberately not in the cute.h umbrella. @@ -31,6 +32,11 @@ def read(path): return "" +def registered(needle, text): + """True when needle appears as a path/token-bounded occurrence in text.""" + return re.search(r"(? Date: Wed, 5 Aug 2026 23:07:41 +0200 Subject: [PATCH 07/20] Wire docs-tag and registration hooks into settings --- .claude/settings.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.claude/settings.json b/.claude/settings.json index b9e91f16f..26a3ad9dd 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -18,6 +18,14 @@ { "type": "command", "command": "python3 .claude/hooks/check-include-guard.py" + }, + { + "type": "command", + "command": "python3 .claude/hooks/check-docs-tags.py" + }, + { + "type": "command", + "command": "python3 .claude/hooks/check-registration.py" } ] } From 76a8df548276d155a6c2a949aa9b8e2801bf9b71 Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Wed, 5 Aug 2026 23:10:02 +0200 Subject: [PATCH 08/20] Add cmake-conventions skill --- .claude/skills/cmake-conventions/SKILL.md | 74 +++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .claude/skills/cmake-conventions/SKILL.md diff --git a/.claude/skills/cmake-conventions/SKILL.md b/.claude/skills/cmake-conventions/SKILL.md new file mode 100644 index 000000000..9e02283fe --- /dev/null +++ b/.claude/skills/cmake-conventions/SKILL.md @@ -0,0 +1,74 @@ +--- +name: cmake-conventions +description: Modern CMake conventions for Cute Framework, SDL-inspired. Reference before writing or modifying any CMakeLists.txt or cmake/ file in this repo. +user-invocable: false +--- + +# CMake Conventions (SDL-inspired) + +Cute Framework is a framework consumed by other projects. Every CMake change +must keep it well-behaved both standalone and as a dependency (FetchContent / +add_subdirectory / installed package). SDL3 is the reference implementation +of these practices. + +## Consumability rules + +- **Namespaced targets.** Consumers link `cute::cute`, never bare `cute`. + Any new library target gets `add_library(cute:: ALIAS )`. +- **Export sets.** Installed targets use + `install(TARGETS ... EXPORT cute-targets ...)` + + `install(EXPORT cute-targets NAMESPACE cute:: DESTINATION lib/cmake/cute)`. +- **Config package.** `find_package(cute CONFIG)` must work: + `configure_package_config_file` + `write_basic_package_version_file` + (SameMajorVersion). The config file declares dependencies with + `find_dependency` — a static cute must propagate what it links. +- **Headers.** Public headers are attached to the target via + `target_sources(cute PUBLIC FILE_SET HEADERS BASE_DIRS include FILES ...)` + and installed through the FILE_SET (CMake ≥ 4.2 is required, so FILE_SET + is always available). +- **Interface hygiene.** Every public include dir carries BOTH generator + expressions: `$` and `$`. +- **No global state.** Never `add_definitions`, bare `add_compile_options`, + or `set(CMAKE_*_FLAGS ...)` at directory scope for things a consumer would + inherit. Use `target_compile_definitions/options/features(... PRIVATE|PUBLIC)`. + Anything that must be global (output dirs, folders) goes behind + `if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)` — i.e. only when cute is + the top-level project. +- **Options.** All options are `CF_`-prefixed, declared with `option()`, and + work in any combination when cute is a subproject. +- **Never** hardcode compiler flags a consumer can't override; prefer + `target_compile_features(cute PUBLIC cxx_std_20 c_std_23)` over setting + global CMAKE_CXX_STANDARD for consumers. + +## Repo-specific rules + +- **Vendored SDL3 must win the include-path race.** The build vendors SDL + via FetchContent; its include dirs must stay ahead of any system SDL + (use `BEFORE` where needed). Rationale: a Homebrew `CPATH` export can leak + a newer system SDL3 with an incompatible ABI into the build. +- **Registration points** (a file that exists but is unregistered builds + green and does nothing): + - new `src/*.cpp` → `CF_SRCS` in the root `CMakeLists.txt` + - new public header → `#include` in `include/cute.h` + - new `test/test_*.cpp` → `CF_TEST_SRCS` in `test/CMakeLists.txt` AND + `TEST_SUITE(...)`/`RUN_TRACED(...)` in `test/main.cpp` + - new sample → `add_sample( )` in `samples/CMakeLists.txt` +- **Emscripten sample assets:** a sample with a `_data/` folder needs + `target_link_options( PRIVATE --preload-file + "${CMAKE_CURRENT_SOURCE_DIR}/_data@/_data")` inside the + `if (EMSCRIPTEN)` block of `samples/CMakeLists.txt`, or the web build + ships a sample that exits at startup. +- **Platform detection** happens once, near the top of the root + CMakeLists.txt (EMSCRIPTEN first so it doesn't fall into the UNIX path); + extend that block rather than sprinkling `if(APPLE)` checks. +- **Version files** `include/cute_version.h` and `src/cute_version.cpp` are + generated via `configure_file` — edit the `.in` templates. + +## Checklist for any CMake change + +1. Does it still configure as a subproject? Quick check: + a scratch consumer with `FetchContent_Declare(cute SOURCE_DIR )`. +2. Did you introduce any directory-scope flags/definitions? Move them onto + targets. +3. New target? Add the `cute::` alias and decide install/export membership. +4. Anything user-visible (option, target name) documented in README/docs? From 9a5c89a07be4123c81f79a8c5c1e198ba76e30dd Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Wed, 5 Aug 2026 23:14:55 +0200 Subject: [PATCH 09/20] Clarify cmake-conventions consumability rules as target state --- .claude/skills/cmake-conventions/SKILL.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.claude/skills/cmake-conventions/SKILL.md b/.claude/skills/cmake-conventions/SKILL.md index 9e02283fe..d96801a0d 100644 --- a/.claude/skills/cmake-conventions/SKILL.md +++ b/.claude/skills/cmake-conventions/SKILL.md @@ -13,6 +13,15 @@ of these practices. ## Consumability rules +> **Status: target-state rules.** These describe what any new or modified +> CMake must move toward — the planned CMake-modernization project will +> implement them wholesale. Several are NOT yet true of this repo: there is +> no `cute::cute` alias, no `FILE_SET`/`install(EXPORT)`, no +> `$`, and the top-level file still sets global +> `CMAKE_CXX_STANDARD`/`CMAKE_C_STANDARD`. Do not assume this +> infrastructure exists when writing build code today — in-tree consumers +> (samples, tests) link the bare `cute` target. + - **Namespaced targets.** Consumers link `cute::cute`, never bare `cute`. Any new library target gets `add_library(cute:: ALIAS )`. - **Export sets.** Installed targets use From 1119b8be3f45e5f33865a28767c57874003c3249 Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Wed, 5 Aug 2026 23:17:02 +0200 Subject: [PATCH 10/20] Add perf-benchmarking methodology skill --- .claude/skills/perf-benchmarking/SKILL.md | 58 +++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .claude/skills/perf-benchmarking/SKILL.md diff --git a/.claude/skills/perf-benchmarking/SKILL.md b/.claude/skills/perf-benchmarking/SKILL.md new file mode 100644 index 000000000..780fb29fd --- /dev/null +++ b/.claude/skills/perf-benchmarking/SKILL.md @@ -0,0 +1,58 @@ +--- +name: perf-benchmarking +description: Benchmark methodology for Cute Framework performance work on this Mac. Use before ANY perf claim, comparison, or optimization — no perf statement without same-harness numbers. +--- + +# Performance Benchmarking Methodology + +Hard rule: **no perf claim without same-harness before/after numbers.** +"Should be faster" is a hypothesis, not a result. + +## Why the obvious approach lies on this machine + +Frame times on this Mac are **bimodal** — ~3× swings from P-core vs E-core +scheduling and compositor throttle, and runs sometimes vsync-lock to +~16.6 ms even with `cf_app_set_vsync(false)`. Two sequential runs of the +same binary can differ more than the optimization you're measuring. + +## The method + +1. **Define the metric first.** Usually frame-time p50 at a fixed workload; + for CPU-side work prefer phase timers (submit / batch / present split) or + a microbench median — end-to-end frame deltas at small workloads hide + under a ~1.9 ms present/compositor floor. +2. **Interleave A/B.** Never run all of A then all of B. Alternate the two + binaries round-robin within one session so thermal/scheduler drift hits + both sides equally. +3. **Min-of-rounds.** Compare the minimum of each round's p50s (or medians + for microbenches). The minimum is the least-noisy estimator here. +4. **Fixed workload tiers: 100 / 1k / 10k / 100k draws.** + **Never exceed 100k draws/frame on this machine** — 100k-churn and 1M + workloads hard-crash the Mac. Meaningful end-to-end deltas only show at + 100k; ≤10k sits under the present floor. +5. **Record numbers in the report** — actual µs/ms values for both sides, + the workload, and which estimator you used. Also record **refuted + hypotheses** so nobody re-chases them (e.g. hash lookups ~16 ns/sprite — + not worth chasing; the report-phase memset was ~2–3%). +6. **Watch bystander metrics.** A win in batch time that regresses submit + time by 10% must be called out, not buried. + +## Tools + +- Phase timers / microbenches in the code beat external sampling for A/B. +- `xctrace record --template 'Time Profiler'` (Instruments CLI) for finding + where time goes when you don't yet have a hypothesis. +- Crash forensics: `.ips` reports land in `~/Library/Logs/DiagnosticReports/` + (auto-moved to `Retired/` within minutes); JSON after the first line. + +## Known landscape (don't rediscover) + +- Draw batching: no-op sorter + geometry-by-pointer + memo cache already + landed; spritebatch CPU at 10k sprites ~12× faster than pre-2026-07. +- Render-pass churn: per-batch vertex re-upload forces a render-pass + teardown per batch; batching uploads before draws was validated ~10× but + check current state before assuming it landed. Raising SDL + frames-in-flight makes it WORSE. +- Command-stream churn workload shows superlinear cost (separate, unexplored). +- Upstream perf issues on file: #47 (vertex-data ceiling), #501 + (canvas-size fps regression). From 23906394ed9918c625b3c49f5965969019b0dc8d Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Wed, 5 Aug 2026 23:21:37 +0200 Subject: [PATCH 11/20] Add test-writing skill --- .claude/skills/test-writing/SKILL.md | 70 ++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .claude/skills/test-writing/SKILL.md diff --git a/.claude/skills/test-writing/SKILL.md b/.claude/skills/test-writing/SKILL.md new file mode 100644 index 000000000..e4e7033c0 --- /dev/null +++ b/.claude/skills/test-writing/SKILL.md @@ -0,0 +1,70 @@ +--- +name: test-writing +description: How to write and run Cute Framework tests — harness macros, registration points, headless runs, and suite traps. Use when adding or modifying anything under test/. +--- + +# Writing Cute Framework Tests + +The test suite is one binary (`build/tests`) built from `test/`, using +pico_unit (`libraries/pico/pico_unit.h`) via `test/test_harness.h`. + +## Adding a test + +Three registration points — miss one and the test silently never runs: + +1. Create `test/test_.cpp`: + + ```c + #include "test_harness.h" + #include + using namespace Cute; + + TEST_CASE(test__does_thing) + { + REQUIRE(1 + 1 == 2); + return true; + } + + TEST_SUITE(test_) + { + RUN_TEST_CASE(test__does_thing); + } + ``` + +2. Add `test_.cpp` to `CF_TEST_SRCS` in `test/CMakeLists.txt`. +3. In `test/main.cpp`: add `TEST_SUITE(test_);` to the declarations + AND `RUN_TRACED(test_);` to the run list. + +Macros: `REQUIRE(cond)` (truthy), `CHECK(x)` = `REQUIRE(!(x))` (for +0-means-success results), `CHECK_POINTER(x)`. Test cases return `true`. + +## Running + +- Build: `cmake --build build --target tests` +- All: `./build/tests` +- **CLI filters by SUITE name only**: `./build/tests test_draw3d test_mrt`. + Passing a CASE name silently runs nothing (Total: 0) — that is the trap. +- `CF_TEST_DUMP=1` writes readback dumps (`build/dump_*.png`) for graphics + tests. Linux CI runs everything under + `xvfb-run -a -s "-screen 0 1280x720x24"` with `SDL_AUDIODRIVER=dummy` + and `LIBGL_ALWAYS_SOFTWARE=1`. + +## Traps (all learned the hard way) + +- **Baseline first.** Run the full suite on a clean master BEFORE judging + your branch — this Retina Mac has known display-dependent failures. + Compare failure lists, not pass percentages. +- **`AppDestroyGuard` is a reserved name.** Defining a same-named struct + with a different inline dtor in another test file is an ODR violation — + the linker silently merges them and you get a segfault in an unrelated + suite. Use a distinct guard-struct name per file (`OwnedAppGuard` etc.). +- **Display-query tests must run before any app create/destroy** in a + suite: `cf_destroy_app` calls `SDL_Quit()`, after which + `cf_display_count()` reports 0. +- **Apps are shared between tests** via `test_app_shared.h` fixtures + (`test_make_app`/`test_destroy_app`); don't create raw apps in graphics + tests — reuse the fixture, and read its header before touching lifecycle. +- **Graphics readback:** results are only valid after submit — draw, call + the readback helper from `test_app_shared.h`, THEN assert pixels. +- **TDD default:** write the failing test, watch it fail + (`./build/tests test_`), then implement. From 6ea0f7d6a87163a7f44e7deedba985ec2c71bbd5 Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Wed, 5 Aug 2026 23:24:30 +0200 Subject: [PATCH 12/20] Add sample-writer agent --- .claude/agents/sample-writer.md | 47 +++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .claude/agents/sample-writer.md diff --git a/.claude/agents/sample-writer.md b/.claude/agents/sample-writer.md new file mode 100644 index 000000000..6dd050749 --- /dev/null +++ b/.claude/agents/sample-writer.md @@ -0,0 +1,47 @@ +--- +name: sample-writer +description: Writes or updates samples in samples/ for Cute Framework. Use when a new feature needs a demo, an existing sample is stale or broken, or a bug report needs a minimal reproduction sample. +color: magenta +--- + +You are a sample writer for Cute Framework, a C/C++ 2D game framework. +Samples are the framework's front door — most users learn the API by reading +them. Your samples must be the cleanest possible demonstration of one idea. + +**The idiom** — before writing anything, read 2-3 existing samples closest +to your topic (grep `samples/` for the APIs involved). The house style: + +- Single file, C (`.c`) or C++ (`.cpp`) — match whichever the nearest + neighbors use. C++ samples use `using namespace Cute;`. +- Shape: `cf_make_app(...)` → `while (cf_app_is_running()) { cf_app_update(NULL); ... draw ...; cf_app_draw_onto_screen(...); }` → `cf_destroy_app()`. +- Minimal comments — one short block at the top saying what the sample + shows, inline comments only where the API is genuinely surprising. +- No engine-style abstraction: no wrapper classes, no config systems. Flat, + readable, deletable code. A sample that needs scrolling to understand the + point is too long. + +**Registration** (a sample that builds but isn't registered doesn't exist): + +1. `add_sample( )` in `samples/CMakeLists.txt` (targets are + lowercase, no underscores in older ones — match existing naming). +2. Assets go in `samples/_data/`; web builds ALSO need + `target_link_options( PRIVATE --preload-file + "${CMAKE_CURRENT_SOURCE_DIR}/_data@/_data")` in the + `if (EMSCRIPTEN)` block — missing preloads ship a web sample that exits + at startup. +3. Web presence (when asked to publish the sample to the docs site): + nav entry in `mkdocs.yml`, a `docs/samples/.md` page (copy an + existing one — iframe embed + fullscreen button), and a card in + `docs/samples/index.md`. + +**Verification — required before you report done:** + +1. Build: `cmake --build build --target `. +2. Run it a few seconds and confirm it doesn't crash: + `./.github/scripts/smoke_test.sh ./build/ 5`. +3. If you touched assets, state where they load from (mount path) and + confirm the preload entry for web. + +**Deliverable** — the sample file, its registration, what you verified +(commands + actual output summarized), and a one-line description suitable +for the docs nav. From afa2385c4eb5668fb5e05be1a00dbd7c3909b6b4 Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Wed, 5 Aug 2026 23:44:05 +0200 Subject: [PATCH 13/20] Add researcher agent --- .claude/.gitignore | 1 + .claude/agents/researcher.md | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 .claude/.gitignore create mode 100644 .claude/agents/researcher.md diff --git a/.claude/.gitignore b/.claude/.gitignore new file mode 100644 index 000000000..26c2bce5f --- /dev/null +++ b/.claude/.gitignore @@ -0,0 +1 @@ +!agents/ diff --git a/.claude/agents/researcher.md b/.claude/agents/researcher.md new file mode 100644 index 000000000..11d1f9987 --- /dev/null +++ b/.claude/agents/researcher.md @@ -0,0 +1,44 @@ +--- +name: researcher +description: Researches technical questions for Cute Framework development — SDL3/SDL_GPU internals, Emscripten/WebGL2 constraints, peer-framework API design (raylib, sokol), platform graphics behavior, CMake practice. Use for any "how does X actually work" or "how do others do this" question. Read-only; produces a findings brief. +tools: Read, Grep, Glob, Bash, WebFetch, WebSearch +color: purple +--- + +You are a technical researcher for Cute Framework, a C/C++ 2D game +framework built on SDL3. You answer questions with evidence, not vibes. + +**Source hierarchy — in this order:** + +1. **Vendored source in this repo.** SDL3's actual code is in the build tree + (`build/_deps/*sdl*-src/`) and single-file libs in `libraries/`. Read the + implementation before trusting any documentation about it. +2. **Official upstream sources:** SDL wiki/headers, Emscripten docs, Khronos + specs, vendor docs (Apple Metal, etc.). +3. **Peer framework source** (raylib, sokol, SDL examples) — how others + solved it, fetched via web when not local. +4. **Forums/issues/blogs** — leads only, never load-bearing evidence. + +**Method:** + +- Distinguish **verified-in-source** (you read the code; cite `file:line`) + from **claimed-in-docs** (cite URL) from **hearsay** (say so). Label each + key claim with which it is. +- Version-check everything: SDL3 APIs move; note the vendored SDL version + (`build/_deps` CMake cache or SDL_version.h) when it matters. +- When the question is "how do peer frameworks do X", survey at least two + and describe trade-offs, not just existence. +- If the evidence is inconclusive, say so and state what experiment would + settle it — do not paper over gaps. +- You never edit files. Bash is for read-only exploration (find, grep, git + log) only. + +**Deliverable — a findings brief:** + +1. **Answer** — the direct answer in 2-3 sentences. +2. **Confidence** — high / medium / low, with the reason. +3. **Evidence** — the key claims, each labeled verified/claimed/hearsay + with its citation. +4. **Implications for CF** — what this means for the task that spawned the + question. +5. **Open questions** — anything that needs an experiment or a decision. From feaad3f62e5887246058c2cba37b6a537cc23603 Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Wed, 5 Aug 2026 23:49:55 +0200 Subject: [PATCH 14/20] Add performance-engineer agent --- .claude/agents/performance-engineer.md | 50 ++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .claude/agents/performance-engineer.md diff --git a/.claude/agents/performance-engineer.md b/.claude/agents/performance-engineer.md new file mode 100644 index 000000000..a0cbab8fd --- /dev/null +++ b/.claude/agents/performance-engineer.md @@ -0,0 +1,50 @@ +--- +name: performance-engineer +description: Profiles, benchmarks, and optimizes Cute Framework code. Use for any perf-motivated change — before optimizing (to measure and find the real bottleneck) and after (to prove the win). Also use to adjudicate competing optimization approaches with data. +color: orange +--- + +You are a performance engineer for Cute Framework, a C/C++ 2D game +framework. Your currency is measurements; you never assert a perf outcome +you have not measured. + +**Methodology contract** — invoke the `perf-benchmarking` skill at the start +of every engagement and follow it exactly: metric first, interleaved A/B, +min-of-rounds, fixed workload tiers, never above 100k draws/frame on this +machine, numbers in the report, refuted hypotheses recorded. + +**Workflow:** + +1. **Baseline before touching anything.** Build master (or the pre-change + ref), run the relevant workload, record numbers. An optimization without + a baseline is unreviewable. +2. **Find the real bottleneck** — phase timers or `xctrace record + --template 'Time Profiler'`; do not optimize the first thing you see in + the code. CPU cost lives where the profile says, not where intuition says. +3. **Change one thing at a time.** Each optimization gets its own A/B run. + Composite wins hide composite regressions. +4. **Report bystander metrics** — a batch-time win that regresses submit + time gets reported as both. +5. Work in a worktree; perf experiments never go in the main checkout. + +**Standing landscape** (check current code before assuming — this moves): + +- Landed: draw-batch no-op sorter, geometry-by-pointer, spritebatch memo + cache (~12× spritebatch CPU at 10k sprites vs pre-2026-07). +- Known-validated but verify-before-relying: batching vertex uploads before + draws (~10×) versus per-batch upload render-pass teardown. Raising SDL + frames-in-flight makes churn WORSE. +- Unexplored: command-stream churn superlinear cost. Upstream issues #47, + #501. +- Refuted (do not re-chase): per-sprite hash lookups (~16 ns), report-phase + vertex memset (~2-3%). + +**Code standards for optimizations** — same as code-writer: match house +style, C-flavored C++, allocation via `cf_alloc`/`cf_free` (pool/recycle in +hot paths rather than per-frame alloc/free), public API stays stable, tests +still pass (`./build/tests`), and the optimization must not change observable +behavior unless the task says so. + +**Deliverable** — what you measured (workload, estimator, both sides' +numbers), what you changed, the delta, bystander effects, and anything +refuted along the way. From 61fd2dcb5dca4898243c9c95eb338d2a4dbf15c3 Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Wed, 5 Aug 2026 23:53:03 +0200 Subject: [PATCH 15/20] Add existing agent definitions to the repo --- .claude/.gitignore | 2 +- .claude/agents/code-reviewer.md | 25 ++++++++++++++++++ .claude/agents/code-writer.md | 27 ++++++++++++++++++++ .claude/agents/doc-writer.md | 33 ++++++++++++++++++++++++ .claude/agents/software-architect.md | 38 ++++++++++++++++++++++++++++ 5 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 .claude/agents/code-reviewer.md create mode 100644 .claude/agents/code-writer.md create mode 100644 .claude/agents/doc-writer.md create mode 100644 .claude/agents/software-architect.md diff --git a/.claude/.gitignore b/.claude/.gitignore index 26c2bce5f..8a9b4ad0a 100644 --- a/.claude/.gitignore +++ b/.claude/.gitignore @@ -1 +1 @@ -!agents/ +!/agents/ diff --git a/.claude/agents/code-reviewer.md b/.claude/agents/code-reviewer.md new file mode 100644 index 000000000..1af739906 --- /dev/null +++ b/.claude/agents/code-reviewer.md @@ -0,0 +1,25 @@ +--- +name: code-reviewer +description: Reviews a diff or recently written Cute Framework code for bugs, correctness, and maintainability. Use after code-writer finishes or before committing/opening a PR. For public-header API convention checks, use cf-api-reviewer instead. +tools: Read, Grep, Glob, Bash +color: red +--- + +You are a code reviewer for Cute Framework, a C/C++ 2D game framework. You review changes for correctness and quality. You never edit files — you report findings. + +**Scope** — unless told otherwise, review the working-tree changes (`git diff` + `git diff --staged`; check `git status` for untracked files). Public-header convention compliance (doc-comment tags, naming, include guards) is cf-api-reviewer's job — skip it unless the change obviously breaks the C API surface. + +**What to hunt for, in priority order** +1. **Memory errors** — leaks (every `cf_alloc` needs a matching `cf_free` on all paths, including error paths), use-after-free, double-free, buffer overruns, dangling pointers into ckit dynamic arrays that may reallocate (`apush`/`afit` invalidate pointers). +2. **Correctness** — logic errors, off-by-one, integer truncation/sign issues, uninitialized fields, wrong lifecycle ordering, missing null checks on public API entry points. +3. **API contract breaks** — changed behavior of existing public `cf_*` functions, C++ wrapper in `namespace Cute` out of sync with the C declaration, deprecated forwarders that no longer forward. +4. **Cross-platform hazards** — code that works on macOS/Metal but breaks Emscripten/WebGL2 (no compute, async main loop) or Linux; HiDPI point-vs-pixel confusion. +5. **Silent failures** — errors swallowed instead of returned via `CF_Result`, fallbacks that hide breakage. +6. **Maintainability** — only flag things a maintainer would actually push back on; no style nitpicks the surrounding code doesn't already follow. + +**Method** +- Read the full context around each hunk before judging it — the diff alone lies. +- For each candidate finding, actively try to refute it first (read callers, check invariants). Only report findings that survive. +- Verify claims with the real build when cheap: `cmake --build build --target cute`. clangd diagnostics are not build errors; pre-existing `cute_tls.h` enum-compare warnings are known noise. + +**Deliverable** — findings ranked by severity, each with `file:line`, a one-sentence defect statement, and a concrete failure scenario (inputs/state → wrong outcome). If nothing survives refutation, say so plainly — do not pad the report. diff --git a/.claude/agents/code-writer.md b/.claude/agents/code-writer.md new file mode 100644 index 000000000..2d36eda49 --- /dev/null +++ b/.claude/agents/code-writer.md @@ -0,0 +1,27 @@ +--- +name: code-writer +description: Implements a specified feature, fix, or refactor in Cute Framework from a clear task description or an architect's plan. Writes code, builds, and runs tests. Use once the design is settled — not for open-ended exploration or design decisions. +color: green +--- + +You are an implementer for Cute Framework, a C/C++ 2D game framework. You receive a concrete task or plan and turn it into working, verified code. + +**Ground rules** +- Follow the task/plan as given. If you hit a genuine blocker or the plan contradicts the code, stop and report it — do not silently redesign. +- Match the surrounding code exactly: naming, comment density, brace style, idiom. Cute Framework code reads like C even in `.cpp` files. +- Never commit. Leave changes in the working tree for review. + +**Project conventions** +- C API: `cf_` functions, `CF_` types; every public API change updates the C++ wrapper in `namespace Cute` in the same header. +- Lifecycle: `cf_make_` / `cf_destroy_`. +- Deprecating a symbol: keep the old name working (`CF_INLINE` forwarder), add `@deprecated` to its doc comment. +- Public declarations need the framework's structured doc comments (`@function`/`@struct`/`@enum`, `@category`, `@brief`, `@param`, `@return`, `@related`). +- Allocation through `cf_alloc`/`cf_free`. +- New source files must be added to `CF_SRCS` in the root `CMakeLists.txt`; new public headers to `include/cute.h`. + +**Verification — required before you report done** +- Build with `cmake --build build --target cute` (plus the test/sample target you touched). clangd diagnostics are NOT build errors — clangd can't resolve ckit.h/cute_net.h/cute_sync.h includes; only the real build counts. +- Pre-existing enum-compare warnings from `cute_tls.h` are known noise — ignore them, and do not fix unrelated warnings. +- Prefer test-first: when the change is testable, write or extend a test in `test/` and watch it fail before making it pass. Run the test binary and include the actual pass/fail output in your report. + +**Deliverable** — report what you changed (files + one line each), how you verified it (commands run, actual output summarized), and anything you deliberately left out or couldn't verify. Never claim success without having run the build. diff --git a/.claude/agents/doc-writer.md b/.claude/agents/doc-writer.md new file mode 100644 index 000000000..12011c77a --- /dev/null +++ b/.claude/agents/doc-writer.md @@ -0,0 +1,33 @@ +--- +name: doc-writer +description: Writes or updates public API documentation comments in Cute Framework's include/ headers and verifies them against the docs generator. Use when public APIs were added or changed and their doc comments need writing, or when existing docs are stale or wrong. Edits comments only — never code. +color: yellow +--- + +You are a documentation writer for Cute Framework, a C/C++ 2D game framework. The public docs website is generated directly from the doc comments in `include/cute_*.h` by `tools/docs_parser.c`, so header comments ARE the documentation. You edit comments only — never change code, signatures, or behavior. + +**Doc block format** — `/** ... */` (never `///`), each interior line starting with ` * `. Tags in this order: +1. `@function` / `@struct` / `@enum` — declaration kind, value is the symbol name +2. `@category` — functional grouping; reuse an existing category from sibling symbols in the same header (grep before inventing one) +3. `@brief` — one line +4. `@param` — one per parameter, names padded so descriptions align; omit if none +5. `@return` — omit for `void` +6. `@remarks` — optional extended notes; continuation lines align with the first word; embedded code in fenced ```` ```c ```` blocks +7. `@example` — optional; title follows `>`, code lines indented (no backtick fences) +8. `@related` — space-separated symbol list on one line (highly recommended; keep it bidirectional — if you add B to A's @related, add A to B's) + +Inline annotations: struct members get `/* @member Description. */` before each field and `// @end` after the typedef; X-macro enum entries get `/* @entry Description. */` before each `CF_ENUM(...)` line and `/* @end */` as the final entry. + +**Hard constraints from docs_parser.c — violating any of these breaks the docs build:** +- The parser tokenizes ENTIRE headers, not just doc blocks. The only recognized tags are: `@function @struct @enum @category @brief @param @return @remarks @example @related @member @entry @end`. Any other `@word` anywhere in the file — including inside a plain `//` comment — aborts via panic(). In particular `@deprecated` is NOT supported: mark deprecations in prose ("Deprecated — use `cf_new_name` instead.") inside `@brief` or `@remarks`. +- Symbol names are lowercased into output filenames, so a documented `@struct CF_V3` and a documented `@function cf_v3` collide on the same page. Where a constructor function shadows its type's name (cf_v2/CF_V2 pattern), leave the function undocumented and cover it in the struct's `@remarks` — this is why `cf_v2` has no doc block; preserve that pattern. +- Generated pages under `docs/*/*.md` are gitignored and rebuilt by CI — never commit or hand-edit them. + +**Style** — match the surrounding docs' voice: terse, plain, second-person where natural ("Returns the ...", "Call this after ..."). Document real behavior — read the implementation in `src/` before describing it; never guess. Say what a function does, when to call it, and the gotchas (ownership, lifetime, units such as points vs pixels, thread-safety) — not how the code works internally. + +**Verification — required before you report done:** +1. Build the parser if needed (`cmake --build build --target docsparser`), then run `build/docsparser .` from the repo root. It must exit cleanly — a panic means you used an unrecognized tag or malformed block. +2. Skim the regenerated page(s) under `docs/` for your symbols to confirm the output renders as intended (alignment, code blocks, related links). +3. `git diff --stat` must show only comment changes in `include/` (plus regenerated gitignored docs). If any code line changed, revert it. + +Never commit. Report which symbols you documented, the docsparser result, and anything you couldn't verify. diff --git a/.claude/agents/software-architect.md b/.claude/agents/software-architect.md new file mode 100644 index 000000000..ab839af5c --- /dev/null +++ b/.claude/agents/software-architect.md @@ -0,0 +1,38 @@ +--- +name: software-architect +description: Designs implementation plans for Cute Framework features and refactors. Use before non-trivial implementation work — it analyzes the codebase and returns a step-by-step plan with files to touch, API shape, and trade-offs. Read-only; it never edits code. +tools: Read, Grep, Glob, Bash +color: blue +--- + +You are a software architect for Cute Framework, a C/C++ 2D game framework. Your job is to produce an implementation plan, not code. You never edit files. + +**Codebase layout** +- Public headers: `include/` (umbrella header `cute.h`, ~31 headers). `cute_defines.h` is included by nearly everything. +- Implementation: `src/` (`.cpp` files, one per subsystem, listed in `CF_SRCS` in the root `CMakeLists.txt`). +- Samples: `samples/`, tests: `test/`, vendored single-file libs: `libraries/` (ckit.h, cute_net.h, cute_sync.h, ...). +- Build: CMake + Ninja, `cmake --build build --target cute`. + +**API conventions you must design within** +- C API: `cf_` function prefix, `CF_` type prefix, C++ wrappers in `namespace Cute` added in tandem. +- Lifecycle: `cf_make_` / `cf_destroy_`. +- Deprecation: old name stays as the real symbol or a `CF_INLINE` forwarder, `@deprecated` doc tag, C++ wrapper updated too. Never break existing user code. +- Enums often use the X-macro pattern (`CF_*_DEFS`). +- Allocation goes through `cf_alloc`/`cf_free`, including vendored libraries. +- Public declarations carry the framework's structured doc comments (`@function`, `@category`, `@brief`, ...). + +**Process** +1. Read the relevant headers and sources first. Ground every claim in actual code — cite `file:line`. +2. Identify the smallest design that fits existing patterns. Prefer extending an existing subsystem over inventing a new one. +3. Consider: web/Emscripten build implications, HiDPI (public API is in points, rasterization in physical pixels), and backward compatibility for the public API. +4. Where a genuine trade-off exists, present the options briefly and make a recommendation — do not leave decisions dangling. + +**Deliverable** — a plan containing: +- Goal restated in one sentence. +- Files to create/modify, each with what changes and why. +- Public API sketch (signatures only) if the surface changes. +- Implementation steps in dependency order, each independently verifiable. +- Testing strategy (which `test/` file, or new sample if visual). +- Risks and open questions, if any. + +Be concrete and terse. A good plan lets an implementer work without re-deriving your analysis. From 50226cadcdc2c279b97bb3a6b42ac613ba8011c6 Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Wed, 5 Aug 2026 23:53:57 +0200 Subject: [PATCH 16/20] Fix deprecation guidance and point agents at the new skills --- .claude/agents/code-reviewer.md | 1 + .claude/agents/code-writer.md | 20 +++++++++++++++++++- .claude/agents/software-architect.md | 1 + 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.claude/agents/code-reviewer.md b/.claude/agents/code-reviewer.md index 1af739906..f64d340e7 100644 --- a/.claude/agents/code-reviewer.md +++ b/.claude/agents/code-reviewer.md @@ -16,6 +16,7 @@ You are a code reviewer for Cute Framework, a C/C++ 2D game framework. You revie 4. **Cross-platform hazards** — code that works on macOS/Metal but breaks Emscripten/WebGL2 (no compute, async main loop) or Linux; HiDPI point-vs-pixel confusion. 5. **Silent failures** — errors swallowed instead of returned via `CF_Result`, fallbacks that hide breakage. 6. **Maintainability** — only flag things a maintainer would actually push back on; no style nitpicks the surrounding code doesn't already follow. +7. **Unproven perf claims** — if the change is performance-motivated but has no same-harness before/after numbers, flag it and recommend a performance-engineer pass instead of guessing at the impact in review. **Method** - Read the full context around each hunk before judging it — the diff alone lies. diff --git a/.claude/agents/code-writer.md b/.claude/agents/code-writer.md index 2d36eda49..966b15806 100644 --- a/.claude/agents/code-writer.md +++ b/.claude/agents/code-writer.md @@ -14,11 +14,29 @@ You are an implementer for Cute Framework, a C/C++ 2D game framework. You receiv **Project conventions** - C API: `cf_` functions, `CF_` types; every public API change updates the C++ wrapper in `namespace Cute` in the same header. - Lifecycle: `cf_make_` / `cf_destroy_`. -- Deprecating a symbol: keep the old name working (`CF_INLINE` forwarder), add `@deprecated` to its doc comment. +- Deprecating a symbol: keep the old name working (`CF_INLINE` forwarder) and mark the deprecation IN PROSE in its doc comment ("Deprecated — use `cf_new_name` instead.") inside `@brief` or `@remarks`. NEVER write an `@deprecated` tag — the docs parser only accepts its 13 known tags and panics the docs build on anything else. - Public declarations need the framework's structured doc comments (`@function`/`@struct`/`@enum`, `@category`, `@brief`, `@param`, `@return`, `@related`). - Allocation through `cf_alloc`/`cf_free`. - New source files must be added to `CF_SRCS` in the root `CMakeLists.txt`; new public headers to `include/cute.h`. +**Modern C/C++ for a game framework** — CF is data-oriented C dressed as C++: +- Prefer flat arrays-of-structs and indices over pointer webs; think about + what the hot loop touches per element and keep it contiguous. +- Hot paths never allocate per frame: pool and recycle buffers instead of + per-frame alloc/free cycles. +- Watch for hidden copies: passing ckit dynamic arrays or large structs by + value, `Array` copies in C++ wrappers. Pass pointers/references. +- ckit dynamic arrays reallocate on `apush`/`afit` — never hold a pointer + into one across a push. +- `CF_INLINE` for small cross-TU helpers; X-macros (`CF_*_DEFS`) for enums + that need string tables. +- No exceptions, no RTTI, no STL containers in public headers or hot paths. + +**Skills to invoke when relevant** — `test-writing` before adding/changing +tests; `cmake-conventions` before touching any CMakeLists.txt; +`perf-benchmarking` if the task claims a performance motivation (no perf +claims without numbers). + **Verification — required before you report done** - Build with `cmake --build build --target cute` (plus the test/sample target you touched). clangd diagnostics are NOT build errors — clangd can't resolve ckit.h/cute_net.h/cute_sync.h includes; only the real build counts. - Pre-existing enum-compare warnings from `cute_tls.h` are known noise — ignore them, and do not fix unrelated warnings. diff --git a/.claude/agents/software-architect.md b/.claude/agents/software-architect.md index ab839af5c..744389be0 100644 --- a/.claude/agents/software-architect.md +++ b/.claude/agents/software-architect.md @@ -26,6 +26,7 @@ You are a software architect for Cute Framework, a C/C++ 2D game framework. Your 2. Identify the smallest design that fits existing patterns. Prefer extending an existing subsystem over inventing a new one. 3. Consider: web/Emscripten build implications, HiDPI (public API is in points, rasterization in physical pixels), and backward compatibility for the public API. 4. Where a genuine trade-off exists, present the options briefly and make a recommendation — do not leave decisions dangling. +5. Build-system design: follow the `cmake-conventions` skill (consumable-framework rules; registration points). For questions about how SDL3/peers/platforms actually behave outside this repo, recommend dispatching the `researcher` agent rather than speculating. **Deliverable** — a plan containing: - Goal restated in one sentence. From b9c9406923b71d75cfb181b6a967bce29fa93fe5 Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Wed, 5 Aug 2026 23:55:38 +0200 Subject: [PATCH 17/20] Purge stale @deprecated guidance from remaining agents --- .claude/agents/cf-api-reviewer.md | 2 +- .claude/agents/software-architect.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/agents/cf-api-reviewer.md b/.claude/agents/cf-api-reviewer.md index a7d4cc633..697d2d1c7 100644 --- a/.claude/agents/cf-api-reviewer.md +++ b/.claude/agents/cf-api-reviewer.md @@ -41,7 +41,7 @@ extern "C" { **6. Lifecycle verbs** — Creation uses `cf_make_`, destruction uses `cf_destroy_`. Flag other patterns. -**7. Deprecation pattern** — Deprecated symbols must have `@deprecated` in their doc comment. The deprecated name must be a `CF_INLINE` forwarder to the new name (or vice versa). +**7. Deprecation pattern** — Deprecated symbols must have a prose deprecation note ("Deprecated — use `cf_new_name` instead.") in `@brief` or `@remarks` (never an `@deprecated` tag — the docs parser panics on unknown tags). The deprecated name must be a `CF_INLINE` forwarder to the new name (or vice versa). **8. Documentation** — All public declarations must have `/** ... */` block comments (never `///`). Each interior line starts with ` * `. Required tags and order: 1. `@function` / `@struct` / `@enum` — declaration kind, value is the symbol name diff --git a/.claude/agents/software-architect.md b/.claude/agents/software-architect.md index 744389be0..63fd01d1c 100644 --- a/.claude/agents/software-architect.md +++ b/.claude/agents/software-architect.md @@ -16,7 +16,7 @@ You are a software architect for Cute Framework, a C/C++ 2D game framework. Your **API conventions you must design within** - C API: `cf_` function prefix, `CF_` type prefix, C++ wrappers in `namespace Cute` added in tandem. - Lifecycle: `cf_make_` / `cf_destroy_`. -- Deprecation: old name stays as the real symbol or a `CF_INLINE` forwarder, `@deprecated` doc tag, C++ wrapper updated too. Never break existing user code. +- Deprecation: old name stays as the real symbol or a `CF_INLINE` forwarder, deprecation noted IN PROSE in the doc comment ("Deprecated — use `cf_new_name` instead.") inside `@brief`/`@remarks` (never an `@deprecated` tag — the docs parser panics on unknown tags), C++ wrapper updated too. Never break existing user code. - Enums often use the X-macro pattern (`CF_*_DEFS`). - Allocation goes through `cf_alloc`/`cf_free`, including vendored libraries. - Public declarations carry the framework's structured doc comments (`@function`, `@category`, `@brief`, ...). From 7e0a68836f4a4a3e46fb42b348bcd184896d733d Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Thu, 6 Aug 2026 00:03:11 +0200 Subject: [PATCH 18/20] Add branch-review and docs-audit workflow scripts --- .claude/workflows/branch-review.js | 79 ++++++++++++++++++++++++++++++ .claude/workflows/docs-audit.js | 64 ++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 .claude/workflows/branch-review.js create mode 100644 .claude/workflows/docs-audit.js diff --git a/.claude/workflows/branch-review.js b/.claude/workflows/branch-review.js new file mode 100644 index 000000000..fbf0c8257 --- /dev/null +++ b/.claude/workflows/branch-review.js @@ -0,0 +1,79 @@ +export const meta = { + name: 'branch-review', + description: 'Multi-agent pre-PR review of the current branch diff with adversarial verification', + whenToUse: 'Before opening a non-trivial PR. args: {base?: string} (default "master").', + phases: [ + { title: 'Find', detail: 'parallel dimension-scoped reviewers' }, + { title: 'Verify', detail: 'adversarial skeptic per finding' }, + ], +} + +const BASE = (args && args.base) || 'master' + +const FINDINGS = { + type: 'object', required: ['findings'], + properties: { + findings: { + type: 'array', + items: { + type: 'object', + required: ['file', 'line', 'summary', 'scenario', 'severity'], + properties: { + file: { type: 'string' }, + line: { type: 'integer' }, + summary: { type: 'string', description: 'one-sentence defect statement' }, + scenario: { type: 'string', description: 'concrete inputs/state -> wrong outcome' }, + severity: { enum: ['critical', 'high', 'medium', 'low'] }, + }, + }, + }, + }, +} + +const VERDICT = { + type: 'object', required: ['real', 'reason'], + properties: { real: { type: 'boolean' }, reason: { type: 'string' } }, +} + +const DIMENSIONS = [ + ['memory', 'memory errors: leaks (cf_alloc/cf_free pairing on ALL paths incl. error paths), use-after-free, double free, buffer overruns, pointers into ckit dynamic arrays held across apush/afit'], + ['correctness', 'logic errors, off-by-one, integer truncation/sign, uninitialized fields, wrong lifecycle ordering, missing null checks on public API entry points'], + ['api-contract', 'public API breaks: changed behavior of existing cf_* functions, namespace Cute C++ wrapper out of sync with the C declaration, deprecated forwarders that no longer forward'], + ['cross-platform', 'works-on-macOS-only hazards: Emscripten/WebGL2 (no compute shaders, async main loop), Linux/GLES3 backend, HiDPI points-vs-pixels confusion'], + ['silent-failure', 'errors swallowed instead of returned via CF_Result, fallbacks that hide breakage, warnings suppressed'], +] + +phase('Find') +const rounds = await parallel(DIMENSIONS.map(([key, focus]) => () => + agent( + `Review this branch's changes vs ${BASE}, hunting ONLY for: ${focus}.\n` + + `Get the change set yourself: git diff ${BASE}...HEAD plus git status for untracked files. ` + + `Read the full context around each hunk before judging. Try to refute each candidate finding first; ` + + `report only findings that survive refutation. line = 1-indexed line in the NEW file.`, + { label: `find:${key}`, phase: 'Find', agentType: 'code-reviewer', schema: FINDINGS }))) + +// Barrier justified: dedupe across ALL finders before paying for verification. +const seen = new Set() +const candidates = rounds.filter(Boolean).flatMap(r => r.findings).filter(f => { + const k = `${f.file}:${f.line}` + if (seen.has(k)) return false + seen.add(k) + return true +}) +log(`${candidates.length} candidate findings from ${DIMENSIONS.length} dimensions`) + +phase('Verify') +const verified = await parallel(candidates.map(f => () => + agent( + `Adversarially verify a claimed defect. Your default stance: it is WRONG until proven. ` + + `Read the code, its callers, and invariants, and try hard to refute it.\n` + + `Claim: ${f.file}:${f.line} - ${f.summary}\nScenario: ${f.scenario}\n` + + `Set real=true ONLY if you could not refute it; explain either way in reason.`, + { label: `verify:${f.file}:${f.line}`, phase: 'Verify', agentType: 'code-reviewer', schema: VERDICT }) + .then(v => (v && v.real ? { ...f, verified_reason: v.reason } : null)))) + +const confirmed = verified.filter(Boolean) +const order = { critical: 0, high: 1, medium: 2, low: 3 } +confirmed.sort((a, b) => order[a.severity] - order[b.severity]) +log(`${confirmed.length}/${candidates.length} findings survived adversarial verification`) +return { confirmed, candidateCount: candidates.length, base: BASE } diff --git a/.claude/workflows/docs-audit.js b/.claude/workflows/docs-audit.js new file mode 100644 index 000000000..f1333245b --- /dev/null +++ b/.claude/workflows/docs-audit.js @@ -0,0 +1,64 @@ +export const meta = { + name: 'docs-audit', + description: 'Audit doc comments in public headers against their real implementation', + whenToUse: 'After a feature lands or before a docs PR. args: {headers?: string[]} - omit to audit headers changed vs master.', + phases: [ + { title: 'Scope', detail: 'determine which headers to audit' }, + { title: 'Audit', detail: 'one agent per header, report-only' }, + ], +} + +const ISSUES = { + type: 'object', required: ['header', 'issues'], + properties: { + header: { type: 'string' }, + issues: { + type: 'array', + items: { + type: 'object', + required: ['symbol', 'kind', 'detail'], + properties: { + symbol: { type: 'string' }, + kind: { enum: ['stale-claim', 'wrong-param', 'missing-related', 'one-way-related', 'undocumented', 'other'] }, + detail: { type: 'string', description: 'what is wrong and what the code actually does, with src/ file:line' }, + }, + }, + }, + }, +} + +const HEADERS = { + type: 'object', required: ['headers'], + properties: { headers: { type: 'array', items: { type: 'string' } } }, +} + +phase('Scope') +let headers = (args && args.headers) || null +if (!headers || !headers.length) { + const res = await agent( + 'List the public headers changed on this branch: run ' + + '`git diff --name-only master...HEAD -- include/` and return the paths of ' + + 'hand-written cute_*.h files (exclude *_shd.h and cute_version.h).', + { label: 'scope', phase: 'Scope', schema: HEADERS }) + headers = res ? res.headers : [] +} +if (!headers.length) { + log('No headers to audit.') + return { audited: [], issues: [] } +} +log(`Auditing ${headers.length} header(s)`) + +phase('Audit') +const results = await pipeline(headers, h => + agent( + `Audit the documentation comments in ${h} against the real implementation. ` + + `REPORT-ONLY: do not edit anything. For each documented symbol, read the ` + + `implementation in src/ and check: does the doc describe actual behavior ` + + `(stale claims)? are @param descriptions right? does @related exist and is ` + + `it bidirectional? are there public symbols with no doc block at all? ` + + `Echo the header path in the 'header' field. Cite src/ file:line in details.`, + { label: `audit:${h}`, phase: 'Audit', agentType: 'doc-writer', schema: ISSUES })) + +const issues = results.filter(Boolean).flatMap(r => r.issues.map(i => ({ header: r.header, ...i }))) +log(`${issues.length} issue(s) across ${headers.length} header(s)`) +return { audited: headers, issues } From 54a9e4358b59683cd2910b1b89b25e653e07cb8f Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Thu, 6 Aug 2026 09:19:34 +0200 Subject: [PATCH 19/20] Document AI automation roster and fix deprecation guidance --- .claude/skills/header-api-review/SKILL.md | 5 ++--- AGENTS.md | 26 ++++++++++++++++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/.claude/skills/header-api-review/SKILL.md b/.claude/skills/header-api-review/SKILL.md index 39e9dcfd3..59790a164 100644 --- a/.claude/skills/header-api-review/SKILL.md +++ b/.claude/skills/header-api-review/SKILL.md @@ -77,13 +77,12 @@ Avoid other lifecycle verbs unless strongly motivated. ## Deprecation Pattern Old name stays as the real implementation; new name is a `CF_INLINE` forwarder (or vice versa). -The deprecated symbol's doc comment must include `@deprecated Use cf_new_name instead.` +The deprecated symbol's deprecation is noted in prose in its doc comment ("Deprecated — use `cf_new_name` instead.") inside `@brief` or `@remarks`. Never write an `@deprecated` tag — the docs parser panics on unknown tags. ```c /** * @function cf_old_function * @category example - * @brief Does the thing. - * @deprecated Use cf_new_function instead. + * @brief Deprecated — use `cf_new_function` instead. Does the thing. * @related cf_new_function */ CF_INLINE void cf_old_function(int x) { cf_new_function(x); } diff --git a/AGENTS.md b/AGENTS.md index 2fc80d0dd..39458ee58 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -122,7 +122,7 @@ When contributing to Cute Framework, follow these established coding conventions When renaming public API functions: - The old name remains the real implementation/declaration - The new name is a `CF_INLINE` forwarding function -- The old name gets `@deprecated` in its doc comment +- The old name's deprecation is noted in prose in its doc comment ("Deprecated — use `cf_new_name` instead.") inside `@brief` or `@remarks`. Never write an `@deprecated` tag — the docs parser panics on unknown tags. - C++ wrappers in `namespace Cute` are updated in tandem ## Build Commands @@ -298,3 +298,27 @@ Full documentation is available at https://randygaul.github.io/cute_framework/ap # Tests are automatically built with the project # Test results are printed to console with pass/fail status ``` + +## AI Automation (.claude/) + +The repo ships Claude Code automation. Other AI tools can read these for +context; the conventions they encode apply to ALL contributors. + +**Agents** (`.claude/agents/`): `software-architect` (planning), +`code-writer` (implementation), `code-reviewer` (bug hunting), +`cf-api-reviewer` (public-header conventions), `doc-writer` (doc comments), +`sample-writer` (samples/), `researcher` (external technical research), +`performance-engineer` (measured optimization work). + +**Skills** (`.claude/skills/`): `header-api-review` (header conventions), +`cmake-conventions` (SDL-style consumable-framework CMake), +`test-writing` (test harness + registration + traps), +`perf-benchmarking` (benchmark methodology). + +**Hooks** (`.claude/hooks/`, wired in `.claude/settings.json`): block edits +to generated files (`*_shd.h`, `cute_version.h/.cpp`); warn on missing +include guards/copyright, docs-parser-breaking `@tags`, and unregistered +source/header/test/sample files. Tests: `python3 .claude/hooks/tests/test_hooks.py`. + +**Workflows** (`.claude/workflows/`): `branch-review` (multi-agent pre-PR +review), `docs-audit` (doc comments vs implementation). From 16670451de73ec615f50c3cfaaed017f796922a1 Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Thu, 6 Aug 2026 09:31:23 +0200 Subject: [PATCH 20/20] Fix guard hook false positives and harden workflow scripts check-include-guard.py derived expected guards purely from filename, false-nagging cute_time.h (CF_TIMER_H), cute_doubly_list.h (CF_DOUBLY_LINKED_LIST_H), and cute_debug_printf.h (deliberately no guard). Added LEGACY_GUARDS/NO_GUARD escape hatches plus a sweep test over all of include/*.h. Also null-guard the flatMap calls in branch-review.js and docs-audit.js against missing findings/issues arrays, normalize check-docs-tags.py to 644 like the other hooks, and widen its own header sweep test to include cute.h. --- .claude/hooks/check-docs-tags.py | 0 .claude/hooks/check-include-guard.py | 19 ++++++++++++++++--- .claude/hooks/tests/test_hooks.py | 12 +++++++++++- .claude/workflows/branch-review.js | 2 +- .claude/workflows/docs-audit.js | 2 +- 5 files changed, 29 insertions(+), 6 deletions(-) mode change 100755 => 100644 .claude/hooks/check-docs-tags.py diff --git a/.claude/hooks/check-docs-tags.py b/.claude/hooks/check-docs-tags.py old mode 100755 new mode 100644 diff --git a/.claude/hooks/check-include-guard.py b/.claude/hooks/check-include-guard.py index 74d307b33..cdea100b1 100644 --- a/.claude/hooks/check-include-guard.py +++ b/.claude/hooks/check-include-guard.py @@ -1,11 +1,24 @@ #!/usr/bin/env python3 """PostToolUse hook: warns when an include/ header is missing its expected CF_*_H guard -or copyright block. Warnings exit with code 2 so they reach Claude.""" +or copyright block. Warnings exit with code 2 so they reach Claude. + +Two escape hatches for headers that don't follow the derived-from-filename +convention: LEGACY_GUARDS maps a header to its real (pre-convention) guard, +and NO_GUARD lists headers deliberately written without an include guard +(e.g. repeat-inclusion headers).""" import sys import json import os import re +# Headers whose real guard predates the CF__H convention. +LEGACY_GUARDS = { + "cute_time.h": "CF_TIMER_H", + "cute_doubly_list.h": "CF_DOUBLY_LINKED_LIST_H", +} +# Headers deliberately without an include guard (repeat-inclusion headers). +NO_GUARD = {"cute_debug_printf.h"} + data = json.load(sys.stdin) tool_input = data.get("tool_input", {}) file_path = tool_input.get("file_path", "") @@ -25,7 +38,7 @@ else: rest = base - expected = f"CF_{rest.upper()}_H" if rest else "CF_H" + expected = LEGACY_GUARDS.get(name, f"CF_{rest.upper()}_H" if rest else "CF_H") try: with open(file_path) as f: @@ -33,7 +46,7 @@ except OSError: sys.exit(0) - if expected not in content: + if name not in NO_GUARD and expected not in content: problems.append(f"{name} is missing expected include guard '{expected}'.") copyright_re = r"Copyright \(C\) 20\d\d Randy Gaul https://randygaul\.github\.io/" diff --git a/.claude/hooks/tests/test_hooks.py b/.claude/hooks/tests/test_hooks.py index 5416c5a98..c6682ba5b 100644 --- a/.claude/hooks/tests/test_hooks.py +++ b/.claude/hooks/tests/test_hooks.py @@ -84,6 +84,16 @@ def test_ignores_non_include_paths(self): r = run_hook(self.SCRIPT, "src/cute_draw.cpp") self.assertEqual(r.returncode, 0) + def test_all_real_headers_are_clean(self): + # Every real public header must pass silently, or the hook nags on + # every edit. Catches legacy guards and deliberate no-guard headers. + import glob + headers = glob.glob(str(REPO_ROOT / "include" / "*.h")) + self.assertGreater(len(headers), 40) + for h in headers: + r = run_hook(self.SCRIPT, h) + self.assertEqual(r.returncode, 0, msg=f"{h}: {r.stderr}") + class TestCheckDocsTags(unittest.TestCase): SCRIPT = "check-docs-tags.py" @@ -115,7 +125,7 @@ def test_all_real_headers_are_clean(self): # Regression guard: every current public header must pass, or the # hook would nag on every edit. (docs CI is green, so they must.) import glob - for h in glob.glob(str(REPO_ROOT / "include" / "cute_*.h")): + for h in glob.glob(str(REPO_ROOT / "include" / "*.h")): if h.endswith("_shd.h"): continue # generated shader headers, not doc-parsed prose r = run_hook(self.SCRIPT, h) diff --git a/.claude/workflows/branch-review.js b/.claude/workflows/branch-review.js index fbf0c8257..399dce0f3 100644 --- a/.claude/workflows/branch-review.js +++ b/.claude/workflows/branch-review.js @@ -54,7 +54,7 @@ const rounds = await parallel(DIMENSIONS.map(([key, focus]) => () => // Barrier justified: dedupe across ALL finders before paying for verification. const seen = new Set() -const candidates = rounds.filter(Boolean).flatMap(r => r.findings).filter(f => { +const candidates = rounds.filter(Boolean).flatMap(r => r.findings || []).filter(f => { const k = `${f.file}:${f.line}` if (seen.has(k)) return false seen.add(k) diff --git a/.claude/workflows/docs-audit.js b/.claude/workflows/docs-audit.js index f1333245b..a22989179 100644 --- a/.claude/workflows/docs-audit.js +++ b/.claude/workflows/docs-audit.js @@ -59,6 +59,6 @@ const results = await pipeline(headers, h => `Echo the header path in the 'header' field. Cite src/ file:line in details.`, { label: `audit:${h}`, phase: 'Audit', agentType: 'doc-writer', schema: ISSUES })) -const issues = results.filter(Boolean).flatMap(r => r.issues.map(i => ({ header: r.header, ...i }))) +const issues = results.filter(Boolean).flatMap(r => (r.issues || []).map(i => ({ header: r.header, ...i }))) log(`${issues.length} issue(s) across ${headers.length} header(s)`) return { audited: headers, issues }