Git profiles
English | Tiếng Việt
One knob in .babysit/git-flow.yaml decides where finished work goes and how
hard QA tests it:
profile: startup # pet | startup | enterprise
base_branch: mainsetup-project asks one question — what does a mistake cost in this repo? —
and writes the answer. Everything else derives. Check what you have:
bbs autopilot git-flow # prints BBS_PROFILE / BBS_MODE / BBS_LAND / BBS_RIGOR / …pet | startup | enterprise | |
|---|---|---|---|
| long-lived branches | main | develop + main | develop + staging + main |
| how work lands | push is the release | PR into develop, author merges | PR into develop, someone else merges |
| review happens | nowhere | locally, in the browser | locally and on GitHub |
| QA rigor | smoke, 3–5 cases | standard, 5–10 | strict, 8–12 |
Rigor scales breadth only. PASS means the same in all three: every
applicable rubric dimension at B or better and a fresh end-to-end run on the
final code. A pet project runs fewer cases — never zero.
A repo with no profile: key resolves to pet. Nobody configured it, so it
behaves like plain git. An explicit mode:/land:/push:/ticket_branch: key
still wins over the preset, so a config written before profiles existed keeps
the shape it spelled out.
What no profile changes: where you work
2build works on the branch you are standing on. No profile cuts a branch,
and no profile moves you into a worktree — bbs autopilot git-flow prints
BBS_MODE='trunk' under all three. A tool that silently relocates your work is
a tool you cannot manage, so isolation is something a run asks for, never
something a config file turns on behind you:
bbs ticket ensure --slug-hint thing --mode=worktree # this ticket gets its own checkout
bbs ticket ensure --slug-hint thing --mode=branch # …or a feat/… branch cut in place
/bbs:foreman # a batch: foreman gives each ticket a worktreeA repo that always wants one of those can write mode: worktree (or
mode: branch) by hand — see Running tickets in parallel.
Everything else in this doc is about what the profile does decide.
The catch: identity rides an environment variable
With no ticket branch, there is nothing in git to identify the ticket. Identity
moves to BABYSIT_TICKET, which ensure prints for you:
export BABYSIT_TICKET=bs-xxxxxxxxLose that variable — new shell, new pane, a session that crashed and came
back — and the pre-push hook resolves no ticket and passes. The qa and
review-pr verdicts stop gating anything, silently. Under pet that means an
ungated push, and the push is the release. Recover with:
bbs ticket session list
bbs ticket session attach <id> # re-echoes the export lineThe branch topology each profile assumes
base_branch is the one thing the profile can't derive for you — it depends on
how you release. Set it to match the shape below.
pet — one branch
main ────────●────●────●──────► push is the releaseprofile: pet
base_branch: maincreate-pr deliberately BLOCKs here (land: none); there is no PR step, so
the qa + review-pr verdicts are the only thing standing between a session and
your released code.
startup — develop integrates, main releases
feat/bs-a ──┐
feat/bs-b ──┼──► develop ───────────────► PRs land here
feat/bs-c ──┘ │
└──► main ──► tagged releaseprofile: startup
base_branch: developWhy not just main: with base_branch: main, merging the PR is shipping,
so your local QA is the only gate that ever runs. develop splits integrated
from shipped into two events — a bad merge is contained on an integration
branch, and main stays releasable.
If you deploy on every merge and have no separate release moment, develop is
ceremony — set base_branch: main and know that you've made local QA the last
gate.
enterprise — a deployed integration environment in the middle
feat/bs-a ──┐
feat/bs-b ──┼──► develop ──► staging ──► main
feat/bs-c ──┘ (CI) (deployed QA) (tagged release)profile: enterprise
base_branch: developTwo review venues answering different questions: your local browser check is
product QA (does it work?), the GitHub PR on develop is code review by
someone else. staging is where the integrated result gets QA'd as a deployment
rather than as a checkout. Strict QA also releases the surface before handing
off — evidence travels in the PR body, so don't expect the app to still be up
after a QA run (smoke/standard leave it running and hand you the URL).
Promotion is never babysit's job
create-pr targets base_branch and stops — the same reason it never merges.
Every promotion above it is yours or CI's:
git switch main && git merge --ff-only develop && git tag v1.2.0 && git push --follow-tagsThe one exception is a hotfix, which forks production directly instead of
waiting behind everything on develop:
BBS_BASE_BRANCH=main bbs ticket ensure --slug-hint hotfix-thing --type hotfix --mode=branch
# …fix, QA, PR into main, then back-merge main into developOnce branches exist: local base is a test surface, not a git base
This rule only bites once something is actually cut — a --mode=branch ticket,
or a worktree batch.
Every ticket-branch write references origin/<base>, never local <base>.
Merging local base into a ticket branch drags whatever else you have in flight
into that ticket's PR. To pull in upstream changes, use:
bbs ticket refresh # fetch + merge origin/<base>; BLOCKs on dirty tree or conflictOnly four commands touch local base — surface compose, surface revert,
serve, land — and none of them writes to a ticket branch.
ensure fetches origin/<base> before cutting, then forks from the
remote-tracking ref with --no-track. Two fallbacks, in this order:
- No
origin/<base>ref at all (no remote, or the remote lacks the branch) → forks from your local<base>. - Fetch failed but the ref exists → forks from the last-known
origin/<base>and says so:ensure: warning — fetch failed, using the last-known origin/<base>.
bbs ticket board closes the footer with where your local base actually stands,
so drift is visible before it bites:
BASE: main — 3 ahead / 12 behind origin/main (last fetch 2h ago)
↑ 3 commit(s) on local 'main' are not on origin/main — mode: trunk cuts no branch, so tickets build on them; they are only unpushed
↓ origin/main moved on — bbs ticket refresh (in a ticket) or bbs ticket surface revert (on the primary)It never blocks — a diverged base is normal mid-flight. Board doesn't fetch, so
the numbers are as fresh as the age in parentheses. The ↑ line words itself
per mode: with nothing cut, tickets build on those commits and the only thing
wrong is that they aren't pushed; once tickets are cut from origin/<base>,
they are invisible to every ticket cut after them.
Running tickets in parallel
Several sessions in one checkout means all changes interleave on one branch: you
cannot review one ticket in isolation, drop a bad one, or attribute a
regression. Worktrees buy exactly that separation, and cost a commit +
bbs ticket surface compose per test iteration instead of edit-and-refresh.
That trade is why nothing turns them on for you.
The one rule
The primary checkout stays on
base_branch. It is the shared test surface —node_modulesand the dev server live there. If a ticket branch occupies it, nothing can be composed there.
serve enforces this rather than guessing:
STATUS: BLOCKED
REASON: primary checkout … is on 'feat/…', not base 'main'.Option A — foreman dispatches for you
Needs Orca with orchestration enabled. Hand it a large project; it decomposes a parent requirement into a Task DAG, creates each ticket worktree, and supervises plan then build/QA autopilot Dispatches — so it works the same on any repo, configured or not:
/bbs:foremanIt gates each design before code, serializes the shared QA surface, runs integration QA for interacting tickets, and applies the configured finish policy in dependency order.
Option B — dispatch them yourself
OUT=$(bbs ticket ensure --slug-hint <slug> --mode=worktree) # one worktree per ticket
cd "$(printf '%s\n' "$OUT" | sed -n 's/^WORKTREE=//p')" # into the path it printed
/bbs:autopilot "<requirement>"A repo that works this way every time can stop typing the flag:
profile: startup
base_branch: develop
mode: worktree # every ticket gets its own checkout
land: local # …and the finished batch is composed locally before any PRland: local is what makes the composed human-QA checkpoint the default
handoff. It only makes sense next to mode: worktree: a branch cut in place
takes the primary off base_branch, and every compose then BLOCKs.
Then: compose, review, ship
bbs ticket board # status: tickets, verdicts, PRs, who holds the lease
bbs ticket serve # compose ALL finished tickets (qa + review-pr DONE) onto local base
bbs ticket serve <t1> <t2> # …or exactly the ones you name
bbs ticket serve --release # hand the surface backserve <t1> <t2> composes exactly the tickets you name — the payoff is that you
can leave a bad one out and each still lands as its own clean PR.
| command | what it does |
|---|---|
bbs ticket surface compose | run bare from a worktree — lands that ticket on the shared surface for QA |
bbs ticket surface compose <t…> | run from the primary — resets to base, then merges exactly the named tickets |
bbs ticket surface revert | after PRs merge upstream, snap local base back to origin/<base> |
bbs ticket surface clear --ticket <t> --head <sha> | recovery only: clear one stale marker after a retained landing; proves the ticket is done and its reviewed head is on base, without changing the tree |
bbs ticket surface acquire | one QA session at a time on the shared surface; others BLOCK naming the owner |
bbs ticket land <t…> | merge finished tickets into local base and keep the merge (see below) |
All of them refuse loudly rather than losing work.
surface clear is not a substitute for surface revert. Use it only when the
ticket was retained on the local base but the old marker survived and reverting
would discard valid commits. It takes the surface lease and BLOCKs unless the
marker names exactly one completed ticket and <sha> is an ancestor of the
current base. Tracked and untracked work is left untouched.
The QA loop, when tickets live in worktrees
- Implement and commit in the worktree.
bbs ticket surface composefrom the worktree.- QA finds a problem → fix in the worktree, commit, re-run
surface compose. Never fix in the base checkout — QA must test a committed ticket state. - Push the ticket branch;
create-prtargetsbase_branch. - After PRs merge:
bbs ticket surface revertfrom the primary; in-flight worktrees re-runsurface compose.
finish — let a verified ticket close itself out
serve is a look, not a landing: it resets base and re-composes from scratch
each time, so nothing it puts there survives the next surface revert. land
is the opposite — a --no-ff merge into local base that stays.
A repo can ask for that to happen by itself:
profile: startup
finish: land # review (default) | land | prNow foreman merges each ticket into local base as its worker finishes, instead of leaving a pile of branches for you to compose. You review the base branch — which is already running on the dev server — and push it.
finish: pr is the other half of the same key: foreman runs create-pr per
ticket as it finishes, so the batch ends with open PRs instead of a pile of
branches. It needs a PR venue — under land: none the config is rejected when
it is read, because create-pr BLOCKs there by design.
finish is review by default and opt-in per repo, because it is the one key
that lets a run act on its own verdicts while you are asleep — moving your base
branch, or pushing where your team can see it. Both values gate on the same
thing: qa + review-pr must both be DONE, per ticket, with no --force
flag to reach for. Unverified work never closes out.
The rest of the guards are land's, and they are what make a merge you did not
watch survivable:
- The whole batch is checked before any of it merges — a blocked third ticket doesn't leave the first two on a base you never asked for.
- It takes the QA lease, so it can't move base under a running QA session.
- It never pushes. Base ends up ahead of origin and the push stays yours.
- Re-running is a no-op (
LANDED=0 … already on main).
finish: pr is the one that does push — that is what opening a PR is — but it
opens a PR and nothing more: the review, and the merge, stay human.
Don't run serve after landing: serve re-composes from origin/<base>,
which snaps base to origin and discards the merges — the ticket branches keep
the work, but your review surface vanishes.
Switching profiles
Re-run /bbs:setup-project — on a configured repo it offers the switch and
rewrites only profile:. A profile switch changes your review venue and QA
rigor; it never relocates work in flight.
Changing base_branch to develop on a repo that doesn't have one: create and
push it from main before the switch, or the first cut finds no
origin/develop and forks from your local base instead.
git switch -c develop main && git push -u origin developSetting mode:, land:, push: or finish: by hand overrides the
profile's preset. That's the escape hatch, not the normal shape — a knob written
out by hand stops tracking its profile. mode: is read at branch-cut time, so
adding or removing it affects new tickets only; before moving into worktree
work the primary must end clean on base_branch, and before moving out of it,
finish or park in-flight worktrees (bbs ticket board) and release any
surface lease.
Full schema and the derivation table: .claude/skills/references/git-flow.md;
the worktree machinery: references/worktrees.md.
