with one click
ship
// Delivery — committing, pushing, creating MR/PR, pipeline monitoring, review requests. Use when user says "commit", "push", "MR", "merge request", "pull request", "finalize", "deliver", "ship", or is in the delivery phase.
// Delivery — committing, pushing, creating MR/PR, pipeline monitoring, review requests. Use when user says "commit", "push", "MR", "merge request", "pull request", "finalize", "deliver", "ship", or is in the delivery phase.
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | ship |
| description | Delivery — committing, pushing, creating MR/PR, pipeline monitoring, review requests. Use when user says "commit", "push", "MR", "merge request", "pull request", "finalize", "deliver", "ship", or is in the delivery phase. |
| compatibility | macOS/Linux, git, glab or gh CLI, CI system. |
| requires | ["workspace","rules"] |
| companions | ["finishing-a-development-branch"] |
| triggers | {"priority":10,"exclude":"\\breview\\b","keywords":["\\b(merge request|pull request|create an? (mr|pr)|\\bmr\\b|push\\b|finalize|deliver|ship it|create mr|create pr)\\b","\\bcommit\\b"]} |
| search_hints | ["commit","push","ship","deliver","merge request","pull request"] |
| metadata | {"version":"0.0.1","subagent_safe":false} |
This skill delegates the generic branch-finalization doctrine to:
finishing-a-development-branch — decide how to wrap up a ready branchverification-before-completion — fresh verification before claiming the branch is readyThese are optional companion skills from obra/superpowers. If not installed, this skill still works — you just won't get the external branch-finalization and verification guidelines. TeaTree keeps the delivery-specific mechanics locally: ticket-aware commit metadata, MR creation rules, CI cancellation, and post-review branch policy.
From "code is done" to "MR is merged."
/t3:workspace now if not already loaded.When the active overlay has require_ticket = True, refuse to commit or push without a ticket reference.
overlay.config.require_ticket. Overlays that dogfood their own workflow enable this flag.type(scope): description (TICKET_URL)) — that is the linking mechanism teatree's validate_mr_title_and_description enforces. Use Fixes #<number> / Closes #<number> only when the active overlay sets mr_close_ticket = True (grep the overlay's OverlayBase subclass to confirm — there is no CLI for it). Default is False./t3:retro (format fix(<skill>): ...) are exempt — retro findings are small tactical fixes committed directly on the current branch.prek install before the first commit in any worktree (Non-Negotiable). This wires prek as the git pre-commit hook runner. Without it, whatever hook runner the repo happens to have (or nothing) runs on git commit — you cannot rely on prek's quality gates. This applies in every worktree, including colleagues' worktrees and review worktrees.git branch --show-current before every commit. If you are on main, master, development, or release, STOP — create a feature branch first (git checkout -b <prefix>-<repo>-<topic>), then commit there. This applies even for "quick fixes" and hotfixes with no ticket.type(scope): description (TICKET_URL)) when the active overlay has require_ticket = True (see § 0). Overlays with the default require_ticket = False (teatree itself) do NOT need the URL — a plain type(scope): description subject is correct and validate_mr_title_and_description will not reject it. Use Fixes #<number> / Closes #<number> in the body only when the active overlay sets mr_close_ticket = True (grep the overlay's OverlayBase subclass — no CLI exposes this).TICKET_URL from .env.worktree — never construct it from the branch name.quality-gates and module-health hooks have no relax: escape hatch (souliane/teatree#525). When the gate fires, fix the architecture: split the file by concern, refactor module-level functions onto a class, replace dict[str, object] with a typed dataclass, or delegate the suppressed import/call to a module the hook already exempts (tests/, scripts/hooks/, e2e/, skills/, docs/). The legitimate house pattern around subprocess (# noqa: S404 on the import, # noqa: S603 on each call) belongs in a CLI helper module that already lives under one of those exempt paths — not in newly suppressed source files.t3 <overlay> workspace finalize [msg] — squash commits + rebase on default branch.Squash rules:
git reset --soft, not interactive rebase. git rebase -i with custom editors is fragile when pre-commit hooks run on each commit. Use git reset --soft $(git merge-base origin/<default-branch> HEAD) && git commit to squash, or cherry-pick for non-adjacent commits.git log origin/<branch>..HEAD to confirm which commits are local-only.OLD_TIP=$(git rev-parse HEAD), verify git diff $OLD_TIP..HEAD is empty after rewrite.T3_AUTO_SQUASH (true = auto, false = ask first).git merge-base for the squash target. NEVER use origin/master or origin/main directly — the branch may have been created from a stale local copy, causing the squash to include unrelated commits. The t3 <overlay> workspace finalize command handles this correctly.If the changes touch architecture, add new modules, rename commands, or change extension points:
BLUEPRINT.md and check if it reflects the current state.BLUEPRINT.md.Before every push, run the self-review gate from ../review/SKILL.md § "Active Verification Against Repo Rules":
/code-review) if available. This skill contains the exact rules enforced by automated review bots — loading it prevents multi-round push-fix-push cycles.AGENTS.md (or equivalent agent instructions file).Skipping this step is the #1 cause of wasted push-fix-push cycles. The rules exist in t3:review and the project's code-review skill — this step ensures they are applied even when the agent goes directly from code to ship without a formal review phase.
Run /t3:retro before pushing — not after merge. Retro findings often surface skill fixes, guardrail improvements, or documentation updates that belong in the same PR as the feature. Running retro after push/merge means those improvements ship as a separate follow-up PR (or, worse, get lost to context compaction before they land anywhere).
Sequence: code → test → review gate → retro → push → MR → monitor CI.
If retro produces file edits (skill fixes, reference updates, docs), commit them on the current branch before § 4 Push so they ride along with the MR.
Enforcement: t3 <overlay> pr create refuses to create the MR until the retro phase is marked visited on the active session. Retro marks its own visit via t3 <overlay> lifecycle visit-phase <ticket_id> retro (documented in /t3:retro). If the shipping gate complains that retro is missing, run retro — do not bypass with --skip-validation unless explicitly instructed.
git fetch origin <default> && git log <branch>..origin/<default> --oneline — if any commits appear, merge them in (git merge origin/<default>) and re-run lint/tests before pushing. Opening a PR that is already BEHIND main forces the user (or you) to do a second round-trip to resolve conflicts; catch them now, while you have the context of your own change open.Before creating an MR, the pr create command automatically checks the session gate:
testing, reviewing, and retro phases.pr create returns an error with a hint to run /t3:review.--skip-validation is reserved for bypasses the user explicitly authorised in the same session — never the agent's own choice.The reviewing phase MUST be earned by spawning the t3:reviewer sub-agent — not by self-review against repo rules alone. § 3b ("Self-Review Against Repo Rules") is necessary but not sufficient: it is the implementer reviewing their own work, which is the exact pattern that allowed #545 to claim "implementation finished" while review later found six rounds of missed renames, broken tests, undocumented contract changes, and a bypassed shipping gate. An independent reviewer that has not seen the implementation conversation is the corrective.
Teatree has two independent gates layered on top of each other:
Ticket.state FSM (STARTED → CODED → TESTED → REVIEWED → SHIPPED → IN_REVIEW → ...) — owned by django_fsm transitions on core.models.ticket.Ticket. This is the system of record. pr create calls ticket.ship() and refuses if the state isn't REVIEWED.Session.visited_phases — a flat record consulted by _check_shipping_gate when a session exists.t3 <overlay> lifecycle visit-phase only writes the second one. Calling it directly leaves the FSM untouched, so the ticket state stays at TESTED (or earlier), pr create errors "Transition 'ship' not allowed from state 'tested'", and the workflow is silently inconsistent — exactly the breakage the user flagged after #545.
The agent must drive transitions, not visit phases. The transition-driven path keeps both gates aligned automatically: Task.complete() → _advance_ticket() → fires the matching FSM transition → _consume_pending_phase_tasks clears stragglers → next-phase task auto-scheduled.
Locate the reviewing task. When the worktree was created via t3 <overlay> workspace ticket <url>, a Task(phase="reviewing") was scheduled by Ticket.schedule_review() once test() fired. If the branch is ad-hoc (no session/ticket exists yet), create one first with t3 <overlay> workspace ticket <issue_url> — never push from a session-less branch. The session is the receipt; without it the FSM has nothing to advance.
Spawn the reviewer sub-agent from the main conversation (not from another sub-agent — see ../rules/SKILL.md § "Sub-Agent Limitations") via the Agent tool:
Agent(
description: "Pre-push review of <ticket>",
subagent_type: "t3:reviewer",
prompt: "Review the diverging code on this branch against main. Branch: <name>. \
Ticket: <url>. Scope: <one-line summary>. Read the linked ticket end-to-end \
before touching the diff (per /t3:review § 'Step 0 — Gather Ticket Context'). \
Apply both the per-file repo-rules check (/t3:review § 'Active Verification \
Against Repo Rules') and the module-level architectural check (full files of \
every touched module). Report findings as a punch list — no code edits."
)
Apply every finding. Reviewer sub-agents are read-only; the implementing conversation owns the edits. Do not cherry-pick which findings to take — if a finding is wrong, push back with evidence in the same conversation, do not silently drop it.
Drive the review transition so the FSM advances TESTED → REVIEWED and the shipping task is auto-scheduled. Two equivalent entry points (use the first one available):
ticket.review() via Task._advance_ticket(). This matches how every other phase advances and keeps the task ledger clean.t3 <overlay> ticket transition <ticket_id> review. The FSM still requires a completed reviewing task as a conditions= predicate, so this only works after step 3 has already produced one.Do not use t3 <overlay> lifecycle visit-phase <ticket_id> reviewing as a substitute. It only writes Session.visited_phases and leaves Ticket.state stuck at TESTED, which will cause pr create to fail at the FSM layer even though the session gate looks satisfied.
Verify before pushing. t3 <overlay> ticket list --state reviewed (or --id <ticket_id>) should show the ticket in reviewed. If it's still tested, the transition didn't fire — re-check that the reviewing task actually completed (task.status == COMPLETED) and that no FSM conditions= predicate is still failing. Don't paper over with lifecycle visit-phase; fix the root cause.
Why a sub-agent, not just self-review. The implementer's context is contaminated by the implementation: every "looks done" judgment carries the same blind spots that produced the gaps. A sub-agent starts cold, reads the diff with fresh eyes, and applies the review skill's checklists without the implementer's "I already checked that" shortcuts. The cost of one extra Agent call is ~30s of wall time and a few hundred tokens; the cost of skipping it is multi-round push-fix-push cycles after the PR is already public.
Why the FSM, not just the session gate. The session gate is a soft safety net that fail-opens when no session exists. The FSM is the actual contract — every downstream consumer (pr create, execute_ship, the loop dispatcher, the dashboard) reads Ticket.state. Bypassing the FSM produces tickets that look reviewed in the session record but are still TESTED in the source of truth, which then breaks every later transition until someone notices and rewinds by hand.
pr create also runs a pre-push browser sanity gate as a side effect of the shipping flow. It loads the page(s) the branch diff actually touches and reports silent-render regressions (page crashes, console errors, raw app.* translation keys, blocking asset 404s). Target URLs come from the overlay's get_visual_qa_targets(changed_files) — overlays opt in by mapping diff paths to URLs.
Ticket.extra['visual_qa'].report_markdown for a ## Visual QA section.t3 <overlay> pr create <ticket> --skip-visual-qa "<reason>" or T3_VISUAL_QA=disabled in the environment.t3 <overlay> pr create is mandatory (Non-Negotiable). Raw gh pr create / glab mr create is forbidden whenever the active overlay exposes a pr create subcommand. The CLI is not a convenience wrapper — it is the only path that runs the shipping gate (§ 4b: testing + reviewing + retro phases visited), the visual-QA gate (§ 4c), the title/description format validator, the ticket URL injection, the assignee defaults, and the fork-vs-upstream remote resolution. Bypassing it ships PRs that look published but have skipped every guard — exactly the failure mode that produced souliane/teatree#545 (PR pushed via gh pr create, shipping gate never ran, six rounds of follow-up review fixes).
Allowed exceptions (must hold all):
pr create subcommand (e.g., a brand-new overlay still being built). Verify with t3 <overlay> pr --help before assuming.t3 <overlay> pr create command itself and need a one-shot bypass to land the fix.If t3 <overlay> pr create errors, fix the error (create the missing session, run the reviewer, set the missing env var) — do not work around it with raw gh/glab. Treating the CLI as optional is the same anti-pattern as "Fix the CLI, Never Work Around It" in ../workspace/SKILL.md, applied to the shipping flow.
Closes/Fixes #N (Non-Negotiable)Before writing Closes #N or Fixes #N in a PR body, re-read the linked issue end-to-end and enumerate every acceptance criterion, phase, or deliverable it names. For each one, mark it ✅ shipped, ⚠️ partial, or ❌ not started, and paste the matrix into the PR body. Closes/Fixes is only legal when every row is ✅. Otherwise:
Relates-to #N (so the issue stays open).STOP — resolve the ticket URL before typing the glab command.
Before composing any glab mr create or glab mr update call, answer these three questions:
glab issue create) and copy the URL. Do NOT proceed without a URL.[none] if there is no flag.type(scope): description [flag] (ticket_url) — both --title and the first line of --description must be identical and match this format exactly.This gate exists because the CI validate_mr_title_and_description job fails on every MR that skips the ticket URL, causing a red pipeline that requires a separate fix push. The CI validator is the safety net — not the first line of defence. Always resolve the URL before creating the MR.
Read the project's delivery hooks reference (e.g. references/delivery-hooks.md) for the concrete MR creation template. Critical rules:
type(scope): description [flag_if_feat] (TICKET_URL)## Summary — that fails validation.t3 <overlay> pr create command handles the correct flags automatically.PreToolUse hook: A
validate-mr-metadata.shhook automatically intercepts MR create/update commands in project repos. It validates the title and description first line against the release-notes format rules and blocks non-compliant calls with a clear error. Fix the reported issues and retry — no manual validation needed.
When a PR ships work spanning more than one numbered phase of an issue (e.g. phase 3 of an 8-phase rewrite), the title MUST list every phase the diff covers — not only the lead phase. A reviewer must be able to read the title and know exactly what scope to expect.
feat(scope): <subject> (#NNN phase 3) — fine.feat(scope): <subject> (#NNN phases 1-6 + 8) — preferred range form.feat(scope): <subject> (#NNN phases 2, 3, 8) — explicit list.Why: A title that says "phase 3" while the diff also demolishes the dashboard (phase 2), adds the statusline file (phase 1), introduces the loop scaffolding (phase 4), and lands the no-leak gate (phase 8) is misleading. Reviewers gate their attention by title; mis-titling forces them to discover scope from the diff, which is the slow path. Past failure: PR #543 advertised "phase 3" but bundled phases 1, 2, 3, 3.6, 4, 5, 6, 8.
How to apply: before creating the PR, list the commits and decide which phase each commit belongs to. If the answer covers more than one phase, use the bundled form. The same rule applies when a description says "phase X" — the description's first line must match the title.
/t3:test).When fixing review comments on an already-existing MR:
git branch --show-current vs MR metadata). If the worktree uses a different branch name, resolve the mismatch before editing: either checkout the MR branch or plan to cherry-pick onto it after committing. Discovering the mismatch mid-push wastes time on branch gymnastics.git merge origin/main. Never rebase — the branch has already been reviewed.prek run --all-files or equivalent) after merging — merges can expose new lint violations in your code even without conflicts.Before touching the MR branch to "prepare" it for a merge, reason through what a clean 3-way merge would produce on its own:
json.dumps round-tripping normalizes unrelated formatting).Merge conflict resolution for JSON files:
result = theirs + (ours_keys − base_keys). This correctly applies the default branch's removals while keeping the MR's own additions.json.dumps to serialise back — it normalises indentation and whitespace across the entire file, producing a noisy diff far beyond the intended change. Remove keys surgically (line-by-line) to preserve original formatting.git checkout --ours on whole files — this discards the default branch's removals and reintroduces whatever it had cleaned up.After resolving conflicts, verify before asking anything:
When a CI failure (or any bug found during work) is pre-existing — not introduced by the current branch:
<prefix>-myproject-fix-flaky-test-ordering).How to detect: git diff origin/main...HEAD --name-only — if the failing file was never touched by the feature branch, the bug is pre-existing.
Before opening a new MR/PR, check whether a sibling MR for the same ticket is already open on the same repo:
gh pr list --repo <repo> --search "<ticket-ref> is:open" --json number,headRefName,baseRefName
If a sibling is open, do not open a second MR targeting the default branch — the two branches will diverge on the same files and the second one will need a painful 3-way merge. Pick one:
gh pr create --base <sibling-branch>). Update the base to the default branch after the sibling merges, so the stacked MR stays minimal.Never open two MRs on the same ticket targeting the default branch in parallel. The only exception is when the two MRs touch genuinely disjoint files (different repos, different modules with no shared imports, no overlapping generated docs) — and even then, the second MR's description must name the sibling PR it races with.
The ticket-ref query above misses retro fixes, skill edits, and other PRs without a ticket reference. Before opening any such PR, also run a content sweep against open and recently-merged PRs on the same repo and look for overlap on title keywords or touched files:
# Open PRs (parallel work in flight)
gh pr list --repo <repo> --state open --json number,title,headRefName
# Recently-merged PRs (work that landed minutes ago — same risk)
gh pr list --repo <repo> --state merged --limit 10 --json number,title,mergedAt
Match against:
git diff --name-only origin/main..HEAD on the local branch — for skill PRs especially, multiple agents/users converge on the same skills/<topic>/SKILL.md file.Treat a hit on either signal as a sibling and apply the same options (wait, stack, or bundle per ## Bundle Into an Existing Open PR below). If the hit is in the recently-merged list, run git fetch origin main && git log origin/main..HEAD — if the local diff is now empty, abandon the branch instead of pushing an empty PR.
When a session uncovers a small unique commit on a now-stale branch (typical during cleanup or retro), and opening a dedicated PR for that one commit would be more ceremony than the change deserves, bundle it into a sibling open PR instead. This trades a little PR-scope discipline for delivery speed.
Eligibility — all must hold:
Procedure:
t3 <overlay> workspace ticket <issue-url> — use the same issue as the target PR).git cherry-pick <sha>. Resolve any conflicts surgically.type(scope1): X + type(scope2): Y if the two are heterogeneous. Body explains both fixes.git worktree remove --force <path> + git branch -D <branch>).Anti-pattern: bundling into a PR that's already passed review. The reviewer's approval covered the original scope, not the bundled commit.
git log origin/<branch>..HEAD to confirm which commits are local-only. Even within local-only commits, only squash commits from the current work session — older commits on the branch that predate the current task are settled history. When the user says "squash what belongs together", ask which commit range is in scope rather than assuming the entire local history is fair game.../rules/SKILL.md § "Clickable References".t3 <overlay> workspace finalize.../rules/SKILL.md § "Publishing Actions Are Mode-Conditional". In interactive mode (default) every push/MR/merge/remote-delete needs separate explicit approval. In auto mode (t3.mode = "auto" or T3_MODE=auto) the agent ships end-to-end without confirm prompts; only the always-gated list (force-push to defaults, history rewrites on shared defaults, destructive shared-state ops, unauthorised external writes, --no-verify) remains confirm-gated.Co-Authored-By) live in the user's global agent config — check it before committing; when in doubt, omit the trailer.When rewriting commit messages, use filter-branch --msg-filter (matches by full hash). Do NOT use git rebase -i with GIT_SEQUENCE_EDITOR="sed" — the short hash may differ from git log --oneline, causing a silent no-op.
Post-rewrite verification (Non-Negotiable): After ANY rebase or filter-branch, verify the hash changed. Same hash = no-op.