ADR-145: PR title format with ticket ID prefix
Status: Proposed Date: 2026-05-05
Context
On 2026-05-05 the fleet merged 18 PRs against this and sister repos. Every PR cited its driving Warp ticket in the body (Closes warp#NNN), and gyrum-complete-pr correctly fired the auto-tally close-verb extractor (per warp#1055) so the tickets closed on merge. But the title-level surface — gh pr list, the GitHub web UI's notification feed, the email/Slack PR-review notification, the git log --oneline history after squashing, and the repo's release-page commit roll-up — never sees the ticket. An operator scanning the merge stream sees fourteen feat: ... and four fix: ... lines and has to click through each to recover which warp item drove the change.
This is a load-bearing readability problem because the ticket — not the PR — is the primary key the rest of the fleet's surfaces (Warp board, fleet-velocity event log, agent-cost ledger from warp#1357, per-project changelog from warp#1359) keep their state against. When the title is ticket-blind, every downstream surface that reads PR titles has to either re-derive the ticket from the body (the auto-tally extractor — the close-verb-gated parser at ~/.gyrum/devtools/test/auto-tally-pr-title.test.sh per warp#1055 contract — already does this on merge, but every additional reader paying that cost again is duplicate work) or settle for a ticket-blind view (and lose the cross-surface join).
This ADR is sub-ticket warp#1361 of EPIC warp#1358. It locks the contract — title format, regex, edge cases, enforcement schedule — so Phase-1 children warp#1362 / warp#1363 (gyrum-start-work emit; gyrum-review-pr / gyrum-complete-pr gate) can implement against a stable shape. Sister to warp#1330 (close-verb distinction) and warp#1325 (review-pr body splice) — same family of narrowing the ticket↔PR linkage discipline.
Decision
PR titles use the format [ID:warp#NNN]: <type>: <description>, where <type> is a conventional-commits type and the segment after the ]: prefix is exactly conventional-commits.
Format spec
The canonical regex (copy-paste-stable for both gyrum-start-work emission and gyrum-review-pr / gyrum-complete-pr validation):
^\[ID:warp#\d+(?:,\s*warp#\d+)*\]:\s+(feat|fix|chore|docs|refactor|test|perf|build|ci|style)!?:\s+.+$
The !? after the type allows the conventional-commits breaking-change marker (feat!:, fix!:) so title-format gating does not collide with the release-tooling convention for major-version-bumping changes.
Examples
- Single ticket:
[ID:warp#1361]: feat: ADR for PR title ticket-ID prefix - Multi-ticket (a single PR closes more than one ticket — common for atomic ADR-cluster landings):
[ID:warp#1352, warp#1353]: feat: dual-extractor close-verb gate
- Breaking change:
[ID:warp#1400]: feat!: drop legacy auto-tally bare-mention extractor(illustrative; warp#1400 is a placeholder ticket number for the example) - Bug fix:
[ID:warp#1330]: fix: auto-tally close-verb gate excludes parenthesised refs
No-ticket exempt path
PRs created with gyrum-start-work --no-ticket <name> (the explicitly-trivial-work escape hatch — typo fix, comment tweak; per the global preference rule on warp-ticket-first) emit plain conventional-commits titles with no ID prefix:
chore: typo in CLAUDE.md
docs: fix broken link in ADR-110
The validation regex matches either the prefix-form or the plain-form, so --no-ticket PRs pass without a special case in callers. The validator distinguishes by inspecting the --no-ticket audit log entry: if the PR was started with that flag, plain-form is required (a prefix-form title on a --no-ticket PR is a structural mismatch and the audit gate flags it); otherwise prefix-form is required.
Conventional-commits tooling compatibility
The segment after [ID:warp#NNN]: is exactly conventional-commits. Existing tooling that keys on ^(feat|fix|chore|...): continues to work after a one-line strip-prefix step:
title_for_cc_tools = re.sub(r"^\[ID:warp#\d+(?:,\s*warp#\d+)*\]:\s+", "", title)
This means release-please, semantic-release, GitHub's release-notes auto-categoriser, and any pull_request_target workflow that reads github.event.pull_request.title and matches the conventional-commits regex can be adapted by a single regex substitution at the point of consumption — no replacement of the underlying tool, no fork.
Title-generation responsibility
gyrum-start-work <name> --ticket warp#NNN emits the draft-PR title as [ID:warp#NNN]: feat: <name> at draft creation time. The current emission (gh pr create --draft --title "feat: $name" at start-work.sh:200) is updated to prepend the prefix when --ticket is supplied. With --no-ticket, emission is unchanged (plain feat: $name).
When --ticket is supplied with a comma-separated list (already supported by warp-claim semantics), the emitted prefix preserves the order: [ID:warp#1352, warp#1353]: feat: <name>.
Title-validation responsibility
Two checkpoints, both already structured for it:
gyrum-review-prruns the title-format regex check during the structural-checks loop, alongside the existing commit-message-format warn (review-pr.sh:699-707). A failing title is a structural-fail (the PR is not review-ready until it matches).gyrum-complete-prruns the same regex check as a final-line gate before merge. Defence-in-depth: review-pr can be skipped (with audited reason); complete-pr is the merge boundary and the regex MUST pass there.
Soft-warn period
For the first 7 calendar days after this ADR merges, the gyrum-review-pr title check is advisory-only — it prints ⚠ title format and the suggested fix but does not return non-zero. After day 7, it promotes to hard-gate (returns non-zero, structural-fail). gyrum-complete-pr follows the same schedule — advisory for 7 days, hard-gate after.
The 7-day soft-warn lets the fleet adapt without spiking merge-pain: on day 1 every in-flight PR is non-conformant, and forcing them to retitle in lockstep with the ADR landing is gratuitous churn. By day 7 every fresh PR has the format, so the hard-gate only rejects regressions.
Forward-only enforcement
No retroactive backfill of historical PR titles. The decision applies to PRs opened after this ADR merges. PRs opened before but merged after are exempt — the ADR's contract is the title-at-creation-time, not the title-at-merge-time. Backfill would churn every closed-PR URL's display title, break every link that quotes the old title, and produce zero downstream benefit (the auto-tally extractor already pulled the ticket from the body at merge time; rewriting the title now changes nothing about the historical record).
Tooling-integrations inventory (lockstep update path)
Every consumer of PR titles inside the fleet that keys on a regex shape is enumerated below, with the lockstep update path. Implementing children (warp#1362, warp#1363) update each in turn.
| Consumer | File / location | What it does | Lockstep update |
|---|---|---|---|
gyrum-start-work (title emission) |
~/.gyrum/devtools/start-work.sh:200 |
Emits draft-PR title as feat: $name |
Prepend [ID:warp#NNN]: when --ticket is supplied; unchanged on --no-ticket. |
gyrum-review-pr (commit-msg warn) |
~/.gyrum/devtools/review-pr.sh:699-707 |
Warns when commit titles miss conventional-commits prefix | Add a sibling check on PR title (not commits) using the new regex; soft-warn for 7 days, hard-gate after. |
gyrum-complete-pr (final-line gate) |
~/.gyrum/devtools/complete-pr.sh |
Pre-merge checks | Add title-format regex check; soft-warn for 7 days, hard-gate after. |
auto-tally PR-title extractor |
~/.gyrum/devtools/test/auto-tally-pr-title.test.sh (warp#1055 contract) |
Extracts close-verb-gated Closes warp#NNN from PR titles |
Unchanged. The new format is [ID:warp#NNN]: ... — no Closes/Fixes/Resolves verb, so the warp#1055 close-verb gate correctly does not extract from the prefix. The body still carries the close-verb. The extractor's verb-gate stays the source of truth for closes; the title prefix is for display. |
release-please / semantic-release (downstream, when adopted) |
not yet used in dark-factory; will be adopted per the per-project changelog initiative (warp#1359) | Reads PR title to categorise release notes | One-line strip-prefix (re.sub(r"^\[ID:warp#\d+(?:,\s*warp#\d+)*\]:\s+", "", title)) at the point of consumption; tools work unchanged after. |
| GitHub Actions title-matchers | .github/workflows/*.yml keying on github.event.pull_request.title |
Project-specific gates (e.g. validate-projects.yml) |
None currently match on the conventional-commits prefix in this repo. Future workflows that want to gate on type adopt the strip-prefix-first pattern. |
gh pr list / GitHub web UI |
external | Display | Unchanged consumer; this is the primary beneficiary — the ticket is now visible. |
Consequences
Easier:
- An operator scanning
gh pr listor the GitHub PR feed sees the ticket without click-through. - Cross-surface joins (PR ↔ ticket ↔ changelog entry ↔ velocity event) become title-level, not body-level.
- The ticket is the primary key in every surface that reads PR titles, including future agents that resume work from
gh pr listoutput. - Multi-ticket PRs (atomic ADR-cluster landings, dual-fix PRs) make the multi-ticket linkage visible without parsing the body.
Harder:
- One-week soft-warn window operationally — the validator is in two states (advise, gate) for a finite period; the rollover requires a calendar-driven change in
review-pr.shandcomplete-pr.sh. The soft-warn period is itself logged as a Warp item so the rollover is not forgotten. gyrum-start-workis now branchless on the--ticketflag — when neither--ticketnor--no-ticketis passed, the global rule (warp-ticket-first) already requires the ticket; the implementing child clarifies the error message to point at the new title-format requirement.- Conventional-commits tooling needs the strip-prefix step. For tools the fleet doesn't yet run (
release-please,semantic-release), this is a docs note in the per-project changelog ADR (warp#1359). For tools the fleet does run (none currently key on conventional-commits prefix in PR titles), zero immediate change.
Signed up to operate:
- The 7-day soft-warn rollover (a one-line change in two scripts at the calendar deadline).
- Periodic audit that the format is being honoured —
gh pr list --search "is:pr created:>YYYY-MM-DD"filtered through the regex; if drift exceeds a threshold, the gate is failing closed somewhere and warrants investigation. - Forward-only is irrevocable. If a future operator asks "why does PR #423 have a plain title and PR #621 has the prefix?", the answer is "this ADR; backfill was deemed lower-value than display-stability".
Alternatives considered
Body-only ticket reference (status quo). The auto-tally extractor closes the ticket on merge from the body, so tickets do close correctly. But the display surface (PR list, notifications, web UI) is ticket-blind, which is the cost this ADR is paid to remove. Status quo lost because every downstream surface paying its own body-extraction cost is duplicate work the title-prefix collapses.
Branch-name encoding (e.g.
warp-1361/feat/adr-pr-title-format). Branch names are visible ingh pr listalready, so embedding the ticket there would surface it without a title change. Lost on two grounds: (1) branch names are operator-readable, not machine-stable — slashes have side-effects with some Git tooling, and the warp-NNN segment varies in punctuation across implementations; (2) once a PR is merged, the branch is deleted — the ticket evaporates from the merged-PR view, defeating the purpose. The title persists past merge.Bare
warp#NNNmention in title (no ID-prefix structure). A title likefeat: ADR for PR title prefix (warp#1361)already gets the ticket into the title surface. Lost because it collides with the warp#1055 close-verb gate (auto-tally would either close on bare mentions, regressing the warp#1055 fix, or never close, regressing the ticket-on-merge contract); and it doesn't survive squash-merge title shortening as cleanly as a leading ID prefix does.Hard-gate from day one (no soft-warn period). Simpler — no calendar rollover to track. Lost because every in-flight PR on the day of merge would be non-conformant, and forcing the fleet into a same-day retitle of all open PRs is operator-pain for zero structural benefit. The 7-day window matches the rollover shape used in similar gate-introduction ADRs.
Retroactive backfill of historical PR titles. Would let the prefix be a uniform property of every PR in the repo. Lost because the auto-tally extractor already drained the ticket information from historical bodies; rewriting closed-PR titles changes display but not behaviour, breaks every external link that quotes the old title, and burns a non-trivial GitHub API rate budget for cosmetic uniformity.
Supersedes: none Superseded by: (leave blank until a later ADR reverses this one)
Refs:
- warp#1358 (parent EPIC: per-project changelog + user-visible /changes page)
- warp#1330 (sister: auto-tally close-verb distinction)
- warp#1325 (sister: review-pr body splice)
- warp#1342 (sister: review-pr READY-TO-MERGE directive)
- warp#1055 (auto-tally PR-title close-verb gate — preserved unchanged by this ADR)
- ADR-110 (observe-and-file: option for soft-warn period observer enforcement)