Skip to content

feat: AI-agent worktree lifecycle (git-native metadata, reap/lock, ports, CC plugin) - #62

Merged
zpyoung merged 12 commits into
mainfrom
zpyoung/AI-worktree-management
Jul 23, 2026
Merged

feat: AI-agent worktree lifecycle (git-native metadata, reap/lock, ports, CC plugin)#62
zpyoung merged 12 commits into
mainfrom
zpyoung/AI-worktree-management

Conversation

@zpyoung

@zpyoung zpyoung commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the AI-agent worktree lifecycle design — a stateful lifecycle layer for pando worktrees with no external state file. All state lives in each worktree's own git config (pando.* namespace), which git cleans up on git worktree remove. Core stays tool-agnostic (usable from a plain terminal); a bundled Claude Code plugin is a convenience layer over the CLI.

Built as 9 tasks across the design's phases (foundation → reaping/locking → ports → plugin), plus a whole-branch-review fix pass.

What's new

  • Git-native metadata (worktreeMetadata): per-worktree pando.* keys (kind, createdAt, sourceBranch, owner, ttl, ports, dbName); safe auto-enable of extensions.worktreeConfig (migrates core.*); git ≥ 2.38 guard.
  • Kind + TTL: every worktree is ephemeral or long-lived (flag > worktree.defaultKind > inference); pando add gains --ephemeral/--long-lived/--ttl/--owner/--ports; list/health surface kind/age/ttl/owner/locked.
  • pando reap: age/kind-based reclamation of expired ephemeral worktrees — fail-closed (never removes uncommitted/unmerged/locked/long-lived/main); --dry-run/--force/--json/--owner.
  • pando lock / pando unlock + auto-lock while an agent session is active.
  • Concurrency hardening (gitRetry): mutating git ops retry transient ref-lock contention with full-jitter backoff (tunable via concurrency.retry.*).
  • Opt-in port isolation (portAllocator): per-worktree ports + derived DB name, exposed to postCommands as PANDO_PORT_<NAME> / PANDO_DB_NAME (off by default).
  • Bundled Claude Code plugin (plugin/ + .claude-plugin/): lifecycle skill, /pando-status + /pando-reap commands, and WorktreeCreate/SessionEnd hooks that delegate to the CLI with a safe git fallback.

Notable design boundaries (intentional)

  • Reap's safety guarantee covers committed work only (design §2); it disposes an ephemeral worktree's gitignored / pando-skip-worktree symlink state by design.
  • Port allocation uses optimistic re-enumerate+reconcile (not a global lock), matching the design's retry-not-lock concurrency stance.

Test plan

  • pnpm validate (format + lint + typecheck + 816 unit tests) green
  • pnpm build (dist) green; built binary exposes new commands/flags
  • E2E (Docker): pnpm test:e2e --hookTimeout=60000
  • Manual: install plugin locally, confirm a delegated worktree gets pando metadata and is reaped at session end

🤖 Implemented via subagent-driven-development (pi agents: codex implementers, gemini + codex adversarial review per task, whole-branch review by Claude).

zpyoung added 10 commits July 22, 2026 15:13
withGitRetry retries transient git ref-lock errors with full-jitter
exponential backoff (5/100ms/2s); isTransientLockError classifies exit-128
lock errors while excluding permission/auth/disk-full failures.

Part of AI-agent worktree lifecycle (design gap #4). SDD T1 (pi: codex impl +
gemini/codex review, 1 codex fix cycle).
worktreeMetadata stores per-worktree lifecycle state (kind, createdAt,
sourceBranch, owner, ttl, ports, dbName) in each worktree's own git config
under pando.* keys; enumerateAll reads linked worktrees via config.worktree
files; ensureWorktreeConfigEnabled safely enables extensions.worktreeConfig
(migrating core.* out of shared config); assertGitVersion guards git >= 2.38.

Design gap #1. SDD T2 (pi: codex impl + gemini/codex review, 1 fix cycle:
--local core migration/unset + NUL-framed config/worktree parsing).
Adds worktree.{defaultKind,ephemeralTtl,autoLockActive}, reap.requireMerged,
concurrency.retry.{maxAttempts,baseMs,capMs}, and ports.* config sections with
Zod schemas, defaults, PANDO_* env mappings (incl. numeric/array/bool env
typing), .pando.toml.example docs, and full config-init generate+merge support.

Design gap #1-4/#6 foundation. SDD T3 (pi: codex impl + gemini/codex review,
1 fix cycle: init.ts merge/generate + required interface sections).
GitHelper gains lockWorktree/unlockWorktree (idempotent), getWorktreeAgeMs
(metadata createdAt then dir mtime), isReapClean (fail-closed: direct status
check + exact local-branch merge verification), inferOwner, and withGitRetry
wrapping of mutating ops (add/remove worktree, create/delete/force branch,
reset, fetch --prune).

Also fixes gitRetry to actually fire: simple-git GitError exposes no exit code,
so classification now recognizes real ref-lock contention by message signature
while excluding permanent permission and ref D/F-conflict errors; isBranchMerged
now strips the '+' (checked-out-elsewhere) marker so worktree branches reap.

Design gaps #2/#3/#4. SDD T4 (pi: codex impl + gemini/codex review, 2 codex
cycles + 1 guarded captain patch; cross-task fix to T1 gitRetry).
add gains --ephemeral/--long-lived/--ttl/--owner/--ports; after setup it
resolves kind (flag>config>inference), writes git-native metadata, auto-locks
only when an agent session is active, and exposes PANDO_KIND/PANDO_TTL to
postCommands. Metadata is non-fatal (add still succeeds on failure) and --json
stays a single document with lifecycle fields on success and error paths.
list/health surface kind/age/ttl/owner/locked (locked parsed from porcelain).

Design gap #1. SDD T5 (pi: codex impl + gemini/codex review, 1 fix cycle:
JSON single-doc hygiene, error-path field parity, session-gated auto-lock,
boolean PANDO_EPHEMERAL).
reap reclaims expired ephemeral worktrees (kind=ephemeral && !locked && age >
effective TTL) that are safe to remove — fail-closed cleanliness (direct status
check + merge verification), per-candidate lock revalidation, no-force removal,
loud skip reporting, --dry-run/--force/--json/--owner, interactive confirm.
lock/unlock wrap git worktree lock/unlock with realpath-canonical path/branch
resolution. --json exits non-zero on partial failure.

Note: reap intentionally disposes an ephemeral worktree's gitignored and
skip-worktree symlink state; the safety guarantee covers committed work (design
S2). Design gaps #2/#3. SDD T6 (pi: codex impl + gemini/codex review, 2 cycles).
allocate assigns the lowest free port per name within a range, treating both
OS-probed (IPv4+IPv6) and peer-worktree metadata ports as taken, persisting to
git-native metadata; optimistic re-enumerate+reconcile narrows the concurrent-
allocation race and omits (never persists) a known duplicate on exhaustion.
Service names are validated against git's config-key rule; deriveDbName collapses
separators with a stable-hash fallback. Never throws on exhaustion/probe error.

Design gap #6. SDD T7 (pi: codex impl + gemini/codex review, 2 cycles).
When ports are enabled (config or --ports), add allocates ports + derives a db
name during lifecycle setup and injects PANDO_PORT_<NAME>/PANDO_DB_NAME into the
postCommands env alongside existing PANDO_* vars; allocation is non-fatal (add
succeeds with a warning on failure/exhaustion) and disabled by default (zero
behavior change). ports/dbName appear in add --json on success and error paths.
remove --json now includes each removed worktree's metadata for parity.

Design gap #6. SDD T8 (pi: codex impl + gemini/codex review; PASS, no CRITICAL/HIGH).
Ships a repo-local Claude Code plugin (marketplace.json + plugin.json without a
version so git SHA drives it): a pando-worktrees lifecycle skill, /pando-status
and /pando-reap commands, and WorktreeCreate/SessionEnd hooks. worktree-create.sh
delegates creation to 'pando add --ephemeral --owner --path --json' under
.claude/worktrees/<name>, always emitting one absolute path and exiting 0 with a
git-worktree-add fallback; session-end.sh unlocks the session's worktrees then
reaps by owner. Hooks honor the verified CC contract (name-only stdin, replace
git, explicit SessionEnd timeout) and degrade gracefully without pando/jq.

Design gap: plugin phase. SDD T9 (pi: codex impl + gemini/codex review, 1 fix cycle).
- Per-worktree DB name now derives from the worktree's OWN branch (path
  basename fallback for detached), not the source branch — so worktrees
  branched from the same base no longer collide on PANDO_DB_NAME.
- Thread concurrency.retry config through GitHelper.setRetryConfig into every
  withGitRetry call (add/reap/remove/clean) so the documented knob is honored.
- worktreeMetadata.unsetPort clears a pando.port.<name> key when port
  reconciliation drops it, keeping git metadata and the returned map in sync.

Found by the final whole-branch reviewer (Claude). SDD final gate (pi codex fix
+ codex re-review, all confirmed PASS).
Copilot AI review requested due to automatic review settings July 23, 2026 01:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements an AI-agent-oriented worktree lifecycle layer for the pando CLI, storing all lifecycle state in per-worktree Git config (pando.*) with additional commands for reaping expired ephemeral worktrees and locking/unlocking active worktrees, plus an optional bundled Claude Code plugin to delegate worktree creation and session-end cleanup.

Changes:

  • Added Git-native per-worktree metadata (worktreeMetadata) and lifecycle wiring in core commands (add, list, health, remove).
  • Introduced concurrency hardening via retry-with-jitter for transient Git ref-lock contention (gitRetry) and applied it to mutating Git operations.
  • Added optional port isolation (portAllocator) and a Claude Code plugin (hooks + docs + tests) for delegating worktree creation and session-end reaping/unlocking.

Reviewed changes

Copilot reviewed 44 out of 44 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
test/utils/worktreeMetadata.test.ts Adds unit coverage for reading/writing/enumerating per-worktree pando.* metadata and enabling extensions.worktreeConfig.
test/utils/postCommands.test.ts Verifies new lifecycle/port/db env vars injected into post-command script execution.
test/utils/portAllocator.test.ts Adds unit coverage for port allocation, reconciliation, and DB name derivation.
test/utils/gitRetry.test.ts Adds tests for transient lock detection and retry/backoff behavior.
test/utils/git.test.ts Extends GitHelper tests for lock/unlock, retry wiring, lifecycle age computation, and reap-clean checks.
test/plugin/plugin.test.ts Adds integration-style tests validating plugin manifests and hook script behaviors.
test/config/schema.test.ts Verifies defaults for new lifecycle/reap/concurrency/ports config schema sections.
test/config/env.test.ts Adds coverage for env parsing of new lifecycle/concurrency/ports variables and number parsing behavior.
test/commands/unlock.test.ts Adds tests for the new unlock command and target resolution.
test/commands/remove.test.ts Ensures remove captures lifecycle metadata before Git removes per-worktree config, and wires retry config.
test/commands/reap.test.ts Adds tests for TTL parsing, candidate selection/partitioning, and reap command JSON behavior.
test/commands/lock.test.ts Adds tests for the new lock command and target resolution.
test/commands/list.test.ts Updates list tests for lifecycle fields in JSON and verbose output.
test/commands/health.test.ts Updates health tests for lifecycle fields and locked state output.
test/commands/config/init.test.ts Ensures config init/merge includes lifecycle/reap/concurrency/ports defaults and comments.
test/commands/clean.test.ts Wires retry config into clean command tests.
test/commands/add.test.ts Adds extensive coverage for lifecycle kind resolution, metadata setup, port allocation, and JSON shape consistency.
src/utils/worktreeMetadata.ts Implements reading/writing/enumerating pando.* per-worktree config and enabling worktree config support.
src/utils/postCommands.ts Injects lifecycle + port/db environment variables into post-command scripts.
src/utils/portAllocator.ts Implements opt-in port allocation with reconciliation and DB name derivation.
src/utils/gitRetry.ts Adds transient lock detection and full-jitter exponential backoff retry helper.
src/utils/git.ts Adds lock detection, lock/unlock operations, lifecycle age calculation, reap-clean check, and applies retry to mutating ops.
src/config/schema.ts Extends config schema with lifecycle defaults, reap safeguards, concurrency retry config, and ports config.
src/config/env.ts Adds env var mapping + parsing for new lifecycle/reap/concurrency/ports settings (including numbers/arrays).
src/commands/unlock.ts Introduces pando unlock command with path/branch resolution and JSON output.
src/commands/remove.ts Captures lifecycle metadata before removal and wires retry config from config.
src/commands/reap.ts Introduces pando reap command with TTL parsing, safety checks, and JSON/human output modes.
src/commands/lock.ts Introduces pando lock command with optional reason and JSON output.
src/commands/list.ts Adds lifecycle enrichment (kind/owner/ttl/age/locked) to JSON and verbose human output.
src/commands/health.ts Adds lifecycle fields to health results (human + JSON).
src/commands/config/init.ts Adds lifecycle/reap/concurrency/ports defaults to generated config and merge behavior.
src/commands/clean.ts Wires concurrency retry config into GitHelper usage.
src/commands/add.ts Adds lifecycle flags, resolves kind/TTL/owner, best-effort metadata setup, optional ports/db allocation, and JSON warning consistency.
README.md Documents new lifecycle flags/behavior, new commands, and new env vars for post-commands.
plugin/skills/pando-worktrees/SKILL.md Adds a Claude Code skill describing lifecycle conventions and JSON-first usage.
plugin/scripts/worktree-create.sh Implements WorktreeCreate hook: delegates to pando add with safe fallback to git worktree.
plugin/scripts/session-end.sh Implements SessionEnd hook: unlocks owned locked worktrees and runs owner-scoped pando reap.
plugin/README.md Documents plugin requirements, install, configuration, and usage.
plugin/hooks/hooks.json Registers WorktreeCreate and SessionEnd hooks for the plugin.
plugin/commands/pando-status.md Adds a Claude Code command to summarize pando list/health --json.
plugin/commands/pando-reap.md Adds a Claude Code command to preview/confirm pando reap using --json.
plugin/.claude-plugin/plugin.json Adds the plugin manifest with userConfig options.
.pando.toml.example Documents new lifecycle/reap/concurrency/ports config sections and env overrides.
.claude-plugin/marketplace.json Adds marketplace manifest to distribute the plugin from this repo.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/utils/worktreeMetadata.ts Outdated
Comment on lines +62 to +65
if (name && value.trim() && Number.isFinite(port)) {
metadata.ports ??= {}
metadata.ports[name] = port
}
Comment thread src/commands/add.ts
Comment on lines +139 to +142
if (worktreeConfig.autoLockActive && hasActiveSession) {
await gitHelper.lockWorktree(resolvedPath, `pando: active session ${owner}`)
result.locked = true
}
Comment thread src/utils/git.ts
Comment on lines +374 to 376
inferOwner(): string {
return process.env.CLAUDE_SESSION_ID ?? process.env.PANDO_SESSION ?? ''
}
Comment thread src/commands/list.ts Outdated
Comment on lines +32 to +35
const [metadata, ageMs] = await Promise.all([
readMetadata(worktree.path),
gitHelper.getWorktreeAgeMs(worktree.path),
])
Comment thread src/commands/health.ts Outdated
Comment on lines +97 to +100
const [metadata, ageMs] = await Promise.all([
readMetadata(worktree.path),
gitHelper.getWorktreeAgeMs(worktree.path),
])
zpyoung added 2 commits July 22, 2026 21:08
- parseMetadata: use a null-prototype ports map and validate port names
  against git's config-key rule, so keys like __proto__/constructor cannot
  pollute or land.
- add: wrap auto-lock in its own try/catch so a lock failure no longer skips
  port allocation/DB naming; drop trailing space in the lock reason when owner
  is empty.
- inferOwner: use trimmed values with || so an empty CLAUDE_SESSION_ID falls
  back to PANDO_SESSION.
- list/health: pass the already-read metadata into getWorktreeAgeMs to avoid a
  second git-config read per worktree.
ensureWorktreeConfigEnabled migrated worktree-specific core.* keys into
config.worktree, assuming git.raw throws for absent keys. simple-git
instead resolves an absent 'config --get' to '', so an unset core.worktree
was written back as 'git config --worktree core.worktree ""'. With
extensions.worktreeConfig enabled, git then reads an empty core.worktree
for the main worktree and fails every command with
'fatal: cannot chdir to ""', surfacing in CI as 'Not a git repository'.

This bricked the shared repo after the first 'pando add', cascading into
53 Docker E2E failures across add/list/remove/symlink/health/clean.

Only migrate keys that hold a real value. Add a regression test covering
simple-git's empty-string-for-absent-key behavior.
@zpyoung
zpyoung merged commit 6bfda9d into main Jul 23, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants