🔒 Protected file. External PRs that change this file will be auto-closed. Open an issue to request a change — see
AI-INSTRUCTIONS-POLICY.md.
Last updated: 2026-04-20 (post-v1.5.6 / develop-realignment pass). Branching model reset: release/* is now the only path into main; hotfixes route through develop instead of branching off main; no more back-merges. New hard rule (learned the hard way via PR #49): main-facing PRs merge via GitHub's "Rebase and merge" button only — never "Squash and merge". See Merge method below.
Everything else about how to contribute (commit style, pre-merge checks, PR shape, non-negotiables, git-lock rule) lives where it already lived — .github/CONTRIBUTING.md, .github/PULL_REQUEST_TEMPLATE.md, CLAUDE.md, AI-GIT-PROTOCOL.md. This file only documents what's new.
Normal work:
feature/* ─► develop ─► release/vX.Y.Z ─► main ─► tag + CWS zip
│
└─► preserved forever (legacy testing)
Hotfixes:
hotfix/* ─► develop ─► release/vX.Y.(Z+1) ─► main ─► tag + CWS zip
Never:
hotfix/* ─► main (no direct-to-main PRs)
main ─► develop (no back-merge)
release/* branches are never deleted — every shipped version keeps its branch forever so legacy testing can check out any tag's exact build context. main and release/* share commit SHAs for release-sourced work (rebase/ff merge, never squash).
main= production + occasionally intermediate versions that never shipped to CWS (Seth iterates fast; a newer version sometimes lands before the previous one goes out) + historical record of every finalized version.- All
mainmerges come fromrelease/vX.Y.Z.release/*is cut fromdevelop, then rebased or fast-forwarded ontomainsomainanddevelopstay commit-identical for release-sourced commits. - Feature / bugfix / research work = cut from
develop, PR back todevelop, merged (squash), branch deleted. Seth doesn't review develop-facing PRs. - Only persistent branches are
develop,main, andrelease/*. Every release'srelease/*branch is preserved forever. Feature branches (includinghotfix/*) are deleted on merge.
The GitHub UI offers three merge buttons: Merge, Squash and merge, and Rebase and merge. The choice matters more than it looks like it does, because rule #2 above depends on main, develop, and release/* sharing commit SHAs for release-sourced commits.
- Main-facing PRs (
release/* → main, and the rare emergencyhotfix/* → mainif Seth ever greenlights one): "Rebase and merge" only. Fast-forward is also fine whenmainis a strict ancestor of the PR head (usually is). Never "Squash and merge" — squashing creates a fresh commit SHA onmainthat doesn't exist ondevelop, which breaks the SHA identity rule #2 is built on. Once the SHAs diverge, the nextrelease/*cut fromdevelopwill conflict againstmainat rebase time and you're stuck shipping viahotfix/*-with-delta (see Historical note — v1.5.5 squash accident below). - Develop-facing PRs (
feature/* → develop,hotfix/* → develop, daemon-drivenclaude/* → develop): squash is fine and encouraged. The local Linear daemon usesgh pr merge --squash --auto --delete-branchon every PR it opens; CI-green → auto-squash → branch deleted. Develop's history doesn't need SHA identity with anything else, so squash keeps the log readable without costing us anything.
Operational note for Seth (and future Seths): when merging a release/* PR from the GitHub UI, double-check that the dropdown on the merge button reads "Rebase and merge" before clicking. GitHub remembers the last choice per repo — if you've been squash-merging develop-facing PRs all day, the button can silently be left on "Squash and merge". A one-second glance before the click saves the cleanup.
If an accidental squash (or any other history-rewriting misstep) leaves main no longer a strict ancestor of develop, the fix is mechanical but requires --force-with-lease on develop. It happened once (v1.5.5 → v1.5.6, 2026-04-20). The recipe:
- Cut the current release as a
hotfix/*offmain(notrelease/*). Therelease/*branch for that version stays preserved per rule #4, but you skip using it for the ship. Reason:release/*was cut fromdevelop, which now diverges frommain, so rebasingrelease/*ontomainwill conflict. A freshhotfix/*offmaincarries only the delta you actually need to ship. - Ship the
hotfix/* → mainPR via "Rebase and merge" (per the merge-method rule above). Tagmain, upload CWS, done. - Realign
developon top of the newmain:git checkout develop git fetch origin # Identify the commits on develop that landed AFTER the last release's squash commit — # those are the ones you need to preserve. git log origin/main..origin/develop --oneline # what's "ahead" on develop that you want to keep git tag backup/develop-pre-realign-$(date -u +%Y%m%d-%H%M%S) # safety net before force-reset git reset --hard origin/main git cherry-pick <each post-release-squash commit from the log above, in order> git push --force-with-lease origin develop
- Verify the invariant is restored:
Must print
git merge-base --is-ancestor origin/main origin/develop && echo "OK" || echo "STILL BROKEN"
OK. If it doesn't, stop and diagnose before any new work lands. - Push the backup tag (
git push origin backup/develop-pre-realign-…) so the pre-realignment develop tip is recoverable from the remote if anyone needs to audit what got thrown away.
The backup tag from the 2026-04-20 realignment is backup/develop-pre-realign-20260420-230739 — still on the remote for reference.
2026-04-20 incident, documented for future readers (humans + Claudes) so nobody re-derives the reasoning cold:
- v1.5.5 (PR #49) was squash-merged into
mainby accident. The GitHub UI's merge dropdown was left on "Squash and merge" from an earlier develop-facing PR. The merge created a new single-commit SHA onmainfor v1.5.5 that did not match any commit ondevelop, quietly breaking rule #2's SHA-identity invariant. - v1.5.6 first attempt was cut as
release/v1.5.6fromdevelop(PR #58, closed). Rebasingrelease/v1.5.6ontomainconflicted because of the v1.5.5 squash mismatch — the commits that went into the v1.5.5 squash were still present individually ondevelop, and rebase couldn't reconcile the identity of "already merged" with them. - v1.5.6 actual ship was a
hotfix/v1.5.6cut directly offmain(PR #59). This was a one-time hotfix-delta exception to the rule-#2 shape: becausemainanddevelophad already diverged, the only way to ship without rewritingmainwas to carry the delta in a branch that was alwaysmain-rooted.release/v1.5.6is preserved on the remote per rule #4 even though it was never merged — future readers who grep the branch list for "why is there a release branch that never shipped" will land here. - After v1.5.6 merged, the realignment recipe above was executed:
developwas force-reset tomain's tip and the 8 post-v1.5.5 commits (the local-daemon stack: PRs #51–#57 plus one auto-commitwip:) were cherry-picked back on top. This restored rule #2's invariant (git merge-base --is-ancestor origin/main origin/developnow returns true). The pre-realignment tip is preserved on the remote asbackup/develop-pre-realign-20260420-230739.
The fix for the root cause — the squash itself — is the "Rebase and merge" only rule above. The fix for the symptom — a broken SHA-identity invariant — is the realignment recipe. Both are documented here so the next accident (if it happens) is a 10-minute cleanup instead of an afternoon.
| Branch | Cut from | Merged to | Merge method | Deleted after merge? | Notes |
|---|---|---|---|---|---|
develop |
— | — | — | No | Long-lived integration trunk. Claude self-merges here under the contract below. |
main |
— | — | — | No | Long-lived. Only release/* lands here. 1 approval required, linear history, signed commits. |
release/vX.Y.Z |
develop |
main |
"Rebase and merge" (UI button) or fast-forward. Squash is prohibited — see Merge method. | No — preserved forever | Holds the manifest bump + CHANGELOG entry. Shares SHAs with main and develop post-merge. |
feature/* (also feat/, fix/, chore/, docs/, refactor/, test/, ci/) |
develop |
develop |
squash | Yes | Claude can self-merge on green npm run check + self-merge contract below. |
hotfix/* |
develop |
develop |
squash | Yes | Ships to main via the next release/vX.Y.(Z+1). No direct-to-main PRs. |
- Gate check.
developgreen?npm run checkclean (typecheck + extension lint + director fixtures)? CI passing? No stranded in-flight hotfix that should ride with this release? - Sync local develop.
git checkout develop && git pull --ff-only. - Cut the release branch.
git checkout -b release/vX.Y.Z- Bump
"version"inextension/manifest.json. - Update
CHANGELOG.md: add a## [X.Y.Z] — YYYY-MM-DDheader, then grouped bullets by commit type (### Added/### Changed/### Fixed/### Docs/### Chore) summarizing thedevelopcommits since the previous release. Move matching entries out of## [Unreleased]. - Commit with prefix
release:— message body explains the scope of this version and anything operationally interesting about it. Trailer:Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>on Claude-authored commits.
- Push + open the
mainPR.git push -u origin release/vX.Y.Z. Then:gh pr create --base main --head release/vX.Y.Z --title "release: vX.Y.Z — <codename>" --body-file <CHANGELOG-delta.md>- Body = the CHANGELOG entry, grouped by type. Draft if anything is still canary; otherwise ready-to-merge.
- Merge method. Click "Rebase and merge" in the GitHub UI (preserves individual commit SHAs on
main), or fast-forward ifmainis a strict ancestor ofdevelop(usually is under this model). Never click "Squash and merge" for a main-facing PR — squashing breaks the SHA identity betweenmain,develop, andrelease/*that rule #2 depends on. Full reasoning + the realignment recipe for when this rule gets broken live in Merge method and Realignment recipe below. Seth merges; Claude cannot (branch protection +.claude/settings.jsondeny list). - Tag on
main.git tag -a vX.Y.Z -m "vX.Y.Z — <codename>" <main-sha>thengit push origin vX.Y.Z. Do NOT deleterelease/vX.Y.Z— it stays forever. Feature branches on the other hand get deleted on squash-merge. - CWS upload. Build + upload the
.zipperOPS.md. Or skip if a newer version is already ready — that's the "intermediate versioning" case from rule #1: the tag exists onmainfor the historical record, the CWS just skips to the newest version.
Seth's 2026-04-20 decision: hotfixes route through develop, not through main. Tradeoff: slower urgent-fix latency in exchange for perfectly linear history between main and develop. Worth it.
- Sync.
git checkout develop && git pull --ff-only. - Branch.
git checkout -b hotfix/<slug>. - Fix + test. Commit with the appropriate conventional-commit prefix (
fix:is typical). Same commit-message contract as any other Claude-authored work: why + alternatives considered + Co-Authored-By trailer. - PR →
develop.gh pr create --base develop --head hotfix/<slug>. Merge (squash) oncenpm run checkand CI are green. Squash-merge deletes the branch. - Ship via a new release. Cut
release/vX.Y.(Z+1)fromdevelopand follow the release workflow above. The hotfix reachesmainas part of that release PR. - If the fix is genuinely production-down and can't wait for a release cycle: flag it to Seth in-session. The documented flow routes through
develop. Do not improvise a direct-to-main hotfix PR without Seth's explicit override in the current conversation. Under the new model there's no expected direct-to-main path at all; if Seth greenlights one as an emergency, the PR slips pastprotect-main-branch.ymlvia the @Sethmr author carve-out — the workflow'sallowedHeadPatternsis[/^release\//]only (PR #48 dropped the legacyhotfix/*+develophead allowlist), so any non-release head branch targetingmainrelies on the Sethmr-author bypass. How the fix back-fills todevelopafterwards is Seth's call (the v1.5.6 realignment recipe above is the canonical pattern; the precedent landed successfully in this repo).
Seth's ask, 2026-04-19 verbatim: "I am fine with Claude self-merging into develop so long as everything it's doing is documented to the point it can explain why it did what it did in a fresh context window."
Before a Claude-authored PR self-merges into develop, all of the following must be true:
npm run checkpasses locally (typecheck + extension lint + director fixtures).- The commit body contains the why and the alternatives considered, not just the what. Per
CONTRIBUTING.md § Commit style. - The PR body fills every field of
PULL_REQUEST_TEMPLATE.md— especially the "How" section (why X over Y) and the Risk tier. - Any decision that isn't recoverable from the diff is linked to a session-note, memory file, issue, or design doc. Fresh-Claude reads those.
- Every Claude-authored commit carries the
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>trailer. The merge-checklist bot's skip logic depends on it — missing trailer means the bot spams every push. - If any of the above can't be met, stop self-merging — leave the PR open and pull Seth in.
If in doubt, treat the PR as if it targets main and needs Seth's eyes anyway.
Claude proactively drafts the release/vX.Y.Z → main PR as soon as develop crosses the threshold. Seth doesn't ask; the button should already be waiting.
- Any user-visible feature landing on
develop(afeat:commit that ships behavior to the extension or backend). - A bundle of
fix:commits that closes ≥1 known issue and stabilizes a flow. - A documentation pass that materially changes how a contributor or self-host operator works (new architecture doc, new install path, breaking change to the backend wire-spec).
- A coordinated chore set worth versioning (e.g. a release-pipeline change — workflow + docs + settings together).
If the next thing on develop is a single-line typo fix or an unverified canary commit, don't cut a release yet. Wait until there's something worth Seth's review attention.
.claude/settings.json deny-lists the destructive commands: direct push to main/master, force push, hard reset, branch/tag/release/repo deletions. Belt to branch protection's suspenders — fails fast inside the Claude Code session before GitHub ever sees the command.
Three workflows run the checks Seth used to do by hand:
.github/workflows/pr-checklist-comment.ymlposts a fresh comment on every PR open + every new commit, telling the author exactly what needs to be true to merge. Comment text links the canonical docs (this file, CONTRIBUTING.md, AI-GIT-PROTOCOL.md, the upgrade policy) instead of duplicating them. Skips Claude-authored PRs by default (detection: Co-Authored-By trailer on every commit; escape hatch<!-- bot-review -->in the PR body); flags Dependabot PRs with the framework-upgrade heads-up inline; addsmain-only gates (CODEOWNERS approval, CHANGELOG, tag plan) when the PR targetsmain..github/workflows/protect-main-branch.ymlauto-closes any PR targetingmainthat isn't a release-flow PR. Carve-outs: @Sethmr (release author) anddependabot[bot]. Head-branch allowlist is/^release\//— onlyrelease/*branches pass the pattern check.developandhotfix/*were removed from the allowlist in PR #48 under the new model (develop no longer merges direct to main; hotfixes route through develop). Emergency direct-to-main hotfix PRs rely on the @Sethmr author carve-out bypass..github/workflows/claude-triage.ymlfires fresh-Claude on Dependabot PRs and emits a single triage verdict comment — MERGE-NOW / QUEUE-AND-CLOSE / NEEDS-HUMAN — followingBOT-TRIAGE-RUBRIC.md. RequiresANTHROPIC_API_KEYrepo secret (GITHUB-MANUAL-STEPS.md § 16).
Branch protection rules live in the GitHub web UI. Click-path for both rules (and the new release/* carve-out) is in GITHUB-MANUAL-STEPS.md § 8.
Before 2026-04-20 this repo used a different shape:
- Old release flow: PR directly from
develop → main(no intermediaterelease/*branch). Merge-commit style. - Old hotfix flow: branch
hotfix/*offmain, PR back tomain, then back-mergemain → developas a follow-up PR. - Old diff pattern: "diff develop vs main, PR the delta to a
hotfix/*branch off main" — used briefly during the transition from the merge-commit era.
All three were retired once the CWS release cadence stabilized and the messy rebases from back-merges stopped being worth the latency savings. Anyone reading the git log for hotfixes dated before 2026-04-20 will see the older pattern — that's fine, it's history. Don't use it going forward.
Claude's auto-memory has been updated to match (feedback_backmerge_main_to_develop.md in Seth's personal memory, referenced in-session).