Skip to content

Releases: yoiailabs/rabi

rabi v7.0.0

Choose a tag to compare

@aku11i aku11i released this 24 Oct 10:16
4c673c8

rabi (ラビ, from "rabbit") v7.0.0 is the first stable-tagged release of the Rust rewrite and the first v7 release published as the repository's Latest release. It contains no functional changes from v7.0.0-alpha.11. It is not published to npm, crates.io or Homebrew.

Important

v7 is a beta. rabi is under active development. During the v7 beta, minor releases (7.x) may contain breaking changes, the same rule as semver 0.x. Patch releases (7.x.y) contain fixes only. Every breaking change is listed in CHANGELOG. Pin a version for agents and CI.

Install

curl -fsSL https://github.com/yoiailabs/rabi/releases/latest/download/install.sh | sh

No --prerelease and no version pin are needed any more: the unpinned installer picks the newest stable v7 release. Add sh -s -- --verify-attestation (needs gh) to also verify GitHub artifact attestations. The installer always verifies SHA-256 against SHA256SUMS, installs rabi to ~/.local/bin without sudo, and replaces it atomically. Linux binaries are static musl builds (Alpine included); macOS x64/arm64 is supported; use the Linux binaries in WSL.

Pinned (agents and CI):

curl -fsSL https://github.com/yoiailabs/rabi/releases/download/v7.0.0/install.sh \
  | RABI_VERSION=v7.0.0 sh -s -- --verify-attestation

From source: cargo install --git https://github.com/yoiailabs/rabi --tag v7.0.0 --locked rabi.

Changes

  • v7.0.0 is published as a stable release and becomes Latest; v7 enters beta with the versioning rule above.
  • Docs describe the beta and the unpinned install (#902).
  • No functional changes since v7.0.0-alpha.11; paths, config and records are unchanged.

Upgrading

  • From v7.0.0-alpha.10 or alpha.11: run the install command above. No data migration is needed.
  • From older v7 alphas (mabo / Maboroshi / ptm): see the v7.0.0-alpha.10 notes in CHANGELOG; there is no automatic migration.
  • From phantom v6 (npm @phantompane/cli, Homebrew phantom): v7 is a separate Rust rewrite under a new command name (rabi) with its own commands, config and data layout; it does not read v6 data. v6 keeps working and is not removed or updated by this release. npm @phantompane/cli and Homebrew phantom stay on v6.3.2.

PRs: #902, #903.

rabi v7.0.0-alpha.11

rabi v7.0.0-alpha.11 Pre-release
Pre-release

Choose a tag to compare

@aku11i aku11i released this 24 Oct 09:18
f0d65ce

rabi (ラビ, from "rabbit") v7.0.0-alpha.11 is a prerelease of the Rust rewrite. The repository moved to yoiailabs/rabi; old phantompane/phantom URLs redirect. There are no functional changes from alpha.10. It is not published to npm, crates.io or Homebrew.

  • This is an alpha. Commands, flags, paths and the JSON schema may still change before 7.0.0, without backward compatibility.
  • This is the first release built and attested in yoiailabs/rabi.

Install

curl -fsSL https://github.com/yoiailabs/rabi/releases/download/v7.0.0-alpha.11/install.sh \
  | RABI_VERSION=v7.0.0-alpha.11 sh -s -- --verify-attestation

--verify-attestation needs gh. The installer always verifies SHA-256 against SHA256SUMS, installs rabi to ~/.local/bin and replaces it atomically. Linux binaries are static musl builds (Alpine included). From source: cargo install --git https://github.com/yoiailabs/rabi --tag v7.0.0-alpha.11 --locked rabi.

Changes

  • The repository is now yoiailabs/rabi (#900). install.sh downloads from and verifies attestations against yoiailabs/rabi; docs, help examples and Cargo.toml (repository) point there.
  • Attestations for v7.0.0-alpha.10 and earlier stay with the old organization. The alpha.10 installer's --verify-attestation no longer works; install alpha.10 without it, or verify by hand with gh attestation verify <file> --owner phantompane.

Upgrading from alpha.10

Run the install command above. No data migration is needed: paths, config and records are the same as alpha.10.

PRs: #900, #901.

rabi v7.0.0-alpha.10

rabi v7.0.0-alpha.10 Pre-release
Pre-release

Choose a tag to compare

@aku11i aku11i released this 24 Oct 06:02
a6c3140

rabi (ラビ, from "rabbit") v7.0.0-alpha.10 is a prerelease of the Rust rewrite. The product, command, paths, environment variables and release assets are renamed from mabo to rabi. There is no compatibility with mabo: rabi does not read or migrate mabo data. It is not published to npm, crates.io or Homebrew; npm @phantompane/cli and Homebrew phantom remain the TypeScript v6.

Please note:

  • This is an alpha. Commands, flags, paths and the JSON schema may still change before 7.0.0.
  • No compatibility until 7.0.0. No mabo alias, no MABO_* variables, no .mabo.toml, no reading of mabo/ directories. If you used an earlier alpha, move your data by hand (see below) or recreate your workspaces.
  • Assets are now rabi-v<version>-<os>-<arch>.tar.gz, and install.sh accepts only that name. Install v7.0.0-alpha.9 (mabo-) or v7.0.0-alpha.8 (maboroshi-) with that release's own installer; v7.0.0-alpha.7 and older have no installer.
  • The repository is still phantompane/phantom.

Install

curl -fsSL https://github.com/phantompane/phantom/releases/download/v7.0.0-alpha.10/install.sh | RABI_VERSION=v7.0.0-alpha.10 sh

Or … | sh -s -- --prerelease for the newest prerelease. Add --verify-attestation (needs gh) to check the GitHub artifact attestation as well. The installer needs curl, installs rabi to ~/.local/bin, always verifies SHA-256 against SHA256SUMS, and replaces the binary atomically. Linux binaries are static musl builds (Alpine included). From source: cargo install --git https://github.com/phantompane/phantom --tag v7.0.0-alpha.10 --locked rabi.

Changes

  • Renamed to rabi (#896): the binary and Cargo package are rabi, help, completions and shell-init (rabi cd) use rabi, and release archives are rabi-v….
  • rabi paths only: config ~/.config/rabi, state ~/.local/state/rabi, per-repository records <git-common-dir>/rabi/, new workspaces ~/.local/share/rabi/workspaces/<repo>-<hash>/<name> (XDG variables respected), repository config .rabi.toml.
  • Environment variables are RABI_* (RABI_OUTPUT, RABI_NO_INPUT, RABI_TRUST_ALL, RABI_WORKSPACE, RABI_WORKSPACE_PATH, RABI_REPO_PATH, RABI_WORKSPACE_ID; installer RABI_VERSION, RABI_INSTALL_DIR, …). The child-process marker is RABI=1.
  • Behavior is otherwise unchanged from alpha.9: commands, flags, JSON (schema 2), exit codes, hooks, trust and safety checks.
  • Docs: rebuilt README with reference pages under docs/ (#887, #888); development moved to main (#895).

Moving data from mabo (alpha.9) by hand

This is what we did on our own machines. Stop all mabo processes first and back up.

mv ~/.config/mabo ~/.config/rabi              # if it exists
mv ~/.local/state/mabo ~/.local/state/rabi    # if it exists
# In each repository (repeat per repo):
mv "$(git rev-parse --git-common-dir)/mabo" "$(git rev-parse --git-common-dir)/rabi"
[ -f .mabo.toml ] && git mv .mabo.toml .rabi.toml   # repository config, if you have one
  • If $XDG_CONFIG_HOME or $XDG_STATE_HOME is set, move the mabo directory there instead.
  • Rename MABO_* environment variables in scripts, hooks and shell profiles to RABI_*, and switch shell integration to rabi completions … / rabi shell-init ….
  • Leave existing checkouts where they are (for example under ~/.local/share/mabo/workspaces/). Their moved records still point at them, so they stay managed; new workspaces go under ~/.local/share/rabi/workspaces/. Do not move checkout directories with mv; Git would lose track of them.
  • If <git-common-dir>/rabi already exists, merge by hand: copy any missing workspaces/ws_*.json records; never overwrite.
  • Checkouts without a record still appear in rabi list as unmanaged Git worktrees; rabi adopt PATH records them.

PRs: #887, #888, #890, #895, #896, #897.

mabo v7.0.0-alpha.9

mabo v7.0.0-alpha.9 Pre-release
Pre-release

Choose a tag to compare

@aku11i aku11i released this 23 Oct 16:04
621ee54

mabo (Maboroshi, 幻) v7.0.0-alpha.9 is a prerelease of the Rust rewrite. The product, its paths and its release assets are now all named mabo, and all backward compatibility with earlier alphas was removed. It is not published to npm; npm @phantompane/cli and Homebrew phantom remain the TypeScript v6.

Please note:

  • This is an alpha. Commands, flags, paths and the JSON schema may still change before 7.0.0.
  • No compatibility until 7.0.0. mabo does not read or migrate data from earlier alphas, and removed commands, flags and keys fail as ordinary unknown input (no migration hints). If you used an earlier alpha, move your data by hand (see below) or recreate your workspaces.
  • Assets are now mabo-v<version>-<os>-<arch>.tar.gz, and install.sh accepts only that name. Earlier releases must be installed with their own installer (alpha.8) or by hand (alpha.7 and older).
  • The repository is still phantompane/phantom.

Install

curl -fsSL https://github.com/phantompane/phantom/releases/download/v7.0.0-alpha.9/install.sh | MABO_VERSION=v7.0.0-alpha.9 sh

Or … | sh -s -- --prerelease for the newest prerelease. Add --verify-attestation (needs gh) to check the GitHub artifact attestation as well. The installer needs curl, installs to ~/.local/bin, always verifies SHA-256 against SHA256SUMS, and replaces the binary atomically. Linux binaries are static musl builds (Alpine included).

Changes

  • Renamed to mabo. The Cargo package is mabo (cargo install --git https://github.com/phantompane/phantom --tag v7.0.0-alpha.9 --locked mabo) and release archives are mabo-v… (#879, #880).
  • mabo paths only. Config ~/.config/mabo, state ~/.local/state/mabo, per-repository records <git-common-dir>/mabo/, new workspaces ~/.local/share/mabo/workspaces/<repo>-<hash>/<name> (XDG variables respected) (#881, #884).
  • Compatibility removed (#882–#885):
    • no reading or migration of maboroshi/ directories, worktrees.json / projects.json stores, or old worktree layouts and markers;
    • no migration hints for removed commands and flags (delete, attach, gh, where, meta, preferences, project, trust --yes/--list, -p, --owner, --mine, …);
    • no renaming of old config keys or .mabo.toml hook keys (post-create / pre-delete are now unknown keys; use setup / pre-remove);
    • no special handling of retired environment variables;
    • install.sh no longer falls back to maboroshi- archives.
  • Safety behavior is unchanged: preservation checks before deleting branches, dirty-work protection, sync refusals and hook trust all work as in alpha.8.

Moving data from an earlier alpha by hand

This is what we did on our own machines. Stop all mabo processes first and back up.

mv ~/.config/maboroshi ~/.config/mabo            # if it exists
mv ~/.local/state/maboroshi ~/.local/state/mabo  # if it exists
# In each repository (repeat per repo):
mv "$(git rev-parse --git-common-dir)/maboroshi" "$(git rev-parse --git-common-dir)/mabo"
  • If $XDG_CONFIG_HOME or $XDG_STATE_HOME is set, move the maboroshi directory there instead.
  • Leave existing checkouts where they are (for example under ~/.local/share/maboroshi/workspaces/). Their moved records still point at them, so they stay managed. Do not move checkout directories with mv; Git would lose track of them.
  • If <git-common-dir>/mabo already exists, merge by hand: copy any workspaces/ws_*.json records that are missing; never overwrite.
  • Records from alpha.6 or older (worktrees.json, projects.json) are not read. Use mabo adopt PATH --name NAME for existing checkouts and mabo repo add PATH for registrations.
  • Checkouts without a record still appear in mabo list as unmanaged Git worktrees.

PRs: #879, #880, #881, #882, #883, #884, #885, #886.

Maboroshi v7.0.0-alpha.8

Pre-release

Choose a tag to compare

@aku11i aku11i released this 22 Oct 00:19
2653f21

Maboroshi (mabo) v7.0.0-alpha.8 is a prerelease of the Rust rewrite. It is not published to npm; npm @phantompane/cli and Homebrew phantom remain the TypeScript v6.

This release changes how mabo is shipped and installed. CLI behavior is unchanged from alpha.7.

Install

Pinned (recommended for agents and CI):

curl -fsSL https://github.com/phantompane/phantom/releases/download/v7.0.0-alpha.8/install.sh | MABO_VERSION=v7.0.0-alpha.8 sh

The installer needs curl (Alpine: apk add curl), installs to ~/.local/bin (MABO_INSTALL_DIR to change), always verifies SHA-256 against SHA256SUMS, and replaces the binary atomically. It never prompts, uses sudo or edits shell profiles. Add sh -s -- --verify-attestation to also check the GitHub artifact attestation with gh. Re-run it with a newer tag to update. See the README for unpinned installs and other methods.

Changes

  • install.sh: new installer, attached to every release. Prereleases install only when pinned or with --prerelease. Every network call has a time limit.
  • Static musl Linux binaries: linux-x64 and linux-arm64 now run on any distribution, including Alpine and older glibc systems (alpha.7 needed glibc 2.34+). Asset names are unchanged.
  • SHA256SUMS: covers the four archives and install.sh.
  • GitHub artifact attestations for the archives, install.sh and SHA256SUMS. Verify with gh attestation verify <file> --repo phantompane/phantom.

PRs: #875, #876, #877, #878.

Maboroshi v7.0.0-alpha.7

Pre-release

Choose a tag to compare

@aku11i aku11i released this 21 Oct 20:54
261444e

This alpha is the v8 command redesign of the Rust mabo CLI: Maboroshi is now "workspaces for parallel coding agents", safe by default and explicit when you want otherwise. Almost every command changed. Read the migration table below before upgrading scripts or agent skills.

Please note:

  • This is an alpha. Commands, flags, and the JSON schema may still change before 7.0.0.
  • Breaking: the command set, flags, JSON output (now envelope schema 2) and exit codes changed. Removed commands and flags (delete, attach, github/gh, where, meta, preferences, project, sync -f, …) exit 2 with a migration hint instead of acting as aliases. See Migration from alpha.6.
  • Nothing is deleted on uncertainty. remove/gc delete a branch only when it is proven preserved (merged into the base, matching its upstream, or — new — a merged GitHub PR with the same head). Uncommitted work needs --discard-changes; ignored files (target/, node_modules/) are not dirty and go with the workspace.
  • No locks, no ownership. Internal locks, MABO_OPERATION_LOCK, owners and MABO_AGENT_ID are gone; bulk commands rely on the dirty / unpreserved / published checks.
  • This release is not published to npm. npm install still gives you the TypeScript v6 phantom. Download a prebuilt mabo binary from this release (see Install).
  • Existing state is migrated automatically: alpha.6 metadata is imported on the first write; old config keys keep working with warnings; .mabo.toml post-create/pre-delete must be renamed to setup/pre-remove (old keys fail with a rename hint).
  • The repository is still phantompane/phantom.

Install

Prebuilt binaries are attached to this release. Each archive contains a single mabo binary:

OS Architecture Asset
Linux x86_64 maboroshi-v7.0.0-alpha.7-linux-x64.tar.gz
Linux arm64 maboroshi-v7.0.0-alpha.7-linux-arm64.tar.gz
macOS Apple silicon maboroshi-v7.0.0-alpha.7-darwin-arm64.tar.gz
macOS Intel maboroshi-v7.0.0-alpha.7-darwin-x64.tar.gz
# Pick <os>-<arch> from the table, e.g. linux-x64 or darwin-arm64.
gh release download v7.0.0-alpha.7 --repo phantompane/phantom --pattern 'maboroshi-v7.0.0-alpha.7-linux-x64.tar.gz'
tar -xzf maboroshi-v7.0.0-alpha.7-linux-x64.tar.gz
install -m 755 mabo ~/.local/bin/mabo
mabo --version   # mabo v7.0.0-alpha.7+rust (…)

On macOS, a binary downloaded with a browser may be quarantined. Run xattr -d com.apple.quarantine ~/.local/bin/mabo if it won't start.

To build from source instead: git clone https://github.com/phantompane/phantom.git && cd phantom && git checkout v7.0.0-alpha.7 && cargo install --path . --locked.

Optional shell helpers (new): eval "$(mabo shell-init bash)" adds mabo cd NAME; eval "$(mabo completions bash)" adds completions (zsh: same with zsh; fish: mabo shell-init fish | source, mabo completions fish | source).


Added

  • remove and gc --merged now accept GitHub merged PR evidence after local preservation checks fail. Linked PRs require a verified repository and a merged head equal to or containing the local tip; ancestry uses existing objects only. Partial clones (extensions.partialClone configured or any remote with promisor=true) require an exact tip match and skip PR object and ancestry probes on every Git version. Unlinked branches require exactly one merged PR with the same head name and exact tip. Bounded, non-prompting gh queries are cached per command; uncertainty keeps the local verdict. Plans use the same checks, and item preservationReason plus text details identify merged PR proof. GitHub Enterprise hosts on plain HTTP or a non-default port cannot prove a merged PR yet; their branches are kept.

Breaking changes

This release redesigns the Rust mabo CLI around workspaces. Update scripts written for alpha.6 before upgrading. The installed binary remains mabo.

  • Creation is unified under create --branch, --pr, --issue, and --detach. The old attach, github/gh, and create-time --exec interfaces are removed. create --pr and --issue now resolve GitHub sources using authenticated gh.
  • Use show, path, and set for inspection and intent; use repo, config, and trust add|remove|list for the outer commands. -R/--repo replaces -p/--project. Removed commands and flags exit 2 with migration hints rather than acting as aliases.
  • remove replaces delete. Working-file loss (--discard-changes) and deleting unpreserved history (--delete-unpreserved-branch) are separate permissions. Branches are deleted only when preservation is proven; otherwise they stay with a warning. Ignored files are clean and are deleted with the checkout, including nested Git repositories inside ignored directories. Populated submodules or retained submodule data still require --discard-changes.
  • sync selects the current workspace by default. Advancing primary is explicit with --main; --all selects every workspace. Auto rebases unpublished work and merges published work; --rewrite-published permits a published rebase without selecting that strategy. Sync checks fetch destinations before any fetch or integration, including in dry runs.
  • Machine output uses the schema 2 envelope and the shared exit table below. Workspace keys and exported environment variables use workspace vocabulary. Ownership, lastUsedAt, internal locks, and MABO_OPERATION_LOCK are removed.
  • Hooks are [hooks] setup and pre-remove. Copy sources are always the primary checkout. create --no-setup skips copying and hooks; rerun preparation with setup. State is imported on the first write, and new workspace directories use workspaces/; existing directories stay in place.

Migration from alpha.6

B marks an incompatible change. “Migrated” describes automatic state/config migration; “new” is additive. Unchanged flags listed under a renamed command still require changing the command name. See README.md for current examples and the full JSON contract.

Old (alpha.6) New Breaking
create [name] create [NAME] B: flags change
create --base REF create --base REF —
create --owner X / attach --owner / gh checkout --owner Removed (record intent with --purpose) B
create --purpose/--pr/--issue --purpose still records intent; --pr selects the PR head; --issue looks up the issue, deriving issues/N and its title when used alone B: PR/issue flags now resolve GitHub sources through gh
create --copy-file P create --copy-file P (passed through to setup) —
create --no-hooks create --no-setup B
create -x/--exec CMD create NAME … then exec NAME -- CMD (name the new workspace; a bare exec -- CMD runs in the current checkout) B (removed)
create --json --json (envelope schema 2) B
attach B / attach remote/B create --branch B / create --branch remote/B B
github checkout N (PR) / gh checkout N create --pr N B
github checkout N (issue) create --issue N B
github checkout N --base X create --issue N --base X B
list list (primary checkout included, kind) B (output)
list --no-default list --kind workspace B
list --names list --names —
list --owner X Removed B
(none) list --all-repos, list --paths, list --kind new
where NAME path NAME B
where NAME --json show NAME --json B
delete N… remove N… B
delete -f remove --discard-changes and/or --delete-unpreserved-branch B
delete --current remove . B
delete --keep-branch remove --keep-branch B: command renamed; flag retained
delete --no-hooks remove --no-hooks B: command renamed; flag retained
sync (in the main checkout) sync (same as --main) —
sync (in a workspace: also ff'd the default branch) sync (that workspace only); sync . --main advances the primary too B
sync N… Same —
sync --mine Removed (sync in the workspace, or name it) B
sync --all (ownership-based scope) sync --all (every workspace) B
sync --no-default (default) B
sync --rebase / --merge Same (+ --auto) —
sync --rebase --force sync --rebase --rewrite-published B
sync --keep-conflicts Same —
sync --json (schema:1) Envelope schema 2 B
gc gc (plan) —
gc --apply gc --apply —
gc -f/--force remove … --discard-changes per target B (removed)
gc --merged / --orphaned Same —
gc --pr gc --closed-pr B
gc --older-than D (lastUsedAt, a criterion) gc --older-than D (createdAt, a narrowing filter) B (meaning)
gc --owner X Removed B
gc --keep-branch / gc --no-hooks Same —
trust [path] / trust --yes trust add [REPO] B
trust --list trust list B
untrust [path] trust remove [REPO] B
exec NAME CMD… exec NAME -- CMD… (-- required, NAME optional) B
meta set N --owner X Removed B
meta set N --purpose/--pr/--issue/--base set N … B
preferences get/set/remove K config get/set/unset K B
config keys worktrees-directory / root / ghq-discovery workspace-root / clone-root / discover-ghq Migrated: old file keys warn and still work; rewritten on the next config write; new names win
config keys keep-branch / directory-name-separator Ignored with warnings and removed on the next config write; use the --keep-branch flag for retirement B
version command / -v --version / -v (only the command is dropped) B
clone … Same output only
`project add/list/r...
Read more

Maboroshi v7.0.0-alpha.6

Pre-release

Choose a tag to compare

@aku11i aku11i released this 20 Oct 21:42
7016b8f

This alpha adds mabo sync, which keeps your default branch and worktrees current without risking your work (#850). It also makes attach work with remote branches (#848, closes #776), lets gc and delete recognize squash-merged branches (#847), and fixes github checkout for merged PRs whose branch is gone (#845).

Please note:

  • This is an alpha. Commands, flags, and the JSON schema may still change before 7.0.0.
  • Breaking: mabo create <name> now refuses a name that already exists as a remote-tracking branch (exit 3) and tells you to run mabo attach <name> instead. See Remote-aware attach.
  • Behavior change: mabo create no longer sets an upstream, even with --base origin/<branch>. Your first push uses git push -u origin <name>. See Smaller fixes.
  • This release is not published to npm. npm install still gives you the TypeScript v6 phantom. Download a prebuilt mabo binary from this release (see Install).
  • JSON output stays at schema version 1. New fields are additive: orphaned on worktree objects, base in metadata, github.source on checkout. mabo sync --json introduces its own document, "schema": 1.
  • The repository is still phantompane/phantom.

Install

Prebuilt binaries are attached to this release. Each archive contains a single mabo binary:

OS Architecture Asset
Linux x86_64 maboroshi-v7.0.0-alpha.6-linux-x64.tar.gz
Linux arm64 maboroshi-v7.0.0-alpha.6-linux-arm64.tar.gz
macOS Apple silicon maboroshi-v7.0.0-alpha.6-darwin-arm64.tar.gz
macOS Intel maboroshi-v7.0.0-alpha.6-darwin-x64.tar.gz
# Pick <os>-<arch> from the table, e.g. linux-x64 or darwin-arm64.
gh release download v7.0.0-alpha.6 --repo phantompane/phantom --pattern 'maboroshi-v7.0.0-alpha.6-linux-x64.tar.gz'
tar -xzf maboroshi-v7.0.0-alpha.6-linux-x64.tar.gz
install -m 755 mabo ~/.local/bin/mabo
mabo --version   # mabo v7.0.0-alpha.6+rust (…)

On macOS, a binary downloaded with a browser may be quarantined. Run xattr -d com.apple.quarantine ~/.local/bin/mabo if it won't start.

To build from source instead: git clone https://github.com/phantompane/phantom.git && cd phantom && git checkout v7.0.0-alpha.6 && cargo install --path . --locked.

Upgrading from alpha.5 needs no migration. Existing metadata keeps working: records without a base sync against the default branch.


Highlights

mabo sync (#850)

What's new. One command brings the default branch and your worktrees up to date with their bases. It fetches, fast-forwards the default branch, and rebases or merges each selected worktree onto its recorded base. It never pushes and never leaves a worktree half-integrated.

Why. With several worktrees, and often several agents, staying current meant cd into each one and running git fetch && git rebase origin/main (or merge), then hoping nothing was dirty or already pushed. Rebasing pushed commits rewrites history other people see. A conflict halfway through leaves a worktree mid-rebase. And it's easy to update the wrong branch. mabo sync makes the safe choice for each worktree and skips anything it can't update cleanly.

Which worktrees it touches.

Invocation Default branch Worktrees
mabo sync in the main checkout fast-forward none
mabo sync in a linked worktree (any subdirectory) fast-forward, best effort the current worktree
mabo sync <name>… fast-forward, best effort the named worktrees
mabo sync --all fast-forward, best effort every recorded worktree (see below for agents)
mabo sync --mine fast-forward, best effort worktrees whose owner is $MABO_AGENT_ID
add --no-default skipped unchanged
mabo sync                                 # from the main checkout: just update main
cd "$(mabo where feature/auth)" && mabo sync   # main (best effort) + feature/auth
mabo sync feature/auth feature/tests      # named worktrees
mabo sync --all --no-default              # every worktree, leave main alone
MABO_AGENT_ID=worker-1 mabo sync --mine --json   # an agent syncing only its own worktrees
main          updated
feature/auth  updated     
feature/old   merged      Use mabo gc to remove landed worktrees
feature/wip   dirty       
feature/api   conflict    src/api.rs (rebase aborted; worktree unchanged)
Sync complete: 2 updated, 1 merged, 1 dirty, 1 conflict

How each worktree is integrated. The default strategy is auto:

  • Rebase when none of the branch's own commits (base..branch) is on any remote-tracking ref. Local-only history stays linear.
  • Merge as soon as any of them has been pushed. Published history is never rewritten behind your back; you get a Merge origin/main into feature/x commit instead.
  • --merge always merges. --rebase always rebases, but on pushed commits it refuses with code pushed (exit 3) unless you also pass --force.
mabo sync feature/x --rebase          # feature/x  failed  Published commits require --force with --rebase   (exit 3)
mabo sync feature/x --rebase --force  # you've decided to rewrite it; you push with --force-with-lease yourself

Safety guarantees.

  • Dirty worktrees are skipped, including untracked files. So is a worktree where the rebase or merge would overwrite an ignored file, for example a local .env or build output at a path some commit touches. mabo checks every commit it would replay or apply, not just the end states.
  • Conflicts abort cleanly. On a conflict, even in the middle of a multi-commit rebase, mabo lists the conflicting files and aborts. HEAD, the index, refs, the stash, and file contents end up exactly as they were. Git may refresh the timestamps of files the rebase touched. Want to resolve it yourself? Run mabo sync <name> --keep-conflicts on a single worktree, and the rebase or merge stays in progress.
  • Skipped instead of touched: detached worktrees (detached), branches whose work already landed in their base, including squash merges (merged, use mabo gc), and, with --all while MABO_AGENT_ID is set, worktrees another agent owns (owned). Naming another agent's worktree explicitly still works.
  • The default branch only fast-forwards, including when it's checked out in a linked worktree. Local commits on it report diverged, and blocking changes report dirty. When you also asked for worktrees, these are best effort and don't stop the worktree updates.
  • Nothing leaves your machine. sync never pushes, never changes upstream configuration, and runs no hooks.
  • One mabo operation at a time. sync holds a repository operation lock for the whole run, so a concurrent create, attach, delete, gc, or github checkout waits ("Waiting for another mabo operation to finish…" on stderr). It doesn't wait on .mabo.toml hooks or --exec commands, because those run after the lock is released. A mabo command started from a Git hook inside another mabo operation (for example, a post-checkout hook) reuses that operation's lock instead of deadlocking.
  • History is never silently rewritten or dropped. A rebase keeps commits that upstream cherry-picked and then reverted, as well as intentionally empty commits. A local merge commit that carries changes of its own (an "evil merge" or a conflict resolution) isn't flattened: sync refuses it with merge commit <sha> has changes of its own; use --merge. History with no common base is refused too. Commits count as published when they're on any ref your fetch refspecs write to, not only refs/remotes/*.
  • A failure stays with its own item. A failed fetch only fails the targets whose base comes from that remote. A default branch that can't be determined only fails the default item. When a run has both an ordinary failure and a published-history refusal, the exit code is 1.
  • Broken worktrees stay contained. A worktree whose directory was deleted is skipped as dirty. A default branch that's mid-rebase or mid-bisect anywhere is skipped as dirty (for example, "main is being rebased in /repo"). Your own Git config, such as --no-commit merge options, merge.ff=only, or rebase.autoSquash, can't make sync stop halfway or report success without integrating. Sync also disables Git hooks and core.fsmonitor for every Git command it runs.
  • Fresh worktrees stay fresh. A worktree with no commits of its own is simply moved to the new base tip. gc doesn't mistake it for merged work afterwards.

Requirements. Sync's rebase needs Git 2.29 or later (--reapply-cherry-picks). Checking merge commits before a rebase needs Git 2.38 or later (git merge-tree --write-tree). With an older Git, a branch that contains merges is refused with a --merge hint instead of being flattened.

Bases. mabo create and mabo github checkout now record each worktree's sync base in metadata: your --base, normalized to a full ref such as refs/remotes/origin/next, or else the remote-tracking default branch. A PR checkout records the PR's target branch (for example refs/remotes/origin/next). Worktrees from older releases have no recorded base and sync against the default branch. To change a base, or to repair one whose branch was deleted (reported as base-gone with a hint):

mabo meta set feature/x --base origin/release-7

Exit codes and JSON.

  • 0: everything updated, up-to-date, or skipped.
  • 1: any conflict or failed, or a default-only run that was dirty or diverged.
  • 2: a usage error, such as --keep-conflicts with several targets or --mine without MABO_AGENT_ID.
  • 3: a refusal to rewrite pushed history, when nothing else failed. Any conflict or failure in the same run makes it 1.

--json prints one document:

{
  "schema": 1,
  "default": { "name": "main", "...
Read more

Maboroshi v7.0.0-alpha.5

Pre-release

Choose a tag to compare

@aku11i aku11i released this 20 Oct 16:41
2783f2e

This alpha gives the tool its new name: Maboroshi (幻), with the command mabo (#841). It also moves worktrees out of your repository into the XDG data directory (#837), and stops delete from throwing away commits that exist nowhere else (#839).

Please note:

  • This is an alpha. Commands, flags, and the JSON schema may still change before 7.0.0.
  • Breaking: everything named ptm is now mabo or maboroshi, and there is no migration. The old binary, config file, env vars, and data directories are not read. See Breaking changes.
  • Breaking: new worktrees no longer go in .git/ptm/worktrees. They go under $XDG_DATA_HOME/maboroshi/worktrees/. See Worktrees under the XDG data directory.
  • There is no compatibility with v6. The TypeScript phantom on main and the npm package are unchanged.
  • This release is not published to npm. npm install still gives you the TypeScript v6 phantom. To get Maboroshi, download a prebuilt mabo binary from this release. See Install.
  • JSON output stays at schema version 1. Error codes are unchanged.
  • The repository is still phantompane/phantom. It will move to yoiailabs/maboroshi later.

Install

Prebuilt binaries are attached to this release. Each archive contains a single mabo binary:

OS Architecture Asset
Linux x86_64 maboroshi-v7.0.0-alpha.5-linux-x64.tar.gz
Linux arm64 maboroshi-v7.0.0-alpha.5-linux-arm64.tar.gz
macOS Apple silicon maboroshi-v7.0.0-alpha.5-darwin-arm64.tar.gz
macOS Intel maboroshi-v7.0.0-alpha.5-darwin-x64.tar.gz
# Pick <os>-<arch> from the table, e.g. linux-x64 or darwin-arm64.
gh release download v7.0.0-alpha.5 --repo phantompane/phantom --pattern 'maboroshi-v7.0.0-alpha.5-linux-x64.tar.gz'
tar -xzf maboroshi-v7.0.0-alpha.5-linux-x64.tar.gz
install -m 755 mabo ~/.local/bin/mabo
mabo --version   # mabo v7.0.0-alpha.5+rust (…)

On macOS, a binary downloaded with a browser may be quarantined. Run xattr -d com.apple.quarantine ~/.local/bin/mabo if it won't start.

To build from source instead: git clone https://github.com/phantompane/phantom.git && cd phantom && git checkout v7.0.0-alpha.5 && cargo install --path . --locked.


Highlights

Maboroshi (幻) and the mabo command (#841, #842)

Maboroshi is Japanese for a phantom or illusion. It is the same idea as before, written in the project's own language, and it gets the tool away from a crowded English name. The command is the short form, mabo.

$ mabo --version
mabo v7.0.0-alpha.5+rust (…)
$ mabo create feature-auth
Created worktree 'feature-auth' at /home/me/.local/share/maboroshi/worktrees/app-1c941637/feature-auth
$ cd "$(mabo where feature-auth)"

The rename is complete, and nothing falls back to the old names:

  • Help, errors, and docs all say mabo.
  • Hooks and mabo exec export MABO_* variables.
  • Data lives in maboroshi directories.
  • ptm, PTM_*, .ptm.toml, ptm/ directories, and the older phantom and PHANTOM_* names are all ignored.

Breaking changes (old → new)

What alpha.4 alpha.5
Command ptm mabo
Cargo package / crate phantom maboroshi
Project config .ptm.toml .mabo.toml
Hook and exec env flag PTM=1 MABO=1
Hook and exec env vars PTM_NAME, PTM_PATH, PTM_ROOT_PATH MABO_NAME, MABO_PATH, MABO_ROOT_PATH
Agent owner PTM_AGENT_ID MABO_AGENT_ID
Trust everything for one process PTM_TRUST_ALL=1 MABO_TRUST_ALL=1
User preferences $XDG_CONFIG_HOME/ptm/config.toml $XDG_CONFIG_HOME/maboroshi/config.toml
Trust records $XDG_STATE_HOME/ptm/trust.toml $XDG_STATE_HOME/maboroshi/trust.toml
Worktree metadata <git-common-dir>/ptm/worktrees.json <git-common-dir>/maboroshi/worktrees.json
Default worktree location <git-common-dir>/ptm/worktrees/<name> $XDG_DATA_HOME/maboroshi/worktrees/<checkout>-<hash>/<name>
Project marker / lock — .maboroshi-common-dir, .maboroshi-worktrees.lock
Release asset ptm-v<version>-<os>-<arch>.tar.gz (contains ptm) maboroshi-v<version>-<os>-<arch>.tar.gz (contains mabo)

Preference keys (worktrees-directory, directory-name-separator, keep-branch, ghq-discovery, root) and JSON error codes are unchanged.

Upgrading from alpha.4

There is no automatic migration. Do these once by hand:

# 1. Install mabo (see Install above). Keep ptm until you no longer need it.

# 2. Rename project config and env vars.
git mv .ptm.toml .mabo.toml           # in each repository that has hooks
#    PTM_AGENT_ID → MABO_AGENT_ID, PTM_TRUST_ALL → MABO_TRUST_ALL, and $PTM_* in hook scripts → $MABO_*

# 3. Move your preferences, if any.
mkdir -p ~/.config/maboroshi && mv ~/.config/ptm/config.toml ~/.config/maboroshi/

# 4. Re-trust repositories whose hooks you run (trust is not carried over).
mabo trust --yes

Existing worktrees made by ptm keep working as ordinary Git worktrees. mabo lists them, under their branch name or their short commit hash when detached, but it doesn't consider them its own:

  • mabo delete refuses them. Remove them with git worktree remove <path>, or finish with them using the old ptm binary.
  • mabo gc never touches them.

To adopt one, record it with mabo meta set <name> --purpose "…".

Worktrees under the XDG data directory (#837)

What's new. mabo create and mabo attach no longer put worktrees inside your repository's .git. The default location is:

$XDG_DATA_HOME/maboroshi/worktrees/<checkout>-<hash>/<name>
# ~/.local/share/maboroshi/worktrees/... when XDG_DATA_HOME is unset, empty, or relative
  • <checkout> is the main checkout's directory name (characters outside A–Z a–z 0–9 . _ - become -).
  • <hash> is the first 8 hex digits of the SHA-256 of the canonical git common directory. Two clones of the same remote get different folders, and a symlinked spelling of the same checkout gets the same folder.

Why.

  • Worktrees inside .git were hidden from your file manager and editor.
  • They got swept up by tools that walk .git.
  • They were lost when the clone was deleted.

A single XDG location keeps every project's worktrees in one predictable place, out of the repository, with .git left as Git's own.

How to use it. Nothing to configure. mabo where <name> and mabo list show the paths, and mabo list abbreviates paths under your home with ~ (#840):

$ mabo list
main (~/src/app)
feature-auth (~/.local/share/maboroshi/worktrees/app-1c941637/feature-auth)
$ mabo list --json | jq -r '.worktrees[].path'     # always absolute

To keep worktrees next to the repository instead, set a path relative to the repository root:

mabo preferences set worktrees-directory ../maboroshi-worktrees

Safety rules. mabo deletes or garbage-collects only worktrees it owns: those inside the resolved worktrees directory, or recorded in <git-common-dir>/maboroshi/worktrees.json (compared by canonical path).

  • The per-project folder is removed only when it is empty and its .maboroshi-common-dir marker names this repository.
  • maboroshi/worktrees itself is never removed.
  • A custom worktrees-directory is left in place when it becomes empty.

delete protects unpushed and detached commits (#839, closes #838)

What's new. mabo delete used to delete the branch along with the worktree even if the branch had commits that existed nowhere else. It now refuses unless every commit is on the default branch or on a remote:

$ mabo delete spike
Worktree 'spike' has unpushed commits. Use --keep-branch to keep the branch, or --force to delete it.
$ echo $?
3

Why. A worktree is cheap, but an unpushed branch can be hours of an agent's work. Deleting it should be a decision, not a side effect.

How it works.

  • Branch worktrees. Commits reachable from refs/heads/<branch> but not from the default branch (origin/HEAD, else main/master) or any remote-tracking ref are "unpushed". The check uses the full ref, so a tag or a remote ref with the same name can't hide them.
  • Detached worktrees. The check runs on the worktree's HEAD commit. A detached worktree with a new commit is refused, even with --keep-branch, because there's no branch to keep.
  • When the check can't run (for example, no default branch can be found), delete refuses rather than guess.
  • Options:
    • --keep-branch deletes the worktree but keeps the branch, so nothing is lost; it's allowed without --force.
    • --force deletes anyway.
  • The current directory. Deleting the worktree you are standing in, by name, now needs --current or --force. This works from a subdirectory and through symlinked paths (#840).
  • Several names at once. Every name is checked before anything happens. If any is refused, nothing is deleted and no pre-delete hook runs. With --json, every refusal is listed. A repeated name, or two names for the same worktree, is handled once (#840).
$ mabo delete done-1 spike done-2 --json   # spike is unpushed → nothing deleted, exit 3
$ mabo delete spike --keep-branch          # remove the worktree, keep the branch
$ mabo delete spike --force                # really delete both

JSON refusals use code unpushed (or uncommitted / validation) with exit code 3. mabo gc uses the same unpushed check, so it now also skips a branch whose only copy is shadowed by a same-named tag.


Smaller fixes

  • Recorded worktrees outside the managed directory keep their names (#840). A worktree recorded in metadata is listed and found by its recorded name, for example an older detached next worktree, not by its short commit hash. where, exec, delete, meta, and gc all use that same name...
Read more

ptm v7.0.0-alpha.4

ptm v7.0.0-alpha.4 Pre-release
Pre-release

Choose a tag to compare

@aku11i aku11i released this 20 Oct 12:45
6376426

This alpha adds hook trust: ptm no longer runs a repository's .ptm.toml hooks until you have agreed to trust that project (#833, #835). It also fixes ptm gc so it never collects a worktree that was just created (#832), and makes ptm find the right main checkout in separate-git-dir and submodule layouts (#828, #831).

Please note:

  • This is an alpha. Commands, flags, and the JSON schema may still change before 7.0.0.
  • Behavior change: a repository that defines [hooks] in .ptm.toml now refuses create, attach, delete, and github checkout until it is trusted, or until you pass --no-hooks. Agents and CI get an untrusted error instead of a prompt. Repositories without hooks are not affected. See Hook trust.
  • There is no compatibility with v6.
  • This release is not published to npm. The npm package stays on v6. Prebuilt ptm binaries for Linux and macOS (x64 and arm64) are attached to this release.
  • JSON output stays at schema version 1. New fields are additive, and there is one new error code, untrusted.

Highlights

Hook trust (#833, #835, closes #792, #834)

.ptm.toml hooks are shell commands that come from the repository:

# .ptm.toml
[hooks]
post-create = ["pnpm install"]
pre-delete = ["docker compose down"]

Until now, cloning a repository and running ptm create executed its post-create commands with no question asked. That is fine for your own projects, but not for something you just cloned to review, or for an agent that was pointed at an unfamiliar repository. ptm now works like VS Code's Workspace Trust: hooks run only in projects you have trusted.

How trust works

  • Trust is per project, not per command. You trust a repository once, and its main checkout and all its linked worktrees share that decision. Editing the hook commands later does not ask again.

  • Trust is keyed by the repository's git common directory (git rev-parse --git-common-dir, canonicalized). In a normal clone that is <checkout>/.git; in a separate-git-dir layout it is the git dir. Symlinked or relative paths resolve to the same record.

    • A second clone of the same remote is a different project and is not trusted automatically.
    • Deleting a repository and cloning it again at the same path keeps the old record. Run ptm untrust first if that matters to you.
  • Only hooks are gated. post-create and pre-delete from .ptm.toml need trust. .worktreeinclude copies, --exec, and ptm exec are things you ask for explicitly and are not gated. A repository with no hook commands (or an empty [hooks] table) never asks.

  • Records live in $XDG_STATE_HOME/ptm/trust.toml (default ~/.local/state/ptm/trust.toml):

    [[project]]
    path = "/home/me/app/.git"
    trustedAt = "2026-10-20T12:37:41.844Z"

What happens in an untrusted project

At a terminal, ptm shows the project and the exact commands, then asks. Only y or yes continues; anything else, including just pressing Enter, refuses:

$ ptm create feature-x
Project: /home/me/app
post-create:
  pnpm install
pre-delete:
  docker compose down
Trust this project and run its hooks? [y/N] y
Running post-create commands...
Executing: pnpm install
Created worktree 'feature-x' at /home/me/app/.git/ptm/worktrees/feature-x

Answering y saves trust for the project, so the next command doesn't ask. Control characters and invisible Unicode in hook commands (escape sequences, bidi overrides, zero-width characters) are shown escaped, for example \u{1b}, so a hook can't hide what it does in the prompt.

Without a person to answer, ptm never asks. It refuses before it changes anything — no branch, no worktree, no fetch — and exits 1. That applies when:

  • --json is passed,
  • stdin or stderr is not a terminal,
  • CI is set, or
  • PTM_AGENT_ID is set (#835). Agents often run under a pseudo-terminal, where a prompt would hang forever. An empty value counts as unset.
$ PTM_AGENT_ID=grok ptm create feature-x
Untrusted project: /home/me/app
post-create:
  pnpm install
pre-delete:
  docker compose down
Refusing to run project hooks. Run `ptm trust` or pass `--no-hooks`.
$ echo $?
1

With --json, stdout is the usual error envelope with the new code untrusted:

{
  "version": 1,
  "error": {
    "message": "Untrusted project: /home/me/app\npost-create:\n  pnpm install\npre-delete:\n  docker compose down\nRefusing to run project hooks. Run `ptm trust` or pass `--no-hooks`.",
    "code": "untrusted",
    "exitCode": 1
  }
}

This covers every command that runs hooks: create, attach, and github checkout (post-create), and delete (pre-delete), including delete --force. github checkout checks trust before it fetches the PR branch.

Managing trust: ptm trust / ptm untrust

$ ptm trust                 # shows the hooks, then asks "Trust this project? [y/N]"
Trusted /home/me/app/.git

$ ptm trust --yes ~/src/other-repo     # no question; required for agents, CI, and --json
$ ptm trust --list
/home/me/app/.git	2026-10-20T12:37:41.844Z

$ ptm untrust
Removed trust for /home/me/app/.git
  • ptm trust [path] and ptm untrust [path] take any directory inside the repository; the default is the current directory. A repository with no hooks can still be trusted.
  • ptm trust doesn't ask where ptm wouldn't prompt (no terminal, --json, CI, PTM_AGENT_ID); it tells you to re-run with --yes.
  • ptm untrust on a project that isn't trusted is a no-op that exits 0. Untrust takes effect right away, including for linked worktrees.
  • All three accept --json: trust → {"version":1,"path","trustedAt"}, untrust → {"version":1,"path","removed"}, trust --list → {"version":1,"projects":[{"path","trustedAt"}]}.

Skipping or bypassing hooks

  • --no-hooks on create, attach, delete, gc, and github checkout does the worktree change and runs no hooks. It does not trust the project.

    $ ptm create feature-x --no-hooks
    Created worktree 'feature-x' at /home/me/app/.git/ptm/worktrees/feature-x
  • PTM_TRUST_ALL=1 treats every repository as trusted for that one process and writes nothing. It's meant for CI jobs and throwaway containers. The value must be exactly 1.

ptm gc and trust

gc never prompts, even at a terminal. In an untrusted project with pre-delete hooks, it skips the candidates it would have removed, with the detail untrusted hooks, and moves on:

$ ptm gc --apply
skip fix-login [merged] untrusted hooks
Removed 0. Skipped 1. Failed 0.

$ ptm gc --apply --no-hooks      # remove them without running pre-delete
removed fix-login [merged]
Removed 1. Skipped 0. Failed 0.

Setting up agents and CI

  • Your own repositories: run ptm trust once per repository (or ptm trust --yes <path> from a script). Agents working there then run hooks as before.
  • Repositories you don't control: have agents pass --no-hooks, or leave them untrusted and handle the untrusted error code.
  • CI / containers: set PTM_TRUST_ALL=1 if the job should run hooks.

ptm gc never collects a fresh worktree (#832, closes #826)

alpha.3's gc decided that a worktree was "still at its creation point" by reading the branch reflog. If the reflog was missing (core.logAllRefUpdates=false, or expired entries), a brand-new worktree with no commits counted as merged once the default branch moved ahead, and gc --apply removed it, even when another agent had just created it.

ptm now records the commit a branch started from as createdFrom in the worktree metadata:

  • ptm create (and github checkout of an issue) stores the new branch's starting commit.
  • ptm attach and github checkout of an existing PR branch store the oldest reflog entry for that branch, or null when there is no reflog.
  • gc does not treat a branch as merged while its tip still equals createdFrom. That holds even without a reflog.
  • Records written before alpha.4 have no createdFrom and use the reflog as before.

createdFrom also appears in the worktree JSON:

"createdAt": "2026-10-20T08:31:32.236Z",
"createdFrom": "38b97374176d03bd95cc65936040b7eeed79b9c7",

Merged branches are still collected as before: fast-forward merges, merge commits, and squash merges through a closed or merged PR.

Main checkout in separate-git-dir and submodule layouts (#828, #831, closes #824, #829, #830)

Most repositories keep their git directory at <checkout>/.git. With git init --separate-git-dir, in submodules, and in similar setups, the git directory lives somewhere else, and Git itself reports that directory, not your checkout, as the "main worktree". ptm used to pass that along, which led to several bugs:

  • ptm delete --current failed from inside a linked worktree.
  • A relative worktrees-directory nested new worktrees inside the current one.
  • ptm list showed the git directory as the main entry with "default": false, and ptm exec main ran commands inside the git directory.

ptm now finds the main checkout with a fixed set of rules and never guesses:

  1. If the command is running inside a checkout whose .git file points at the git directory, that checkout is the main checkout.
  2. Otherwise, if core.worktree is set, that directory is the main checkout. Submodules set it automatically. If it doesn't point back to the git directory, ptm says so:
    core.worktree is set to /home/me/nogit, which has no .git gitfile pointing at /home/me/var/sep.git.
    
  3. Otherwise ptm runs a short, bounded search near the git directory for a checkout whose .git file points at it. If it finds exactly one, that is the main checkout. If it finds several (for example after cp -r of a checkout) or none, ptm stops with an error that tells you how to fix it:
    More than one c...
    
Read more

ptm v7.0.0-alpha.3

ptm v7.0.0-alpha.3 Pre-release
Pre-release

Choose a tag to compare

@aku11i aku11i released this 20 Oct 07:32
6d521bd

This alpha helps agents that share a machine keep their worktrees in order. ptm now records who created each worktree and why (#813). It also gains ptm gc to remove the ones nobody needs anymore (#825), and keeps worktrees under .git/ptm/worktrees (#812, #823).

Please note:

  • This is an alpha. Commands, flags, and the JSON schema may still change before 7.0.0.
  • There is no compatibility with v6, and none was added in this release. Existing worktrees under .git/phantom/worktrees are not migrated automatically. See Moving existing worktrees.
  • This release is not published to npm. The npm package stays on v6. Prebuilt ptm binaries for Linux and macOS (x64 and arm64) are attached to this release.
  • JSON output stays at schema version 1. Every new field is additive.

Highlights

ptm gc: clean up stale worktrees safely (#825, closes #775)

Agents create worktrees all the time and rarely clean up after themselves. Over a few days a repository collects dozens of checkouts for branches that were merged long ago, PRs that were closed, and directories that were deleted by hand. ptm gc finds those worktrees and removes them, and it is careful never to throw away work.

gc is a dry run by default. It prints the plan and changes nothing until you pass --apply (or --force):

$ ptm gc
skip docs-typo [merged] dirty
would remove fix-login [merged]
Dry run. 1 would be removed, 1 skipped. Re-run with --apply to remove them.

$ ptm gc --apply
skip docs-typo [merged] dirty
removed fix-login [merged]
Removed 1. Skipped 1. Failed 0.

$ ptm gc --apply
skip docs-typo [merged] dirty
Removed 0. Skipped 1. Failed 0.

Each line names the worktree, why it matched (in brackets), and for a skip, why it was kept. Running gc again is safe: a second --apply removes nothing new.

What gc selects

With no criterion flag, gc selects worktrees that are merged, have a closed or merged pull request, or are orphaned. If you pass one or more criterion flags, only those criteria are used, and a worktree matching any of them is a candidate.

Flag Selects a worktree when…
--merged its branch is already contained in the default branch. The default branch is origin/HEAD when that ref exists, otherwise main, otherwise master. A branch that still points at the commit it was created from is not merged, so a fresh worktree is never removed just because its branch has no commits yet.
--pr its linked pull request is closed or merged (see Pull request checks).
--orphaned its directory is missing, Git reports the registration as prunable, or its branch ref is gone. A metadata record whose directory is already gone is dropped without touching anything Git does not list.
--older-than <N><unit> it was last used (or created, if never used) at least that long ago. Units are m, h, d, w, for example 14d. Only used when you pass it.

Two more flags narrow the scope or change what gets deleted:

  • --owner <id> only considers worktrees recorded for that owner (see worktree metadata). Unowned worktrees are excluded.
  • --keep-branch removes the worktree but keeps its branch. The keep-branch preference has the same effect.

--project <ref> / -p works as it does for other commands.

Safety rules

gc is meant to run unattended, so its rules lean towards keeping things:

  • Never removed, not even with --force:
    • the current worktree (where you run gc),
    • the primary checkout,
    • any worktree checked out on the default branch (its branch is kept too),
    • a locked worktree (git worktree lock).
  • Another agent's worktree is removed only when it is merged, its PR is closed or merged, it is orphaned, or --owner names that agent. Age alone never removes another agent's worktree. "Another agent" means a worktree recorded with an owner that is not PTM_AGENT_ID. When PTM_AGENT_ID is unset, every owned worktree counts as another agent's.
  • Uncommitted or unpushed work is kept unless you use --force. Worktrees with modified or untracked files, or whose branch has commits that are on neither the default branch nor any remote-tracking branch, are skipped (dirty, unpushed). If gc can't determine the state, it skips the worktree as status unknown.
  • Branches with unpushed commits survive. When a worktree is removed without --force and gc can't confirm its commits are pushed, the branch is kept (kept the branch because it has unpushed commits).
  • Removal goes through the normal delete path. The [hooks] pre-delete commands from .ptm.toml run first, then the same deletion as ptm delete, including the metadata record. If a hook fails, that worktree is skipped and gc moves on to the next one.

-f, --force implies --apply. It removes matching worktrees even when they are dirty or have unpushed commits, and deletes their branches. It still never touches the protected worktrees listed above.

$ ptm gc --merged --force
skip current [merged] current worktree
removed dirty [merged]
skip locked [merged] locked
removed untracked [merged]
Removed 2. Skipped 2. Failed 0.

Pull request checks

The --pr criterion uses the PR link stored in the worktree's metadata, which is set by ptm github checkout, --pr, or ptm meta set --pr:

  • A stored URL is passed directly to gh pr view <url>. A PR in a fork or another repository is checked in that repository, never mixed up with a same-numbered PR in the current repo.
  • A stored number without a URL is checked only when gh repo view can name the current repository.
  • When gh is missing, not signed in, or can't answer, gc warns on stderr (Warning: pull request status unavailable for <name>) and skips the worktree with pr status unavailable. It never fails the run or removes the worktree on a guess.

Examples

ptm gc                               # show what would be removed
ptm gc --apply                       # remove merged, closed-PR, and orphaned worktrees
ptm gc --older-than 14d --apply      # remove worktrees unused for 14 days
ptm gc --owner grok --force          # remove one agent's stale worktrees, including dirty ones
ptm gc --merged --keep-branch --apply
ptm gc -p owner/repo --json

--json

ptm gc --json prints one object listing each candidate, why it matched, and what happened to it:

{
  "version": 1,
  "applied": false,
  "candidates": [
    {
      "name": "docs-typo",
      "path": "/home/me/demo/.git/ptm/worktrees/docs-typo",
      "branch": "docs-typo",
      "owner": "claude",
      "reasons": ["merged"],
      "action": "skipped",
      "detail": "dirty",
      "branchDeleted": null
    },
    {
      "name": "fix-login",
      "path": "/home/me/demo/.git/ptm/worktrees/fix-login",
      "branch": "fix-login",
      "owner": "grok",
      "reasons": ["merged"],
      "action": "wouldRemove",
      "detail": null,
      "branchDeleted": null
    }
  ],
  "error": null
}
  • applied is true when removal was requested (--apply or --force).
  • reasons contains one or more of merged, pullRequest, orphaned, olderThan.
  • action is wouldRemove, removed, skipped, or failed.
  • detail explains a skip or failure: current worktree, default branch, locked, other agent, dirty, unpushed, status unknown, pr status unavailable, a hook or Git error, or a note that the branch was kept.
  • branchDeleted is null for a dry run or a skip.
  • If a removal fails, the top-level error object is set and the exit code is 1. The other candidates are still listed. An empty plan exits 0.
  • An invalid --older-than value is a validation error (exit 3).

Worktree metadata: owner, purpose, and links (#813, closes #774)

When several agents work in the same repository, Git can tell you a worktree's branch and path, but not who created it, what it's for, or which PR it belongs to. ptm now records that when it creates a worktree and reports it everywhere a worktree is shown. This is also what lets ptm gc tell your worktrees apart from another agent's.

Recording it

ptm create and ptm attach accept four new options. ptm github checkout accepts --owner and --purpose and stores the PR or issue number and URL automatically.

Option Meaning
--owner <id> Who created the worktree. When omitted, PTM_AGENT_ID is used if set.
--purpose <text> Free-text description of the task.
--pr <number|url> Link a pull request (a number or an http(s) URL).
--issue <number|url> Link an issue (a number or an http(s) URL).

The simplest setup for an agent is to export PTM_AGENT_ID once, so every worktree it creates is tagged with its owner:

$ ptm create fix-login --owner grok --purpose "fix the login redirect" --issue 42
Created worktree 'fix-login' at /home/me/demo/.git/ptm/worktrees/fix-login

$ PTM_AGENT_ID=claude ptm create docs-typo --purpose "README typo"
Created worktree 'docs-typo' at /home/me/demo/.git/ptm/worktrees/docs-typo

Updating it: ptm meta set

ptm meta set <name> changes the record of an existing worktree. You need at least one of --owner, --purpose, --pr, --issue. Omitted fields keep their current values, and PTM_AGENT_ID is not applied. A worktree with no record yet gets one. An unknown name exits 2. --json prints the updated worktree object.

$ ptm meta set fix-login --pr https://github.com/acme/demo/pull/43
Updated metadata for 'fix-login'

Reading it

ptm list shows [owner] and the purpose, and ptm list --owner <id> filters by owner (unowned worktrees are omitted):

$ ptm list
main (.)
docs-typo (.git/ptm/worktrees/docs-typo) [claude] README typo
fix-l...
Read more