diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 599141f8..29f7c26c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,13 +1,11 @@ name: CI -# Explicit, not "*.md": CONTRIBUTING.md and docs/ are asserted/executed by tests (guarded by tests/test_documentation/test_ci_paths_ignore.py). +# Explicit, not "*.md": CONTRIBUTING.md, docs/, CLAUDE.md and AGENTS.md are asserted/executed by tests (guarded by tests/test_ci_paths_ignore.py). # Safe only while CI is not a required status check on main: a skipped required check never reports, blocking docs-only PRs. on: push: branches: [ "main" ] paths-ignore: &docs_only - - AGENTS.md - - CLAUDE.md - CODE_OF_CONDUCT.md - NOTICE.md - README.md diff --git a/AGENTS.md b/AGENTS.md index 1b4468d3..3bbc499a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,7 @@ - **Orchestration Only**: Coordinate Test-Driven Development cycles between specialized agents - **No Code Implementation**: NEVER write implementation code or tests directly - **Agent Delegation**: Use Red Agent for test writing, Green Agent for implementation +- **Session root**: `.claude/agents/red-agent.md` and `.claude/agents/green-agent.md` are only auto-loaded when Claude Code's session is rooted at this repository. If working from a parent or workspace directory, start a session inside the clone (`cd code/mloda && claude`) so the agents are available natively. ## TDD Workflow @@ -51,7 +52,7 @@ Each phase should be a clean, validated checkpoint with all tests passing and ch **CRITICAL**: If agent behavior is unexpected or incorrect: 1. **Update Agent Configuration**: Modify `.claude/agents/red-agent.md` or `.claude/agents/green-agent.md` to refine instructions, constraints, or workflow -2. **Update This File**: Modify `CLAUDE.md` and `AGENTS.md` (keep them in sync) to clarify orchestration rules or add missing guidance +2. **Update This File**: Modify `CLAUDE.md` and `AGENTS.md` (`tests/test_agent_docs_sync.py` pins them identical) to clarify orchestration rules or add missing guidance 3. **Document Changes**: Briefly explain what was learned and why the change improves behavior This enables continuous learning and improvement of the TDD workflow based on actual usage patterns. @@ -85,6 +86,12 @@ source .venv/bin/activate - **`attribution/ATTRIBUTION.md`**: `tox` regenerates this file from the installed dependency versions on every run, so a dependency change shows up as a diff here. This is intended: commit the update as part of the same change so the tracked file stays current. The release workflow does not regenerate it; it ships the committed copy, so keeping it up to date in PRs is what keeps releases accurate. - **Commits**: use [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `chore:`, `docs:`, `test:`, `refactor:`, `minor:`). semantic-release computes the next version. This project deviates from the standard: the minor version (middle number) bumps only on `minor:` commits; `feat:` is treated as a patch bump. +### mypy iteration notes + +- `mypy --strict --ignore-missing-imports .` (what tox runs) checks `tests/` as well as `mloda/` and `mloda_plugins/`. Running `mypy mloda/` locally will miss test-tree type errors that block the tox gate. `tox` (or `tox -e python310`) is the only trustworthy mypy invocation. +- A stray local build (`python -m build` or `pip wheel .`) creates a `build/` directory. mypy picks it up and reports spurious errors. Clean it with `rm -rf build/` before running tox. The `[tool.mypy]` exclude list in `pyproject.toml` includes `build/` to prevent this at the gate. +- Running `mypy --strict .` in the dev venv (outside tox) may surface pyspark-related errors that tox's isolated env does not see; trust the tox run as the source of truth. + ## Issue Creation When filing a GitHub issue (via `gh issue create` or otherwise), follow the structure in `.github/ISSUE_TEMPLATE/issue.yml`: diff --git a/CLAUDE.md b/CLAUDE.md index 11c2f0e6..3bbc499a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,7 +52,7 @@ Each phase should be a clean, validated checkpoint with all tests passing and ch **CRITICAL**: If agent behavior is unexpected or incorrect: 1. **Update Agent Configuration**: Modify `.claude/agents/red-agent.md` or `.claude/agents/green-agent.md` to refine instructions, constraints, or workflow -2. **Update This File**: Modify `CLAUDE.md` and `AGENTS.md` (keep them in sync) to clarify orchestration rules or add missing guidance +2. **Update This File**: Modify `CLAUDE.md` and `AGENTS.md` (`tests/test_agent_docs_sync.py` pins them identical) to clarify orchestration rules or add missing guidance 3. **Document Changes**: Briefly explain what was learned and why the change improves behavior This enables continuous learning and improvement of the TDD workflow based on actual usage patterns. diff --git a/tests/test_agent_docs_sync.py b/tests/test_agent_docs_sync.py new file mode 100644 index 00000000..8cd479b3 --- /dev/null +++ b/tests/test_agent_docs_sync.py @@ -0,0 +1,39 @@ +"""CLAUDE.md and AGENTS.md are one instruction set for different agents, so the two files must be identical.""" + +import difflib +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parent.parent + +# Naming both files here makes tests/test_ci_paths_ignore.py treat them as protected: a change to +# either must run the gate, so neither may stay in the ci.yaml docs-only paths-ignore list. +CLAUDE_MD = PROJECT_ROOT / "CLAUDE.md" +AGENTS_MD = PROJECT_ROOT / "AGENTS.md" + + +def _lines(path: Path) -> list[str]: + assert path.is_file(), f"{path.name} must exist at the project root: each agent reads only its own file." + # Bytes, not read_text(): universal-newline translation would let a CRLF rewrite compare equal. + return path.read_bytes().decode("utf-8").split("\n") + + +def _diff(claude: list[str], agents: list[str]) -> str: + """Diff of repr-ed lines, so whitespace-only drift is not printed as two identical-looking lines.""" + return "\n".join( + difflib.unified_diff( + [repr(line) for line in claude], + [repr(line) for line in agents], + fromfile="CLAUDE.md", + tofile="AGENTS.md", + lineterm="", + ) + ) + + +def test_claude_md_and_agents_md_are_identical() -> None: + claude, agents = _lines(CLAUDE_MD), _lines(AGENTS_MD) + assert claude == agents, ( + f"CLAUDE.md and AGENTS.md have drifted:\n{_diff(claude, agents)}\n\n" + "Neither file carries content of its own: whatever belongs in one belongs in the other, verbatim." + ) diff --git a/tests/test_ci_paths_ignore.py b/tests/test_ci_paths_ignore.py index ef9bce09..3b8b8cb8 100644 --- a/tests/test_ci_paths_ignore.py +++ b/tests/test_ci_paths_ignore.py @@ -138,6 +138,8 @@ def test_protected_set_is_not_stale() -> None: protected = _protected_paths() assert "docs/docs/index.md" in protected, "docs/ markdown is executed by tests/test_documentation" assert "CONTRIBUTING.md" in protected, "CONTRIBUTING.md content is asserted by tests/test_project_structure.py" + assert "CLAUDE.md" in protected, "CLAUDE.md content is asserted by tests/test_agent_docs_sync.py" + assert "AGENTS.md" in protected, "AGENTS.md content is asserted by tests/test_agent_docs_sync.py" def test_paths_ignore_declared_for_push_and_pull_request() -> None: