# Git Discipline

> Branch-per-initiative git discipline for all delivery workflows. Defines branch naming by workflow, the propose-confirm creation protocol, dirty-tree and detached-HEAD handling, the always user-gated merge protocol, worktree support for parallel sessions, and abandonment cleanup. Activates only in git repositories — silently inactive everywhere else. Use when starting /build, /fix, /architect, or /build-x at Standard+ scope, when resuming an initiative, when offering a merge at a completion checkpoint, or when the user wants a second concurrent initiative.

- Skill: `xoai/git-discipline` (Agent Skill)
- Install (CLI): `npx skillmds@latest add xoai/git-discipline`
- Raw SKILL.md: https://api.skillmd.com/api/skills/xoai/git-discipline/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: xoai (https://skillmd.com/u/xoai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/xoai/git-discipline

---


<!-- sage-metadata
cost-tier: haiku
activation: auto
tags: [git, branching, worktree, merge, discipline, parallel-sessions]
inputs: [initiative-slug, workflow-mode]
outputs: [initiative-branch, manifest-branch-record]
-->

# Git Discipline

One branch per initiative. Merge only when the user says so.

This capability is the **single source of truth** for branch naming,
creation, merging, worktrees, and cleanup. Workflows cite it — they
never restate its rules. If a workflow's text and this file disagree,
this file wins; flag the workflow as the defect.

## Branch vs worktree — the decision rule

These solve different problems; pick by how many sessions run at once:

- **Branch** isolates *history* — one initiative's commits don't
  interleave with the default branch. A branch is **sufficient for
  one session at a time** (sequential initiatives). This is the
  default (`isolation: branch` in `.sage/config.yaml`).
- **Worktree** isolates the *working tree* — its own directory on
  disk. A worktree is **required for simultaneous sessions**: two
  `claude` processes in one checkout share one working tree and one
  HEAD, so they clobber each other's files regardless of branch, and
  `git checkout -b` in one moves HEAD for the other. Branches cannot
  fix a working-tree race; only a directory-per-session can.

**The hard constraint.** A Claude session is bound to the directory
it was launched in and **cannot relocate itself** into a worktree it
creates. So the parallel path is always launch-time: create the
worktree *first*, then start a session *inside* it. The front door is
**`sage worktree <slug>`** (creates branch + worktree + copies the
gitignored runtime + prints the `cd … && claude` line). Opt in with
`isolation: worktree`; until then, sequential branch-in-place is
unchanged.

## Availability check

Activate only when `git rev-parse --git-dir` exits 0. On failure
(not a git repository): silently inactive — no error, no prompt, no
mention. Every workflow runs exactly as it would without this
capability. Never run any other `git` command before this check
passes.

## Branch naming

| Workflow | Branch |
|----------|--------|
| `/build` | `feat/<slug>` |
| `/fix` | `fix/<slug>` |
| `/architect` | `arch/<slug>` |
| `/build-x` | `feat/<slug>` or `fix/<slug>` — planner proposes from the task shape, user confirms |
| `/autoresearch` | `autoresearch/<slug>` (pre-existing discipline, unchanged by this capability) |

`<slug>` is the initiative's work-dir slug **without** the date
prefix: `.sage/work/20260612-payment-retry/` → `feat/payment-retry`.
The full dated slug remains the work-dir key. On a name collision
(the branch already exists), append `-2`, `-3`, … — and never reuse
an existing branch without the user's explicit choice (see
"Already on a branch" below).

The naming extends the project's conventional-commit convention
(feat, fix, chore, docs) to branch names.

## Worktree bounce (`isolation: worktree` in the main checkout)

Read `isolation:` from `.sage/config.yaml` (default `branch`). When
it is `worktree` AND the workflow is starting in the **main checkout**
(not a linked worktree — the test is `git rev-parse --git-dir` ==
`git rev-parse --git-common-dir`; equal → main checkout), do not
branch in place. Present a **guided menu** (never a hard refusal — a
lone sequential session must still be able to proceed):

- `[1]` **Set up an isolated worktree** (recommended for parallel) —
  run / instruct `sage worktree <slug>`; open a session in the new
  directory and continue there. This session stops here.
- `[2]` **Proceed here in the shared checkout (not isolated)** — fall
  back to the normal in-place branch flow below; record the choice in
  the initiative decision log.
- `[3]` **Sequential, this once** — same as `[2]`, noting
  `isolation: worktree` was overridden for this initiative.

When already running **in a linked worktree** (the user launched via
`sage worktree`), skip the bounce and branch/commit in place as usual
— the session is already isolated. With `isolation: branch` (default),
the bounce never fires.

## Branch creation (propose-confirm)

At the workflow step that consumes this capability, when HEAD is on
the default branch with a clean tree:

1. **Propose** the branch name from the table above.
2. **Let the user confirm or rename.** Never create silently.
3. `git checkout -b <branch>`
4. **Record the actual branch name** in the initiative's manifest
   frontmatter: `branch: feat/payment-retry`. Resume matching reads
   this recorded field — never re-derives from names (see "Branch ↔
   work-dir mapping" below).

**Dirty tree at creation time:** stop and present three options —
stash and proceed, commit current changes first (the user writes or
approves the message), or abort the workflow. Never stash silently.

**Already on a branch** (HEAD on a non-default branch): ask whether
to (a) continue on it — the user may have prepared one — or
(b) create the initiative branch from the default branch. Record the
choice in the initiative decision log.

**Detached HEAD** (tag/SHA checkout; `git worktree add` without
`-b`): surface the state, offer to create the initiative branch from
the default branch or from HEAD (`git checkout -b <branch>` works
from either) — never proceed to implementation commits while
detached.

**User declines branching:** record the decline in the initiative
decision log and proceed exactly as today (commits land on the
current branch). Declining is always available — the discipline is a
default, not a cage. A recorded decline also suppresses the
CLAUDE.md minimum branch gate for this initiative.

## Default-branch resolution

Never hardcode `main`. Resolve via this chain, stopping at the first
success:

1. `git symbolic-ref refs/remotes/origin/HEAD` (strip the
   `refs/remotes/origin/` prefix) — set by clone; absent in
   remote-less repos.
2. `git config init.defaultBranch` — only if a local branch by that
   name exists.
3. An existing local branch named `main`, then `master`.
4. **Ask the user once.**

Persist the answer as `default_branch:` in `.sage/config.yaml` so
every session — and every parallel worktree — resolves identically.
Never silently designate the currently-checked-out branch as the
default.

**Unborn HEAD** (fresh `git init`, no commits): branch creation still
works (`git checkout -b` is valid from unborn HEAD); the merge
protocol requires an existing default branch and otherwise degrades
to "commit on the current branch" with a one-line notice.

## Per-task commits

Implementation commits land on the initiative branch using
conventional-commit subjects. The workflow's existing commit cadence
is unchanged — only the branch it lands on changes.

## Merge protocol (ALWAYS user-gated)

Merging is offered **only** at a completion checkpoint as an explicit
menu option (`[M] Merge to <default>`). No workflow path executes a
merge without the user choosing that option in the same session. This
is a hard line, not a default.

Before offering `[M]`:

- **Run the full test suite and gate on its own exit code.** Capture
  the status, then act — never `suite | tail && merge` (a pipeline's
  exit status is the last command's; a piped suite reads green even
  when red). On a project with no test harness, substitute the
  initiative's documented smoke procedure; if none exists, require an
  explicit user acknowledgment that the merge is unverified.
- **Structural preconditions** — both must hold:
  - `git status --porcelain` is empty (no uncommitted work that a
    checkout would transplant onto the default branch);
  - `git rev-list <default>..<branch>` is non-empty (the branch
    actually has commits to merge).
- Show `git diff <default>...<branch> --stat`.

On user `[M]`:
```
git checkout <default>
git merge --no-ff <branch>
```

On conflict: stop, show the conflicting paths, hand control to the
user. Conflict resolution is out of scope for this capability.

After a successful merge, **offer** (never perform) branch deletion —
**suppressed while the initiative continues** (per-milestone merges):
the deletion offer fires only at initiative completion. Deleting
mid-initiative would strand later milestones on the default branch
with no re-entry into branch creation.

**No remote operations.** This capability never runs `git push`,
never creates PRs, never sets upstreams. Pushing and PRs remain the
user's own actions.

## Branch ↔ work-dir mapping: recorded, not derived

At branch creation the workflow records the **actual** branch name in
the initiative's manifest frontmatter (`branch: feat/payment-retry`).
Resume matching (session-bridge, `/continue`, workflow auto-pickup)
reads the recorded field — never re-derives from names, because the
derivation is non-bijective the moment any sanctioned exception
fires: the collision suffix (`feat/auth-2`), a user rename at the
confirm step, continue-on-existing-branch, or date-stripped slugs
that collide across months (`20260101-auth` and `20260301-auth` both
derive `feat/auth`). The naming-table derivation is only the
**default proposal** at creation time.

## Worktrees (parallel sessions)

**Front door: `sage worktree <slug>`.** When the user wants a second
concurrent initiative — or `isolation: worktree` is set — the launcher
is how a parallel session starts: it creates the branch + worktree,
copies the gitignored runtime, and prints the `cd … && claude` line to
open an isolated session. The user runs one command; the session is
born isolated. (A running session cannot relocate itself — see the
decision rule above — so this is always launch-time.)

The manual recipe below is what the launcher does, kept as
documentation and as the fallback when `sage worktree` is unavailable:

```
git worktree add ../<project>-<slug> <branch>
```

One worktree per initiative, as a sibling directory. A branch can be
checked out in only one worktree at a time; the new worktree shares
`.git` storage. If the target path already exists, offer
`../<project>-<slug>-2` or let the user supply a path — never delete
an existing directory.

**`.sage/` trackedness — two worlds, both handled.** Probe with
`git check-ignore -q .sage` (exit 0 = ignored):

- **Tracked** (`.sage/` committed): state travels with each
  worktree's checkout; the per-initiative state files prevent merge
  conflicts between branches. The offer proceeds as above.
- **Gitignored** (as in sage's own repo): state does NOT travel — a
  fresh worktree contains no `.sage/` at all. The offer still
  proceeds, with a copy step: after `git worktree add`, copy the
  shared runtime (`.sage/config.yaml`, `.sage/constitution.md`,
  `.sage/gates/`; for a `/build-x` initiative also `.sage/scripts/`,
  `.sage/prompts/`, and `.sage/agents.toml` — the multi-agent runtime,
  without which the first dispatcher invocation fails) into the new
  worktree. The seeded set is the `worktree_copy` config list (it
  deliberately does NOT seed `.sage/work/` — a fresh worktree starts a
  fresh initiative); on teardown, `sage worktree remove` harvests the
  `worktree_harvest` list (default `.sage/work/*`) back to the main
  checkout. Both lists are project-configurable in `.sage/config.yaml`.

**The platform runtime needs the same treatment.** The same
`.gitignore` that ignores `.sage/` typically also ignores `.claude/`
and the platform instructions file (`CLAUDE.md` / `AGENTS.md` /
`GEMINI.md`) — sage's own repo ignores all of them. Probe separately:
`git check-ignore -q .claude` (and the instructions file). When
ignored or absent, the worktree offer includes one of two remedies,
user's pick:

- **(a) Copy** `.claude/` and the platform instructions file(s) into
  the worktree alongside the `.sage/` state; or
- **(b) Run `sage update` inside the new worktree** — the platform
  generators regenerate the instructions file and commands; this
  works even when `sage/` itself is absent in the worktree (the CLI
  falls back to the global framework install).

A worktree session without a platform runtime is an inert checkout —
no slash commands, no skills, not even the degraded CLAUDE.md gates.
**The offer must not complete without one of the two remedies.**

**The MCP config and the memory store.** `.mcp.json` is in the default
`worktree_copy` set, so the sage-memory (and other) MCP servers are
configured in the worktree. The memory **database** is different from the
rest of `.sage/`: it is a single shared store, **not** a per-worktree copy.
A SQLite file cannot be merged, so `.sage-memory/` is never seeded into a
worktree and never harvested back. Instead the worktree session points at
the main checkout's store by calling `sage_memory_set_project(<main root>)`
(`dirname "$(git rev-parse --git-common-dir)"`); the session-init hook
surfaces that call automatically when it detects a linked worktree. Result:
one brain, shared by main and every worktree — nothing to reconcile.

**Ownership record.** Record `owner: <worktree-path>` in the **main
checkout's** copy of the initiative manifest (recording in both
copies is acceptable; the main checkout's copy is the one exclusion
reads). Resume surfaces in the original checkout exclude initiatives
owned elsewhere. The comparison probe is
`git rev-parse --show-toplevel` against the recorded `owner:` after
path normalization — trailing slashes and symlinked paths (WSL
mounts alias the same checkout) must not defeat the comparison.

**Ownership return.** When the worktree is removed, always copy back
the work dir **before** the manifest edit that clears `owner:` —
editing first and copying back after re-imports the stale `owner:`
from the worktree copy and permanently locks the initiative out of
every resume surface.

**Cleanup after merge:** `sage worktree remove <slug|dir>` — offered,
user-gated, never automatic. It harvests the gitignored `.sage/work/<slug>/`
back into the main checkout, then removes the worktree (refusing on dirty
*tracked* files unless `--force`). Prefer it over bare `git worktree remove`,
which skips the harvest and silently drops the docs (see Abandonment cleanup).

## Abandonment cleanup

Closing an initiative without merging offers — never performs — the
cleanup pair: `sage worktree remove <slug|dir>` (if a worktree exists)
and branch deletion.

**Ignored-world ordering is mandatory.** When `.sage/` is gitignored,
a bare `git worktree remove` silently deletes the worktree's untracked
`.sage/` state with it — the brief/spec/plan are gone, never merged
(untracked) and never recoverable via git (gitignored files never enter
the object store). `sage worktree remove` exists to prevent exactly this:
it harvests `.sage/work/<slug>/` back into the main checkout **before**
removing the worktree. The abandonment sequence is therefore:

1. `sage worktree remove <slug|dir>` — harvests the work dir back, then
   removes the worktree.
2. In the **main checkout's** harvested manifest, clear `owner:` and write
   `status: abandoned` — after the harvest, never before (see Ownership
   return).

In the tracked world, the harvest is unnecessary (git carries `.sage/`) and
step 2 collapses to a manifest edit + commit on the initiative branch.
Declining the cleanup leaves the branch for later; `status: abandoned` is
recorded either way so resume surfaces stop offering the initiative.

## What this capability never does

- Merge, push, or delete anything without an explicit user menu
  action in the same session.
- Create a branch silently.
- Stash silently.
- Run any `git` command before the availability check passes.
- Touch `/autoresearch`'s pre-existing branch discipline.

