docs/concepts/integration-branches.md
Group epic work on a shared branch, land to main as a unit.
Integration branches provide end-to-end support for epic-scoped work across
the Gas Town pipeline. When you create an integration branch for an epic, it
becomes the automatic target for every stage: polecats spawn their worktrees
from the integration branch (so they start with sibling work already present),
the Refinery merges completed MRs into the integration branch instead of main,
and when all epic children are closed, the Refinery can land the integration
branch back to its base branch (main by default, or whatever was specified
with --base-branch at creation) as a single merge commit.
Landing can happen on command or automatically via patrol. The result is that an entire epic flows through the system as a coherent unit, from first sling to final land, without any manual branch targeting.
Create the epic and its children. Structure your work as an epic with child tasks (or sub-epics) underneath. Set up dependencies between children to define which can run in parallel and which must wait.
Create the integration branch. This is the shared branch where all child work accumulates.
gt mq integration create gt-auth-epic
Create a convoy to track the work. The convoy gives you a single dashboard for the entire epic's progress.
gt convoy create "Auth overhaul" gt-auth-tokens gt-auth-sessions gt-auth-middleware
Sling the first wave. Identify children with no blockers and sling them
to the rig. Use --no-convoy since the tracking convoy already exists.
gt sling gt-auth-tokens gastown --no-convoy
gt sling gt-auth-sessions gastown --no-convoy
Polecats process the work. Each polecat spawns its worktree from the integration branch, so it starts with any sibling work that has already landed there. When a polecat finishes, it submits a merge request.
Refinery merges to the integration branch. Instead of merging to main, the Refinery merges each MR into the integration branch and marks the child task as complete.
Track progress via the convoy. The convoy status updates each time the Refinery completes a task.
gt convoy status hq-cv-abc
Sling the next wave. When a wave completes and its dependent children unblock, sling the next batch. Those polecats will start from the integration branch — which now contains all the work from the preceding wave.
gt sling gt-auth-middleware gastown --no-convoy
Land when complete. When all children under the epic are closed, the
integration branch is ready to land. If integration_branch_auto_land is
enabled, the Refinery does this automatically during patrol. Otherwise,
land manually:
gt mq integration land gt-auth-epic
This merges the integration branch back to its base branch (main by default) as a single merge commit, deletes the branch, and closes the epic.
Without integration branches, epic work lands piecemeal:
Child A ──► MR ──► main (lands Tuesday)
Child B ──► MR ──► main (lands Wednesday, breaks A's work)
Child C ──► MR ──► main (lands Thursday, depends on A+B together)
Each child merges independently. If Child C depends on A and B being coherent together, you're relying on merge order and hoping nothing breaks between lands.
Integration branches batch epic work on a shared branch, then land atomically:
Epic: gt-auth-epic
│
┌─────────────┼─────────────┐
│ │ │
Child A Child B Child C
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ MR A │ │ MR B │ │ MR C │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└───────────┼───────────┘
▼
integration/gt-auth-epic
(shared branch)
│
▼ gt mq integration land
base branch
(main or --base-branch)
(single merge commit)
All child MRs merge into the integration branch first. Children can build on each other's work. When everything is ready, one command lands it all.
| Aspect | Without | With Integration Branch |
|---|---|---|
| MR target | main | integration/{epic} |
| Land timing | Each MR lands independently | All MRs land together |
| Cross-child deps | Risky—depends on merge order | Safe—children share a branch |
| Rollback | Revert individual commits | Revert one merge commit |
| CI on main | Runs per-MR | Runs once on combined work |
bd create --type=epic --title="Auth overhaul"
# → gt-auth-epic
Create child issues under the epic as normal.
gt mq integration create gt-auth-epic
# → Created integration/gt-auth-epic from origin/main
# → Stored branch name in epic metadata
This pushes a new branch to origin and records its name on the epic.
Assign children to polecats as normal:
gt sling gt-auth-tokens gastown
gt sling gt-auth-sessions gastown
Polecats auto-detect the integration branch when their issue is a child of an epic that has one. No manual targeting needed.
When polecats run gt done or gt mq submit, auto-detection kicks in:
gt done
→ Detects parent epic gt-auth-epic
→ Finds integration/gt-auth-epic branch
→ Submits MR targeting integration/gt-auth-epic (not main)
The Refinery processes these MRs and merges them to the integration branch.
Once all children are closed and all MRs merged:
gt mq integration land gt-auth-epic
# → Verified all MRs merged
# → Merged integration/gt-auth-epic → base branch (--no-ff)
# → Tests passed
# → Pushed to origin
# → Deleted integration/gt-auth-epic
# → Closed epic gt-auth-epic
Integration branches work without manual targeting. Three systems auto-detect them:
| System | What It Does | Config Gate |
|---|---|---|
gt done / gt mq submit | Targets MR at integration branch instead of main | integration_branch_refinery_enabled |
| Polecat spawn | Sources worktree from integration branch | integration_branch_polecat_enabled |
| Refinery patrol | Checks if integration branches are ready to land | integration_branch_auto_land |
When gt done or gt mq submit runs:
| Step | Action | Result |
|---|---|---|
| 1 | Load config, check integration_branch_refinery_enabled | If false, skip detection |
| 2 | Get current issue ID from branch name | e.g., gt-auth-tokens |
| 3 | Walk parent chain (max 10 levels) | Find ancestor epics |
| 4 | For each epic: read integration_branch: from metadata | Get stored branch name |
| 5 | Fallback: generate name from template | e.g., integration/{title} |
| 6 | Check if branch exists (local, then remote) | Verify it's real |
| 7 | If found, target MR at that branch | Instead of main |
The --epic flag on gt mq submit bypasses auto-detection and resolves
the target branch using the configured template (defaulting to
integration/{epic}).
| Variable | Description | Example |
|---|---|---|
{epic} | Full epic ID | gt-auth-epic |
{prefix} | Epic prefix (before first hyphen) | gt |
{user} | From git config user.name | klauern |
| Priority | Source | Example |
|---|---|---|
| 1 (highest) | --branch flag on create | --branch "feat/{epic}" |
| 2 | integration_branch_template in config | "{user}/{epic}" |
| 3 (lowest) | Default | "integration/{title}" |
| Variable | Description | Example |
|---|---|---|
{title} | Sanitized epic title (lowercase, hyphenated, max 60 chars) | add-user-authentication |
{epic} | Full epic ID | RA-123 |
{prefix} | Epic prefix before first hyphen | RA |
{user} | Git user.name | klauern |
# Default template (uses epic title)
gt mq integration create gt-auth-epic
# → integration/add-user-authentication (from epic title)
# Custom template in config: "{user}/{prefix}/{epic}"
gt mq integration create RA-123
# → klauern/RA/RA-123
# Override with --branch flag
gt mq integration create RA-123 --branch "feature/{epic}"
# → feature/RA-123
The actual branch name created is stored in the epic's metadata, so auto-detection always finds the right branch regardless of which template was used.
If two epics produce the same branch name (same title), a numeric suffix from the
epic ID is appended automatically (e.g., integration/add-auth-456).
gt mq integration create <epic-id>Create an integration branch for an epic.
gt mq integration create <epic-id> [flags]
Flags:
| Flag | Description | Default |
|---|---|---|
--branch | Override branch name template | Config template or integration/{title} |
--base-branch | Create from this branch instead of the rig's default branch (also sets where land merges back to) | origin/<default_branch> |
What it does:
Error cases:
gt mq integration status <epic-id>Display integration branch status for an epic.
gt mq integration status <epic-id> [flags]
Flags:
| Flag | Description |
|---|---|
--json | Output as JSON |
Output includes:
Ready-to-land criteria (all must be true):
gt mq integration land <epic-id>Merge an epic's integration branch back to its base branch.
gt mq integration land <epic-id> [flags]
Flags:
| Flag | Description | Default |
|---|---|---|
--force | Land even if some MRs still open | false |
--skip-tests | Skip test run after merge | false |
--dry-run | Preview only, make no changes | false |
What it does:
default_branch if not stored)--no-ff--skip-tests)Idempotent retry: If land crashes after pushing but before cleanup (branch deletion / epic close), rerunning the same command is safe. The idempotency check detects that the integration branch is already an ancestor of the target and skips directly to cleanup.
Error cases:
--force to override)The rig's default_branch (set in config.json, auto-detected during gt rig add)
controls where work merges when no integration branch is active. It's also the
default base branch when creating integration branches. If your project uses
develop or master instead of main, set it once in rig config and the whole
pipeline follows:
{
"type": "rig",
"name": "myproject",
"default_branch": "develop"
}
All integration branch fields live under merge_queue in rig settings (settings/config.json):
{
"merge_queue": {
"enabled": true,
"integration_branch_polecat_enabled": true,
"integration_branch_refinery_enabled": true,
"integration_branch_template": "integration/{title}",
"integration_branch_auto_land": false
}
}
| Field | Type | Default | Description |
|---|---|---|---|
integration_branch_polecat_enabled | *bool | true | Polecats auto-source worktrees from integration branches |
integration_branch_refinery_enabled | *bool | true | gt mq submit and gt done auto-detect integration branches as MR targets |
integration_branch_template | string | "integration/{title}" | Branch name template (supports {title}, {epic}, {prefix}, {user}) |
integration_branch_auto_land | *bool | false | Refinery patrol auto-lands when all children closed |
Note: *bool fields use pointer semantics — null/omitted means "use default"
(true for polecat/refinery enabled, false for auto-land). Set explicitly to false
to disable.
When integration_branch_auto_land is true, the Refinery patrol automatically
lands integration branches that are ready.
During each patrol cycle, the Refinery:
bd list --type=epic --status=opengt mq integration status <epic-id>ready_to_land: true: runs gt mq integration land <epic-id>Both config gates must be true:
integration_branch_refinery_enabled: true (integration feature is on)integration_branch_auto_land: true (auto-landing is on)If either is false, the patrol step exits early.
| Scenario | Recommendation |
|---|---|
| Trusted CI, no human review needed | Enable auto-land |
| Need human sign-off before landing | Keep disabled (default), land manually |
| Mix of both | Keep disabled, use gt mq integration land for manual control |
Integration branch landing is protected by a three-layer defense:
The refinery formula and role template explicitly forbid landing integration
branches via raw git commands. Only gt mq integration land is authorized.
The .githooks/pre-push hook detects when a push to the default branch
introduces integration branch content. It uses ancestry-based detection:
if any origin/integration/* branch tip becomes newly reachable from the
pushed commits, the push is blocked unless GT_INTEGRATION_LAND=1 is set.
The default branch is detected dynamically via refs/remotes/origin/HEAD
(fallback: main), so this works regardless of the rig's branch naming.
This catches all merge styles: --no-ff, --ff-only, default merge, and
rebase. Only cherry-picks (which produce new SHAs) are not detected.
Scope: This check matches branches under the integration/ prefix (the
default template). Custom templates that produce branches outside integration/
are not covered by the hook — Layer 1 (formula language) is the guardrail for
those cases.
Requires: core.hooksPath must be configured for the hook to be active.
New rigs get this automatically. Existing rigs: run gt doctor --fix.
The gt mq integration land command uses PushWithEnv() to set
GT_INTEGRATION_LAND=1, allowing the push through the hook. Raw git push
from any agent or user does not set this variable and will be blocked.
Manually setting the env var is possible but is not part of the supported
workflow — the variable is a policy-based trust boundary, not a
capability-based security mechanism.
| Layer | Type | Strength | Limitation |
|---|---|---|---|
| Formula/Role | Soft | Covers all branch patterns | AI agents can ignore instructions |
| Pre-push hook | Hard | Blocks all merge styles at git boundary | Only matches integration/* prefix; env var is policy-based |
| Code path | Hard | Land command sets bypass env var | Requires hook to be active |
The layers complement each other. The formula covers custom templates; the hook provides hard enforcement for default templates (catching merges, fast-forwards, and rebases via ancestry detection); the code path ensures the CLI command can bypass the hook.
Integration branches work with different project toolchains. The rig's build pipeline commands are auto-injected into polecat-work, refinery-patrol, and sync-workspace formulas so agents know how to validate work for each project.
Commands run in this order (any can be empty = skip):
pnpm install)tsc --noEmit)eslint .)go test ./...)go build ./...)Go project (all commands empty by default — configure per-rig):
{
"merge_queue": {
"test_command": "go test ./...",
"lint_command": "golangci-lint run ./...",
"build_command": "go build ./..."
}
}
TypeScript project:
{
"merge_queue": {
"setup_command": "pnpm install",
"typecheck_command": "tsc --noEmit",
"lint_command": "eslint .",
"test_command": "pnpm test:unit",
"build_command": "pnpm build"
}
}
Commands are auto-injected from <rig>/settings/config.json into formula vars:
buildRefineryPatrolVars() reads rig config during gt primeloadRigCommandVars() reads rig config during gt slingUser-provided --var flags on gt sling override rig config values.
Any command left empty (or not configured) is skipped silently by the formula.
This means a Go rig doesn't need setup_command or typecheck_command, and a
TypeScript rig can add all five without affecting Go rigs.
Polecats working on integration branches inherit the rig's build pipeline automatically — no per-branch configuration is needed.
Wrong: Sling children, then create the integration branch later.
Children slung before the integration branch exists will target main. Their MRs won't flow to the integration branch. Create the integration branch first, before slinging any child work.
Wrong: Using --branch integration/gt-epic on gt mq submit.
Auto-detection handles this. If you find yourself manually targeting, check that:
integration_branch_refinery_enabled is not falseWrong: Using --force to land when children are still open.
This defeats the purpose. The integration branch exists so work lands together. If you need to land early, close or remove the incomplete children first.