diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4bd0632..758ca88 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- # See https://pre-commit.com for more information # Run: uvx pre-commit run --all-files -# Install: uvx pre-commit install +# Install: uvx pre-commit install && uvx pre-commit install --hook-type commit-msg repos: # General file hygiene @@ -118,6 +118,7 @@ repos: language: system always_run: true pass_filenames: false + stages: [commit-msg] # Documentation validation - repo: local diff --git a/docs/how-to/agent-change-process.md b/docs/how-to/agent-change-process.md index e531237..fe7219a 100644 --- a/docs/how-to/agent-change-process.md +++ b/docs/how-to/agent-change-process.md @@ -151,7 +151,7 @@ C2(deploy-authentik): close configure-postgres C2(deploy-authentik): finalize rewrite cards as historical documentation ``` -The `mikado-branch-invariant-check` pre-commit hook validates this convention and the invariant ordering. +The `mikado-branch-invariant-check` commit-msg hook validates this convention and the invariant ordering. ### Process @@ -260,7 +260,7 @@ tags: | `mise run docs-mikado --resume` | Resume a chain: detect branch, show state and next steps | | `mise run docs-mikado --resume ` | Resume a specific chain with branch consistency check | -The `mikado-branch-invariant-check` pre-commit hook runs automatically on `mikado/*` branches, validating commit message conventions and invariant ordering. +The `mikado-branch-invariant-check` commit-msg hook runs automatically on `mikado/*` branches, validating commit message conventions and invariant ordering. Requires `uvx pre-commit install --hook-type commit-msg`. ## Related diff --git a/mise-tasks/mikado-branch-invariant-check b/mise-tasks/mikado-branch-invariant-check index 80e3a8d..ee8a370 100755 --- a/mise-tasks/mikado-branch-invariant-check +++ b/mise-tasks/mikado-branch-invariant-check @@ -6,11 +6,18 @@ #MISE description="Validate Mikado Branch Invariant on mikado/* branches" """Validate the Mikado Branch Invariant for C2 change branches. -Runs as a pre-commit hook on mikado/* branches. Checks: +Runs as a commit-msg hook on mikado/* branches. Receives the commit message +file as its first argument, classifies the incoming commit, appends it to the +existing branch history, and validates the full sequence. + +Checks: 1. All commits follow the C2(): convention 2. The invariant ordering is maintained: plan commits come before impl/close 3. No plan commits appear after any impl or close commits 4. Close commits don't appear before impl commits in the same cycle +5. The chain stem in commit messages matches the branch name + +Can also be run standalone (no arguments) to validate existing branch history. Exit code 0 if valid (or not on a mikado/* branch), 1 if violations found. """ @@ -71,6 +78,29 @@ def get_branch_commits(branch: str) -> list[dict]: return commits +def parse_commit_message(msg_path: str) -> str: + """Read and return the first line of a commit message file.""" + with open(msg_path) as f: + for line in f: + line = line.strip() + if line and not line.startswith("#"): + return line + return "" + + +def make_pending_commit(subject: str) -> dict: + """Create a pseudo-commit dict for the incoming (not yet created) commit.""" + match = C2_COMMIT_RE.match(subject) + return { + "sha": "(pending)", + "subject": subject, + "chain": match.group(1) if match else None, + "verb": match.group(2) if match else None, + "description": match.group(3) if match else None, + "conventional": match is not None, + } + + def check_invariant(commits: list[dict], chain_stem: str) -> list[str]: """Check the Mikado Branch Invariant. Returns list of violation messages.""" errors: list[str] = [] @@ -136,6 +166,13 @@ def main() -> None: chain_stem = branch.removeprefix("mikado/") commits = get_branch_commits(branch) + # If called with a commit message file (commit-msg hook), include the + # pending commit in the validation + if len(sys.argv) > 1: + subject = parse_commit_message(sys.argv[1]) + if subject: + commits.append(make_pending_commit(subject)) + if not commits: # No commits on branch yet — valid (length-zero case) sys.exit(0)