A personal workflow harness wrapped around an AI coding agent. Single-author tool; the code lives in a public repo in case any of it is useful to anyone else.
Experimental: APIs and config keys may change without backwards-compatibility guarantees — the only obligation is that all registered overlays are updated in the same change.
Teatree sits at the shell, alongside the editor. It turns a ticket URL into a merged pull request by creating synchronized worktrees across the repos a ticket touches, provisioning isolated databases and ports, driving the work through code → test → review → ship phases, and keeping enough durable state to survive context loss and long waits.
Under the hood it is a Django project with a plugin system (overlays) that adapts it to a given set of repos, CI, and services.
graph TB
subgraph "t3 CLI"
direction LR
lifecycle["lifecycle<br/>worktree provisioning"]
workspace["workspace<br/>multi-repo setup"]
db["db<br/>database operations"]
run["run<br/>service management"]
pr["pr<br/>PR creation"]
followup["followup<br/>code host sync"]
end
subgraph "Loop & Statusline"
direction LR
scanners["scanners<br/>my PRs, reviewer PRs,<br/>assigned issues, Slack, Notion"]
statusline["statusline.txt<br/>3 zones: anchors / action / in flight"]
dispatch["dispatch<br/>turns signals into agent actions"]
end
subgraph "Claude Plugin"
direction LR
skills["lifecycle skills"]
hooks["hooks<br/>routing, guards, tracking"]
required["required skills<br/>superpowers, ac-*"]
end
scanners -->|"feed signals to"| dispatch
dispatch -->|"renders"| statusline
scanners -->|"query"| pr & followup
skills -->|"delegates to"| lifecycle & workspace & db & run & pr
hooks -->|"enforces"| skills
Each section below names a piece of friction the author kept hitting and what teatree does about it. None of these are framed as comparisons — other tools solve some of the same shapes well, and teatree borrows from them where it can.
The author kept ending up at one of two extremes: either babysitting every merge by hand (every PR a separate context switch back to the browser), or flipping on full auto-merge and watching the agent merge PRs against branches the reviewer had moved underneath. The first wastes the day; the second loses trust in a single bad merge.
Teatree puts a two-step contract between "looks done" and "merged". An orchestrator pass issues a per-diff CLEAR after an independent cold review; a separate merge worker re-verifies the live HEAD SHA, the green checks, and the non-draft state at the moment of merge, and refuses raw merge commands by default. Two independent passes have to agree on the same SHA before the host API gets called.
The author wanted the agent to handle Slack DMs, PR comments, MR approvals,
Notion writes — anything that normally needs a hand on the keyboard — without
the dread of finding out tomorrow that a message went to the wrong place or
under the wrong voice. Logs in ~/.shell_history are not enough; the agent's
own claim about what it posted is the thing that needs verifying.
Every on-behalf write records an OutboundClaim row, then re-reads the target
surface (Slack permalink, GitLab note, Notion block) and compares the live
content against the claim. A drift means the post landed wrong, was edited, or
never made it; teatree surfaces those as actionable rows rather than silent
failures. Combined with an approval gate (OnBehalfApproval), nothing
colleague-facing ships unless the user has explicitly opted into either
per-action approval or end-to-end autonomy for that overlay.
The author kept losing the picture every time a conversation ended or
context got compacted — what was in flight, which PR was waiting on what,
which review was half-done. Rebuilding it from gh pr list and chat
history every morning gets old.
Teatree puts the state into a Django-backed SQLite/Postgres database:
Ticket, Worktree, Task, PullRequest, each with its own state
machine. Transitions are guarded methods (Ticket.code(), Ticket.ship()),
not free-form field writes — the predicates run, the dependent gates stay
aligned, illegal moves raise InvalidTransitionError. Snapshots before
context compaction get recovered automatically on the next session start.
Not perfect, but more reliable than picking up the picture from scratch.
The author carries a laptop around and answers questions from a phone. Most agent harnesses assume a TTY: when the agent has a question, the run blocks until someone is in front of a terminal to answer. That makes long autonomous sessions impossible — every clarifying question becomes a hard stop.
Teatree resolves one operating mode whose posture says whether the user is
reachable. Under a deferring posture, structured questions become durable
DeferredQuestion rows instead of blocking; the user answers them later from
Slack, via t3 teatree questions answer. The agent keeps working on whatever it can in the meantime. Replies
and prompts the user receives all happen in Slack DMs; no shared dashboard,
no shared SaaS, no DevOps onboarding.
The author's typical ticket is not "edit one file"; it is "change the backend, the frontend, the translations bundle, and the CI config, all at once." Each repo needs its own isolated worktree, its own database, its own ports, so two tickets in flight do not collide on the same dev server.
t3 <overlay> workspace ticket <url> reads the ticket, decides which repos
are affected, creates one git worktree per repo under a single ticket
directory, allocates ports, provisions DBs, generates env files, and starts
the services. Each ticket runs in isolation; multiple tickets in flight share
no infrastructure. Overlay packages carry the project-specific glue
(which repos, which CI, which probes); the core stays generic.
The author kept noticing the agent only works while someone is actively prompting it. PRs sit waiting for review nudges, CI failures go unreviewed, ticket changes pile up in the inbox until someone glances at them.
A long-running /loop slot inside the interactive session ticks every ~12
minutes. Each tick fans out to scanners that watch assigned issues, open PRs,
PRs assigned for review, Slack mentions, the Notion bridge, and the local
task queue. Findings render to a statusline file the Claude Code statusline
hook reads in under 10ms, so live status sits at the top of every session
without polling. Lease-gated dispatch turns scanner findings into agent
actions when there is something to do, and keeps quiet when there is not.
A few honest scope statements, so anyone evaluating this knows what they are looking at:
- Not a shared corporate platform. Single-author tool. No multi-tenant SaaS, no team dashboard, no SSO. It uses the user's own GitLab / GitHub / Slack credentials and runs on the user's laptop.
- Not an IDE plugin. It lives at the shell. The editor stays whatever editor the user prefers.
- Not a replacement for the agent CLI. Teatree wraps the agent CLI (currently Claude Code; the agent runtime is pluggable) with state, loops, integrations, and skills. The agent does the creative work; teatree does the mechanical work.
- Not a stable, polished product. It is in motion, expected to break, expected to change shape. The author dogfoods it daily on real client work; bugs surface fast because every broken edge stops a real ticket.
Teatree coordinates work through four state machines — each transition is a
typed code path with tests, not a prompt the model might skip. The models live
in src/teatree/core/models/ (ticket.py, worktree.py, task.py,
pull_request.py).
Ticket — tracks a unit of work from intake to delivery. The lifecycle phases
(ticket → code → test → review → ship) drive corresponding ticket states. The
full Ticket.State set is not_started → scoped → started → planned → coded → tested → reviewed → shipped → in_review → merged → retrospected → delivered, plus
ignored for work that is consciously skipped. This diagram is generated from
the Ticket model's @transition decorators; edit the model, not the diagram
(scripts/hooks/generate_fsm_diagrams.py).
stateDiagram-v2
[*] --> not_started
not_started --> scoped : scope
not_started --> coded : code_direct
not_started --> reviewed : reconcile_reviewed
not_started --> merged : reconcile_merged
not_started --> review_posted : mark_review_no_action
not_started --> review_posted : mark_reviewed_externally
not_started --> ignored : ignore
scoped --> started : start
scoped --> coded : code_direct
scoped --> reviewed : reconcile_reviewed
scoped --> merged : reconcile_merged
scoped --> review_posted : mark_review_no_action
scoped --> review_posted : mark_reviewed_externally
scoped --> ignored : ignore
started --> started : start
started --> planned : plan
started --> coded : code_direct
started --> reviewed : reconcile_reviewed
started --> merged : reconcile_merged
started --> review_posted : mark_review_no_action
started --> review_posted : mark_reviewed_externally
started --> ignored : ignore
planned --> coded : code
planned --> reviewed : reconcile_reviewed
planned --> merged : reconcile_merged
planned --> review_posted : mark_review_no_action
planned --> review_posted : mark_reviewed_externally
planned --> ignored : ignore
coded --> started : rework
coded --> tested : test
coded --> reviewed : reconcile_reviewed
coded --> merged : reconcile_merged
coded --> review_posted : mark_review_no_action
coded --> review_posted : mark_reviewed_externally
coded --> ignored : ignore
tested --> started : rework
tested --> reviewed : reconcile_reviewed
tested --> reviewed : review
tested --> merged : reconcile_merged
tested --> review_posted : mark_review_no_action
tested --> review_posted : mark_reviewed_externally
tested --> ignored : ignore
reviewed --> started : rework
reviewed --> reviewed : reconcile_reviewed
reviewed --> shipped : ship
reviewed --> merged : reconcile_merged
reviewed --> review_posted : mark_review_no_action
reviewed --> review_posted : mark_reviewed_externally
reviewed --> ignored : ignore
shipped --> started : reopen
shipped --> shipped : ship
shipped --> in_review : request_review
shipped --> merged : reconcile_merged
shipped --> ignored : ignore
in_review --> started : reopen
in_review --> reviewed : reconcile_reviewed
in_review --> merged : mark_merged
in_review --> merged : reconcile_merged
in_review --> ignored : ignore
merged --> started : reopen
merged --> reviewed : reopen_for_followup
merged --> merged : mark_merged
merged --> merged : reconcile_merged
merged --> retrospected : retrospect
merged --> ignored : ignore
retrospected --> started : reopen
retrospected --> reviewed : reconcile_reviewed
retrospected --> retrospected : retrospect
retrospected --> delivered : mark_delivered
retrospected --> ignored : ignore
delivered --> started : reopen
delivered --> reviewed : reopen_for_followup
review_posted --> review_posted : mark_review_no_action
review_posted --> review_posted : mark_reviewed_externally
Worktree — one repo checkout inside a ticket's workspace. This diagram is
generated from the Worktree model's @transition decorators; edit the model,
not the diagram (scripts/hooks/generate_fsm_diagrams.py).
stateDiagram-v2
[*] --> created
created --> created : teardown
created --> provisioned : provision
provisioned --> created : teardown
provisioned --> provisioned : db_refresh
provisioned --> provisioned : provision
provisioned --> services_up : start_services
services_up --> created : teardown
services_up --> provisioned : db_refresh
services_up --> provisioned : start_failed
services_up --> provisioned : stop_services
services_up --> services_up : start_services
services_up --> ready : verify
ready --> created : teardown
ready --> provisioned : db_refresh
ready --> provisioned : stop_services
ready --> services_up : start_services
ready --> ready : verify
Task — claimable work unit with lease and heartbeat. Unlike the others,
Task advances through guarded methods (claim, complete, fail, reopen)
that take a row lock and a lease rather than @transition decorators, so this
diagram is illustrative and maintained by hand, not generated.
stateDiagram-v2
[*] --> pending
pending --> claimed: claim
claimed --> completed: success
claimed --> failed: error
claimed --> pending: lease_expired
PullRequest — tracks delivery state on the code host. This diagram is
generated from the PullRequest model's @transition decorators; edit the
model, not the diagram (scripts/hooks/generate_fsm_diagrams.py).
stateDiagram-v2
[*] --> open
open --> review_requested : request_review
open --> merged : mark_merged
open --> closed : mark_closed
review_requested --> approved : approve
review_requested --> merged : mark_merged
review_requested --> closed : mark_closed
approved --> merged : mark_merged
approved --> closed : mark_closed
These models are surfaced in a small Django admin dashboard. A rendered HTML
snapshot of that dashboard is generated through Django's test client and
drift-checked in CI, so it stays an always-fresh "screenshot":
docs/generated/dashboard/admin-index.html
(scripts/hooks/generate_dashboard_snapshot.py).
The CLI gets the same treatment: the rendered output of the canonical t3
commands (t3 --help, t3 loop --help) is captured deterministically and
drift-checked, an always-fresh fixture that complements the exhaustive CLI
reference:
docs/generated/cli/representative-output.md
(scripts/hooks/generate_cli_output_snapshot.py).
Every state change goes through a method with code behind it. Ticket,
Worktree, and PullRequest use django-fsm-style @transition decorators
that declare the legal source and target states; Ticket.code() requires
state == STARTED, Ticket.ship() requires state == REVIEWED, and so on.
Task status moves through guarded methods (claim, complete, fail,
reopen) that take a row lock and a lease, raising InvalidTransitionError
on an illegal move. Agents do not write to these fields directly; they call
the transition, and the transition enforces its own preconditions. The same
rule applies to the CLI: any command that affects a state machine calls into
a transition, never mutates the field.
Agents read skills to do the creative work (writing code, reviewing a diff, choosing how to test); the CLI owns the mechanical work (branching, ports, DB refresh, pipeline waits, PR validation). Three interfaces sit on top:
- CLI (
t3 ...) — the source of truth. Everything else is a view on top. - Loop & Statusline — a long-running
/loopslot scans signals, dispatches actions, renders a statusline file the Claude Code hook reads on every prompt. - Claude plugin — skills and hooks that teach an agent how to drive the CLI.
The core of teatree. Django management commands handle everything deterministic: state machines, port allocation, database provisioning, worktree creation, PR validation, code host sync. Tested with >90% branch coverage — no prose, no model variance.
t3 teatree worktree provision # provision worktrees, DBs, ports for a ticket
t3 teatree worktree start # start all services
t3 teatree workspace ticket # create multi-repo worktrees from a ticket URL
t3 teatree db refresh # restore a database dump
t3 teatree pr create # create a pull request with metadata validation
t3 teatree followup sync # sync tickets and PRs from code host
t3 goal set/list/clear # register / list / clear a standing verified-green goal (a Stop-gate blocks a loop turn ending "as done" while the goal's check command is red)
t3 cost # cycle-to-date SDK-equivalent spend + effective-token (ET) totals, split by subscription/metered lane
t3 capabilities --json # machine-readable registry of which t3 commands emit JSON and their exit-code contract (a front-end drives teatree from this)
t3 speak # read text aloud on local speakers per [teatree.speak] (no-op unless local = all)
t3 recover # find/recover work stranded by a network-outage death (dry-run by default)
t3 push [--repo p] [--remote r] [--branch b] [--force-with-lease] # the supported push path from the worker container: resolves the forge token (GH_TOKEN → TEATREE_GH_TOKEN → the overlay's pass store) and hands it to git as env only, disables every interactive credential prompt so a missing token fails fast instead of hanging, and refuses a remote whose URL embeds a credential. Never passes --no-verify — the pre-push hooks still run
t3 fast-push [-m msg] [--remaining txt] # leak-gated escape hatch for session hand-offs: stage → in-process leak gates (banned-terms, secret-scan, overlay-leak, public-repo author-identity; fail-closed) → commit → push → create-or-update the PR; skips every non-leak gate; any finding refuses the push
t3 mutation run # scoped mutation testing — mutate only the high-value safety modules a diff touches
t3 hook run <name> [args...] # run a packaged portable repo-quality gate by name (module-health, no-silent-skip, broad-except, test-shape, test-path-mirror, refuse-main-clone-commit) — no teatree.__file__ shim; `t3 hook list` names them. A consuming repo wires them via `entry: t3 hook run <name>` or pins `repo: <teatree-url>` against the root .pre-commit-hooks.yaml
t3 ui # browse and run the whole command tree in a terminal UI (needs `uv sync --group ui`)
t3 admin # run the Django admin for the teatree project under a local gunicorn server (WSGI, not runserver)
t3 mcp serve # serve teatree's structured search (tickets, worktrees, tasks, loop stats, incoming events) + gate-preserving writes as an MCP server over stdio
# registered automatically via the plugin-bundled .mcp.json (surfaces as mcp__teatree__* tools) — `t3 setup`/`t3 doctor check` verify it
t3 notion whoami|doctor # headless Notion access via an integration token (no interactive connector, so a scheduled run reaches a page at all): verify the token / triage one page (token valid, page shared, page still LIVE)
t3 notion fetch <page> # read a page as Markdown (or raw blocks), optionally with its open comments; refuses an ARCHIVED page with its own exit code and names the successor, because a dead page renders exactly like a current one
t3 notion audit-fetch <page> # read a DEAD page for a postmortem — deliberately its own command so it is not reachable by habit
t3 notion comments|append|query # list open comments; append at the end of a page; query a database/data source as JSON
t3 notion section show|replace # the owned-section write primitive — block-scoped, so replacing one heading's body leaves every discussion on the rest of the page intact (there is deliberately no whole-page replace)
t3 notion comment post # marker-keyed, so a caller that forgets the flag under-posts rather than double-posts
t3 notion property get|set # read/write one page property — the poll a block-tree fetch cannot answer; every write re-reads and refuses to report success unless the change landed
t3 dream run [--since <iso>] [--dry-run] # run one memory-consolidation pass NOW (ignores cadence)
t3 dream tick # cadence-gated pass (~04:00 slot); the worker's off-live-tick driver chain fires it, decoupled from the live loop
t3 outer status|history # T4 autoresearch outer loop — guard-chain verdict + experiment ledger (read-only)
t3 outer tick # cadence-gated step the worker's off-live-tick driver chain fires (propose→ratify→measure→keep-only-if-better; ships quadruple-OFF)
t3 notion whoami|fetch|audit-fetch # read a Notion work item headlessly through the overlay's routed token — `whoami` proves the token resolves, `fetch` pulls a page (optionally one section), `audit-fetch` records what was read so a review can show its retrieval
t3 directive capture "<text>" [--scope <overlay>] # record a plain-language directive about teatree's own behaviour (verbatim, CAPTURED)
t3 directive list|status <id>|history # inspect the directive ledger, one directive's sketch/state, decisions (read-only)
t3 directive tick # cadence-gated step the worker's off-live-tick driver chain fires (implement→configure→verify→keep-or-revert; ships triple-OFF)
t3 directive resolve-revert <id> [--revert-sha <sha>] # close a REVERT_PENDING directive to terminal REVERTED (config already rolled back)Replace
teatreewith your overlay's name (t3 <overlay>) when working in another overlay.
t3 ui is a trogon-backed browser for
the full t3 command tree (core plus every installed overlay). It is in the
optional ui dependency group — install it with uv sync --group ui before the
first run.
t3 admin runs the Django admin for the teatree project under a local gunicorn
server (teatree.wsgi:application, a production WSGI server — not Django's dev
runserver; http://127.0.0.1:8000/admin/ by default). It applies migrations,
collects static into STATIC_ROOT (so WhiteNoise serves the admin and dashboard
assets with DEBUG off), ensures a superuser exists — creating one
non-interactively when absent and printing its generated password (override via
T3_ADMIN_USER / T3_ADMIN_PASSWORD) — and opens the browser at /admin/
(--no-browser to skip; --host / --port to override). The admin binds to the
same teatree database every other t3 command reads, so no overlay context is
needed.
The singleton t3 worker drives the day (#1796 / PR-28, default ON): it drains one
self-rescheduling loop_timer chain per enabled DB Loop row, each firing
t3 loops tick --loop <name> on its own cadence, so the loops run with no Claude
Code session open. Those ticks fan out to scanners that watch assigned issues, open
PRs, PRs assigned for review, Slack mentions, the Notion → GitLab bridge, and the
local task queue. Findings render to
${XDG_DATA_HOME:-~/.local/share}/teatree/statusline.txt (three zones: anchors /
action needed / in flight). The Claude Code statusline hook cats that file in
<10ms, so live status sits at the top of every session without polling.
# Run the worker (the cadence owner). Bare `t3 worker` is the run alias:
t3 worker
# Check the worker: live flock holder, resolved loop_runner_enabled + source, timer counts:
t3 worker status # --json for a machine-readable payload
# Ensure one is running (spawns a detached worker iff enabled AND the flock is free):
t3 worker ensure # refuses (with the reason) when OFF or already running
# Spawn a Claude Code session (registers the reactive infra loops: self-improve/slack-answer/drain-queue):
t3 loop start
# Enable/disable an individual loop (the reconciler adds/prunes its timer at once):
t3 loop enable <name>
t3 loop disable <name>
# Out of band, run one by-hand full-scan tick or read the last-rendered statusline:
t3 loop tick
t3 loop status
# List the DB-configured autonomous loops (name, enabled, delay, last run, next due):
t3 loops listThe cadence is configurable via T3_LOOP_CADENCE (seconds), or by setting
loop_cadence_seconds in the teatree DB (t3 <overlay> config_setting set loop_cadence_seconds 720; env wins; default 720).
loop_runner_enabled is the kill-switch — set it false to stop the loops
entirely (there is no fallback plane; PR-28 retired the native /loop cron mirror).
On a headless box with no Claude session ever opening, start t3 worker once from a
login profile.
Wire up the Claude Code statusline hook so the rendered file actually shows
in the bottom bar. This is a top-level statusLine key in
~/.claude/settings.json — enabling the t3 plugin does not wire it for
you: a plugin's settings.json only honours the agent and
subagentStatusLine keys, so a statusLine declared there is silently ignored.
Point the command at an absolute path to the script (the user-level settings
file does not expand ${CLAUDE_PLUGIN_ROOT}):
{
"statusLine": {
"type": "command",
"command": "bash /absolute/path/to/teatree/hooks/scripts/statusline.sh"
}
}Skills and hooks that drive AI-assisted development. Each skill covers one phase
of the development lifecycle — ticket intake, coding, testing, review, shipping
— and contains the methodology, guardrails, and domain knowledge the agent needs
to do the work well: TDD discipline, debugging process, review checklists, retro
learning, verification rules. Skills declare dependencies (requires:,
transitive) — including methodology skills from third-party packages like
superpowers. Hooks handle automatic skill
routing, branch protection, and session tracking.
Skills use the CLI for infrastructure (worktrees, databases, ports, CI), but the actual development work — writing code, reasoning about architecture, reviewing diffs, running retros — is guided by skill content, not CLI commands.
A few rules in the lifecycle skills are non-negotiable. They exist because each one prevents a specific class of failure that has bitten a real session:
- PRs go through
t3 <overlay> pr create. Rawgh pr create/glab mr createskips the shipping gate (testing + reviewing phases), the visual-QA gate, and the title/description validator. The CLI is the only path that runs every guard; using it is mandatory whenever the overlay exposes the subcommand. - The
reviewingphase is satisfied by an independent sub-agent, not by self-review. Before push, the implementing conversation spawns thet3:reviewersub-agent (read-only, no edits) and applies its findings. Self-review against repo rules is a complement, not a substitute — the implementer's context carries the same blind spots that allowed the gap. - State machine changes happen via transitions, never via direct field
writes. This holds for both code and CLI: every command that affects a
state machine must call into a transition (
Ticket.code(),Ticket.review(),Ticket.ship(), etc.) so the predicates run and the dependent gates stay aligned. - Mass renames and cross-cutting refactors require an exhaustive sweep
before "done". A single
rgpass is not enough — the agent runs every surface form (plain, quoted, attribute access, subscript, CamelCase variants, sibling repos) and confirms zero hits before claiming the rename is complete. - A PUBLIC-repo PR never auto-merges unless its author is trusted. On a
public repo anyone who is not the user is a potential malicious actor, so the
merge keystone refuses to auto-merge a PR whose author is not one of the
user's known identities (fail-closed: an unknown, empty, or unfetchable
author is refused; an unresolvable repo visibility is treated as public).
Private/internal repos skip the check entirely — the user owns access
control there. The trusted set lives in the DB; manage it with
t3 identities {seed,add,list,remove}(the configureduser_identity_aliasesis the fallback during the config-to-DB migration window). The same trust classifier flags an untrusted public-repo PR as adversarial across the reviewing scanners, so a malicious PR is never treated like a colleague's.
These rules live in the ship, review, code, and rules skills. The CLI
enforces what it can mechanically (gate checks, transition predicates); the
skills carry the rest.
Prerequisites: Python 3.13+, uv.
Teatree is not on PyPI. Install the t3 CLI straight from the repo:
uv tool install --from git+https://github.com/souliane/teatree.git teatree \
--overrides https://raw.githubusercontent.com/souliane/teatree/main/uv-overrides.txt # installs `t3` globally
apm install -g souliane/teatree # installs skills + companion dependencies
t3 setup # links plugin, syncs skills, migrates self-DB
t3 startoverlay my-overlay ~/workspace/my-overlayuv tool install puts t3 in ~/.local/bin/. If that directory is not on your
PATH, add export PATH="$HOME/.local/bin:$PATH" to your shell rc.
--overrides is required, not optional: claude-agent-sdk declares an mcp bound
broader than the surface it imports, and uv tool install does not read the
[tool.uv] override-dependencies that corrects it — so without the flag the install
fails with an unsatisfiable-requirements error. See
uv-overrides.txt.
Installing the plugin does not force teatree on. By default a fresh Claude
session does not auto-engage teatree — no skill auto-suggest, no load-block, no
loop scheduling — and just shows a one-line how-to. Run /teatree (or load any
t3: skill) to engage teatree for that session, or set autoload in the teatree
DB (t3 <overlay> config_setting set autoload true; env T3_AUTOLOAD=1) to
auto-engage every session.
Fork the repo, then:
git clone git@github.com:YOUR_USERNAME/teatree.git ~/workspace/teatree
cd ~/workspace/teatree
uv tool install --editable . --overrides uv-overrides.txt # global `t3`, live-reloaded from this clone
t3 setup # installs skills globally, respects local symlinksNew here? docs/MAP.md lists every package directory with a one-line
purpose and links to the relevant BLUEPRINT.md section — read it first to
find where something lives.
uv tool install --editable . --overrides uv-overrides.txt produces the same global ~/.local/bin/t3 as
the user flow — edits in this clone take effect on the next invocation, no
uv run prefix. t3 setup runs APM to
install companion dependencies (superpowers, ac-django, etc.), symlinks teatree
skills to ~/.claude/skills/, registers the Claude plugin in
~/.claude/plugins/installed_plugins.json with installPath pointing at the
clone so hooks and agents always read from the live checkout, applies any
pending self-DB migrations, and — if t3 is not on PATH — re-runs
uv tool install --editable . (with the overrides file) to self-install. Must be run from the main
clone, not a worktree.
t3 setup also self-heals when teatree adds a new dep: editable installs do
not auto-resync their venv when pyproject.toml changes, so on every run
t3 setup compares the declared [project].dependencies against the dists in
the running interpreter and re-runs uv tool install --editable . --reinstall
(with the overrides file) automatically when anything is missing. After the reinstall, setup re-execs
itself against the refreshed venv. No manual --reinstall step is needed when
pulling teatree updates.
Each skill teaches the agent one phase of development:
graph LR
ticket["ticket<br/>(intake)"] --> code["code<br/>(implement)"]
code --> test["test<br/>(verify)"]
test --> review["review<br/>(inspect)"]
review --> ship["ship<br/>(deliver)"]
retro["retro<br/>(orchestrator-level)"] -.-> ticket
ship --> rr["review-request<br/>(notify)"]
debug["debug<br/>(troubleshoot)"] -.-> code
debug -.-> test
followup["followup<br/>(batch)"] -.-> ticket
workspace["workspace<br/>(provision)"] -..-> code & test & review & ship
| Skill | Phase |
|---|---|
ac-reviewing-codebase |
Periodic holistic architectural review — the third of teatree's three review tiers (design-time architecture-design, per-PR deterministic check_antipatterns.py, periodic holistic ac-reviewing-codebase). Walks the whole tree for judgement-tier anti-patterns and BLUEPRINT.md staleness that no single diff can catch, implements what it finds, and pushes one PR. Dispatched automatically by ArchitecturalReviewScanner on a time or merge-count cadence — not user-invoked. |
answerer |
Draft a reply to an inbound question, DM the user for approval, post on confirmation |
architecture-design |
Architecture pre-check companion. Loaded transitively by implementation skills (code, ticket-for-features, retro-for-skill-changes) to force an architecture pass — BLUEPRINT alignment, FSM phase boundaries, extension-point contracts, component boundaries, dependency direction, test surface, resilience invariants, removability — BEFORE any code is written. |
checking |
The check-in surface — a SHORT "what did I miss" report, the session task/TODO lists, the pending deferred questions, and the daily follow-up routine (new tickets, ticket statuses, PR reminders) |
code |
Writing code with TDD methodology |
contribute |
Push retro improvements to a branch, open a PR, and optionally create upstream issues |
debug |
Troubleshooting and fixing — something is broken, find and fix it |
directive |
Submit a plain-English directive about how teatree itself should behave — captured verbatim, interpreted into a typed mechanism sketch, human-ratified via Slack/questions, then implemented through the gated pipeline |
dogfooding |
Dogfooding teatree's own CLI, loop, and statusline — two modes sharing one mechanics section for reading a tick and the rendered statusline. "Verify a change" is the run-it-yourself checklist applied after modifying CLI/loop/statusline code, before declaring it done. "Hunt for bugs" is proactive self-QA — dogfood the deployed loop, find/dedupe/confirm real bugs, file them, then fix them in worktrees |
dreaming |
Runs the idle-time "dreaming" memory-consolidation pipeline end to end with one command — replay recent transcripts + curated memories, distil drift into the ConsolidatedMemory ledger, cross-link / re-index / decay the memory files, run the §4 acceptance gates, triage each row into keep-as-memory vs core-gap → drive each core gap to a MERGED fix under the standing umbrella issue, and promote/stage eval candidates |
e2e |
End-to-end testing with Playwright — writing tests, running them, visual snapshots, test-plan posting, and the pre-push visual QA gate |
e2e-review |
Reviewer-side quality gate for Playwright end-to-end specs. Load when reviewing a new or changed E2E test, deciding whether a spec is ready to land, or adopting an outside Playwright suite. Judges specs against Playwright's published best practices — user-visible behaviour over implementation, resilient role/label/test-id locators, web-first auto-retrying assertions instead of hard waits, per-test isolation, page-object structure, and runnable evidence — and tells the implementer what to fix before approval. |
handover |
Use when the user wants to hand all current work from one Claude session to another (or to a not-yet-existing session) with a single command, or to transfer an in-flight TeaTree task from Claude to another runtime, or asks whether it is time to switch because Claude usage is getting high. |
health |
Read and act on the global operational-health chip — the green/yellow/red factory-health verdict and its known-issues registry |
interactive |
ENGAGES TEATREE FOR THE SESSION, and holds the standing rule that no work-bearing state is terminal. Loading this skill — or any skill declaring requires: interactive — writes the .teatree-active marker, one of the two conditions in _loop_auto_load_active() that arm the loop and statusline (#256); a session that never loads it stays unengaged, by design. Also holds teatree's Claude Code harness wiring: how skills are selected, how plugin hooks are registered, and which output belongs to the headless pipeline. Load it when ending an interactive session, when a session-end report names stranded work, or when deciding what to do with uncommitted, unpushed, untracked or unmerged work. Teatree's own architecture and coding rules are /t3:internals; the dogfooding procedure is /t3:dogfooding. |
internals |
How teatree is BUILT and how to change it safely — architecture, lifecycle phases, key models, the overlay API, the t3 CLI reference, and the management-command rules whose violation fails SILENTLY (a typer.Exit under call_command exits 0, so CI reports green on a real failure). Load it when writing or reviewing teatree's own code, or when building an overlay on it. Carries no Claude Code harness wiring — that is /t3:interactive — and no dogfooding procedure — that is /t3:dogfooding. |
mode |
The operating mode — one named posture (reachable / unattended / holiday) that decides whether AskUserQuestion asks the user now or captures a durable DeferredQuestion row, and which loops run |
next |
Wrap up the current session — retro, structured result, pipeline handoff. |
platforms |
Platform-specific API recipes for GitLab, GitHub, Slack, and X (Twitter). Auto-loaded as a dependency by skills that interact with these platforms. |
prompts |
Trigger and manage reusable prompts — list the prompts in the DB, render one by name with its templated params, and point to the admin for authoring + version history |
retro |
Conversation retrospective and skill improvement |
review |
Code review — self-review before finalization, giving review, receiving review feedback |
review-request |
Batch review requests — discover open PRs, validate metadata, check for duplicates, post to review channels |
rules |
Cross-cutting agent safety rules — clickable refs, temp files, sub-agent limits, UX preservation. Auto-loaded as a dependency by other skills. |
running-evals |
Single in-session entrypoint that auto-orchestrates the whole eval picture — model-free deterministic lanes (the eval-coverage gate t3 eval coverage, pinned-regressions) plus the transcript AI/trajectory lane (prepare → produce transcripts in-session → grade) — and prints one unified results table |
scanning-news |
Scans today's TLDR AI and The Rundown AI editions for ideas that could improve teatree, fetches the full article for promising items, and hands each concrete t3-improvement candidate back through the result envelope's article_suggestions field. The loop queues each behind the ask-gate (PendingArticleSuggestion) for per-article user approval before any souliane/teatree issue is filed, and DMs the batch to the user |
setup |
Bootstrap and validate teatree for local use — prerequisites, config, skill symlinks, optional agent hooks, and Django project scaffolding |
ship |
Delivery — committing, pushing, creating MR/PR, pipeline monitoring, review requests |
slack-formatting |
Rendering tables and formatting messages for Slack — the native Block Kit table block, the monospace fence fallback, and the mrkdwn gotchas (no pipe tables, single-asterisk bold, angle-bracket links). Auto-loaded as an overlay companion for work that posts to Slack. |
sweeping-prs |
Maintenance sweep across all your open PRs/PRs — merge the default branch, fix conflicts, monitor CI, push, and (per-repo policy) optionally squash-merge each PR before moving to the next. Never rebases |
sweeping-tickets |
Evidence-gated ticket/issue consolidation and triage — classify every open issue against current main, then consolidate by merging related tickets INTO AN EXISTING ticket — never by minting a new umbrella row and never by discarding ideas — and close only what is demonstrably shipped or now folded into its host. Always asks the operator for the maximum number of tickets to keep before triaging — never assumes a number. Dry-run first; close only on user approval (or auto-close ONLY the high-confidence "shipped by merged PR #X" class), posting a one-line reason on every close |
sweeping-worktrees |
Use when sweeping stale, lost, or abandoned worktrees, branches, or stashes that are NOT actively being worked — deciding per item whether to salvage unmerged work to a fresh PR, delete a shipped/superseded/redundant item, push post-merge commits to a new PR, or keep an uncertain one. The judgment layer over t3 <overlay> workspace emit / salvage / clean-all (the mechanical reaper is /t3:workspace) |
test |
Testing, QA, and CI — running tests, analyzing failures, quality checks, CI interaction, test plans, and posting testing evidence |
ticket |
Ticket intake and kickoff — from zero to ready-to-code |
triaging-issues |
Review and act on the needs-triage assessor's queued recommendations — list PENDING PendingTriageRecommendation rows, approve or reject each, and on approval run gh issue close/edit/comment then stamp the row |
update |
WHEN to bring teatree core and registered overlays up to date with their default branch, and the safety guarantees of doing so |
wip |
The bounded-WIP throughput dial — slow / medium / full / boost — plus the WRITE-parallel / MERGE-serial phase split and the per-ticket unattended delivery cycle. boost keeps boost_concurrency = N workers live; full arms a self-sustaining boost loop; medium (baseline) and slow cap concurrency |
workspace |
Environment and workspace lifecycle — worktree creation, setup, DB provisioning, dev servers, cleanup |
Teatree adds a small schema on top of Claude Code's standard SKILL.md
frontmatter so a skill can declare what it needs loaded alongside it:
---
name: ship
requires: [rules, platforms, verification-before-completion]
---requires— the single skill-dependency edge, resolved transitively in topological order with cycle detection. A required skill with noSKILL.mdin this repo (an external methodology skill from obra/superpowers, installed via APM, never modified by teatree) passes through so theSkilltool still loads it.
Skill loading is fully explicit — slash commands (/t3:ship), phase mapping
(t3 agent --phase shipping), ticket status, the requires chain, and
cwd/overlay context. There is no free-text scan of the prompt: the
UserPromptSubmit hook surfaces only the framework / overlay / companion
skills a prompt's cwd context implies, and PreToolUse blocks Python code
edits until those load.
See BLUEPRINT.md § 11.5 for the explicit-loading model and
docs/claude-code-internals.md for how the
hooks wire into Claude Code.
Teatree's core is generic — it does not know about specific repos, CI, or
environment defaults. Project-specific behaviour lives in a lightweight overlay
package that subclasses OverlayBase and registers via the teatree.overlays
entry point. The overlay carries the project's repos, provisioning steps,
runtime metadata, and service hooks; the core stays the same across projects.
Create one with:
t3 startoverlay my-overlay ~/workspace/my-overlayThe overlay registers via a teatree.overlays entry point:
[project.entry-points."teatree.overlays"]
my-overlay = "myapp.overlay:MyOverlay"Once installed (pip install -e .), the overlay is auto-discovered at startup.
The overlay implements the narrow contract teatree needs: managed repos,
provisioning steps, runtime metadata, and project-specific service hooks. See
docs/overlay-api.md for the full API.
Overlays can live anywhere; they do not need to be vendored into this repo. The author dogfoods this on a private client-codebase overlay; the same extension point is what any other consumer would use.
Teatree stores its config in the teatree DB — the ConfigSetting store, set with
t3 <overlay> config_setting set <key> <json> (add --overlay <name> to scope a
value to one overlay, omit it for the global default). Every key is optional; the
table below lists the ones most users touch. The full set and their defaults
live in UserSettings in src/teatree/config/settings.py. Overlays register via
teatree.overlays entry points plus the DB overlays registry row.
t3 <overlay> config_setting set mode interactive # "auto" (default) | "interactive"
t3 <overlay> config_setting set privacy '""' # privacy-scan profile name
t3 <overlay> config_setting set contribute false # enable skill self-improvement
t3 <overlay> config_setting set excluded_skills '["my-custom-skill"]' # extra skills to exclude
t3 <overlay> config_setting set loop_cadence_seconds 720 # loop tick interval (default 12 min)
t3 <overlay> config_setting set require_human_approval_to_merge true # auto mode: still gate merge on a 👍 / /merge
t3 <overlay> config_setting set require_human_approval_to_answer true # gate t3:answerer behind a DM confirmation
t3 <overlay> config_setting set agent_signature false # append an AI signature to posts (default off)| Key | Default | Effect |
|---|---|---|
workspace_dir |
~/workspace |
Root for per-ticket workspace directories |
mode |
auto |
auto is end-to-end; interactive confirms before publishing |
privacy |
"" |
Named privacy-scan profile applied before pushes |
contribute |
false |
Allow t3:retro to write fixes into core skills |
excluded_skills |
[] |
Skills excluded on top of the built-in exclusions |
loop_cadence_seconds |
720 |
Default cadence (seconds) for a loop's ticks |
require_human_approval_to_merge |
true |
In auto mode, merge still needs a 👍 / /merge |
require_human_approval_to_answer |
true, collapsed to false by the shipped autonomy = full |
t3:answerer drafts a reply and DMs for approval. The answer's own post is separately gated by on_behalf_post_mode, which no tier collapses |
on_behalf_post_mode |
draft_or_ask |
Pre-gate on any post made under your identity to a colleague surface. Read unchanged by every autonomy tier — opening it is its own explicit immediate |
agent_signature |
false |
Whether posts made on your behalf carry an AI signature |
The t3:contribute skill's push gate is the T3_PUSH environment variable
(default false), not a TOML key — it exists as a deliberate stop for
privacy review before any skill improvement leaves the machine.
Run t3 setup after changing config to apply changes to skill
symlinks and caches.
mode (a DB-home setting, or the T3_MODE env var) controls how much autonomy
the agent has for publishing actions:
interactive(conservative on security) — the agent pauses for explicit approval before push, MR create, MR merge, Slack posts, or any other write that leaves the local machine.auto(shipped default) — end-to-end autonomy. The agent ships complete features without confirm prompts: push → MR create → pipeline watch → merge → clean up remote branches. Quality gates (lint, tests, migrations check) still run; they just do not depend on user confirmation. A small always-gated list remains regardless of mode: force-push to default branches, history rewrites on shared defaults, destructive shared-DB operations, and external writes the active overlay has not authorised.
Unknown values raise an error — a typo in mode will never silently downgrade
to a less-safe mode.
mode lives in the teatree DB ConfigSetting store — there is no TOML key for
it (a [teatree] mode / [overlays.<name>] mode value is ignored on read and
warned about). Set it globally or per-overlay, so you can run auto on a personal
dogfooding overlay while keeping interactive on a client project:
t3 <overlay> config_setting set mode interactive # global default
t3 <overlay> config_setting set mode auto --overlay my-project # per-overlay overrideThe resolution chain is, first match wins: T3_MODE env var → the active
overlay's per-overlay DB row → the global DB row → the shipped default (auto).
An autonomous autonomy tier also pins mode = auto unless a per-overlay or env
mode says otherwise. mode is one of the per-overlay-overridable keys; the full
registry is OVERLAY_OVERRIDABLE_SETTINGS in src/teatree/config/settings.py.
See BLUEPRINT.md § 10.1.1 for the full details.
After every non-trivial session, the retro skill runs a retrospective,
extracts what went wrong, and writes fixes back into skill files. When
contributors enable this (t3 <overlay> config_setting set contribute true),
improvements flow back upstream through a fork-based model.
Where improvements go:
contribute = false(default): improvements go to the project overlay onlycontribute = true: the agent also improves core skills, pushes to a branch, opens a PR
Nothing is ever pushed without explicit consent. The contribute skill shows
exactly what will be pushed, runs privacy scans, and checks fork divergence
before creating PRs.
# Run tests locally — the diff-scoped lane is the default; CI's sharded lane is the authority
bash dev/test-affected.sh # only the tests the diff affects (`--full` for the whole suite)
bash dev/test-cov.sh # coverage lane: --cov --doctest-modules, 93% floor (CI parity)
# Pre-commit checks
prek run --all-files # ruff, codespell, banned-termsE2E tests run via t3 <overlay> e2e run, which dispatches to an in-repo
pytest-playwright runner or an external playwright repo based on the overlay's
overlay.metadata.get_e2e_config(). Overlays declare "runner": "project" or
"runner": "external"; the runner is overlay-agnostic from the call site:
t3 <overlay> e2e run # CI default
t3 <overlay> e2e run --no-docker # run against the local stack
t3 <overlay> e2e run --update-snapshots # accept new snapshotsTeatree itself ships no in-repo E2E suite — the top-level e2e/ directory holds
only the /t3:e2e-skill conventions doc. Each overlay owns its own specs, runner
configuration, and failure-triage artifacts (Playwright videos, traces, server
logs); where those artifacts land and how CI attaches them is the overlay
runner's concern, driven by its own pytest-playwright / playwright config.
Skills are prompt instructions — they control what your AI agent does. This makes the supply chain a security surface.
Safe defaults: self-improvement is off, pushing is disabled, and there is
no auto-update mechanism. All pushes go to branches (never main) and require
a PR. APM dependencies are pinned to specific commit SHAs in apm.yml.
Supply chain: t3 setup verifies that skills are loaded via symlinks to
the local clone — not stale copies. If you use a fork from someone else, you
are trusting that person's skill files as agent instructions. Review changes
before pulling.
Leak backstop: the banned-terms gate scans diffs, commit messages, and
publish-surface bodies — but a customer/tenant brand name already committed
never appears in a later diff, so it would stay hidden. t3 banned-terms scan-tree is the full-tree backstop: it walks every git-tracked file (git ls-files) and scans its content for the high-confidence brand list, exiting
non-zero with the offending file:line list. Its matcher is
underscore-tolerant — wt_777_<brand> and <brand>_x are caught where the
diff gate's word-boundary matcher misses them — while common-word entries keep
strict boundaries (no substring noise) and the email carve-out is preserved.
The brand list comes from the banned_brands setting in the teatree DB or the
$TEATREE_BANNED_BRANDS environment variable; it is a curated high-confidence
subset (brand-only — common words stay in banned_terms so the
underscore-tolerant tree scan never substring-matches them). The public repo
ships with none, so the brand scan is a no-op — but it reports a loud
brand backstop INERT: banned_brands is unpopulated warning rather than a
silent clean line, so an operator who expected the backstop to be active can
tell it is not yet populated. A CI job runs the scan on push to main and on a
daily schedule.
teatree/
src/teatree/ # Django project (installed as `teatree`)
cli/ # Typer CLI package — bootstrap commands
core/ # Models, FSM transitions, management commands
agents/ # Agent runtime adapters (Claude Code, Codex)
backends/ # Code-host (GitHub, GitLab) + messaging (Slack, Notion) Protocols
loop/ # Fat /loop tick — scanners, dispatch, statusline render
utils/ # Internal helpers (ports, git, DB)
templates/overlay/ # `t3 startoverlay` scaffolding
skills/ # AI agent skills (SKILL.md + references)
hooks/ # Agent platform hooks (routing, guards, statusline)
scripts/ # Pre-commit hooks, utility scripts
tests/ # Unit tests (>90% branch coverage)
docs/ # MkDocs documentation site
Teatree stays a single-author tool for now. The plan is to keep dogfooding it on real client work, let the rough edges surface through daily use, and only broaden adoption once the patterns it relies on have been pushed through enough sessions to be trustworthy. The public repo is a side effect of that workflow — the code lives somewhere reachable in case any of the patterns help someone else, not as a finished product looking for users.
Why "teatree"?
TEA's Extensible Architecture for worktree management.
MIT
