ops-branching
The repo's branch model lives here and nowhere else. Before this capability existed, base-branch knowledge lived in four places — a config schema, a detection skill, the merge loop's own resolve-and-compare, and a forge operation that deferred back to the detection skill. They drifted. Collapsing them into one owner is the point of this skill.
Visibility: supporting primitive. A service (ops-integrate, ops-release,
ops-change) may call it. A framework loop must never call it directly — if a loop
holds a branch name, this capability has failed.
The privacy rule
The integration branches, the release base and the merge strategy never leave this
skill. There is no resolve-base, no merge-strategy, and no classify-pr. A caller
says "merge this PR" and reads back what happened; it does not ask "what is the base?" and
then compare.
Why it is worth the inconvenience: the moment a caller can read the base, two callers can disagree about it, and the drift is back. An outcome cannot drift.
Command-only. Every action below does something. This skill answers no questions.
Invocation
ops-branching <action> '<context-json>'
| Action | Context | Returns |
|---|---|---|
merge |
{ pr } |
{ ok, merged, merge_commit, refused, line } |
open-pr |
{ branch, line, title, body } |
{ ok, pr_number, url } |
start-branch |
{ line, slug } |
{ ok, branch } |
An absent context is {}. Reject any other action — do not guess. All three are
idempotent: see each action.
All GitHub work goes through github-ops by operation name. This skill decides what
and onto what; github-ops owns how.
What it knows, privately
Resolve these once per invocation, and never return them:
The live lines — from
ops-repo-meta · lines. Read it every time; never cache it. A major-version cutover adds a line, and no engine change may be needed for that.The branch model — one of
gitflow,main-only,versioned-gitflow,custom. Detect it from the branches when the repo has not declared it:Branches present Model Reference a vN/devand avN/mainversioned-gitflowversioned-gitflow.mda devand amaingitflowgitflow.mdonly main, nodevmain-onlymain-only.mdanything else ask — never invent a devbranch— The integration branches — a SET, not a branch. One entry per live line:
vN/devunder versioned-gitflow,devunder gitflow,mainunder main-only. Every base check is set membership. A repo with v13, v17 and v18 live has three legitimate bases, and equality against one of them would flag the other two as wrong-base.The release bases —
vN/mainper live line under versioned-gitflow,mainunder gitflow, none under main-only (releases land on the integration branch itself).The merge strategy —
squashby default;merge-commitwhere the model calls for it (a release branch into a release base preserves history). The caller never passes it.
For a custom model this skill decides nothing: the repo must ship its own
ops-branching, and the presence of a custom model with no override is a configuration
error worth reporting rather than guessing around.
Action: merge
Merge the PR in front of you, using this repo's strategy.
- Read the PR (
github-ops→ Get a PR): base, mergeable state, head branch. - Check the base is one of the integration branches — membership, not equality. It is
not this skill's job to decide whether the PR should land (that is
ops-integrate's gates); it is this skill's job to refuse to merge into a branch the model says is not a merge target. Refuse rather than merging somewhere plausible. - Merge with the resolved strategy and delete the head branch (
github-ops→ Merge a PR (+ delete branch)).
A refusal is classified, not narrated. Return { ok: false, refused: … } with one of:
refused |
The base is |
|---|---|
release-base |
this line's release base. The release path owns it, and this is a normal outcome. |
not-a-merge-target |
neither a release base nor an integration branch of any live line. |
Set refused: null when the merge went ahead. This is the one thing about the model that
does leave the skill, and it is deliberate: the caller has to tell a release PR apart from a
mistargeted one to report the right outcome, and making it read that out of a prose detail
puts a string-match where a decision belongs. A classification is not a branch name — the
caller still learns nothing about which branch, or how many there are. Add a plain-language
detail alongside it for the human-facing comment.
Also return line: which live line the PR landed on. A line is declared public data —
ops-repo-meta · lines hands the whole set to anyone who asks — so returning it breaks no
privacy rule. The branch it maps to stays private, and that mapping lives only here, which
makes this the only place the question can be answered. It matters because everything that
happens after a landing needs it: which issue to close, and which lines to port to. Without it
the caller has to read a line out of the base ref, which is the guess this whole capability
exists to remove.
Idempotent: a PR that is already merged MUST return { ok: true, merged: true }
with the existing merge_commit, not attempt a second merge. The landing label stays on a
PR after it lands, so a sweeping caller will hand you the same PR twice.
Never force-merge, never use GitHub's native auto-merge (it would land without the
caller's gates), never merge into a release base here — a release lands through
ops-release.
Action: open-pr
Open a PR from a work branch onto the right base for its line.
- Resolve the integration branch for that line — the caller names the line (
v17), not the branch (v17/dev). A caller that knows the branch already has the leak this skill exists to prevent. - Open it (
github-ops→ Create a PR) with the given title and body.
Idempotent: an open PR from that head branch onto that base MUST be returned as-is, not duplicated.
Action: start-branch
Create a work branch, named this repo's way, rooted on the right base.
- Resolve the integration branch for the named line.
- Name the branch to the repo's convention — by default
<type>/<slug>with type fromfeature/fix/chore/docs/refactor/test. - Create it from that base (
github-ops→ Create a branch).
Idempotent: if the branch exists, return it.
Rules
- Never return a branch name or a strategy except the head branch a caller just asked
you to create (
start-branch) — that one is the caller's own handle on its work, not knowledge of the model. - Never commit directly to an integration branch or a release base. Always a branch, always a PR.
- Base checks are membership over the live-line set, re-read every invocation.
- Never force-push.
- A red PR is not this skill's problem. It merges what it is told to merge, once the
base is legitimate. Gating is
ops-integrate's job, and duplicating it here would put merge policy back in two places. custommodel with no repo override is an error, not an improvisation.