Skip to content

feat(governance): ADR-003 enforcement hooks — commit-msg, branch-naming, pickup-issue skill (#186) - #679

Open
scottschreckengaust wants to merge 5 commits into
mainfrom
feat/186-adr003-hooks
Open

feat(governance): ADR-003 enforcement hooks — commit-msg, branch-naming, pickup-issue skill (#186)#679
scottschreckengaust wants to merge 5 commits into
mainfrom
feat/186-adr003-hooks

Conversation

@scottschreckengaust

@scottschreckengaust scottschreckengaust commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

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-issue Claude 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:

Mechanism Prior status
Branch name convention Planned
Commit-msg hook (Tier 0) Planned
Skill gate: pickup-issue Planned

.pre-commit-config.yaml had no commit-msg hook, no branch-naming hook, and default_install_hook_types was [pre-commit, pre-push] (no commit-msg type).

The fix

1. commit-msg hook (Tier 0)scripts/hooks/check-commit-msg.mjs

  • Enforces: a commit message must carry an issue reference. ADR-003 states "Rejects commits without Refs #N or Fixes #N". Accepts the full GitHub closing-keyword family (Close/Closes/Closed, Fix/Fixes/Fixed, Resolve/Resolves/Resolved) plus Ref/Refs, each followed by #N, GH-N, or owner/repo#N. A bare #N with no keyword does not pass (must be an intentional link, not incidental prose). Git comment lines (#-leading) are stripped before scanning.
  • Exemption: git-generated commits (merge, revert, fixup!/squash!/amend!) are exempt, keyed off the subject line only — so a routine git 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.
  • Wired as a commit-msg-type local hook; commit-msg added to default_install_hook_types.

2. branch-name hook (pre-push)scripts/hooks/check-branch-name.mjs

  • Enforces: branch must match (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.
  • Exemptions: main (the trunk is not a feature branch), dependabot/* (bot-authored upgrade branches), and the detached-HEAD sentinel. 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.
  • Wired as a pre-push-type local hook.

3. pickup-issue skilldocs/abca-plugin/skills/pickup-issue/SKILL.md

  • An agent-workflow gate that hard-fails if there is no approved, assigned, open issue before implementation. Mirrors the existing plugin skill structure (frontmatter + phased workflow). Advertised in the plugin SessionStart hook (hooks/hooks.json) and README skills table.

Deferred AC (flagged, not dropped)

The pre-push Tier 1 approved-label gh check (a separate ADR-003 table row) remains Planned. It needs a network gh API call at push time, which this offline-capable hook set intentionally avoids; the pickup-issue skill covers the approved-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. (The PreToolUse: Write Claude 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):

mise run test:hooks            # wired into the build DAG
# or: node --test scripts/hooks/*.test.mjs
# 31 pass, 0 fail

Hooks proven to fire (dogfooded on this very branch/commits):

# commit-msg: BAD (no ref) -> rc=1 (rejected);  GOOD (Closes #186) -> rc=0;  MERGE commit -> rc=0 (exempt)
node scripts/hooks/check-commit-msg.mjs <msg-file>

# branch-name: feat/186-adr003-hooks -> rc=0 (Passes);  my-bad-branch -> rc=1 (rejected);  main -> rc=0 (exempt)
node scripts/hooks/check-branch-name.mjs [branch]

# via prek (end-to-end wiring):
prek run adr003-commit-msg --hook-stage commit-msg --commit-msg-filename <file>   # Passed on Closes #186
prek run adr003-branch-name --hook-stage pre-push --all-files                     # Passed on feat/186-adr003-hooks

Dogfood: every commit on this branch references #186 (or is the exempt merge commit) and passes the adr003-commit-msg hook; the branch feat/186-adr003-hooks passes the branch-name hook.

Gates: hook unit tests PASS (31/31) · mise //docs:build + drift-prevention PASS · security:sast (semgrep, 0 findings) / security:gh-actions (zizmor, 0 findings) PASS with zero findings in the new scripts · gitleaks over origin/main..HEAD clean.

Notes / pre-existing findings (NOT fixed here — out of scope)

  • Stale base fixed: the branch was cut from a pre-#672 main, so the origin/main..HEAD diff previously showed 8 .github/workflows/*.yml files as spurious "downgrades" of actions/checkout, jdx/mise-action, actions/setup-node, and configure-aws-credentials pins. This was an artifact of the stale base, not an authored change — resolved by merging origin/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.
  • CDK synth fails locally with ec2:DescribeAvailabilityZones not authorized (local Isengard role lacks EC2 perms for the VPC AZ lookup) — environmental, not code; my diff touches zero cdk/ files. Passes in CI.

Dependencies / related

🤖 Generated with Claude Code

scottschreckengaust and others added 5 commits July 29, 2026 01:06
…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>
…186)

Document the commit-msg hook's exemption for git-generated commits so the ADR
matches the implementation. Regenerated the Starlight mirror. Refs #186.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@scottschreckengaust

Copy link
Copy Markdown
Contributor Author

Self-review (/review_pr dimensions, run inline) — Approve with nits, ZERO blocking

Verdict: Approve. All ADR-003 acceptance criteria met; two real defects found and fixed during this review pass (below). Diff is 13 in-scope files, zero out-of-scope.

Vision alignment: Fits directly — advances the Reviewable outcomes and governance tenets by making ADR-003 rules mechanically enforced instead of prose-only. No tenet traded.

Two defects fixed during review (were real, now resolved):

  1. closingIssuesReferences was empty — the PR body's closing keyword was backticked (`Closes #186`), which GitHub does not parse as a linking directive. Un-backticked to a bare Closes #186; gh pr view 679 --json closingIssuesReferences now resolves to [186].
  2. commit-msg hook rejected merge commits — the Tier 0 gate blocked any message with no issue ref, including the auto-generated Merge remote-tracking branch ... subject, which made the CONTRIBUTING-mandated git merge origin/main impossible to complete. Fixed with a subject-keyed exemption for merge/revert/fixup!/squash!/amend! commits (commit ae118f57), mirroring conventional commit-msg linters. Verified \b boundaries do NOT falsely exempt "Merged"/"Reverts"/"Mergeworld". ADR + Starlight mirror updated (28e9a8a3).

Also resolved: the branch was cut from a pre-#672 main, so the diff previously showed 8 .github/workflows/*.yml files as spurious action-pin "downgrades." Merged origin/main to bring the current pins in — diff is now scope-only.

Dimensions run inline (I am a background worker; nested subagent dispatch is prohibited, so these were reviewed by hand + tooling rather than via Task agents):

  • Correctness — hooks reject bad input and accept good, with no false rejects that would block legit commits (dogfooded: bad→rc1, good→rc0, merge/main→exempt, this branch→pass). Branch regex correctly rejects feat/186, feat/186-, feature/..., maindev. Merge-exemption boundary verified against false positives.
  • Scope creep — none; 13 in-scope files, no package.json changes.
  • Governanceno new hook-runner dependency: both hooks are plain bash/node under prek's existing language: system framework. No new tool/action/dep added. ✅
  • Security (shell/SAST)security:sast (semgrep) 0 findings, none in scripts/hooks/; security:gh-actions (zizmor) 0 findings; gitleaks over origin/main..HEAD clean.
  • Doc drift — ADR-003 marks only the 3 implemented rows "Implemented (feat(governance): implement ADR-003 enforcement hooks (commit-msg, branch naming, pickup-issue skill) #186)"; Tier 1 approved-label check + PreToolUse: Write correctly stay "Planned" (Tier 1 flagged as deferred with rationale). Starlight mirror in sync (drift-prevention PASS).
  • Tests — 31 cases (node --test), wired into the build DAG via test:hooks; adversarial + exemption coverage. No new framework.

Could not run: the pr-review-toolkit Task agents (code-reviewer, etc.) — nested subagent dispatch is prohibited for a background worker. Substituted a rigorous inline hand-review + the full security/build tooling above. type-design-analyzer N/A (no new TS types; scripts are .mjs). silent-failure-hunter: the two try/catch blocks (readFileSync, git rev-parse) both fail loud with rc=2 rather than defaulting to a pass — correct fail-closed behavior.

Nits (non-blocking): none material.

🤖 @scottschreckengaust (agent:w4)

@scottschreckengaust

Copy link
Copy Markdown
Contributor Author

✅ Acceptance summary (for the reviewer)

#186 — ADR-003 enforcement hooks, implementing the enforcement table rows that were marked "Planned".

  • commit-msg hook (Tier 0): rejects commits lacking a Refs/Fixes/Closes #N reference (accepts the full GitHub closing-keyword family + GH-N / owner/repo#N); exempts merge/revert/fixup!/squash!/amend! subjects.
  • pre-push branch-name hook: enforces (feat|fix|chore|docs)/<issue>-*; exempts main/dependabot/HEAD.
  • pickup-issue skill: hard-fails without an approved + assigned + open issue.
  • Wired via prek language:system (plain bash/node — no new dependency) + a test:hooks build-DAG target + 31 unit tests. Dogfooded: this branch and its commits pass the very hooks it adds.

Two real defects fixed during self-review: the PR bodys Closeskeyword was backticked (would not have auto-closed #186 — now unformatted, resolves to [186]); and the commit-msg hook initially rejected merge commits (would block the mandatedgit merge origin/main` — merge-commit exemption added).

/review_pr = approve-with-nits, zero blocking. Deferred (flagged): the pre-push Tier-1 approved-label gh check stays "Planned" (needs network at push time; the pickup-issue skill covers it at the workflow layer). Follow-up recommended.

🔀 Merge guidance (for the reviewer)

Independent — no ordering constraint. Cluster gov-hooks (disjoint); touches .pre-commit-config.yaml, hook scripts, the skill, ADR-003 + its mirror. No file overlap with any other batch PR. Native auto-merge disabled repo-wide; awaits your manual squash-merge.

🤖 orchestrator note (agent) — promotion is orchestrator-driven; merge remains a human action.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(governance): implement ADR-003 enforcement hooks (commit-msg, branch naming, pickup-issue skill)

1 participant