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
4 changes: 2 additions & 2 deletions .opencode-plugin/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ const allowsOrbitCommand = (cmd: string): boolean => {
const sub = parts[1] ?? ""
if (!SAFE_SUBCOMMANDS.has(sub)) return false
// sync --force resets the pool repo; sync --branch rewrites pool-wide state
// (checked-out branch, origin/HEAD, fetch refspecs every worktree relies on).
// Both stay behind a prompt. Strip quotes and backslashes per token first:
// (checked-out branch, origin/HEAD). Both stay behind a prompt. Strip
// quotes and backslashes per token first:
// bash hands `'--force'`, `--force''` and `\-\-force` to orbit as the very
// same `--force`, so the guard must match what the CLI will actually see.
if (sub === "sync") {
Expand Down
15 changes: 11 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,21 @@ Hardens the destructive surface: `prune` and `sync --force`/`--branch` become ma
- **BREAKING:** `prune` left the skill's action surface — agents report the need, humans run it.
- **BREAKING:** `prune --verify` removed. PR evidence is now used automatically whenever the workspace recorded `pr.url` entries and `gh` is available — no flag, and no `gh` call at all when nothing was recorded. The old flag gated a per-workspace "all PRs merged" check that force-deleted every branch in the workspace; verdicts are now per-branch, and a merged PR only clears the branch it covers — [`docs/spec-lifecycle.md`](docs/spec-lifecycle.md) → Prune Safety Guards → Branch Verdicts.
- Prune's messages changed — stderr diagnostics and the stdout report shape alike (worktree counts, residue groups, closing block) — contract in [`docs/spec-warnings.md`](docs/spec-warnings.md) → Refusals and skips.
- **BREAKING:** pool fetch config is now orbit-maintained state. Pools converge to the full wildcard map `+refs/heads/*:refs/remotes/origin/*` plus `fetch.prune=true` — written at clone, re-asserted at every `sync`/`info`/session-start/`prune` touchpoint, **removing any other `remote.origin.fetch` mapping** (per-branch entries from older orbit versions, hand edits, emptied configs all converge; each convergence is reported on stderr as it happens). To keep a custom refspec layout, set `orbit config git.fetchAllBranches once` (write the baseline at birth, never correct it) or `never` (fully self-managed); `git.fetchPrune` takes the same three modes. Visible consequences: `@{u}` / `git status` upstream lines work for every branch with upstream config — scoped or raw, no registration step — and a push materializes the tracking ref on the spot; tracking refs self-clean as branches are deleted upstream; and a bare `git fetch` or `git pull` in any worktree now pulls every branch's objects (a one-time step onto full-clone footing on huge repos — orbit's own commands still fetch named branches only, so agent/headless paths never trigger it).
- **BREAKING:** the `removed stale fetch refspec` / `added fetch refspec` / `would remove` / `would add` output lines are gone, and prune's `pool maintenance:` section no longer carries refspec content — the per-branch registration/reconciliation machinery was deleted outright. In their place, config convergence reports fixed per-key steering lines (`orbit: <repo>: fetch config converged: …` / `orbit: <repo>: push routing converged: …`) — contract in [`docs/spec-warnings.md`](docs/spec-warnings.md) → Config convergence lines.
- **BREAKING:** `push.default=upstream` joins the maintained set — re-asserted at the same touchpoints (scoped local names differ from remote names, so git's default `simple` would refuse a bare `git push`); escape with `orbit config git.pushUpstreamByDefault once` or `never`. And `push.autoSetupRemote` is gone: its only beneficiary was raw-mode bare push, and raw mode's contract is plain git — a fresh raw branch now gets git's native "no upstream" error naming `git push -u`, while the documented explicit `git push origin <branch>` needs no config at all. Scoped mode is unaffected (its upstream is wired by `switch` up front), and the git ≥ 2.37 soft gate drops with the key.
- The prune recovery narrative is stated precisely everywhere (spec-lifecycle Recoverability, USAGE, spec-warnings): recovery is the report's `(was <sha>)` plus object survival until gc (`gc.pruneExpire`, two weeks by default) — a deleted branch's own reflog is deleted with it, so the 90-day reflog window never applied post-prune. `core.logAllRefUpdates` / `gc.*` are declared premise-only in the dependency closure — user policy orbit deliberately does not manage.

### Changes by Kind

#### Security

- `orbit prune`: root-only, target-independent initiation guard via process ancestry, uncommitted-changes skip, bypass-free refusals — guards in [`docs/spec-lifecycle.md`](docs/spec-lifecycle.md) → Prune Safety Guards.
- `prune` now reclaims **residue**: ghost-workspace scoped branches (grouped by the reclaimed workspace, same merged/unmerged protection; `orbit prune <ws> --force` force-deletes a ghost's branches) and untraceable raw branches (reported with status + shell-quoted native delete commands, never auto-deleted). Kept branches end in a closing suggestion block. Git's own `Deleted branch …` chatter no longer leaks into the report — the deletion lines carry the commit instead (`(was <sha>)`), which is the reflog handle and the only recovery path — [`docs/spec-lifecycle.md`](docs/spec-lifecycle.md) → Residue Cleanup and Maintenance Self-Heal.
- `prune` now reclaims **residue**: ghost-workspace scoped branches (grouped by the reclaimed workspace, same merged/unmerged protection; `orbit prune <ws> --force` force-deletes a ghost's branches) and untraceable raw branches (reported with status + shell-quoted native delete commands, never auto-deleted). Kept branches end in a closing suggestion block. Git's own `Deleted branch …` chatter no longer leaks into the report — the deletion lines carry the commit instead (`(was <sha>)`), the recovery handle: good while the objects survive gc — [`docs/spec-lifecycle.md`](docs/spec-lifecycle.md) → Residue Cleanup and Maintenance Self-Heal.
- Prune's guard set is now stated over **arbitrary prior state**, not just interrupted runs: every file in a workspace — including the `.orbit` marker and the gitdir pointers the guards read — is agent-writable, so each reachable state must be self-healing, self-evident and repairable, or explicitly out of scope. New in that sweep: a **damaged worktree** (registered by the pool, gitdir pointer deleted or corrupted) is detected and the workspace skipped, instead of reading as ordinary content and being destroyed with the directory unannounced — `--force` still clears it, after saying that what it removes cannot be read and cannot be recovered; a failed `rm -rf` keeps the workspace and never prints `pruned:`; stale worktree registrations and orphan `branch.*` config are repaired automatically; `--force` announces that discarding un-persisted work cannot be undone. What prune deliberately does **not** protect is listed too (ignored files, non-repo content, stashes, detached HEAD, concurrency, direct writes into `.repos/`).
- Refusals replay the intended command (`cd <root> && orbit <cmd> <args>`, your argv verbatim) only when the ancestry walk ran and came back clean; a blind walk (no `/proc`, no `lsof`, no usable `ps`) states the fact alone — a guard that cannot see must not hand out a ready-to-run destructive command. Workspace detection needs no `.orbit` marker either: metadata is disposable, and a guard a lost file can disable is no guard.
- Prune also skips top-level non-pool git repos (a `.git` directory is an independent clone, whatever its name) and unmerged jots (`--force` overrides, `--dry-run` reports).
- `sync --force`/`--branch` root-only; `--branch` keeps unrelated fetch refspecs and rolls back on failure — [`docs/spec-commands.md`](docs/spec-commands.md) → sync.
- `sync --force`/`--branch` root-only; `--branch` writes no fetch config (the wildcard map covers any new default branch) and moves `origin/HEAD` — [`docs/spec-commands.md`](docs/spec-commands.md) → sync.
- Repo names validated as pool basenames in `sync`/`add`/`info`/`memo`/`clone --name` (path-traversal fix). Charset aligned with GitHub (`[A-Za-z0-9._-]`); leading `.`/`-` rejected — contract in [`docs/spec-commands.md`](docs/spec-commands.md) → Repo Name Contract.
- Branch prefix moved from env var to `orbit config branch.prefix`; validated, immovable while branches carry it.
- `orbit config` refuses `repos.*` writes.
Expand All @@ -42,13 +46,14 @@ Hardens the destructive surface: `prune` and `sync --force`/`--branch` become ma
- `orbit done` per-repo one-line warnings (jots / thin memo / over-budget card). ([#17](https://github.com/orbcli/orbit/pull/17))
- `jot.bufferSize` config replaces the hardcoded aggregation threshold. ([#17](https://github.com/orbcli/orbit/pull/17), [#20](https://github.com/orbcli/orbit/pull/20))
- One-shot explore/curate stderr on `add` (thin memo) and `memo` (over-budget). ([#17](https://github.com/orbcli/orbit/pull/17))
- `docs/spec-branching.md` is restructured and renamed to **`docs/spec-worktree.md`** — the worktree model doc: the root decision (one pool, many worktrees, argued from the principles), the design priorities, actors/invocation layers, config ownership, and the touchpoint fetch discipline (moved out of spec-lifecycle, where they never belonged) join the branching strategy, push routing, and tracking display. It also gains **Git Dependency Closure**: orbit's complete git dependency surface enumerated as a closed set in three categories — invocations, feature/behavior premises, config keys — with the rule that the set only shrinks (any new dependency requires an explicit set extension in the same PR) and that a config key orbit depends on is managed state (converged or lifecycle-managed — or explicitly declared premise-only).

#### Bug or Regression

- Fetch refspecs reconciled with remote — bare `git fetch` heals after a branch is deleted upstream. ([#21](https://github.com/orbcli/orbit/pull/21))
- Bare `git fetch` can no longer be broken by orbit's own state: a tracked branch deleted upstream no longer produces `couldn't find remote ref` anywhere — orbit's touchpoints fetch named branches only (git's fatal is swallowed) and converge the dead ref with one native `git remote prune origin`. The earlier per-branch refspec registry that caused the class is gone entirely. ([#21](https://github.com/orbcli/orbit/pull/21))
- `orbit switch <remote-branch>` always fetches first — no stale checkouts. ([#22](https://github.com/orbcli/orbit/pull/22))
- Workspace-name validation actually rejects ref-illegal names (space, `~ ^ : ? * [ \`) — the bracket expression had a stray `]` that let every such name through (e.g. `orbit new --name 'dev*'`).
- Brief parser, status steering and refspec lifecycle hardened. ([#23](https://github.com/orbcli/orbit/pull/23))
- Brief parser and status steering hardened. ([#23](https://github.com/orbcli/orbit/pull/23))
- Plugin install works on SSH-less machines — `try.sh` defaults to HTTPS. ([#24](https://github.com/orbcli/orbit/pull/24))
- OpenCode auto-approve matches `--force` token-exactly. ([#25](https://github.com/orbcli/orbit/pull/25))
- Jot queue stores entries in `[jot "<repo>"]` subsections — names plain git-config keys can't hold (`my_repo`, `2048`) now jot and pop correctly.
Expand All @@ -60,6 +65,8 @@ Hardens the destructive surface: `prune` and `sync --force`/`--branch` become ma

- `[seed]` jot sentinel and the gap model — memo state computed inline. ([#17](https://github.com/orbcli/orbit/pull/17))
- Stop hooks and `[nudge]`/`[overlong]` markers — covered by stderr + cruise block + done gate. ([#17](https://github.com/orbcli/orbit/pull/17))
- Fetch-refspec reconciliation (register/remove directions, gating, default-branch exemption) — the wildcard map + `fetch.prune` are maintained instead, and tracked refs converge through native `git remote prune origin`; `orbit switch -c` no longer registers anything.
- `push.autoSetupRemote` clone/switch writes and the `orbit doctor` git ≥ 2.37 check — see the breaking note above; `orbit switch` no longer writes any push config (touchpoint convergence owns it).

## [0.1.0] - 2026-07-06

Expand Down
24 changes: 21 additions & 3 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,9 @@ git checkout -b feature/api-refactor
git push origin feature/api-refactor
```

**Tracking-display limitation:** the pool is a single-branch clone, so a raw-mode branch you push this way won't show remote tracking in `git status` / `@{upstream}` — the branch and push are fine, only the ahead/behind display is blank. Run `git fetch origin <branch>` once to materialize the ref, or just wait: the next `orbit sync` / `orbit info` / session start registers the refspec and materializes it automatically. Scoped mode above wires the upstream config up front and materializes the ref the same way once the branch is pushed.
**Tracking display:** the pool's fetch config carries the full wildcard map (`+refs/heads/*:refs/remotes/origin/*`), so once a branch has upstream config, `git status` / `@{upstream}` resolve and a push materializes the tracking ref on the spot — no registration, no waiting for an orbit touchpoint. The upstream config itself is wired by `orbit switch -c`, by `git push -u`, or by hand.

Fetch habits are a cost question, never a correctness one: orbit's own commands always fetch named branches only, and your `git fetch origin <branch>` does the same — the pool's object pull stays at what you actually track. A bare `git fetch` or `git pull` is just as correct; it simply pulls every branch's objects too (on a huge repo that's a one-time GB-scale step; `fetch.prune` keeps the refs self-cleaning afterward).

## 7. Viewing Status

Expand Down Expand Up @@ -304,6 +306,22 @@ orbit config explore.paths ".:1" # cold-start exploration scope for the first

`memo.minLines`/`memo.maxLines` are surfaced as `card budget: <min>–<max> lines` at the curation checkpoints (jot overflow and `orbit done`). `explore.paths` is a one-time cold-start knob only — once the first exploration writes the card, the jot → incremental-memo pipeline maintains it. Orbit attaches no meaning to what lives at those paths (a pre-generated code-doc works just as well).

### Managed config keys (project-level, on `.repos/.orbit`)

A config key orbit depends on is managed state: converged at every touchpoint under the default mode, with three-mode escape hatches. Three optional keys select the mode, each taking `always` (default) / `once` / `never`:

```bash
orbit config git.fetchAllBranches always # always: keep remote.origin.fetch at exactly the wildcard map
# (+refs/heads/*:refs/remotes/origin/*), converging
# anything else at the next touchpoint (default)
# once: write the map once at clone, then the layout is yours
# never: never write it (pools stay single-branch)
orbit config git.fetchPrune always # same three modes, for fetch.prune=true (default always)
orbit config git.pushUpstreamByDefault always # same three modes, for push.default=upstream (default always)
```

Under the default `always`, a pool whose config deviates — an older orbit's per-branch entries, a hand-edit, an emptied config — is converged at the next `orbit sync` / `orbit info` / session start / `orbit prune`, with a stderr note at the moment it happens. Set the keys to `once` when your team keeps its own refspec layout (e.g. a `feat/*` scoped map) or its own `push.default`: new pools still get the working baseline at birth, and orbit never corrects it afterward.

## 9. Recording Discoveries (Jot)

During work, record repo knowledge for later memo aggregation:
Expand Down Expand Up @@ -351,7 +369,7 @@ orbit prune --force # Skip validation (accept the loss)
>
> Beyond workspace directories, `prune` also reclaims **residue** left by force-deleted workspace directories (an agent's `rm -rf`, a crash): scoped branches whose workspace is already gone are cleaned per branch (merged deleted, unmerged reported), and untraceable raw branches (no remote copy, no workspace) are reported with review/delete commands for you to dispose of by hand. A run with kept content ends in a closing block: one confirm-useless caveat, then the force-delete suggestions (`orbit prune <ws> --force` for scoped, native `branch -D` for raw). Interruption is safe at any point — directory removal goes through an atomic rename into `.prune-trash/`, so the next run simply resumes; the only case non-force will refuse to resume is a worktree half-removed by a failed deletion (it reads as uncommitted work — review, then `--force`).
>
> Two things worth knowing before you reach for `--force`. Deletion lines name the commit they removed (`deleted branch (force): <branch> (was abc1234)`) — that SHA is the handle for `git reflog`, which keeps the objects for 90 days by default, so a branch deleted by mistake is recoverable as long as you kept the report. Discarding **uncommitted** work is the one step with no recovery path at all, so `--force` says so before it acts. And prune does not protect everything: `.gitignore`d files, hand-written notes at the workspace top level, and commits left on a detached HEAD have no guard — see [`docs/spec-lifecycle.md`](docs/spec-lifecycle.md) → Out of Scope.
> Two things worth knowing before you reach for `--force`. Deletion lines name the commit they removed (`deleted branch (force): <branch> (was abc1234)`) — that SHA is the recovery handle: `git branch <name> abc1234` recreates the ref while the objects survive. Prune deletes refs, never objects; unreachable objects live until `git gc` collects them (`gc.pruneExpire`, two weeks by default — the deleted branch's own reflog goes with it, so don't count on the longer reflog window). Discarding **uncommitted** work is the one step with no recovery path at all, so `--force` says so before it acts. And prune does not protect everything: `.gitignore`d files, hand-written notes at the workspace top level, and commits left on a detached HEAD have no guard — see [`docs/spec-lifecycle.md`](docs/spec-lifecycle.md) → Out of Scope.
>
> `--force` does **not** override `done`. A workspace is reclaimable only once you have marked it done, and no flag substitutes for that — it is the one thing only you can decide. If a workspace's `.orbit` file was lost, prune stops mentioning it entirely; run `orbit done` inside it again to restore the marker.

Expand All @@ -361,7 +379,7 @@ orbit prune --force # Skip validation (accept the loss)
ORBIT_ROOT=<project-root> # Explicitly specify project root
```

The scoped-mode branch prefix is **project config** — `orbit config branch.prefix <prefix>` (default `ws`). It is baked into every branch orbit creates and is what `prune` matches on to find them again, so it has to stay stable across sessions. Changing it is refused while any branch still carries the current prefix (moving it would orphan those branches). See [`docs/spec-lifecycle.md`](docs/spec-lifecycle.md) → Branch Prefix.
The scoped-mode branch prefix is **project config** — `orbit config branch.prefix <prefix>` (default `ws`). It is baked into every branch orbit creates and is what `prune` matches on to find them again, so it has to stay stable across sessions. Changing it is refused while any branch still carries the current prefix (moving it would orphan those branches). See [`docs/spec-worktree.md`](docs/spec-worktree.md) → The Prefix.

## 12. Codex Sandbox Escalation

Expand Down
Loading
Loading