Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "pando",
"owner": {
"name": "zpyoung"
},
"plugins": [
{
"name": "pando",
"source": "./plugin",
"description": "Claude Code worktree lifecycle management powered by pando"
}
]
}
81 changes: 81 additions & 0 deletions .pando.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,20 @@ deleteBranchOnRemove = "local"
# Default: "main"
# targetBranch = "main"

# Default lifecycle kind for newly created worktrees
# "auto" lets Pando choose; explicit options are "ephemeral" and "long-lived"
# Default: "auto"
defaultKind = "auto"

# Time-to-live assigned to ephemeral worktrees
# Use a duration such as "30m", "4h", or "2d"
# Default: "4h"
ephemeralTtl = "4h"

# Automatically lock worktrees that are currently active against reaping
# Default: true
autoLockActive = true

# ============================================================================
# Clean Configuration
# ============================================================================
Expand All @@ -153,6 +167,61 @@ deleteBranchOnRemove = "local"
# Default: false
# fetch = true

# ============================================================================
# Reap Configuration
# ============================================================================
# Controls safety checks when reaping ephemeral worktrees

[reap]
# Require the worktree branch to be merged before it can be reaped
# Default: true
requireMerged = true

# ============================================================================
# Concurrency Configuration
# ============================================================================
# Controls retry behavior when lifecycle operations encounter concurrent updates

[concurrency.retry]
# Maximum number of attempts before failing
# Default: 5
maxAttempts = 5

# Initial retry delay in milliseconds
# Default: 100
baseMs = 100

# Maximum retry delay in milliseconds
# Default: 2000
capMs = 2000

# ============================================================================
# Port Allocation Configuration
# ============================================================================
# Controls deterministic service ports and database names for worktrees

[ports]
# Enable port and database allocation
# Default: false
enabled = false

# Inclusive port range available for allocation
# Default: "3100-3199"
range = "3100-3199"

# Logical service names that receive allocated ports
# Default: ["web"]
names = ["web"]

# Database naming strategy
# Options: "named"
# Default: "named"
dbStrategy = "named"

# Base name used with each worktree's own branch to derive its database name
# Default: "dev"
dbBaseName = "dev"

# ============================================================================
# Post-command Scripts
# ============================================================================
Expand Down Expand Up @@ -216,7 +285,19 @@ deleteBranchOnRemove = "local"
# PANDO_WORKTREE_DELETE_BRANCH_ON_REMOVE=local
# PANDO_WORKTREE_USE_PROJECT_SUBFOLDER=true
# PANDO_WORKTREE_TARGET_BRANCH=main
# PANDO_WORKTREE_DEFAULT_KIND=ephemeral
# PANDO_WORKTREE_EPHEMERAL_TTL=4h
# PANDO_WORKTREE_AUTO_LOCK_ACTIVE=false
# PANDO_CLEAN_FETCH=true
# PANDO_REAP_REQUIRE_MERGED=true
# PANDO_CONCURRENCY_RETRY_MAX_ATTEMPTS=9
# PANDO_CONCURRENCY_RETRY_BASE_MS=100
# PANDO_CONCURRENCY_RETRY_CAP_MS=2000
# PANDO_PORTS_ENABLED=true
# PANDO_PORTS_RANGE=3100-3199
# PANDO_PORTS_NAMES=web,api
# PANDO_PORTS_DB_STRATEGY=named
# PANDO_PORTS_DB_BASE_NAME=dev

# ============================================================================
# CLI Flag Overrides
Expand Down
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ Create a new git worktree (supports both creating new branches and checking out
- `-c, --commit`: Commit hash to base the new branch on
- `-f, --force`: Force create branch even if it exists (uses git worktree add -B)
- `--no-rebase`: Skip automatic rebase of existing branch onto source branch
- `--ephemeral`: Mark the worktree as ephemeral (mutually exclusive with `--long-lived`)
- `--long-lived`: Mark the worktree as long-lived (mutually exclusive with `--ephemeral`)
- `--ttl <duration>`: Set a per-worktree TTL override (for example, `30m` or `2d`)
- `--owner <id>`: Record an owner or agent session id
- `--ports`: Force-enable configured port and database-name allocation for this run
- `--skip-rsync`: Skip the rsync copy step (ignores rsync config)
- `--rsync-flags`: Override rsync flags (comma-separated; repeatable)
- `--rsync-exclude`: Additional rsync exclude patterns (comma-separated; repeatable)
Expand All @@ -96,6 +101,8 @@ Create a new git worktree (supports both creating new branches and checking out

> When `--skip-rsync` is combined with `--rsync-flags` or `--rsync-exclude`, the rsync flags are ignored and a warning is shown.

**Lifecycle metadata**: `pando add` stores the worktree kind, creation time, source branch, and optional owner/TTL in Git's per-worktree config. Flags override `worktree.defaultKind`; `auto` treats Claude worktree paths, active agent-session environments, and boolean `PANDO_EPHEMERAL=true|1|yes` signals as ephemeral, and other worktrees as long-lived. Worktrees created during an active `CLAUDE_SESSION_ID` or `PANDO_SESSION` are automatically Git-locked when `worktree.autoLockActive` is enabled; passing `--owner` alone does not trigger auto-locking. When `[ports].enabled` or `--ports` is set, requested service ports and a database name derived from each worktree's own branch are also stored. Exhausted ports produce a warning without failing worktree creation.

**Automatic Rebase**: When checking out an existing branch, pando automatically rebases it onto the current branch. This keeps your feature branches up-to-date. If the rebase fails (e.g., conflicts), a warning is shown but the worktree is still created. Use `--no-rebase` to skip this behavior, or set `worktree.rebaseOnAdd = false` in config.

**Rsync Behavior**: By default (`rsync.onlyUntracked = true`), rsync carries over only files that git ignores in the source worktree — build artifacts like `.venv/`, `node_modules/`, and caches. Tracked files always come from the new worktree's own checkout, so rsync can never dirty the tree, and artifact syncing works even when the new worktree is on a different branch or commit. Non-ignored untracked files (work in progress) are deliberately not copied. With `rsync.onlyUntracked = false`, pando mirrors the full source tree instead, but only when the source and new worktree are on the same commit — otherwise rsync is skipped with a warning so tracked files from the source branch do not dirty the new checkout. Use `--skip-rsync` to disable rsync explicitly.
Expand Down Expand Up @@ -143,9 +150,12 @@ List all git worktrees

```bash
pando list
pando list --verbose
pando list --json
```

Verbose human output includes compact kind, owner, age, and lock details. JSON output always includes `kind`, `createdAt`, `owner`, `ttl`, `ageMs`, and `locked` for every worktree (nullable metadata fields are `null`).

### `pando health`

Show health status of all worktrees
Expand All @@ -163,6 +173,8 @@ Show health status of all worktrees
- `detached`: Worktree is in detached-HEAD state (no branch)
- `error`: Cannot check status (directory missing, git error, or remote check failed)

Each human report entry also shows lifecycle kind and lock state. JSON worktree entries include `kind`, `ageMs`, `ttl`, `locked`, and `owner` alongside the health fields.

**Examples:**

```bash
Expand Down Expand Up @@ -226,6 +238,7 @@ Remove a git worktree
- Before deleting, checks if branch is merged (use `--force` to skip this check)
- Remote branch deletion requires confirmation unless `--force` is used
- Use `worktree.deleteBranchOnRemove` in config to change default behavior
- JSON results include the worktree's captured lifecycle `metadata`, including assigned ports and database name

**Examples:**

Expand Down Expand Up @@ -291,6 +304,40 @@ pando clean --keep-branch
pando clean --json
```

### `pando reap`

Reclaim expired ephemeral worktrees without touching long-lived, locked, or dirty worktrees. A worktree is eligible after its metadata TTL (or `worktree.ephemeralTtl`, default `4h`) expires. By default its branch must also be merged into `worktree.targetBranch`; set `reap.requireMerged = false` to retain unmerged branches while removing clean worktrees.

**Flags:**

- `--dry-run`: Show eligible and safety-skipped worktrees without removing anything
- `--owner <session>`: Only consider worktrees owned by the specified session
- `-f, --force`: Skip the confirmation prompt; safety checks still apply
- `-j, --json`: Run non-interactively and emit structured results

```bash
pando reap --dry-run
pando reap --owner session-123 --force
pando reap --json
```

### `pando lock` / `pando unlock`

Lock a worktree so Git and `pando reap` leave it alone, or remove that lock. Targets may be an absolute/relative worktree path or an exact branch name; `--path` is available as an alternative to the positional target.

**Flags:**

- `-p, --path <target>`: Worktree path or branch name
- `--reason <text>`: Record a lock reason (`pando lock` only)
- `-j, --json`: Output in JSON format

```bash
pando lock ../feature-x --reason "active work"
pando lock feature/x --json
pando unlock ../feature-x
pando unlock --path feature/x --json
```

### `pando symlink`

Move a file from the current worktree to the main worktree and replace it with a symlink. Useful for keeping configuration files, dependencies, or other shared files in sync across all worktrees.
Expand Down Expand Up @@ -530,6 +577,17 @@ rebaseOnAdd = true # Rebase existing branches when adding worktre
deleteBranchOnRemove = "local" # Delete branch on worktree remove: "none", "local", "remote" (default: "local")
useProjectSubfolder = false # Nest worktrees as defaultPath/projectName/branchName (default: false)
targetBranch = "main" # Target branch for merge checks (used by clean command)
defaultKind = "auto" # auto, ephemeral, or long-lived
ephemeralTtl = "4h" # Default TTL exposed to ephemeral setup hooks
autoLockActive = true # Git-lock worktrees owned by active sessions

# Optional per-worktree resources
[ports]
enabled = false # Or opt in for one add with --ports
range = "3100-3199"
names = ["web"]
dbStrategy = "named"
dbBaseName = "dev" # Prefix for a name derived from each worktree's own branch

# Clean Configuration
[clean]
Expand All @@ -556,6 +614,10 @@ Scripts configured for `add` run **after** the worktree has been created and rsy
- `PANDO_WORKTREE_PATH` — absolute path to the created worktree
- `PANDO_BRANCH` — branch name, or empty in detached HEAD mode
- `PANDO_COMMIT` — created worktree commit
- `PANDO_KIND` — resolved lifecycle kind (`ephemeral` or `long-lived`)
- `PANDO_TTL` — effective TTL when defined (explicit `--ttl`, otherwise the ephemeral default)
- `PANDO_PORT_<NAME>` — each assigned port; service names are uppercased and hyphens become underscores (for example, `web-api` becomes `PANDO_PORT_WEB_API`)
- `PANDO_DB_NAME` — the derived database name when port allocation is enabled

Human-readable output shows each script, its working directory, exit status, stdout, and stderr. JSON output includes a stable `postCommands` array with `name`, `command`, `cwd`, `exitCode`, `signal`, `stdout`, `stderr`, `success`, and `duration` fields. A non-zero exit code stops later scripts and returns the existing JSON error shape with `success: false`, `error`, `postCommands`, and `failedPostCommand`.

Expand Down
27 changes: 27 additions & 0 deletions plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "pando",
"description": "Manage Claude Code worktrees with pando lifecycle metadata, locking, and reaping",
"author": {
"name": "zpyoung",
"url": "https://github.com/zpyoung"
},
"homepage": "https://github.com/zpyoung/pando",
"repository": "https://github.com/zpyoung/pando",
"license": "MIT",
"keywords": ["git", "worktrees", "parallel-agents", "automation"],
"hooks": "./hooks/hooks.json",
"userConfig": {
"delegateCreation": {
"type": "boolean",
"title": "Delegate worktree creation to pando",
"default": true,
"description": "Use pando for Claude Code worktree creation; disable to use plain git worktree add."
},
"reapOnSessionEnd": {
"type": "boolean",
"title": "Reap worktrees when sessions end",
"default": true,
"description": "Unlock and reap eligible ephemeral worktrees owned by the ending Claude Code session."
}
}
}
65 changes: 65 additions & 0 deletions plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# pando plugin for Claude Code

This plugin delegates Claude Code worktree creation and lifecycle cleanup to the `pando` CLI. It also provides the `pando-worktrees` skill and the `/pando:pando-status` and `/pando:pando-reap` commands.

## Requirements

Install these before installing the plugin:

- Git 2.38 or newer (pando uses worktree-specific Git configuration).
- A recent Claude Code release with plugin `userConfig`, `WorktreeCreate`, and `SessionEnd` support.
- The `pando` CLI.
- `jq` is recommended for structured hook input/output parsing. Without it, worktree creation degrades to plain Git and session cleanup is limited to best-effort reaping.

Install the CLI from npm:

```sh
pnpm add -g @zyoung-ff/pando
```

Or install a downloaded release/package tarball:

```sh
pnpm add -g ./zyoung-ff-pando-<version>.tgz
```

Verify both dependencies:

```sh
pando --version
git --version
```

## Install the plugin

In Claude Code, add this repository as a marketplace and install its plugin:

```text
/plugin marketplace add zpyoung/pando
/plugin install pando@pando
```

Restart Claude Code if the newly installed hooks are not active in the current session.

The hooks check `pando --version` at runtime. Worktree creation falls back to `git worktree add` when the CLI is missing, too old, disabled, or returns an unusable response, so Claude Code can still create its isolated worktree.

## Configuration

Claude Code prompts for these options when enabling the plugin. Both default to `true`:

- `delegateCreation`: let pando create Claude Code worktrees and attach ephemeral lifecycle metadata. Set it to `false` to use plain `git worktree add`.
- `reapOnSessionEnd`: unlock worktrees owned by an ending session and run owner-scoped pando reaping. Set it to `false` to disable session-end cleanup.

They can also be supplied during CLI installation, for example:

```sh
claude plugin install pando@pando --config delegateCreation=true --config reapOnSessionEnd=true
```

## Usage

Claude Code automatically uses pando when it creates an isolated worktree. The bundled skill guides ephemeral versus long-lived worktree choices and locking behavior.

Use `/pando:pando-status` to summarize `pando list --json` and `pando health --json`. Use `/pando:pando-reap` to preview expired worktrees and confirm cleanup before running it.

Pando post-commands remain subject to pando's normal configuration trust gate. The plugin does not set `PANDO_TRUST_CONFIG`; trust project configuration separately when appropriate.
5 changes: 5 additions & 0 deletions plugin/commands/pando-reap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
description: Preview and optionally reap expired pando worktrees
---

Run `pando reap --dry-run --json` and summarize the worktrees that would be reaped, plus anything skipped and each safety reason. If there are eligible worktrees, ask for explicit confirmation before taking action. Only after confirmation run `pando reap --force --json`, then summarize the structured result. Never omit `--json` and never run the force command without approval.
5 changes: 5 additions & 0 deletions plugin/commands/pando-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
description: Show pando worktree lifecycle and repository health status
---

Run `pando list --json` and `pando health --json` from the current repository. Parse the JSON responses and summarize each worktree's path/branch, kind, age, lock state, and owner. Then report health warnings or errors and call out ephemeral worktrees that appear stale. Do not use non-JSON pando commands and do not change repository state.
25 changes: 25 additions & 0 deletions plugin/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"hooks": {
"WorktreeCreate": [
{
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/scripts/worktree-create.sh"
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/scripts/session-end.sh",
"timeout": 30
}
]
}
]
}
}
Loading