Skip to content

Honor repository branch-name policy before builder PR creation - #971

Merged
jeffhuber merged 5 commits into
mainfrom
codex/865-branch-policy
Sep 14, 2026
Merged

Honor repository branch-name policy before builder PR creation#971
jeffhuber merged 5 commits into
mainfrom
codex/865-branch-policy

Conversation

@jeffhuber

Copy link
Copy Markdown
Contributor

Issue #865 requires builder delivery to honor each target repository's configured branch naming policy before provider execution while preserving strict builder ownership and handoff boundaries.

This replacement supersedes closed PR #970 after its hosted-Devin qualification run stopped with unresolved exact-head audit findings. The Code Mower Codex builder reconstructed the implementation on a fresh branch without Devin commit history and fixed the remaining defects: existing policy branches now require same-lane provenance at the exact observed remote head, branch absence or the inspected SHA is pinned before provider writes, and configured policy is enforced before ownership or handoff evaluation. Cross-lane handoff for policy-named branches remains fail-closed pending #962.

Validation:

  • 174 focused tests and 206 subtests passed
  • Ruff passed
  • ShellCheck and Bash syntax passed
  • git diff --check passed
  • release readiness passed 20/20

Closes #865

@jeffhuber

Copy link
Copy Markdown
Contributor Author

Codex audit (merge-authority lane)

Head SHA: 0d6078de48583536d1190abcf89370fdea8e5aae
Findings: P0=0, P1=0, P2=1, P3=0 (blocker policy: any P0/P1/P2 → BLOCKED)

Codex Audit: BLOCKED

Summary:

Policy-based branch discovery can hide an existing PR after an issue title changes, allowing duplicate delivery on explicit retries.

Findings:

  • [P2] Detect existing issue PRs before requiring the newly resolved branch -- /private/var/folders/f2/g9zqfdjx7z3ckcsx_f46htwm0000gn/T/codex-audit-b3me3irx/wt/src/code_mower/templates/lanes/run_mac_lane.sh:761
    Finding ID: codex:b785402069b6e835e555
    When an issue title changes after its PR is opened, rerunning --target issue:<n> with a {slug} policy resolves a different branch. This filter hides the existing same-lane PR from the before snapshot, while the guard and prompt require the new branch. A second PR can consequently be opened and counted as a successful pr_opened delivery despite the existing PR. Discover existing same-lane PRs by their closing issue first, then reuse their conforming branch or refuse the mismatch; apply the same correction to the mirrored runners.

Comment on lines +86 to +100
def compile_template(template: Any, *, configured: bool = True) -> BranchPolicy:
"""Validate a template and derive its accepted pattern and one example."""
if not isinstance(template, str) or not template.strip():
raise BranchPolicyError("branch_template must be a non-empty string")
if template != template.strip() or "{" in _VARIABLE_RE.sub("", template) \
or "}" in _VARIABLE_RE.sub("", template):
raise BranchPolicyError("branch_template has unbalanced braces or surrounding whitespace")
names = _variables(template)
if "issue_key" not in names and "issue_number" not in names:
raise BranchPolicyError(
"branch_template must include {issue_key} or {issue_number} so each work item "
"resolves to its own branch"
)
pattern = _pattern(template)
example = render(template, EXAMPLE_VALUES)

@gitar-bot gitar-bot Bot Sep 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Edge Case: Leading {slug} in a branch_template silently breaks on empty slugs

compile_template() always renders its validation example with EXAMPLE_VALUES["slug"] non-empty (branch_policy.py:100, 28-35), so a template like {slug}-{issue_key} or {slug}/{issue_key} (with no character before {slug}) passes config validation. _OPTIONAL_SLUG_RE (branch_policy.py:39) only strips a preceding separator, so at runtime, when a real issue title slugifies to empty (e.g. an emoji-only title), render() produces a branch starting with - (e.g. -ABC-1), which is_valid_ref then rejects, failing the whole delivery for that issue. It fails closed, but the admin's config was accepted as valid and the failure only surfaces later on specific issue titles. Consider having compile_template also render once with an empty slug to catch this class of template at config-validation time, or document that {slug} must not be the first token.

Validate the template with both an empty and non-empty slug during compile_template:

pattern = _pattern(template)
example = render(template, EXAMPLE_VALUES)
if not is_valid_ref(example) or re.fullmatch(pattern, example) is None:
    raise BranchPolicyError(
        f"branch_template {template!r} does not render to a valid git branch name"
    )
empty_slug_values = {**EXAMPLE_VALUES, "slug": ""}
empty_slug_example = render(template, empty_slug_values)
if not is_valid_ref(empty_slug_example):
    raise BranchPolicyError(
        f"branch_template {template!r} does not render to a valid git branch name when "
        "the slug is empty; {slug} must be preceded by -, _, /, or . in the template"
    )

Was this helpful? React with 👍 / 👎

Comment on lines +188 to +193
if self.branch_pattern and (
not _branch(self.branch_example)
or re.fullmatch(self.branch_pattern, self.branch) is None):
raise RemoteError(
f"branch_policy_mismatch: branch must match {self.branch_pattern} "
f"(for example {self.branch_example})")

@gitar-bot gitar-bot Bot Sep 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Quality: WorkOrder direct construction can bypass safe branch_pattern validation

WorkOrder.__post_init__ (devin_work_orders.py:188-193) calls re.fullmatch(self.branch_pattern, self.branch) directly on self.branch_pattern without catching re.error. Every current call path builds branch_pattern via branch_policy.compile_template(), which validates the pattern compiles, so this isn't reachable today. But WorkOrder is a public frozen dataclass constructible directly (as tests already do, e.g. test_branch_policy.py:220-221), so a caller that passes an arbitrary branch_pattern string would get an unhandled re.error instead of the RemoteError the rest of validation consistently raises. Wrapping the re.fullmatch call in a try/except would make the invariant self-enforcing rather than relying on every caller going through compile_template.

Catch re.error from a malformed branch_pattern and raise RemoteError instead:

if self.branch_pattern:
    try:
        matched = re.fullmatch(self.branch_pattern, self.branch)
    except re.error as exc:
        raise RemoteError(f"branch_policy_mismatch: invalid branch_pattern: {exc}") from None
    if not _branch(self.branch_example) or matched is None:
        raise RemoteError(
            f"branch_policy_mismatch: branch must match {self.branch_pattern} "
            f"(for example {self.branch_example})")

Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Sep 13, 2026

Copy link
Copy Markdown

Note

Automatic reviews are paused because your team has used its included automatic processing for this billing period (headroom scales with your seat count). You can still comment "Gitar review" to run one anytime, and automatic reviews resume on their own by October 1. Add seats for more headroom.
Learn more

Code Review 👍 Approved with suggestions 0 resolved / 2 findings

Enforces repository branch-naming policy before builder PR creation, with exact-head provenance validation and fail-closed cross-lane handoff. Two minor suggestions: compile_template() could validate templates with empty slugs to catch branches starting with separators at config time rather than delivery time, and WorkOrder.__post_init__ could wrap re.fullmatch() in error handling to make regex validation self-enforcing for direct construction.

💡 Edge Case: Leading {slug} in a branch_template silently breaks on empty slugs

📄 src/code_mower/branch_policy.py:86-100 📄 src/code_mower/branch_policy.py:39 📄 src/code_mower/branch_policy.py:125-129

compile_template() always renders its validation example with EXAMPLE_VALUES["slug"] non-empty (branch_policy.py:100, 28-35), so a template like {slug}-{issue_key} or {slug}/{issue_key} (with no character before {slug}) passes config validation. _OPTIONAL_SLUG_RE (branch_policy.py:39) only strips a preceding separator, so at runtime, when a real issue title slugifies to empty (e.g. an emoji-only title), render() produces a branch starting with - (e.g. -ABC-1), which is_valid_ref then rejects, failing the whole delivery for that issue. It fails closed, but the admin's config was accepted as valid and the failure only surfaces later on specific issue titles. Consider having compile_template also render once with an empty slug to catch this class of template at config-validation time, or document that {slug} must not be the first token.

Validate the template with both an empty and non-empty slug during compile_template
pattern = _pattern(template)
example = render(template, EXAMPLE_VALUES)
if not is_valid_ref(example) or re.fullmatch(pattern, example) is None:
    raise BranchPolicyError(
        f"branch_template {template!r} does not render to a valid git branch name"
    )
empty_slug_values = {**EXAMPLE_VALUES, "slug": ""}
empty_slug_example = render(template, empty_slug_values)
if not is_valid_ref(empty_slug_example):
    raise BranchPolicyError(
        f"branch_template {template!r} does not render to a valid git branch name when "
        "the slug is empty; {slug} must be preceded by -, _, /, or . in the template"
    )
💡 Quality: WorkOrder direct construction can bypass safe branch_pattern validation

📄 src/code_mower/devin_work_orders.py:188-193

WorkOrder.__post_init__ (devin_work_orders.py:188-193) calls re.fullmatch(self.branch_pattern, self.branch) directly on self.branch_pattern without catching re.error. Every current call path builds branch_pattern via branch_policy.compile_template(), which validates the pattern compiles, so this isn't reachable today. But WorkOrder is a public frozen dataclass constructible directly (as tests already do, e.g. test_branch_policy.py:220-221), so a caller that passes an arbitrary branch_pattern string would get an unhandled re.error instead of the RemoteError the rest of validation consistently raises. Wrapping the re.fullmatch call in a try/except would make the invariant self-enforcing rather than relying on every caller going through compile_template.

Catch re.error from a malformed branch_pattern and raise RemoteError instead
if self.branch_pattern:
    try:
        matched = re.fullmatch(self.branch_pattern, self.branch)
    except re.error as exc:
        raise RemoteError(f"branch_policy_mismatch: invalid branch_pattern: {exc}") from None
    if not _branch(self.branch_example) or matched is None:
        raise RemoteError(
            f"branch_policy_mismatch: branch must match {self.branch_pattern} "
            f"(for example {self.branch_example})")
🤖 Prompt for agents
Code Review: Enforces repository branch-naming policy before builder PR creation, with exact-head provenance validation and fail-closed cross-lane handoff. Two minor suggestions: `compile_template()` could validate templates with empty slugs to catch branches starting with separators at config time rather than delivery time, and `WorkOrder.__post_init__` could wrap `re.fullmatch()` in error handling to make regex validation self-enforcing for direct construction.

1. 💡 Edge Case: Leading {slug} in a branch_template silently breaks on empty slugs
   Files: src/code_mower/branch_policy.py:86-100, src/code_mower/branch_policy.py:39, src/code_mower/branch_policy.py:125-129

   `compile_template()` always renders its validation example with `EXAMPLE_VALUES["slug"]` non-empty (branch_policy.py:100, 28-35), so a template like `{slug}-{issue_key}` or `{slug}/{issue_key}` (with no character before `{slug}`) passes config validation. `_OPTIONAL_SLUG_RE` (branch_policy.py:39) only strips a *preceding* separator, so at runtime, when a real issue title slugifies to empty (e.g. an emoji-only title), `render()` produces a branch starting with `-` (e.g. `-ABC-1`), which `is_valid_ref` then rejects, failing the whole delivery for that issue. It fails closed, but the admin's config was accepted as valid and the failure only surfaces later on specific issue titles. Consider having `compile_template` also render once with an empty slug to catch this class of template at config-validation time, or document that `{slug}` must not be the first token.

   Fix (Validate the template with both an empty and non-empty slug during compile_template):
   pattern = _pattern(template)
   example = render(template, EXAMPLE_VALUES)
   if not is_valid_ref(example) or re.fullmatch(pattern, example) is None:
       raise BranchPolicyError(
           f"branch_template {template!r} does not render to a valid git branch name"
       )
   empty_slug_values = {**EXAMPLE_VALUES, "slug": ""}
   empty_slug_example = render(template, empty_slug_values)
   if not is_valid_ref(empty_slug_example):
       raise BranchPolicyError(
           f"branch_template {template!r} does not render to a valid git branch name when "
           "the slug is empty; {slug} must be preceded by -, _, /, or . in the template"
       )

2. 💡 Quality: WorkOrder direct construction can bypass safe branch_pattern validation
   Files: src/code_mower/devin_work_orders.py:188-193

   `WorkOrder.__post_init__` (devin_work_orders.py:188-193) calls `re.fullmatch(self.branch_pattern, self.branch)` directly on `self.branch_pattern` without catching `re.error`. Every current call path builds `branch_pattern` via `branch_policy.compile_template()`, which validates the pattern compiles, so this isn't reachable today. But `WorkOrder` is a public frozen dataclass constructible directly (as tests already do, e.g. test_branch_policy.py:220-221), so a caller that passes an arbitrary `branch_pattern` string would get an unhandled `re.error` instead of the `RemoteError` the rest of validation consistently raises. Wrapping the `re.fullmatch` call in a try/except would make the invariant self-enforcing rather than relying on every caller going through `compile_template`.

   Fix (Catch re.error from a malformed branch_pattern and raise RemoteError instead):
   if self.branch_pattern:
       try:
           matched = re.fullmatch(self.branch_pattern, self.branch)
       except re.error as exc:
           raise RemoteError(f"branch_policy_mismatch: invalid branch_pattern: {exc}") from None
       if not _branch(self.branch_example) or matched is None:
           raise RemoteError(
               f"branch_policy_mismatch: branch must match {self.branch_pattern} "
               f"(for example {self.branch_example})")

Options

Display: compact → Counting what did not apply, without listing it.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@jeffhuber

Copy link
Copy Markdown
Contributor Author

Codex audit (merge-authority lane)

Head SHA: 59dcbdf9a67217c8ce0bc2280b66b871baf7a071
Findings: P0=0, P1=0, P2=3, P3=0 (blocker policy: any P0/P1/P2 → BLOCKED)

Codex Audit: BLOCKED

Summary:

Three P2 findings concern existing-delivery discovery, closing-issue repository matching, and push authority retained across runs.

Findings:

  • [P2] Discover existing deliveries without requiring a body mention -- /private/var/folders/f2/g9zqfdjx7z3ckcsx_f46htwm0000gn/T/codex-audit-1lfueoes/wt/src/code_mower/templates/lanes/run_mac_lane.sh:640
    Finding ID: codex:97c2f7eda47641f68fae
    An existing PR linked through GitHub's Development sidebar can close the issue without containing #<number> in its body, so this search excludes it before checking closingIssuesReferences. If the issue title has changed, the runner then resolves a new branch and permits a duplicate delivery instead of reusing the existing PR. Query the issue's linked PRs directly, or otherwise enumerate and paginate all relevant candidates before treating the result as absent. Apply the correction to both runner templates and the checked-in runner.
  • [P2] Match the closing issue's repository as well as its number -- /private/var/folders/f2/g9zqfdjx7z3ckcsx_f46htwm0000gn/T/codex-audit-1lfueoes/wt/src/code_mower/templates/lanes/run_mac_lane.sh:647
    Finding ID: codex:5a71b62f9fe145666dd3
    A PR in the target repository can close an issue in another repository. If that issue has the same number and the PR also matches the body search, this predicate treats it as the current work item's delivery; the subsequent ownership checks only validate the PR's head repository, not the linked issue's repository. The runner can consequently select and authorize an unrelated PR branch. Match the closing reference's repository or full issue URL alongside its number in all runner copies.
  • [P2] Scope the policy-branch push ledger to the current run -- /private/var/folders/f2/g9zqfdjx7z3ckcsx_f46htwm0000gn/T/codex-audit-1lfueoes/wt/src/code_mower/templates/lanes/run_mac_lane.sh:466
    Finding ID: codex:4e93e6a3556a0b75dca1
    The reused workspace retains .git/code-mower-lane-guard-pushed, and installing a new guard never clears it. This new policy-branch check therefore accepts heads recorded by previous runs as though the current run had written them. If the remote moves back to a historical recorded head after inspection, the intended concurrent-change refusal is skipped. Reset the ledger when installing each run's guard, or bind its entries to a unique run identifier, across all runner copies.

@jeffhuber

Copy link
Copy Markdown
Contributor Author

Codex audit (merge-authority lane)

Head SHA: b4522774d8a86f1d1d8b602fd56bd13f5b7b108f
Findings: P0=0, P1=0, P2=1, P3=0 (blocker policy: any P0/P1/P2 → BLOCKED)

Codex Audit: BLOCKED

Summary:

Issue selection introduces a repeated-enumeration performance regression for busy repositories. Tests could not run because PyYAML is missing.

Findings:

  • [P2] Reuse the open-PR listing during issue selection -- /private/var/folders/f2/g9zqfdjx7z3ckcsx_f46htwm0000gn/T/codex-audit-zlfn3yui/wt/src/code_mower/templates/lanes/run_mac_lane.sh:311
    Finding ID: codex:a32abea686c230cc81d2
    The selection loop downloads the repository's entire open-PR listing for every candidate. With 100 candidates and 1,000 open PRs, one idle runner invocation can fetch 100,000 PR records across roughly 1,000 paginated requests, substantially increasing selection latency and GitHub API consumption compared with the previous issue-filtered lookup. Fetch one complete listing per selection pass and match candidates locally; refresh it at later ownership and delivery checkpoints. Apply the change to both template copies and the checked-in runner.

@jeffhuber

Copy link
Copy Markdown
Contributor Author

Codex audit (merge-authority lane)

Head SHA: 28127188237097a8dfe1009e1d246ce6bcc47b24
Findings: P0=0, P1=0, P2=2, P3=0 (blocker policy: any P0/P1/P2 → BLOCKED)

Codex Audit: BLOCKED

Summary:

Policy enforcement is incomplete during accepted recovery handoffs, and two existing regression tests retain incompatible GitHub fixtures. These issues should be corrected before merging.

Findings:

  • [P2] Retain repository-policy restrictions during recovery handoffs -- /private/var/folders/f2/g9zqfdjx7z3ckcsx_f46htwm0000gn/T/codex-audit-_ushp6_0/wt/src/code_mower/templates/lanes/run_mac_lane.sh:823
    Finding ID: codex:8dceb4ebd9de6b1d8cae
    When a policy-conforming, provider-prefixed branch receives a valid recovery handoff, this condition skips policy guard setup because HANDOFF_SOURCE_LANE is populated. Consequently, resolved_branch remains empty and install_pre_push_guard retains the destination lane's unrestricted prefix allowance, including names outside the repository policy. Withhold those prefix allowances for policy-bound handoffs while preserving the handoff's exact-branch and pinned-head checks. Apply the correction to both template copies and the checked-in runner.
  • [P2] Update the remaining GitHub fixtures for complete enumeration -- /private/var/folders/f2/g9zqfdjx7z3ckcsx_f46htwm0000gn/T/codex-audit-_ushp6_0/wt/tests/test_init_build_loop.py:1183
    Finding ID: codex:3d0ac8083f3ac8895258
    The fake GitHub commands in test_mac_lane_runner_extra_flags_unset_or_empty_reach_provider and test_mac_lane_runner_mention_only_pr_is_not_a_delivery still recognize only --limit 30, whereas the runner now requests --limit 1001 without --search. Both fixtures therefore reject issue-selection enumeration, making these existing tests fail before reaching the provider. Update their command matching; the successful-delivery fixture also needs a repository-qualified closing reference for the new closes_issue predicate.

@jeffhuber

Copy link
Copy Markdown
Contributor Author

Codex audit (merge-authority lane)

Head SHA: 7c36233c03ad4c7f1b8e78889ff9e93a184f394e
Findings: P0=0, P1=0, P2=0, P3=0 (blocker policy: any P0/P1/P2 → BLOCKED)

Codex Audit: PASS

Summary:

No actionable regressions were identified in the diff or affected delivery paths. Assessment was based on static review; tests were not run in the read-only environment.

Findings: none.

@jeffhuber

Copy link
Copy Markdown
Contributor Author

Claude audit (merge-authority lane)

Head SHA: 7c36233c03ad4c7f1b8e78889ff9e93a184f394e
Findings: P0=0, P1=0, P2=0, P3=1 (blocker policy: any P0/P1/P2 -> BLOCKED)

Claude Audit: PASS

Summary:

Large, carefully engineered PR adding an optional per-repository branch-name policy layer for builder delivery (Python resolver in branch_policy.py, config validation, Devin work-order integration, and a parallel jq/bash re-implementation embedded in the generated/checked-in run_mac_lane.sh runner). Reviewed the resolver/validator, the config schema additions, the WorkOrder binding changes (including the new required config/branch_policy argument), and the pre-push guard's single-writer/provenance logic (label+author conflict detection, existing-branch head pinning, ledger symlink hardening, TOCTOU handling via git's pre-push remote-sha reporting). The design is sound, fails closed in ambiguous/foreign-ownership cases, and is backed by extensive matching test coverage (958-line test_branch_policy.py) including bash-vs-Python parity checks. No P0/P1/P2 correctness, security, or data-loss issues found.

Findings:

  • [P3] compile_template's self-check never exercises the empty-slug rendering path -- src/code_mower/branch_policy.py:100
    Finding ID: claude:508018daf902c597856f
    compile_template() validates a template only by rendering it with EXAMPLE_VALUES, where slug is always non-empty ("short-description"). The optional-slug removal logic (OPTIONAL_SLUG_RE) only strips {slug} together with an immediately preceding '-','','/','.' separator; for a template where {slug} is not directly preceded by one of those characters (e.g. "{issue_key}{slug}"), that template still passes compile_template's self-check (since the non-empty example matches), but at runtime resolve_branch() will raise BranchPolicyError whenever a real work item slugifies to an empty string (e.g. an issue title with no ASCII alphanumeric characters), because the compiled pattern still requires the slug's character class to match at least one character. This is a narrow, fail-closed edge case (denies dispatch for that one item rather than corrupting anything) triggered only by an unusual admin-authored template plus an unusual issue title, so it is not merge-blocking, but the self-check could be strengthened by also rendering/validating with an empty slug.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Builder delivery: honor target repository branch-name policy before PR creation

1 participant