Skip to content

Replace the merge workflow guard with a Go hook #721

Description

@tbhb-agent

Problem

The merge workflow guard guard-merge.sh, the 75-line PreToolUse hook under packages/agents/source/skills/merge-pr/claude/scripts/, keeps every change to a pull request behind the one merge entry point. It is authority-bearing, and its rules are shell regular expressions that read the hook payload with jq. This slice ports it to Go, on top of the hook command, the Bash payload support and the strict payload decoding that #719 adds. It belongs to the workflow guards, feature 9 of #588.

Acceptance criteria

  1. AC1: Given a well-formed hook payload for a Bash command, when the merge guard evaluates it, then the exit status, standard output and standard error equal what the shell guard produced, for allowed and for blocked commands.
  2. AC2: Given a command, the verb is the first run of lowercase letters and hyphens that directly follows gh pr and whitespace at a command position, as the shell guard's pattern captures it. If that verb is outside list, view, diff, status and checks, the guard blocks the command; if it is one of those five, it allows the command whatever follows in the chain; if there is no such verb, it allows the command. So gh pr view 1 && gh pr merge 1 is allowed, gh pr merge 1 && gh pr view 1 is blocked and gh pr MERGE 1, whose spelling starts with no lowercase letter, has no verb and is allowed, all as today.
  3. AC3: Given a payload that is empty, is not valid JSON, or is valid JSON followed by any further non-whitespace bytes, then the guard exits 2 with a reason on standard error, prints nothing on standard output and never allows.
  4. AC4: Given commands that chain, quote, substitute or span several lines, including a heredoc body whose line starts with gh pr, then each verdict equals the shell guard's today, with the first-match and verb-capture behaviour of AC2 and no hardening of chains or of letter case.
  5. AC5: Given a valid payload with no Bash command (the field absent, null or not a string), then the guard allows it with no output, as the shell guard does.
  6. AC6: The skill's hook registration still fires for the same tool matcher and the same activation lifetime as before the change.
  7. AC7: Given the Go binary is not installed where the registration runs, then the registered hook denies the call (exit 2) and never allows it.
  8. AC8: After regeneration, the guard entry point of the skill package and both local deployments are generated from the canonical sources and agree with them.
  9. AC9: Given a change to the Go code that enforces this guard, including the shared payload decoding and dispatch that control its verdict, then the merge policy classifies it as a golden and owner-protected path exactly as it classifies the shell guard, so an unattended merge needs the machine-configured owner's approval of the exact head.
  10. AC10: Non-functional: a guard invocation, measured from the start of the registered hook path to exit, takes under 100 ms at the median over 50 runs on the CI runner, for the slowest allowed command of the corpus.

Scope and non-goals

In scope: the merge guard, its forwarding shim, registration and tests.
Non-goal: the commit and pull request guards (#719) and the rebase and resolve-conflicts guards (#722).
Non-goal: loosening or tightening any rule. The only deliberate difference from the shell guard is AC3's refusal of an unreadable payload (the shell guard exits 5 on malformed JSON or on bytes after a valid object, a status that does not block, and exits 0 on an empty payload).
Non-goal: hardening the first-verb behaviour on chains or the lowercase-only verb capture; a separate issue may tighten them.
Out of scope: the merge scripts the guard protects.

Approach

  1. Add the merge guard as a subcommand of the Go hook command from Replace the commit and pull request workflow guards with Go hooks #719, using its Bash payload type and its strict decoder (the decoder rejects empty input and any bytes after the first JSON value, which is what AC3 needs and why this issue does not add its own). The guard follows the shell contract: reason on standard error, exit 2 to block.
  2. Record the characterization corpus first, from the shell guard at a pinned commit of main, since the merge Bats suite mostly tests the merge scripts and not the guard decisions: every gh pr verb, unknown and mixed-case verbs, both chain orders, separators ;, &&, ||, |, a subshell and a newline, quoting, command substitution, a gh pr line inside a heredoc, and echo gh pr merge. Run the shell guard over it to confirm the recording, then require identical Go results before removing the shell logic.
  3. Keep the script path as a forwarding shim of at most 10 lines that execs the Go command and exits 2 with a reason when the binary is missing, so the registration, matcher and activation lifetime stay as they are. Delete the shell matching logic.
  4. Protection: Replace the commit and pull request workflow guards with Go hooks #719 extends the golden and owner path patterns to cover the hook command, the guard packages and the shared payload library. This issue adds a test that its new files fall under those patterns, and changes the patterns only if they do not.
  5. Deliver in the documented order: edit canonical sources, run mise run generate-skills, then mise run skill-tokens, then apm install, and commit what they rewrite. Add to docs/agent-skills-migration.md that the merge skill needs the release carrying this subcommand installed before the skill is deployed, or state "No consumer impact" with the reason.

Dependencies

Native blocked-by links: #719 (the hook command, Bash payload support and strict decoding it adds, and the protection of that code), #333 (the Go binaries ship as release assets) and #544 (consumers install those assets, because the shim runs in consumer repositories). #719 in turn waits for #713 and #720 and the earlier features of #588, so this issue inherits that sequence, and the shim is not switched to the Go command until #719 has been rolled out.

Size

XS. One PR for one 75-line guard on top of #719.

Verify plan

  • AC1: differential test over the corpus against the recorded shell results, comparing exit status, standard output and exact standard error.
  • AC2: table cases for each allowlisted verb, mutating verbs, a missing verb, an all-uppercase verb (gh pr MERGE 1, allowed as today), a verb with a lowercase first letter and uppercase after it (gh pr mERGE 1, blocked as m), unknown verbs, and both chain orders.
  • AC3: Go tests that feed an empty payload, non-JSON text, a valid object followed by trailing text and a valid object followed by a second object, asserting exit 2, a non-empty reason on standard error and empty standard output.
  • AC4: corpus cases for chaining, quoting, substitution, multi-line input and a heredoc body line that starts with gh pr.
  • AC5: Go tests with the command field absent, null, a number and an empty string, asserting exit 0 and no output.
  • AC6: comparison of the generated hook registration (matcher, command path, scope) with the registration before the change.
  • AC7: integration test running the registered hook with the binary absent, asserting exit 2 and a non-empty reason.
  • AC8: mise run generate-skills, mise run skill-tokens and apm install in that order leave no diff, mise run check-generated-skills passes, the shim has at most 10 lines, and a search shows the matching logic is gone from the source script.
  • AC9: a policy test in the style of tests/skills/agent-merge-pr/policy.bats: a change to the new guard file is refused for an unattended merge without the owner's approval of the exact head, plus a pattern-match check on the new paths.
  • AC10: a process-level timing script that runs 50 invocations of the registered hook path for the slowest allowed corpus command and reports the median, run in CI.
  • Delivery: mise run check is green on the exact head, and so are mise run cover-go-check and mise run cover-py-check; the pull request lists every AC ID with its evidence.

Risks

A guard that allows what it should block weakens the merge path; AC1 to AC5 are the fail-closed checks. A missing binary must not open the guard; AC7 covers it. Lapsed protection of the replacement code is covered by AC9. The undo is reverting the PR, which restores the shell guard and its registration.

Operator authority

Owner-only GitHub action: the repository owner approves the exact head, because the change touches an owner-protected guard path and adds protection for the replacement code.

Activity

  1. updated Work type Planned and Priority Standard on Oct 25, 2026
  2. tbhb-agent commented on Oct 25, 2026

    @tbhb-agent
    ContributorAuthor

    Refinement: NOT READY — reconcile compatibility criteria and preserve enforcement protection

    Checked

    Read the non-empty live body of #721, confirmed parent #588, inspected native blockers #719 and #333, and reviewed siblings #713–#722, the shell guard, shared libraries, hook registration, Go hook infrastructure, and relevant test code. No files changed or mechanical checks run.

    Definition of Ready, in order:

    Gaps

    1. AC2 contradicts the compatibility promise. AC2 unconditionally blocks non-read-only verbs, while AC4 preserves first-match behavior. For example, gh pr view 1; gh pr merge 1 is allowed because the shell captures view and exits immediately (packages/agents/source/skills/merge-pr/claude/scripts/guard-merge.sh:49, packages/agents/source/skills/merge-pr/claude/scripts/guard-merge.sh:56). Qualify AC2 to describe the first matched verb and explicitly verify both chain orders. This preserves the stated non-goal of hardening chains.

    2. AC7 violates the required acceptance-criteria shape. It prescribes a shell implementation, a script layout and line limit, and a generation command, while combining several outcomes. Move those implementation constraints into Approach and Verify; express the acceptance outcome as continued compatibility of the existing hook entry point. Retain shell-removal and generation checks as delivery requirements.

    3. Replacement protection must preserve both protection mechanisms. The shell guard matches both golden_paths and owner_paths (.agents/agent-skills.conf:4, .agents/agent-skills.conf:9). AC8 and its verification mention only owner-path matching. Golden-path classification separately triggers exact-head owner approval (packages/agents/source/skills/merge-pr/shared/scripts/merge-agent.sh:155). Require equivalent protection for the replacement enforcement code, including shared decoding or dispatch code that controls its verdict. Verify refusal without the required owner approval, alongside pattern matching. Existing policy coverage provides a model (tests/skills/agent-merge-pr/policy.bats:335).

    4. Complete the generation and consumer rollout plan. “Regenerate skills” and check-generated-skills do not cover the required generation, token measurement and deployment sequence. Specify mise run generate-skills, then mise run skill-tokens, then apm install, committing their outputs. Add consumer adoption guidance identifying the binary version containing the new subcommand, its installation before skill deployment, and protection updates. Ship the Go binaries as release assets installed through mise's github backend #333 publishes assets but explicitly excludes consumer pin and adoption migration; closing it alone does not establish that deployed hooks can invoke this new command.

    5. Make AC3’s malformed-input verification cover the actual decoder boundary. internal/hookio/hookio.go:52 decodes one JSON value without checking the remaining input. A valid object followed by invalid trailing bytes can therefore escape the intended invalid-JSON refusal if reused unchanged. Include that case in AC3 verification and state whether Replace the commit and pull request workflow guards with Go hooks #719 supplies strict whole-input decoding or this migration must enforce it.

    Split

    No split required. Keep this to the merge guard, compatibility corpus, forwarding entry point, protection update and adoption guidance. Shared Bash payload infrastructure belongs to #719; rebase guards remain in #722.

    Definition of done

    All items below remain unchecked because the issue’s AC and Verify plan do not yet cover each complete obligation.

    • D1 — Pending: AC1–AC9 have proposed checks, but merged-head verification and per-AC evidence in the PR are not specified.
    • D2 — Pending: Exact-head independent review and finding disposition remain delivery requirements.
    • D3 — Pending: Owner approval is stated; passing required checks on the final head is not covered.
    • D4 — Unmet in plan: No explicit gitleaks, private-string leak scan or post leak-review evidence.
    • D5 — Pending: Signed commits and squash are not covered.
    • D6 — Unmet in plan: No documentation update or justified “No doc impact” evidence.
    • D7 — Pending: Assisted-by trailers and the provenance label are not covered.
    • D8 — Pending: XS states intended scope; actual PR size compliance remains to be verified.
    • D9 — Pending: Deferred chain hardening is mentioned, but follow-up issue creation and Related links are not required.
    • D10 — Pending: Closing reference, Done state and final merge-commit status remain delivery work.
    • devtools-1 — Unmet in plan: No explicit mise run check, mise run cover-go-check or mise run cover-py-check.
    • devtools-2 — Partially covered: AC7 checks generated output; the ordered generation, token and deployment steps are missing.
    • devtools-3 — Unmet in plan: This changes a deployed hook; consumer adoption steps are missing.

    Round: 1 of 3

  3. tbhb-agent commented on Oct 25, 2026

    @tbhb-agent
    ContributorAuthor

    Refinement: NOT READY — AC2 still conflicts with compatibility for uppercase verbs

    Checked

    Read the current #721 body, round 1 verdict, parent #588, native blockers, and relevant siblings. Fetched origin and read code exclusively from origin/main. No edits, checks, delegation or publication.

    Definition of Ready, in order:

    Gaps

    1. AC2 needs to distinguish a matched verb from an uppercase token. It requires blocking the first gh pr verb outside the five allowed spellings. However, gh pr MERGE 1 is allowed by the shell: its expression captures only [a-z-]+, and no match exits 0 (packages/agents/source/skills/merge-pr/claude/scripts/guard-merge.sh:49). Thus AC2 requires blocking this input while AC1 and AC4 require allowing it. The uppercase case explicitly required by Verify cannot satisfy both.

      Qualify AC2 around the first regex-matched verb, preserving the existing matching semantics. Give the uppercase fixture its shell-compatible expected result. The ordinary view-then-merge and reversed chain examples already match the early return at packages/agents/source/skills/merge-pr/claude/scripts/guard-merge.sh:56.

    Split

    No split required. Keep the merge guard, corpus, shim, protection verification and adoption guidance together. Shared decoding and dispatch remain prerequisites supplied by #719; description validation belongs to #718.

    Definition of done

    Ticks indicate coverage in the issue’s plan, not completed implementation evidence.

    • D1 — Pending: Per-AC PR evidence and exact-head checks are specified; merged-head verification remains pending.
    • D2 — Pending: Exact-head independent review and finding disposition remain delivery obligations.
    • D3 — Pending: Exact-head checks and owner approval are planned; required-check and any loosened-gate approval evidence remain pending.
    • D4 — Unmet in plan: The complete leak-scan and post-review evidence is not specified.
    • D5 — Pending: Signed commits and squash require delivery evidence.
    • D6: Approach requires migration documentation describing installation before skill deployment.
    • D7 — Pending: Assisted-by trailers and the provenance label require delivery evidence.
    • D8 — Pending: XS defines intended scope; actual PR size compliance remains pending.
    • D9 — Pending: Follow-up issue creation, Related links and code references are not specified.
    • D10 — Pending: Closing reference, Done state and final merge-commit status remain delivery work.
    • devtools-1: Delivery explicitly requires mise run check, mise run cover-go-check and mise run cover-py-check.
    • devtools-2: Approach and AC8 verification specify generation, token measurement and deployment in order, committed outputs and generated-skill validation.
    • devtools-3: Approach requires consumer steps in docs/agent-skills-migration.md, or a justified “No consumer impact” statement.

    Round: 2 of 3

  4. tbhb-agent commented on Oct 25, 2026

    @tbhb-agent
    ContributorAuthor

    Refinement: READY — AC2 now preserves the shell guard’s verb-capture behavior

    Checked

    Read the current #721 body, both prior verdicts, parent #588, native blockers and relevant siblings #713–#722. No edits, mechanical checks, delegation or publication.

    Definition of Ready, in order:

    The round 2 gap is resolved. The expression captures the first matching [a-z-]+ run and allows when none matches (packages/agents/source/skills/merge-pr/claude/scripts/guard-merge.sh:49). It uses that capture and immediately allows the five listed spellings (packages/agents/source/skills/merge-pr/claude/scripts/guard-merge.sh:53, packages/agents/source/skills/merge-pr/claude/scripts/guard-merge.sh:56).

    Consequently, AC1, AC2 and AC4 agree: MERGE alone has no matching verb and allows; mERGE captures m and blocks; the first matching allowlisted verb permits either chain suffix. AC2 verification explicitly covers both case examples, every allowlisted verb, missing and unknown verbs, mutating verbs and both chain orders. AC1’s differential comparison supplies exact output and status evidence.

    Registration and protection verification also match existing code: packages/agents/source/skills/merge-pr/SKILL.claude.md:6, .agents/agent-skills.conf:4, .agents/agent-skills.conf:9 and tests/skills/agent-merge-pr/policy.bats:335.

    Gaps

    None that require implementation guesses or prevent the planned checks from proving their AC.

    Split

    No split required. Keep the merge guard, corpus, shim, protection verification and adoption guidance together; shared infrastructure remains in #719.

    Definition of done

    Ticks indicate coverage in the issue’s plan, not completed implementation.

    • D1 — Pending: Per-AC PR evidence and exact-head checks are specified; merged-head verification remains pending.
    • D2 — Pending: Exact-head independent approval and finding disposition require delivery evidence.
    • D3 — Pending: Exact-head checks and owner approval are planned; required-check results and any loosened-gate approval remain pending.
    • D4 — Unmet in plan: Complete gitleaks, private-string scan and post leak-review evidence is not specified.
    • D5 — Pending: Signed commits, including the squash, require delivery evidence.
    • D6: Approach requires migration documentation explaining installation before skill deployment.
    • D7 — Pending: Assisted-by trailers and the provenance label require delivery evidence.
    • D8 — Pending: XS defines intended scope; actual PR size compliance remains pending.
    • D9 — Pending: Follow-up issues, Related links and code references remain delivery obligations.
    • D10 — Pending: Closing reference, Done state and final merge-commit status remain delivery work.
    • devtools-1: Delivery explicitly requires mise run check, mise run cover-go-check and mise run cover-py-check.
    • devtools-2: Approach and AC8 verification specify ordered generation, token measurement, deployment, committed outputs and generated-skill validation.
    • devtools-3: Approach requires consumer migration steps or a justified “No consumer impact” statement.

    Round: 3 of 3

  5. added
    refinement:readyPassed the Definition of Ready; may be picked up
    and removed
    refinement:activeA refinement round is open; not ready for implementation
    on Oct 25, 2026
  6. tbhb-agent commented on Oct 26, 2026

    @tbhb-agent
    ContributorAuthor

    Refinement amendment for the shell-to-Go epic (#588), covering compatibility and protection. The acceptance criteria stay as written, except where noted.

    1. The merge guard script forwards to the Go hook when present and runs the existing shell guard otherwise. A present Go hook that errors blocks the command (exit 2) rather than falling back.
    2. Fallback rule from docs: state the N-2 compatibility policy #661, applied here: the caller probes for the Go command without writes. When the command is absent (an older pinned binary), the existing shell implementation runs unchanged. When the command is present and fails, prints partial output or returns an invalid result, the caller refuses and never falls back to repeat a protected action. Configuration, flags, streams, statuses and state formats stay as they are, and this change does not raise the binary floor. A separate XS issue removes the fallback after every supported release pin ships the command. Any acceptance criterion or step that demands deleting the shell logic, or a missing-binary refusal, moves to that issue.
    3. No new complex shell: the fallback is the frozen existing code, not a translation of it.
    4. The replacement Go package and its dispatcher registration are covered by the owner-approval protection patterns before any caller switches to them. That protection is its own Wave 1 issue and is a native dependency of this one.
    5. Generated skill output is regenerated in the same PR, in the documented order.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    refinement:readyPassed the Definition of Ready; may be picked up

    Fields

    Priority

    Standard

    Work type

    Planned

    Projects

    • Status
      Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions