treepad

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 23, 2026 License: MIT Imports: 13 Imported by: 0

README

tp

A CLI for managing git worktrees — providing a standardised, extensible set of utilities for working with worktrees.

Overview

tp makes it easy to create, navigate, and manage git worktrees from the command line. The aim is to build a consistent, composable toolset around worktree workflows that can be extended as new patterns emerge.

A primary motivation is parallelising Claude Code instances: each worktree gets its own isolated working directory, allowing multiple AI coding sessions to run simultaneously on different tasks without interfering with each other. tp provides the primitives to spin up, coordinate, and share context between those worktrees.

Built with

Installation

brew install O-Marsters-1997/tap/treepad
go install
go install github.com/O-Marsters-1997/treepad/cmd/tp@latest

This installs the tp binary.

From source
git clone https://github.com/O-Marsters-1997/treepad
cd treepad
just build

This produces a tp binary in the project root. Move it somewhere on your $PATH.

After installing

Initialise a config file in your repo (optional — tp works with zero config):

tp config init

Run tp config show to confirm which config is active.

Agent skill (without installing tp)

If you already have tp, tp skill install sets up the skill for whichever agent harness you use — see below. Without tp, Claude Code users can install it as a plugin instead, since the repo also doubles as a Claude Code plugin marketplace:

/plugin marketplace add O-Marsters-1997/treepad
/plugin install treepad@treepad

Configuration

tp works with zero configuration. See docs/configuration.md for more info on how to configure it and what the defaults are.

Usage

tp [--verbose | -v] [--profile] <command> [options]

Use --profile to print a per-stage timing breakdown to stderr after the command completes:

tp --profile new feature-x
# => profile: new feature-x (total 0.842s)
#      stage              duration    pct
#    ─────────────────────────────────────
#      git.worktree_add   0.501s    59.5%  ◀
#      sync               0.201s    23.9%
#      artifact.write     0.140s    16.6%
#    ─────────────────────────────────────
#      total              0.842s   100.0%
Main commands

sync — Sync configs and generate artifact files across all git worktrees:

# Sync configs and generate artifact files from the main worktree
tp sync

# Sync only — skip artifact file generation
tp sync --sync-only

# Use the current directory as the config source instead of the main worktree
tp sync --use-current

# Include extra file patterns in the sync
tp sync --include ".prettierrc" --include ".eslintrc.json"

# Debug what tp is doing
tp --verbose sync

new — Create a new git worktree with configs synced and artifact file generated:

# Create a new worktree for branch 'feature-x' branched from main (cd's into it automatically)
tp new feature-x

# Create a worktree from a different base ref
tp new bugfix-y --base develop

# Create a worktree and open the artifact file
tp new feature-z --open

# Stay in the current directory instead of cd-ing into the new worktree
tp new feature-z -c

Shell integration: tp new prints a cd directive that is acted on by a shell wrapper function. Add the following to your ~/.zshrc or ~/.bashrc:

eval "$(tp shell-init)"

new --ticket — Create a worktree from a ticket and hand off to an agent:

# Create a worktree from a ticket, using a bare ref against the configured ticket_url
tp new feature-x --ticket ENG-42

# A full ticket URL works with no config, and overrides ticket_url when set
tp new feature-x --ticket https://github.com/acme/api/issues/42

# Create a worktree from a different base ref
tp new bugfix-z --ticket ENG-10 --base develop

tp does not read your tracker and writes no prompt — it hands the ticket URL to [from_spec] agent_command and the agent fetches the body itself. Set [from_spec] ticket_url in .treepad.toml to use bare refs. See docs/from-spec.md.

playbook new — Write a playbook saying which skills a recurring shape of work should use:

tp playbook new task-dashboard < playbook.md

The body is written verbatim to .claude/playbooks/task-dashboard.md in the main worktree and propagated by [sync]. Name it on the ticket (Playbook: task-dashboard) and the agent picks it up when it reads the ticket. See ADR 0002.

batch sync — Reconcile a Batch of Tickets into a fleet of stacked worktrees:

# Preview what a Batch would do
tp batch sync --dry-run

# Reconcile every Batch, spawning agents for newly-ready members
tp batch sync --launch

# List every Batch, its Chains, and each member's ticket/ref/branch/base
tp batch list

A Batch is a collection of Tickets declared by a Manifest — an uncommitted TOML file under <git-common-dir>/treepad/batches/*.toml naming one or more Chains (ordered runs of Tickets, each worktree branched from the one before it). Treepad reads Manifests; it never writes one — that's an external dependency: to-tickets must learn to emit them.

name = "silent-refresh"
[[chain]]
tickets = ["ENG-12", "ENG-13"]
[[chain]]
tickets = ["ENG-14"]

tp batch sync materialises each Chain's worktrees in order and links ready pull requests into a GitHub Stack with gh stack link — requires gh installed and authenticated; without it, only each Chain's first member materialises and the rest report gh-required. Configure [batch] launch to start an agent automatically once a member is ready (see docs/configuration.md); --launch spawns it via the Launcher, which writes to that worktree's Activity file.

Chain depth is a review-latency multiplier. Layer five cannot land until four reviews complete below it, and every merge below rewrites the base under the agents above. Deep Chains optimise writing throughput and pessimise review throughput, which is the opposite of the point. Shallow and wide beats deep and narrow.

gh stack link is additive only — treepad can build a Chain into a Stack but can never take one apart. A Manifest edited after its Chain is linked leaves a Stack on GitHub that no treepad command can correct; fix it by hand on github.com.

See docs/commands.md for the full reference.

remove — Remove a git worktree and its associated files:

# Remove a completed feature branch (switch out of it first)
cd ../main-repo
tp remove feature-x

# Discard uncommitted changes and delete an unmerged branch
tp remove --force feature-x

prune — Remove all worktrees whose branches are merged into a base branch, or force-remove all non-main worktrees:

# Remove all worktrees whose branches are merged into main
tp prune

# Preview without executing
tp prune --dry-run

# Check merges against a different base branch
tp prune --base develop

# Force-remove all non-main worktrees (with confirmation)
tp prune --all

cd — cd into an existing worktree by branch name, or toggle back to the previous one:

# cd into an existing worktree (shell integration handles the directory change)
tp cd feature-x

# toggle back to the previous worktree (like cd -)
tp cd -

Requires eval "$(tp shell-init)" in your shell rc — the same wrapper used by new.

base — return to the main worktree from any branch worktree:

# cd back to the main worktree
tp base

Requires eval "$(tp shell-init)" in your shell rc.

status — List all worktrees with their branch, dirty state, ahead/behind count, and last commit:

# Show status of all worktrees in a table
tp status

# Emit JSON for scripting or dashboards
tp status --json

ui — Open a live interactive fleet view (requires a TTY):

# Open the BubbleTea TUI fleet monitor
tp ui
Key Action
/ k Move cursor up
/ j Move cursor down
Enter cd into selected worktree and exit
s Sync selected worktree configs
S Sync all worktrees (fleet sync)
o Open artifact file for selected worktree
d Diff selected worktree against base branch
e Open an interactive shell ($SHELL) in selected worktree (with confirmation)
y Yank (copy) path of selected worktree to clipboard
r Remove selected worktree (with confirmation)
R Force-remove selected worktree — discards uncommitted changes and unmerged commits (with confirmation)
p Prune merged worktrees (with confirmation)
/ Filter worktrees by branch or path (fuzzy match)
Esc Clear active filter
? Toggle key binding help overlay
q / Ctrl-C Quit

Requires eval "$(tp shell-init)" for the Enter→cd action to work.

exec — Run a command in a specific worktree with full stdio passthrough:

# Run a script (detected task runner handles it automatically)
tp exec feature-x build

# Run a raw command in the worktree root
tp exec feature-x cargo test

# List available scripts and detected runner for a worktree
tp exec feature-x

The exec command auto-detects the project task runner (just, npm/pnpm/yarn/bun, make, poetry/uv) by checking for marker files. If the command matches an enumerated script, it routes through the runner (e.g. just build, pnpm run build). Otherwise, it executes the command directly in the worktree root. Override auto-detection via [exec] runner = "just" in .treepad.toml.

diff — Show the diff of a worktree against a base branch:

# Show diff vs main branch (colored, paged via git config)
tp diff feature-x

# Diff against a different base branch
tp diff feature-x --base develop

# Write a plain patch to a file (no color)
tp diff feature-x -o ~/my-feature.patch

# Show only changed files and line counts
tp diff feature-x -- --stat

# Limit diff to a specific subdirectory
tp diff feature-x -- -- src/

The diff command uses git diff <base>...HEAD three-dot semantics (matches GitHub PR diff view) and respects your git configuration (pager, delta, diff-so-fancy). Inherits color and pager config from the target worktree's git setup. Pass --output / -o to write an uncolored patch to a file.

doctor — Report cross-worktree health issues (stale, merged, remote-gone, artifact-missing, config-drift):

# Show a table of findings
tp doctor

# Flag worktrees with no commit in the last 14 days
tp doctor --stale-days 14

# Check merges against a different base
tp doctor --base develop

# Skip remote branch checks (faster, works offline)
tp doctor --offline

# Emit JSON for scripting
tp doctor --json

# Exit non-zero if any findings are reported (useful in CI)
tp doctor --strict

config — Manage tp configuration:

# Write a default .treepad.toml to the main worktree root
tp config init

# Write config to the global config path
tp config init --global

# Show the resolved config and which source(s) contributed
tp config show

skill — Manage treepad's agent skills:

# Install every skill to ~/.agents/skills (and link ~/.claude/skills if present)
tp skill install

# Install into the repo instead, so it's committed and shared
tp skill install --local

See docs/commands.md for the full command reference.

All commands
tp [--verbose] <command>
├── sync [options] [source-path]
├── new [options] <branch>
├── batch
│   ├── list [--json]
│   └── sync [options]
├── remove <branch>
├── prune [options]
├── cd <branch | ->
├── base
├── status [--json]
├── ui
├── exec <branch> [command] [args...]
├── diff [options] <branch> [-- <git-diff-args>...]
├── doctor [options]
├── shell-init
├── config
│   ├── init [--global]
│   └── show
├── skill
│   ├── install [name...] [--local] [--force]
│   └── list
└── playbook
    └── new <name> [--force]

Testing

tp has two test layers:

  • Unit/integration — mocked git runner, runs with just test (go test ./...). Fast inner-loop feedback.
  • End-to-end — builds tp in-process, drives it against a real throwaway git repo per scenario, asserts on stdout, exit codes, and filesystem state. Runs with just test-e2e (go test -tags=e2e ./cmd/tp/...).
Adding an e2e test
  1. Create cmd/tp/testdata/script/<name>.txtar.
  2. Start the script with tp-init-repo (creates a clean git repo and cds into it).
  3. Call exec tp <command> [args...] to run the binary.
  4. Assert with stdout <pattern>, exists <path>, ! exists <path>, or grep <pattern> <file>.

See the testscript docs for the full command reference.

Development

Command Description
just build Compile the binary
just test Run all unit/integration tests
just test-e2e Run end-to-end tests
just lint Run golangci-lint (via Docker)
just fmt Format all Go files
just ci Lint, build, and test

Documentation

Overview

Package treepad cuts git worktrees from Go code. Unlike the tp CLI, a caller names the repository it means rather than standing in it, so one process can serve several repositories concurrently.

A worktree cut here is indistinguishable from one cut by tp new: config sync runs and lifecycle hooks fire. It diverges from the CLI in two ways, both because nobody is watching a library call:

  • A hook configured interactive = true is refused with ErrInteractiveHook before anything is written. The CLI hands such a hook the terminal; a library caller has no terminal to hand over.
  • RemoveOptions.Force deletes a branch git considers unmerged, but never a worktree with uncommitted changes — Remove returns ErrDirty instead, where tp remove --force would wipe it.

A failed post hook is the one error that leaves the operation done. New and Remove wrap ErrPostHook once the worktree is already cut or already gone, and New returns its fully populated Worktree alongside it. A caller reconciling desired state should report it rather than retry: a second New fails on the branch the first one created, and a second Remove returns ErrNotFound.

Calls are serialised per repository, since two concurrent git worktree adds contend on the same index and ref locks. That lock is held in this process only. Two processes against one repository — or tp run by hand next to a library caller — are not serialised, and need coordinating elsewhere.

Index

Constants

This section is empty.

Variables

View Source
var ErrDirty = errors.New("worktree has uncommitted changes")

ErrDirty reports uncommitted changes in the target worktree. Nothing has been touched. Force does not override it — destroying uncommitted work is a decision for a human who can see it.

View Source
var ErrInteractiveHook = errors.New("interactive hooks are not supported by the library API")

ErrInteractiveHook reports that the repository configures a hook with interactive = true for an event the requested operation fires. Nothing has been written. An interactive hook wants a human at a terminal, which a library caller cannot promise.

View Source
var ErrNotFound = errors.New("worktree not found")

ErrNotFound reports that no worktree is checked out on the named branch. Remove is idempotent against it: a second call on the same branch returns it.

View Source
var ErrPostHook = errors.New("post hook failed")

ErrPostHook reports that a post hook failed. The cut itself succeeded: the returned Worktree is complete and on disk.

Functions

func Remove

func Remove(ctx context.Context, o RemoveOptions) error

Remove deletes the worktree on Branch, its branch and its artifact, firing remove hooks exactly as tp remove does. Unlike the CLI it does not care where the calling process stands, so a caller inside the target worktree succeeds.

Types

type NewOptions

type NewOptions struct {
	Branch string
	Base   string
	// RepoDir is an absolute path inside the target repository.
	RepoDir string
	// OutputDir is where the editor artifact is written. Empty means
	// $HOME/<repo-slug>-workspaces, matching the CLI.
	OutputDir string
	// Stderr receives the same narrative the CLI prints. Nil discards it.
	Stderr io.Writer
}

NewOptions parameterises New. Branch and RepoDir are required.

type RemoveOptions

type RemoveOptions struct {
	Branch string
	// RepoDir is an absolute path inside the target repository.
	RepoDir string
	// OutputDir is where the editor artifact was written. It must match the
	// OutputDir the worktree was cut with, or the artifact is left behind.
	OutputDir string
	// Force deletes a branch git considers unmerged — what a squash merge
	// leaves. It never overrides the dirty-worktree refusal.
	Force bool
	// Stderr receives the same narrative the CLI prints. Nil discards it.
	Stderr io.Writer
}

RemoveOptions parameterises Remove. Branch and RepoDir are required.

type Worktree

type Worktree struct {
	Path    string
	Branch  string
	BaseSHA string
}

Worktree is a created worktree.

func New

func New(ctx context.Context, o NewOptions) (Worktree, error)

New creates a worktree for Branch off Base in the repository at RepoDir, syncing configs and firing hooks exactly as tp new does.

Directories

Path Synopsis
Package batch resolves Manifests into scheduling Members: the Chains and Ticket-to-branch derivation that no fleet tool has an equivalent for.
Package batch resolves Manifests into scheduling Members: the Chains and Ticket-to-branch derivation that no fleet tool has an equivalent for.
cmd
tp command
e2e
internal
artifact
Package artifact renders per-worktree files from config-supplied templates.
Package artifact renders per-worktree files from config-supplied templates.
config
Package config loads optional per-repo configuration from .treepad.toml.
Package config loads optional per-repo configuration from .treepad.toml.
exec
Package exec detects project task runners and enumerates their scripts.
Package exec detects project task runners and enumerates their scripts.
gh
Package gh is the entire gh CLI surface for Batch orchestration (ADR 0003): `gh auth status`, via Available; `gh pr list`, via PRList; and `gh stack link`, via StackLink.
Package gh is the entire gh CLI surface for Batch orchestration (ADR 0003): `gh auth status`, via Available; `gh pr list`, via PRList; and `gh stack link`, via StackLink.
hook
Package hook runs lifecycle hooks defined in .treepad.toml.
Package hook runs lifecycle hooks defined in .treepad.toml.
launcher
Package launcher starts one agent in one worktree and never supervises it again.
Package launcher starts one agent in one worktree and never supervises it again.
passthrough
Package passthrough executes a command in a target directory with stdio inherited from the calling process.
Package passthrough executes a command in a target directory with stdio inherited from the calling process.
profile
Package profile provides lightweight wall-time stage profiling.
Package profile provides lightweight wall-time stage profiling.
sync
Package sync copies files matching gitignore-style patterns between directories.
Package sync copies files matching gitignore-style patterns between directories.
treepad/cdshell
Package cdshell owns the __TREEPAD_CD__ shell-bridge protocol.
Package cdshell owns the __TREEPAD_CD__ shell-bridge protocol.
treepad/lifecycle
Package lifecycle owns the worktree creation, removal, and pruning verbs.
Package lifecycle owns the worktree creation, removal, and pruning verbs.
treepad/repo
Package repo resolves the repository context shared by every treepad verb: the worktree list, the main worktree, the repo slug, and the artifact output directory.
Package repo resolves the repository context shared by every treepad verb: the worktree list, the main worktree, the repo slug, and the artifact output directory.
treepad/treepadtest
Package treepadtest provides shared test helpers for packages that depend on treepad sub-package dependencies.
Package treepadtest provides shared test helpers for packages that depend on treepad sub-package dependencies.
tty
Package tty provides access to the process's controlling terminal.
Package tty provides access to the process's controlling terminal.
ui
Package ui provides a structured, tag-prefixed printer for user-facing stderr output.
Package ui provides a structured, tag-prefixed printer for user-facing stderr output.
worktree/worktreetest
Package worktreetest provides shared test helpers for packages that depend on worktree.CommandRunner.
Package worktreetest provides shared test helpers for packages that depend on worktree.CommandRunner.
Package skills embeds the agent skills treepad ships, so `tp skill install` can copy them onto disk without a separate download step.
Package skills embeds the agent skills treepad ships, so `tp skill install` can copy them onto disk without a separate download step.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL