repo-cli 2.0: ledger → lens (discovery, auto-PR linking, agent worktrees) - #24
Open
sadpandajoe wants to merge 12 commits into
Open
repo-cli 2.0: ledger → lens (discovery, auto-PR linking, agent worktrees)#24sadpandajoe wants to merge 12 commits into
sadpandajoe wants to merge 12 commits into
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
repo-cli/codex/claude/manual(multi-signal classification; unknown →manual, nothing breaks).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-scopedpr_overridesthat win over auto-detection.repo pr linkis 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 atadd, health inlist.repo list2.0 — Repo · Branch · Source · PR · Age · Path, with--source/--prsfilters,--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 viagit config --worktree. Never writescodex.*state, never prints env-file contents..worktreeincludeseeding — new worktrees inherit a clone's gitignored.env-type files (repo create; backfill withrepo 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 orphanedclaude/*branches. Agent rows opt-in,--yestakes only non-agent non-dirty rows, dirty needs--force,--dry-runmutates nothing. PR-lookup outages leave branches untouched rather than mislabel them.repo doctorv2 — git-version floor (2.31), registered-root health (missing / non-git / origin mismatch), worktree drift, PR-cache age, andghauth status.syncrebases only repo-cli-owned worktrees with astart_point;deleterefuses 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.*.rootsfrom the existing bare layout; M2 re-keys worktree annotations by resolved path and lifts a legacypr:into a branch-scopedpr_overrides(coercing a digit-string value rather than dropping it). The config version stamps to2.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 formatclean.Release
App and package version bumped to 2.0.0 (config version already stamps 2.0.0 via M2). The
v2.0.0tag will be applied after merge onmain, not on this branch.🤖 Generated with Claude Code