Skip to content

repo-cli 2.0: ledger → lens (discovery, auto-PR linking, agent worktrees) - #24

Open
sadpandajoe wants to merge 12 commits into
mainfrom
feat/2.0-lens
Open

repo-cli 2.0: ledger → lens (discovery, auto-PR linking, agent worktrees)#24
sadpandajoe wants to merge 12 commits into
mainfrom
feat/2.0-lens

Conversation

@sadpandajoe

Copy link
Copy Markdown
Owner

Summary

repo-cli 2.0 replaces the ledger model — where repo-cli only knew about worktrees it created — with a lens: it discovers every worktree across your registered clone roots (whoever made it) and auto-links each branch's live PR.

Highlights

  • Discovery + provenance — every worktree-taking command resolves through a discovery layer that scans all registered roots. Each worktree is tagged repo-cli / codex / claude / manual (multi-signal classification; unknown → manual, nothing breaks).
  • Automatic PR linking — each branch's best PR is resolved via batched gh api graphql (one call per repo, chunked, concurrent, per-repo failure isolation), a 5-minute cache with atomic writes and offline stale-serve, and branch-scoped pr_overrides that win over auto-detection. repo pr link is now a rarely-needed override (deprecated).
  • repo root add/list/remove — register additional clone roots per repo so discovery spans a bare clone plus normal checkouts; origin-URL check at add, health in list.
  • repo list 2.0 — Repo · Branch · Source · PR · Age · Path, with --source / --prs filters, --refresh, and --json (stable per-row schema). Pure, IO-free render layer.
  • repo env — run Codex-compatible environment files (list/use/up/down/run/actions/status) from inside a worktree; selection persists per-worktree via git config --worktree. Never writes codex.* state, never prints env-file contents.
  • .worktreeinclude seeding — new worktrees inherit a clone's gitignored .env-type files (repo create; backfill with repo env seed). Globs filtered to git-ignored only; ..-traversal and symlink escapes rejected; only file names surfaced, never contents.
  • repo clean — one reviewed list of merged-PR worktrees, prunable records, and orphaned claude/* branches. Agent rows opt-in, --yes takes only non-agent non-dirty rows, dirty needs --force, --dry-run mutates nothing. PR-lookup outages leave branches untouched rather than mislabel them.
  • repo doctor v2 — git-version floor (2.31), registered-root health (missing / non-git / origin mismatch), worktree drift, PR-cache age, and gh auth status.
  • Provenance-aware verbssync rebases only repo-cli-owned worktrees with a start_point; delete refuses agent-owned and root checkouts.

Migrations

Config migrates automatically and transparently on first run — presence-based, idempotent, with a timestamped backup before any write. M1 seeds repos.*.roots from the existing bare layout; M2 re-keys worktree annotations by resolved path and lifts a legacy pr: into a branch-scoped pr_overrides (coercing a digit-string value rather than dropping it). The config version stamps to 2.0.0.

Testing

Two-tier strategy — fast unit tests with injected seams (subprocess runner, prompt, PR-lookup callable, clock, platform, home) plus real-git integration tests (temp clones, local bare remotes, agent-style worktree fixtures). No live GitHub calls in CI. 631 passing tests; ruff check + ruff format clean.

Release

App and package version bumped to 2.0.0 (config version already stamps 2.0.0 via M2). The v2.0.0 tag will be applied after merge on main, not on this branch.

🤖 Generated with Claude Code

sadpandajoe and others added 12 commits July 17, 2026 15:50
…Slice 1)

Turn repo-cli from a registry of what it created into a lens over git
reality: discover every worktree across each repo's registered clone
roots via `git worktree list --porcelain`, classify provenance
(codex/claude/repo-cli/manual), and route every worktree-taking verb
through a discovery-backed resolver.

- discovery.py: porcelain parsing, WorktreeSignals/DiscoveredWorktree
  models, pure provenance classifier (priority codex→claude→repo-cli→
  manual), drift detection, and resolve_worktree (repo/branch, --path,
  or CWD via git-common-dir).
- config: `roots` schema + migration M1 (presence-based, idempotent;
  seeds <base_dir>/<alias>/.bare, stamps 2.0.0).
- commands/root.py: `repo root add/list/remove` with origin-slug
  mismatch warning.
- git_ops: worktree_list_porcelain, get_git_common_dir,
  get_worktree_config_value.
- Rewire list/activate/delete/sync/`pr open` onto the resolver: delete
  refuses agent-owned and root rows; sync rebases only repo-cli-provenance
  worktrees with a start_point and never fetches (prunes) a foreign root;
  list gains a Source column.

Tests: new test_discovery.py; M1 unit + real-shape fixture in
test_config.py; TestCliRoot + migrated verb tests in test_cli.py; real-git
integration for claude/codex/manual/duplicate-root/soft-fail/
unregistered-clone/normal-clone-is_root cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI resolves ruff to 0.15.10 (via uv sync), which wraps long function
signatures the older pinned formatter left on one line. Bring the Slice 1
files into agreement so the branch passes 'ruff format --check'. No logic
changes — signature line-wrapping only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rides (Slice 2)

repo list now shows each branch's live PR without anyone linking it by hand.
Branches are grouped by owner_repo and resolved with one 'gh api graphql'
call per repo (aliased pullRequests, chunked at 30 branches), fetched
concurrently with per-repo failure isolation, and cached at
~/.repo-cli/pr-cache.json (5-min TTL, atomic write). Offline or on failure,
stale entries are served and marked '(cached)'; nothing degrades to '—'.

Pick-best ranks OPEN > MERGED > CLOSED, ties by latest updatedAt. The PR
cell renders state by color, a 'draft' suffix, and dims manual overrides.

'repo pr link' now writes a branch-scoped override (config 'pr_overrides',
keyed repo::branch) that wins over auto-detection and is shared by duplicate
checkouts across roots; with explicit repo+branch it needs no live worktree.
'repo pr open' resolves override -> auto-detection. The registry 'pr:' field
is no longer read or written, and the legacy per-PR gh helpers
(get_pr_status/validate_pr_exists) are removed.

Tests are hermetic: an autouse fixture forces the gh runner offline and
redirects the cache to tmp, so no test touches the network, the real clock,
or the user's cache. New unit + end-to-end coverage for the resolver, the
cache, override semantics, and the list PR column.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ON (Slice 3)

Turn `repo list` into the lens's primary view over discovered worktrees.

- New `render.py`: a pure, IO-free rendering layer (injectable clock + home).
  `WorktreeRow`, `pr_state_key`, cell helpers, and `render_worktree_table`.
  Columns: Repo · Branch · Source · PR · Age · Path. Repo/Branch are no-wrap
  identity columns; Path absorbs the squeeze via ellipsis on narrow terminals.
- PR cell shows the state word: `#123 Open|Merged|Closed`, `#123 Draft`, dim
  `#123 Open (cached)` when stale-served, dim `#55` for a manual override.
- `list` gains `--source` (repo-cli|claude|codex|manual|root), `--prs`
  (open|merged|closed|draft|override|none), `--refresh` (bypass the PR cache),
  and `--json` (stable per-row schema; `[]` when no repos). Filters validate up
  front (bad value → exit 1 listing the valid set).
- Rows sort by repo, then newest-first keyed on the same created_at-or-mtime
  source the Age column shows — so un-annotated agent/manual worktrees order by
  recency instead of collapsing to the bottom.
- `--json` PR object carries a stable key-set across override and auto sources
  (override null-fills the fields it cannot know) so `jq` never hits a missing
  key.

Tests: new `test_list_render.py` (cell markup + full-table over a representative
population, age/sort/path edge cases, NO_COLOR); `test_cli.py` gains
`TestResolvePrMap` and `TestCliListView` (filters, invalid-filter errors, sort
regression, `--json` schema/override/empty, `--refresh`). 452 passing, ruff
check + format clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a `repo env` command group that discovers and runs a worktree's
environment file, using the same schema Codex already writes so existing
`.codex/environments/*.toml` files run unmodified.

- envfile.py: pure parser for the v1 schema (setup/cleanup/actions), with
  platform resolution taken as a parameter (darwin/linux key wins, else
  script/command) so both branches are testable off-host. Unknown keys warn;
  version/name/setup are validated with the offending file named.
- commands/env.py: list / use / up / down / run / actions / status. Context is
  cwd-derived via the resolver (never the registry). Scripts run verbatim
  (shell=True, no quoting, no timeout) in the worktree with CODEX_*/REPO_*
  source-tree + worktree vars injected; non-zero exits propagate as the CLI's
  exit code.
- Selection persists under repo.environmentConfigPath via `git config
  --worktree` (enabling extensions.worktreeConfig first, safe on bare roots).
  Read fallback: repo key -> codex key (read-only) -> sole file -> named error.
- git_ops: is_bare_repo, is_worktree_config_enabled, enable_worktree_config,
  set_worktree_config_value. discovery: public roots_for accessor.

repo-cli never writes codex.* keys and never prints env-file contents.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rees (Slice 5)

A fresh worktree starts empty of the gitignored .env/local-config files a
developer keeps in their working clone. Honor Codex's .worktreeinclude
convention: a pattern file at the seed-source clone root lists the files to
copy into each new worktree.

- seed.py: pattern parser (comments/blanks/negation), literal-vs-glob selection
  (literals copied if present; globs filtered to git-ignored only so a broad
  glob can never rake in tracked source), nested-worktree exclusion, copy engine
  with relative-path preservation, --force and --dry-run. Only file names are
  ever surfaced; contents are copied byte-for-byte, never read for display.
- Seed source resolves to a worktree's owning normal clone, else the repo's
  first non-bare registered root; a bare-only repo is a no-op with a
  'repo root add' hint (repo-cli's bare root holds no working files).
- git_ops: list_worktree_paths + filter_ignored (batched git check-ignore) as
  the git-backed seams behind exclusion and glob safety.
- repo create seeds after worktree add, before setup; best-effort so it can
  never block create. repo env seed backfills an existing worktree.
- Hardening: reject '..' traversal patterns and refuse symlink escapes so a
  hostile clone's include file can't read outside the source or write outside
  the worktree; a malformed '**' glob is warned-and-skipped, never crashing or
  disabling the remaining patterns.

Tests: seed unit tier (injected ignore/exclusion seams), git_ops real-git tier,
and CLI integration tier (real check-ignore + worktree list) covering create
seeding, env seed backfill, nested-worktree exclusion, dry-run, no-source hint,
glob-vs-tracked safety, traversal refusal, and malformed-glob resilience.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nches (Slice 6)

Add `repo clean`: one reviewed list of merged-PR worktrees, prunable worktree
records, and orphaned `claude/*` branches, removing only what the user confirms.
Candidate assembly is pure over pre-resolved inputs with injectable git/PR/prompt
seams; agent rows default unselected, `--yes` takes only non-agent non-dirty rows,
dirty worktrees need `--force`, and `--dry-run` mutates nothing.

Rework `repo doctor` (v2): git resolver floor (2.31), registered-root health
(missing / non-git / origin mismatch), worktree drift, PR-cache age, and gh auth
status, alongside the existing checks.

git_ops: add is_git_repo / list_local_branches / delete_local_branch /
prune_worktrees; consolidate get_remote_url to a single None-returning contract and
make its callers None-safe.

config M2: re-key worktree annotations by resolved path and lift legacy `pr` into
`pr_overrides` (now coercing a digit-string `pr` instead of dropping it).

Review fixes: PR-lookup outages are reported via `resolve_prs(failed_owners=…)` so
an unresolved owner's branch is left untouched rather than offered as an orphan with
a false "no open PR"; an un-statable worktree is treated as dirty instead of
aborting the whole run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ce 7)

Bump app + package version to 2.0.0 (config version already stamps 2.0.0 via M2).

README: reframe around the lens model (discovery over registration, provenance,
automatic PR linking, multiple roots per repo, agent-safety). Document the new
command surface — list 2.0 filters/JSON, `repo root`, `repo env`, `repo clean`,
`.worktreeinclude` seeding, doctor v2 — refresh prerequisites (git 2.31), the
nested bare-repo layout, and an "Upgrading from 1.x" section covering the
idempotent M1/M2 migrations.

CHANGELOG: 2.0.0 entry (Added / Changed / Deprecated / Migrations / Testing).

Mark `repo pr link` deprecated in its help: auto-linking makes manual linking a
rare override rather than the norm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The env-list integration test asserted `"Test Env" in result.stdout`, but
the listing prints an absolute path inline with the parsed env name
(`…/environment.toml  (Test Env)`). At a narrow console width Rich wraps
that line and splits the name at its internal space (`Test\nEnv`), so the
substring check failed whenever the render happened below ~140 columns —
e.g. a pre-push run attached to a real ~137-col PTY where the wide-terminal
fixture's COLUMNS override didn't reach the lazy module console.

Normalize whitespace before asserting so the check tracks what is shown,
not how the terminal folded it, matching the file's existing goal of tests
independent of tmp-path depth and console width.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Several CLI tests assert message substrings that Rich renders inline with a
long temp path. When stdout is non-tty (CliRunner in CI defaults to width 80),
Rich wraps those lines and can split an asserted phrase across a newline, or
fold a long path mid-token. That made the assertions depend on tmp-path depth
rather than on what the command actually printed.

- Replace the ``stdout.replace("\n", "")`` normalization (which joined wrapped
  words with no separator, corrupting multi-word phrases like "repo env use")
  with ``" ".join(stdout.split())``, which collapses any wrap to a single space.
- Pin ``COLUMNS=300`` in the few tests whose asserted token is part of a path
  that can fold mid-token (normalization alone can't rejoin a split path
  component), matching the existing width-robustness pattern used by the doctor
  tests.

No production behavior change; test-only hardening.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The autouse fixture isolated the PR cache but not the config path, so any
flow that reaches the real config.save_config (e.g. run_clean after a
removal, whose remove_annotation stub still leaves config_dirty=True)
wrote to the developer's ~/.repo-cli/config.yaml and sprayed timestamped
backups beside it. Point config.get_config_path at a per-test tmp_path
too; explicit per-test patches still win.

Also clear any ambient NO_COLOR before building the colored baseline in
test_no_color_respected — Rich honors NO_COLOR at Console construction,
so a pre-set value stripped the baseline and collapsed the escape-count
comparison to equal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Nineteen review findings against repo-cli 2.0, all defects in existing
behaviour rather than missing capability, so nothing here adds a flag.

Fail closed where a list decides what to skip. `git worktree list` failing
and a repo having no worktrees are opposite facts that both surfaced as an
empty list, so cleaning and seeding could act on "nothing is checked out"
when the truth was "we could not look". Callers that exclude on the list now
use a strict variant that raises; seeding copies nothing rather than
guessing.

Canonicalize before matching. `root add` accepts any path inside a clone and
registers the clone root it belongs to, so the same clone reached by two
paths is one root. It confirms before registering a clone whose `origin`
names a different repo than the alias, and declines with no terminal to ask
on. `root remove` refuses a repo's last root, and warns when removing a
managed bare clone that `repo create` will re-register.

Treat a merged PR as reachability proof. Squash and rebase merges rewrite
SHAs, so counting commits not on any remote is not a merge test. That count
is now the raw disclosure signal; the force decision is separate and a
verified merged PR overrides it.

A worktree reads the checkout that owns it. Roots are separate clones that
can sit on different commits, so borrowing the first registered root's env
files and scripts would run another checkout's code. Environments now share
seeding's resolver instead of restating the rule.

Also: absent `roots` means never migrated and seeds, explicit `[]` means the
user removed them and stays empty; one config backup per load however many
migrations run; `unregister` asks the lens, not just the config; and a fresh
`init` stamps the version it actually wrote.

743 passing tests, README and CHANGELOG updated alongside.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant