The branch was cut as a HOTFIX. By the time it merged, JIRA said staging task. Nobody had touched the type field on purpose. It just drifted, and it sat that way for two weeks before anyone noticed, because the only thing that ever reads a branch name against its ticket is a human glancing at a PR title.

What actually happened

This is one ticket, and the finding came out of the same afternoon my co-founder was reviewing an image manager rebuild, so it wasn’t a dedicated audit. It was a side effect of me looking at branch state for something else. The branch’s ticket had been reclassified from HOTFIX to a staging task, and the branch itself never got renamed or re-cut to match. Git doesn’t know what a HOTFIX is. JIRA doesn’t watch git. The two records just quietly stopped agreeing, and every intermediate commit, every review comment, every automated post to the team feed treated the mismatch as normal because there was no check that would have flagged it as not normal.

The cost isn’t hypothetical. A HOTFIX branch usually implies a different review bar, a different deploy path, sometimes a different base. If the ticket type says one thing and the branch is actually doing another, you can ship code through the wrong gate and not know it until someone reconciles the two by hand, which is exactly what happened here, two weeks after the fact, as a side finding rather than a caught error.

Why a broad guard was rejected first

The instinct is to build something big: a background job that continuously reconciles every open branch against JIRA state, flags drift, maybe auto-corrects labels. That was on the table implicitly, because we’d already been through this exact debate that same week on a related problem. that ticket was “flag pending change-scripts,” and the review chain on it is the tell: codex’s first answer was to keep an append-only event ledger with deploy-time inputs and a write path fix. Manus’s answer was similar in shape: build a redesigned ledger, immutable events, a maintained current-state projection, a consistency check. Both were the “build infrastructure to track state over time” answer.

Then we actually looked at what the twenty-minute triage produced. Codex’s own follow-up reversed it: “A. My earlier ledger recommendation was wrong given the twenty-minute result. Ship verification only. The database state is the record of fact.” A one-line state query settled what the ledger was being built to infer. Manus concurred on the same scope cut minutes later. That ticket shipped as a direct database check against the live change-script folders (now covering all 18 automatically) instead of a hand-typed index that had been wrong in both directions since May.

That reversal is the reason the branch/ticket guard didn’t get built as a monitor either. A branch only needs to be right at one moment: the moment it’s cut. After that, if someone changes the ticket type mid-flight, that’s a real decision someone should look at and re-title the branch for, not something a background daemon should silently reconcile or repeatedly re-flag on every commit.

What shipped

A hook that runs the moment a branch naming a ticket is cut, checking:

  • the branch’s naming shape matches the ticket’s type
  • the type hasn’t been quietly moved off HOTFIX
  • status is in CODING
  • priority is Working

It warns and prints the fix. It does not block. That last part matters: a false positive on a naming convention shouldn’t be able to stop someone from shipping a real hotfix under time pressure. The cost of a missed warning is a two-week-later discovery, annoying but recoverable. The cost of a false block during an actual incident is worse.

It lives in the repo, not in a per-developer installer, which was a specific choice given how we work: ten clones of one large legacy codebase (site through site10), each a separate working copy a different session might be driving. A guard that lives in someone’s global git hooks directory only fires for the one person who ran the install script. A guard committed to the repo fires identically no matter which clone or which developer’s machine cut the branch, which is the same reasoning already applied elsewhere that week: the per-clone branch audit built the same day reconciles every clone against live JIRA and reports only what disagrees, rather than trusting any single clone’s local state as ground truth.

The pattern underneath both fixes

Both the change-scripts ticket and the branch guard follow the same shape: something was being kept as a hand-maintained side record (a typed-out change-script index, a branch name nobody re-checks against its ticket), and the record drifted silently because nothing compared it to the source of truth at the point where drift is cheap to catch. The fix in both cases wasn’t more infrastructure to track the drift after it happens. It was a check at the one moment the two things are supposed to agree, backed by whichever system actually holds the fact: the database for change-scripts, JIRA for ticket state. Ledgers and monitors get proposed first because they feel thorough. The twenty-minute triage on that ticket is the data point worth keeping: before building a system to track a discrepancy over time, check whether a single point-in-time query would have caught it just as well.