feat(governance): ADR-003 enforcement hooks — commit-msg, branch-naming, pickup-issue skill (#186) - #679
feat(governance): ADR-003 enforcement hooks — commit-msg, branch-naming, pickup-issue skill (#186)#679scottschreckengaust wants to merge 5 commits into
Conversation
…ng, pickup-issue skill (#186) Implements the three "Planned" ADR-003 enforcement rows (docs/decisions/ ADR-003-contribution-governance.md enforcement table) as offline-capable, no-new-dependency hooks + a Claude Code skill: - commit-msg hook (Tier 0): scripts/hooks/check-commit-msg.mjs rejects a commit whose message carries no issue reference (Refs/Fixes/Closes #N, the GitHub closing-keyword family + Ref/Refs). Wired as a commit-msg-type local hook; `commit-msg` added to default_install_hook_types. A bare `#N` with no keyword does not satisfy the gate. - branch-name hook (pre-push): scripts/hooks/check-branch-name.mjs rejects a branch not matching (feat|fix|chore|docs)/<issue-number>-<desc>. Exempts `main`, `dependabot/*`, and the detached-HEAD sentinel (ADR did not enumerate exemptions; minimal set documented in the ADR + PR body). - pickup-issue skill: docs/abca-plugin/skills/pickup-issue/SKILL.md — an agent-workflow gate that hard-fails without an approved, assigned issue before implementation. Advertised in the plugin SessionStart hook + README. Both scripts are plain Node ESM with node --test unit tests (23 cases, adversarial: missing ref, wrong prefix, missing issue number, comment-only ref). No new hook-runner or third-party tool added — reuses the existing prek/ pre-commit framework and Node. Deferred AC (flagged, not dropped): the pre-push Tier 1 `approved`-label `gh` check remains "Planned" — it needs a network call at push time, which this offline hook set intentionally avoids. The pickup-issue skill covers the approved-label gate at the agent-workflow layer in the interim. Docs: flipped the three implemented ADR-003 rows Planned -> Implemented (#186), documented exemptions + reference forms; updated CONTRIBUTING.md hooks list; regenerated Starlight mirrors via docs:sync. Closes #186 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…/boundary (#186) Addresses the /review_pr blocking finding: the new hook unit tests were orphaned — no CI job or mise task ran scripts/hooks/*.test.mjs, so the Tier-0 governance regexes could silently regress. - Add mise task `test:hooks` (node --test scripts/hooks/*.test.mjs; explicit glob avoids Node 22's spurious directory-arg failure) and add it to the `build` DAG so `mise run build` (which CI runs) executes it. - Add the cheap test cases the reviewers flagged: GH-N reference form (a documented-but-untested accepted form), no-separator rejection (Closes#5), word-boundary rejection (refixes), and CRLF handling (body ref survives, comment-only ref does not). 23 -> 27 tests, all green. Refs #186
…te (#186) The Tier 0 commit-msg hook rejected any message without an issue reference, including git-generated merge and revert commits. That made a routine `git merge origin/main` — which CONTRIBUTING instructs contributors to run to refresh a branch — impossible to complete. Exempt auto-generated subjects (Merge/Revert/fixup!/squash!/amend!), keyed off the subject line only so a normal commit that merely mentions "merge" in prose still requires a ref. Mirrors the default ignore set of conventional commit-msg linters. Refs #186. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Self-review (
|
✅ Acceptance summary (for the reviewer)#186 — ADR-003 enforcement hooks, implementing the enforcement table rows that were marked "Planned".
Two real defects fixed during self-review: the PR body
🔀 Merge guidance (for the reviewer)Independent — no ordering constraint. Cluster 🤖 orchestrator note (agent) — promotion is orchestrator-driven; merge remains a human action. |
Summary
Implements the three ADR-003 governance enforcement mechanisms that were marked Planned — a Tier 0 commit-msg hook, a pre-push branch-name check, and a
pickup-issueClaude Code skill — as offline-capable, no-new-dependency hooks reusing the existing prek/pre-commit framework and Node.Closes #186
Root cause
ADR-003 (
docs/decisions/ADR-003-contribution-governance.md, enforcement table) defines governance rules that were prose-only and unenforced. Three table rows were Planned with no implementation:pickup-issue.pre-commit-config.yamlhad no commit-msg hook, no branch-naming hook, anddefault_install_hook_typeswas[pre-commit, pre-push](nocommit-msgtype).The fix
1. commit-msg hook (Tier 0) —
scripts/hooks/check-commit-msg.mjsRefs #NorFixes #N". Accepts the full GitHub closing-keyword family (Close/Closes/Closed,Fix/Fixes/Fixed,Resolve/Resolves/Resolved) plusRef/Refs, each followed by#N,GH-N, orowner/repo#N. A bare#Nwith no keyword does not pass (must be an intentional link, not incidental prose). Git comment lines (#-leading) are stripped before scanning.fixup!/squash!/amend!) are exempt, keyed off the subject line only — so a routinegit merge origin/main(which CONTRIBUTING tells contributors to run) is not blocked. Mirrors the default ignore set of conventional commit-msg linters. A normal commit that merely mentions "merge" in prose still requires a reference.commit-msg-type local hook;commit-msgadded todefault_install_hook_types.2. branch-name hook (pre-push) —
scripts/hooks/check-branch-name.mjs(feat|fix|chore|docs)/<issue-number>-<desc>(ADR-003 "No branches without an Issue"). Reads the current branch via git plumbing (git rev-parse --abbrev-ref HEAD); fails loud rather than defaulting to a pass.main(the trunk is not a feature branch),dependabot/*(bot-authored upgrade branches), and the detached-HEADsentinel. ADR-003 does not enumerate exemptions in prose — this is the minimal set needed so the trunk and machine-generated branches are not falsely rejected. Documented in the ADR "Branch-name exemptions" note + here.pre-push-type local hook.3.
pickup-issueskill —docs/abca-plugin/skills/pickup-issue/SKILL.mdhooks/hooks.json) and README skills table.Deferred AC (flagged, not dropped)
The pre-push Tier 1
approved-labelghcheck (a separate ADR-003 table row) remains Planned. It needs a networkghAPI call at push time, which this offline-capable hook set intentionally avoids; thepickup-issueskill covers theapproved-label gate at the agent-workflow layer in the interim. The ADR row and the "Progressive enforcement" note now say so explicitly. Recommend a follow-up issue. (ThePreToolUse: WriteClaude Code hook row also remains "Planned" — out of scope for #186.)Testing
Unit tests (Node's built-in
node --test, no new framework) — 31 cases, adversarial coverage (missing ref, wrong prefix, missing issue number, comment-only ref, bare#N, CRLF, GH-N, cross-repo, keyword-boundary, and the merge/revert/fixup exemptions):Hooks proven to fire (dogfooded on this very branch/commits):
Dogfood: every commit on this branch references #186 (or is the exempt merge commit) and passes the
adr003-commit-msghook; the branchfeat/186-adr003-hookspasses the branch-name hook.Gates: hook unit tests PASS (31/31) ·
mise //docs:build+drift-preventionPASS ·security:sast(semgrep, 0 findings) /security:gh-actions(zizmor, 0 findings) PASS with zero findings in the new scripts · gitleaks overorigin/main..HEADclean.Notes / pre-existing findings (NOT fixed here — out of scope)
#672main, so theorigin/main..HEADdiff previously showed 8.github/workflows/*.ymlfiles as spurious "downgrades" ofactions/checkout,jdx/mise-action,actions/setup-node, andconfigure-aws-credentialspins. This was an artifact of the stale base, not an authored change — resolved by mergingorigin/main(which brought PR chore(deps): actions: bump the all-actions group across 1 directory with 4 updates #672's newer pins in). The diff now contains only the 13 in-scope files.ec2:DescribeAvailabilityZonesnot authorized (local Isengard role lacks EC2 perms for the VPC AZ lookup) — environmental, not code; my diff touches zerocdk/files. Passes in CI.Dependencies / related
bash/nodeunder the existing preklanguage: systemframework).docs/decisions/ADR-003+CONTRIBUTING.md; Starlight mirrors regenerated viamise //docs:sync.🤖 Generated with Claude Code