diff --git a/.agents/skills/quantmind-dev/SKILL.md b/.agents/skills/quantmind-dev/SKILL.md index c391521..9e01dfc 100644 --- a/.agents/skills/quantmind-dev/SKILL.md +++ b/.agents/skills/quantmind-dev/SKILL.md @@ -9,16 +9,16 @@ Development workflow for contributing to the QuantMind codebase. ## Start Here -1. Use the repository [`contexts/README.md`](../../../contexts/README.md) to +1. Use the repository `contexts/README.md` to select the development context for this contribution. 2. Read the repository root `AGENTS.md` or `CLAUDE.md` for the stable architecture constraints and the module map. 3. Read `docs/README.md` when the task adds, changes, or uses a public operation or public-network source. 4. When creating, updating, or triaging an issue or pull request, use the - canonical [repository label guidance](../../../contexts/dev/labels.md). + canonical repository label guidance in `contexts/dev/labels.md`. 5. Before writing or editing any GitHub body, follow the canonical - [GitHub writing style](../../../contexts/dev/github-writing.md), including + GitHub writing style in `contexts/dev/github-writing.md`, including its no-hard-wrap rule. 6. Pick exactly one workflow reference below; do not load the others. diff --git a/.agents/skills/quantmind-dev/references/commit.md b/.agents/skills/quantmind-dev/references/commit.md index c6e12de..7b61608 100644 --- a/.agents/skills/quantmind-dev/references/commit.md +++ b/.agents/skills/quantmind-dev/references/commit.md @@ -16,6 +16,11 @@ English, [Conventional Commits](https://www.conventionalcommits.org/): `chore(verify): ...`, `docs(news): ...`). Omit the scope only for repo-wide changes (`feat: flows + magic apex layer`). - **summary**: imperative, lower-case start, no trailing period. +- **body** (optional): explain what changed and why. Follow the same + no-hard-wrap prose rule as GitHub bodies (see the GitHub writing style in + `contexts/dev/github-writing.md`, a repo-root-relative path) — keep each + paragraph or list item on one physical line, with no fixed-width wrapping — + so a squash-merge keeps a clean description on GitHub. Examples from history: diff --git a/.agents/skills/quantmind-dev/references/pull-request.md b/.agents/skills/quantmind-dev/references/pull-request.md index d57de56..480bf1c 100644 --- a/.agents/skills/quantmind-dev/references/pull-request.md +++ b/.agents/skills/quantmind-dev/references/pull-request.md @@ -35,7 +35,7 @@ batch input`, `docs(readme): update quick start`. Written in English (external audiences read it: contributors, search indexers, future maintainers). Follow `.github/PULL_REQUEST_TEMPLATE.md` and the canonical -[GitHub writing style](../../../../contexts/dev/github-writing.md). Do not +GitHub writing style in `contexts/dev/github-writing.md`. Do not hard-wrap body prose at 80 columns or another fixed width. Make sure the body covers: diff --git a/.claude/skills/quantmind-dev/SKILL.md b/.claude/skills/quantmind-dev/SKILL.md index c391521..9e01dfc 100644 --- a/.claude/skills/quantmind-dev/SKILL.md +++ b/.claude/skills/quantmind-dev/SKILL.md @@ -9,16 +9,16 @@ Development workflow for contributing to the QuantMind codebase. ## Start Here -1. Use the repository [`contexts/README.md`](../../../contexts/README.md) to +1. Use the repository `contexts/README.md` to select the development context for this contribution. 2. Read the repository root `AGENTS.md` or `CLAUDE.md` for the stable architecture constraints and the module map. 3. Read `docs/README.md` when the task adds, changes, or uses a public operation or public-network source. 4. When creating, updating, or triaging an issue or pull request, use the - canonical [repository label guidance](../../../contexts/dev/labels.md). + canonical repository label guidance in `contexts/dev/labels.md`. 5. Before writing or editing any GitHub body, follow the canonical - [GitHub writing style](../../../contexts/dev/github-writing.md), including + GitHub writing style in `contexts/dev/github-writing.md`, including its no-hard-wrap rule. 6. Pick exactly one workflow reference below; do not load the others. diff --git a/.claude/skills/quantmind-dev/references/commit.md b/.claude/skills/quantmind-dev/references/commit.md index c6e12de..7b61608 100644 --- a/.claude/skills/quantmind-dev/references/commit.md +++ b/.claude/skills/quantmind-dev/references/commit.md @@ -16,6 +16,11 @@ English, [Conventional Commits](https://www.conventionalcommits.org/): `chore(verify): ...`, `docs(news): ...`). Omit the scope only for repo-wide changes (`feat: flows + magic apex layer`). - **summary**: imperative, lower-case start, no trailing period. +- **body** (optional): explain what changed and why. Follow the same + no-hard-wrap prose rule as GitHub bodies (see the GitHub writing style in + `contexts/dev/github-writing.md`, a repo-root-relative path) — keep each + paragraph or list item on one physical line, with no fixed-width wrapping — + so a squash-merge keeps a clean description on GitHub. Examples from history: diff --git a/.claude/skills/quantmind-dev/references/pull-request.md b/.claude/skills/quantmind-dev/references/pull-request.md index d57de56..480bf1c 100644 --- a/.claude/skills/quantmind-dev/references/pull-request.md +++ b/.claude/skills/quantmind-dev/references/pull-request.md @@ -35,7 +35,7 @@ batch input`, `docs(readme): update quick start`. Written in English (external audiences read it: contributors, search indexers, future maintainers). Follow `.github/PULL_REQUEST_TEMPLATE.md` and the canonical -[GitHub writing style](../../../../contexts/dev/github-writing.md). Do not +GitHub writing style in `contexts/dev/github-writing.md`. Do not hard-wrap body prose at 80 columns or another fixed width. Make sure the body covers: diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 0000000..22a8a60 --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,26 @@ +name: PR title + +# The commit-msg git hook only fires for contributors who ran +# `pre-commit install`, so it cannot bind external contributors. This job is +# the server-side floor: it lints the PR title (which becomes the squash-merge +# commit subject) with the same rule as the commit-msg hook, reusing +# scripts/hooks/commit_msg_check.py so the convention has a single source. + +on: + pull_request: + types: [opened, edited, reopened] + +permissions: + contents: read + +jobs: + lint-pr-title: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Lint PR title as an English Conventional Commit + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + printf '%s\n' "$PR_TITLE" > "$RUNNER_TEMP/pr_title.txt" + python3 scripts/hooks/commit_msg_check.py "$RUNNER_TEMP/pr_title.txt" diff --git a/contexts/README.md b/contexts/README.md index 39db7cb..3a841d9 100644 --- a/contexts/README.md +++ b/contexts/README.md @@ -22,6 +22,7 @@ This directory is the public repository context system for coding agents and mai ## Where Information Lives +- **`contexts/` vs `docs/`**: `contexts/` is the internal navigation and design memory for coding agents and maintainers — design decisions in `contexts/design/`, contributor rules in `contexts/dev/`, usage routing in `contexts/usage/`. `docs/` is the user-facing surface for people consuming QuantMind as a library — the public component catalog `docs/README.md` and usage guides such as `docs/library.md`. A fact that helps an agent build the repo lives in `contexts/`; a fact that helps someone use the library lives in `docs/`. Neither keeps a second copy of the other. - Keep each kind of information in one place so agents do not have to compare competing versions. - `contexts/design/` records accepted design decisions and planned behavior. - `contexts/dev/` and `contexts/usage/` route readers to existing rules and examples instead of copying them. diff --git a/contexts/dev/harness-engineering.md b/contexts/dev/harness-engineering.md index a1feab6..d5c0af6 100644 --- a/contexts/dev/harness-engineering.md +++ b/contexts/dev/harness-engineering.md @@ -77,10 +77,11 @@ Each rule is enforced at the layer that fits it, and hooks are kept to mechanica | Contexts page structure (Quick Summary / Contents / anchors / index links) | `tests/test_contexts.py` inside `verify.sh` | CI + local | | English Conventional Commit subject | `scripts/hooks/commit_msg_check.py` | `commit-msg` git hook (tool-agnostic: humans, Claude, Codex) | | No `--no-verify` bypass of the hooks above | `scripts/hooks/pre_tool_use_no_bypass.py` | Agent PreToolUse hook | +| English Conventional Commit PR title | `.github/workflows/pr-title.yml` (reuses `commit_msg_check.py`) | CI check | The anti-bypass hook is the one guarantee that cannot live in a git hook: `--no-verify` is by definition the flag that turns git hooks off, and CI only notices after the fact. A PreToolUse `deny` stops an agent from skipping the checks at the source. It is a mechanical check only: the command is tokenized with `shlex` and split into simple commands, so it denies only when `--no-verify` is a real argument token of a real `git` invocation — a command that merely mentions the flag inside a quoted string (an `echo`, a `grep`, a commit message) keeps it inside one token and passes. The residual gap is shell indirection (`$VAR` / `eval`), which a mechanical guard does not resolve. Commit-*message* format, by contrast, is a git hook rather than an agent hook, so it also catches a human committing from a terminal. -Planned next: enforce the commit convention for external contributors at the CI floor (a PR-title lint), since local git hooks only fire for contributors who ran `pre-commit install`. +The PR-title lint (`.github/workflows/pr-title.yml`) is the server-side floor for the commit convention. Local git hooks only fire for contributors who ran `pre-commit install`, so the PR title — which becomes the squash-merge commit subject — is linted in CI with the same `commit_msg_check.py` rule (single source). Making that workflow a *required* status check is a one-time repository-settings toggle, separate from committing the workflow here. ## References diff --git a/tests/test_contexts.py b/tests/test_contexts.py index 4da59b4..783f0ca 100644 --- a/tests/test_contexts.py +++ b/tests/test_contexts.py @@ -101,50 +101,48 @@ def test_context_index_targets_exist(self) -> None: def test_required_files_link_to_context_entry_point(self) -> None: repo_root = Path(__file__).resolve().parents[1] - required_links = { - "AGENTS.md": "contexts/README.md", - "CLAUDE.md": "contexts/README.md", - ".agents/skills/quantmind-dev/SKILL.md": "../../../contexts/README.md", - ".claude/skills/quantmind-dev/SKILL.md": "../../../contexts/README.md", - } - - for source_path, target in required_links.items(): - source = repo_root / source_path + entry_point = "contexts/README.md" + # Root-level guides link to the entry point; nested skill files + # reference the same repo-root path in backticks. Both contain the + # path string, so one substring check is form-agnostic. + sources = ( + "AGENTS.md", + "CLAUDE.md", + ".agents/skills/quantmind-dev/SKILL.md", + ".claude/skills/quantmind-dev/SKILL.md", + ) + self.assertTrue( + (repo_root / entry_point).is_file(), + f"missing context entry point: {entry_point}", + ) + for source_path in sources: with self.subTest(source=source_path): self.assertIn( - f"]({target})", + entry_point, _guide_text(repo_root, source_path), - f"{source_path} must link to contexts/README.md", - ) - self.assertTrue( - (source.parent / target).resolve().is_file(), - f"broken context entry link from {source_path}: {target}", + f"{source_path} must reference {entry_point}", ) def test_label_guide_has_required_routes(self) -> None: repo_root = Path(__file__).resolve().parents[1] - required_links = { - "contexts/dev/README.md": "labels.md", - ".agents/skills/quantmind-dev/SKILL.md": ( - "../../../contexts/dev/labels.md" - ), - ".claude/skills/quantmind-dev/SKILL.md": ( - "../../../contexts/dev/labels.md" - ), + self.assertTrue( + (repo_root / "contexts/dev/labels.md").is_file(), + "missing canonical label guide", + ) + # The dev index links a sibling page; nested skill files use a + # repo-root backtick reference. + references = { + "contexts/dev/README.md": "](labels.md)", + ".agents/skills/quantmind-dev/SKILL.md": "`contexts/dev/labels.md`", + ".claude/skills/quantmind-dev/SKILL.md": "`contexts/dev/labels.md`", } - - for source_path, target in required_links.items(): - source = repo_root / source_path + for source_path, expected in references.items(): with self.subTest(source=source_path): self.assertIn( - f"]({target})", + expected, _guide_text(repo_root, source_path), f"{source_path} must route to the canonical label guide", ) - self.assertTrue( - (source.parent / target).resolve().is_file(), - f"broken label guide link from {source_path}: {target}", - ) def test_label_guide_has_complete_taxonomy(self) -> None: repo_root = Path(__file__).resolve().parents[1] @@ -186,37 +184,37 @@ def test_label_guide_has_complete_taxonomy(self) -> None: def test_github_writing_guide_has_required_routes(self) -> None: repo_root = Path(__file__).resolve().parents[1] - required_links = { - "contexts/dev/README.md": "github-writing.md", - "contexts/dev/labels.md": "github-writing.md", - "AGENTS.md": "contexts/dev/github-writing.md", - "CLAUDE.md": "contexts/dev/github-writing.md", + self.assertTrue( + (repo_root / "contexts/dev/github-writing.md").is_file(), + "missing GitHub writing guide", + ) + # Sibling contexts pages link the short path; root guides link the + # repo-root path; nested skill files use a repo-root backtick reference. + references = { + "contexts/dev/README.md": "](github-writing.md)", + "contexts/dev/labels.md": "](github-writing.md)", + "AGENTS.md": "](contexts/dev/github-writing.md)", + "CLAUDE.md": "](contexts/dev/github-writing.md)", ".agents/skills/quantmind-dev/SKILL.md": ( - "../../../contexts/dev/github-writing.md" + "`contexts/dev/github-writing.md`" ), ".claude/skills/quantmind-dev/SKILL.md": ( - "../../../contexts/dev/github-writing.md" + "`contexts/dev/github-writing.md`" ), ".agents/skills/quantmind-dev/references/pull-request.md": ( - "../../../../contexts/dev/github-writing.md" + "`contexts/dev/github-writing.md`" ), ".claude/skills/quantmind-dev/references/pull-request.md": ( - "../../../../contexts/dev/github-writing.md" + "`contexts/dev/github-writing.md`" ), } - - for source_path, target in required_links.items(): - source = repo_root / source_path + for source_path, expected in references.items(): with self.subTest(source=source_path): self.assertIn( - f"]({target})", + expected, _guide_text(repo_root, source_path), f"{source_path} must route to the GitHub writing guide", ) - self.assertTrue( - (source.parent / target).resolve().is_file(), - f"broken GitHub writing guide link: {target}", - ) marker = "