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
29 changes: 29 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,34 @@ Notes:
- The created local branch is `ws/<workspace>/<default-branch>` (e.g., `ws/task-01/main`)
- This is the local starting point; the agent branches off from here

### Removing a repo from a workspace

The inverse of `orbit add`: drop a repo's worktree from the current workspace
and (by default) clean up the scoped local branch `add` created. The pool
repo (`.repos/<repo>/`) is never touched — `remove` is a per-workspace
operation, not a clone removal.

```bash
cd task-01/
orbit remove backend
# → removed backend from task-01 (worktree gone)
# → deleted branch: ws/task-01/main
```

Safety defaults mirror `prune` — refuses to silently lose anything:

- **Uncommitted / untracked changes** → refuses; pass `--force` to discard.
- **Unmerged local branch** (no PR, not in origin) → branch is kept and a
review hint is printed; pass `--force` to delete anyway.
- **Foreign repo** (a `.git/` directory, not file — its history lives only
in the worktree) → refuses; pass `--force` to destroy.

Options:

- `--force` — discard uncommitted changes and force-delete the local branch.
- `--json` — machine-readable output (`workspace`, `repo`, `worktreeRemoved`,
`branch`, `branchAction`).

## 6. Branch Operations

### Scoped Mode (default)
Expand Down Expand Up @@ -624,6 +652,7 @@ orbit sync [repo...] [--force] [--branch <branch>] # --force / --branch: proje
# Workspace lifecycle
orbit new "<goal>" [--name <name>] [--no-goal] [--exec "<cmd>"]
orbit add <repo> [--ref <tag/branch>] [-s|--silent]
orbit remove <repo> [--force] [--json]
orbit switch [repo] <name>
orbit switch -c [repo] <name>
orbit jot [<repo>] ["<text>"]
Expand Down
134 changes: 134 additions & 0 deletions orbit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ Usage:
$ORBIT_CMD memo [<repo>] [--refresh|--scaffold]
$ORBIT_CMD new "<goal>" [--name <name>] [--no-goal] [--exec "<cmd>"]
$ORBIT_CMD add <repo> [--ref <tag/branch>] [-s|--silent]
$ORBIT_CMD remove <repo> [--force] [--json]
$ORBIT_CMD switch [-c] [repo] <name>
$ORBIT_CMD sync [repo...] [--force] [--branch <branch>]
$ORBIT_CMD done [--pr <url>...] [--json]
Expand Down Expand Up @@ -1387,6 +1388,131 @@ orbit_add() {
fi
}

orbit_remove() {
local repo_name="" force=0 json_mode=0

while [ "$#" -gt 0 ]; do
case "$1" in
--force) force=1; shift ;;
--json) json_mode=1; shift ;;
-*) orbit_fail "usage: orbit remove <repo> [--force] [--json]" ;;
*)
[ -z "$repo_name" ] || orbit_fail "usage: orbit remove <repo> [--force] [--json]"
repo_name="$1"; shift ;;
esac
done
[ -n "$repo_name" ] || orbit_fail "usage: orbit remove <repo> [--force] [--json]"
orbit_require_repo_name "$repo_name"

local root
root=$(orbit_require_root) || return 1

local ws
ws=$(orbit_infer_workspace "$root") || return 1

local repo_dir="$root/.repos/$repo_name"
[ -d "$repo_dir" ] || orbit_fail "repo not in pool: $repo_name"

local ws_dir="$root/$ws"
local wt="$ws_dir/$repo_name"
[ -d "$wt" ] || orbit_fail "repo not in workspace: $ws/$repo_name (use 'orbit add' first)"

# Foreign-repo guard: a .git directory (not file) means the repo's history
# lives only in the worktree itself — removing the dir destroys history that
# exists nowhere else. Refuse without --force.
if [ -d "$wt/.git" ] && [ ! -f "$wt/.git" ]; then
if [ "$force" -eq 0 ]; then
orbit_fail "$repo_name is a foreign repo (history lives only in its own .git/); refusing to remove without --force"
fi
printf 'orbit: %s: foreign repo — --force discards local history\n' "$repo_name" >&2
fi

# Dirty check: refuse to discard uncommitted/untracked changes without --force.
if [ "$force" -eq 0 ]; then
local dirty
dirty=$(git -C "$wt" status --porcelain 2>/dev/null || true)
if [ -n "$dirty" ]; then
printf 'orbit: %s: worktree has uncommitted changes (refusing to discard; commit/stash first, or pass --force):\n' "$repo_name" >&2
printf '%s\n' "$dirty" | sed 's/^/ /' | head -10 >&2
return 1
fi
fi

# Resolve the scoped branch add created: ws/<ws>/<default>. Empty-pool repos
# have no default branch; in that case branch deletion is skipped.
local default_branch local_branch=""
default_branch=$(orbit_default_branch "$repo_dir" 2>/dev/null || true)
if [ -n "$default_branch" ]; then
local_branch=$(orbit_tracking_branch "$ws" "$default_branch") || local_branch=""
fi

# Branch verdict: re-use the same PR-merged / upstream-merged / content-
# upstream layers prune uses, so an unmerged branch is kept with a hint
# instead of silently vanishing. --force flips the verdict to delete.
# Computed BEFORE worktree removal — git refuses to delete a branch while
# a worktree has it checked out, so the deletion must follow the worktree.
local verdict="delete" v_flag="-d"
if [ -n "$local_branch" ]; then
orbit_branch_verdict "$repo_dir" "$local_branch" "$force"
if [ "$ORBIT_VERDICT" = "delete" ]; then
verdict="delete"
v_flag="$ORBIT_V_FLAG"
else
verdict="keep"
fi
fi

# Worktree removal runs first: a "keep" verdict still needs the checkout
# detached, and the branch deletion below only succeeds once the worktree
# is gone. --force is passed because the dirty guard ran (or --force was
# given); worktree remove --force on a clean tree is a no-op.
if ! git -C "$repo_dir" worktree remove --force "$wt" >/dev/null 2>&1; then
# Stale registration: dir gone already, or admin entry is corrupt.
git -C "$repo_dir" worktree repair "$wt" >/dev/null 2>&1 || true
if ! git -C "$repo_dir" worktree remove --force "$wt" >/dev/null 2>&1; then
rm -rf "$wt" || orbit_fail "$repo_name: failed to remove worktree directory"
git -C "$repo_dir" worktree prune >/dev/null 2>&1 || true
printf 'orbit: %s: worktree registration was stale; directory removed directly\n' "$repo_name" >&2
fi
fi

# Branch deletion: now safe (no worktree is checking out the branch).
local branch_action="skipped"
if [ -n "$local_branch" ]; then
if [ "$verdict" = "delete" ]; then
if LC_ALL=C git -C "$repo_dir" branch "$v_flag" "$local_branch" >/dev/null 2>&1; then
branch_action="deleted"
else
branch_action="kept"
printf 'orbit: %s: branch deletion refused by git — kept\n' "$repo_name" >&2
fi
else
branch_action="kept"
fi
fi

if [ "$json_mode" -eq 1 ]; then
local out="{"
out+="$(orbit_json_kv workspace "$ws"),"
out+="$(orbit_json_kv repo "$repo_name"),"
out+='"worktreeRemoved":true,'
out+="$(orbit_json_kv branch "${local_branch:-}"),"
out+="$(orbit_json_kv branchAction "$branch_action")"
out+='}'
printf '%s\n' "$out"
return 0
fi

printf 'removed %s from %s (worktree gone)\n' "$repo_name" "$ws"
if [ -n "$local_branch" ]; then
case "$branch_action" in
deleted) printf ' deleted branch: %s\n' "$local_branch" ;;
kept) printf ' kept branch (unmerged): %s — review: git -C .repos/%s log origin/%s..%s\n' \
"$local_branch" "$repo_name" "$default_branch" "$local_branch" >&2 ;;
esac
fi
}

orbit_switch() {
local create_mode=0 args=()

Expand Down Expand Up @@ -4489,6 +4615,13 @@ _orbit_completions() {
COMPREPLY=($(compgen -W "$(_orbit_repo_names)" -- "$cur"))
fi
;;
remove)
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "--force --json" -- "$cur"))
else
COMPREPLY=($(compgen -W "$(_orbit_repo_names)" -- "$cur"))
fi
;;
switch)
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "-c" -- "$cur"))
Expand Down Expand Up @@ -4596,6 +4729,7 @@ orbit() {
clone) orbit_clone "$@" ;;
new) orbit_new "$@" ;;
add) orbit_add "$@" ;;
remove) orbit_remove "$@" ;;
switch) orbit_switch "$@" ;;
sync) orbit_sync "$@" ;;
status) orbit_status "$@" ;;
Expand Down
8 changes: 5 additions & 3 deletions skills/orbit/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: orbit
description: "Operate Orbit - the user's Git-native multi-repo workspace manager. If an <orbit-context> hook block is present, invoke BEFORE your first reply - even when the opening request seems unrelated to orbit. Also invoke on 'orbit start', or when asked to start working with no block injected (run `orbit context --startup`). Also use for clone, workspace create/manage, add repo, branch switch, status, goal, jot, memo aggregation, or done - or on mentions of workspaces, .repos/, or cross-repo tasks."
description: "Operate Orbit - the user's Git-native multi-repo workspace manager. If an <orbit-context> hook block is present, invoke BEFORE your first reply - even when the opening request seems unrelated to orbit. Also invoke on 'orbit start', or when asked to start working with no block injected (run `orbit context --startup`). Also use for clone, workspace create/manage, add/remove repo, branch switch, status, goal, jot, memo aggregation, or done - or on mentions of workspaces, .repos/, or cross-repo tasks."
---

Use this skill to operate the user's Git-native multi-repo workspace manager (Orbit).
Expand Down Expand Up @@ -86,6 +86,7 @@ These steps describe the work itself, independent of who performs it. Run them y
- **Jot triggers (event-driven — don't wait for wrap-up).** Jot the moment you realize (a) this repo also serves a role the card doesn't list, or (b) the real entry point for a task differs from — or is absent in — what the card names. If the discovery isn't a role or an entry point the card needs, it isn't a jot.
- **Resume/compact sessions still jot.** A cruise block (durables + per-repo status) does not suppress discovery capture, and a compact can wipe your mental "to-jot" note — so jot findings as they surface, even mid-refactor after a compact.
- **Need another repo?** (e.g., tracing a cross-repo dependency) → go back to steps 2–7: `orbit repos` to screen → `orbit info` to assess → decide whether to add → sync if needed → add → memo check. This cross-repo branch is a natural thing to delegate — a worker runs the same screen → assess → add loop autonomously via its briefing (see "Delegating to sub-agents").
- **Drop a repo mid-work.** If a repo you added no longer belongs to this workspace (scope shrunk, or step 4's call was wrong), `orbit remove <repo>` from inside the workspace — destructive but workspace-scoped; refuses on dirty/unmerged work without `--force` (see Safe to run freely). If a worker reaches the same conclusion, report the need rather than running it.
10. **Wrap-up.** Before finishing, aggregate jot entries and assess PR impact. This step is **incremental aggregation only** — it folds discoveries onto the understanding built in step 7; it is never where first-time exploration you skipped earlier gets done:
- **Reflect first**: before popping, review what you learned this session about repos you added or worked in. If any structural insight never made it into a jot, jot it now, then continue. Keep scope to repos you added or worked in; do not sweep repos you only read via `orbit info`.
- **Jot aggregation**: for each repo with jot entries, run `orbit jot <repo> --pop` to consume entries, then `orbit info <repo>` to read current card, merge entries in — staying within the card budget orbit reports (curate, don't append) and following merge-first rules — write back via `cat <<'EOF' | orbit memo <repo>`. Before `orbit done`, run bare `orbit context` and confirm no repo you developed is left with `memo thin` and no capture — `orbit done` warns per repo as the final backstop.
Expand Down Expand Up @@ -132,7 +133,7 @@ Recording knowledge (the moment you find it, before it's lost):
scope), NOT feature-branch changes, NOT debug notes.
- If you cannot run orbit, put the same items under a "## Discoveries" heading in your report.

Do NOT run: orbit memo / sync / done / new / goal / clone / config — report those needs to me.
Do NOT run: orbit memo / sync / done / new / goal / clone / config / remove — report those needs to me.

Report back: findings, any repos you added, and your Discoveries list.
```
Expand All @@ -150,6 +151,7 @@ orbit sync [repo...] [--force] [--branch <branch>] # updates the POOL repo only
# Workspace lifecycle (from inside a workspace)
orbit new ["<goal>"] [--name <name>] [--exec "<cmd>"] [--no-goal]
orbit add <repo> [--ref <tag/branch>] [-s|--silent]
orbit remove <repo> [--force] [--json] # drop a repo's worktree from this workspace; pool repo untouched
orbit switch [-c] [repo] <name>
orbit jot [<repo>] ["<text>"] # push a discovery to the jot queue
orbit jot [<repo>] --pop [--json] # pop all entries (consume + delete)
Expand Down Expand Up @@ -233,7 +235,7 @@ These orbit subcommands are read-only or idempotent workspace-writes — run the
- **Destructive read:** `jot --pop` — it *consumes* the queue (read + delete, no undo). Safe to run without asking, but only as the first half of pop→merge: never pop until you're ready to write the memo in the same turn.
- **Idempotent workspace-write:** `add` `switch` `sync` (bare or with a repo name) `memo` `jot` `goal`

`done` and `new` are workflow-timing commands — non-destructive and reversible; when to run them is governed by the workflow (step 11; Safety rule 3), and whether they prompt is the user's own permission setup — orbit takes no position at the permission layer. `clone` and `config` change project-level / shared state — confirm before running these. `sync --force` and `sync --branch` are not yours either: both destroy or re-point shared pool state and run only from the project root, so report the need rather than trying them (the bare `sync` being safe does not extend to these flags). `prune` is not on your list at all — see Safety rules.
`done` and `new` are workflow-timing commands — non-destructive and reversible; when to run them is governed by the workflow (step 11; Safety rule 3), and whether they prompt is the user's own permission setup — orbit takes no position at the permission layer. `clone` and `config` change project-level / shared state — confirm before running these. `sync --force` and `sync --branch` are not yours either: both destroy or re-point shared pool state and run only from the project root, so report the need rather than trying them (the bare `sync` being safe does not extend to these flags). `remove` is destructive but workspace-scoped — same call pattern as `prune` for one repo in your own workspace, and it refuses on dirty/unmerged work without `--force`. Run it when you've decided that repo no longer belongs to the workspace (a wrong call loses the local scoped branch unless you've pushed); when in doubt, confirm first. `prune` is not on your list at all — see Safety rules.

## Communication

Expand Down
Loading
Loading