fr-isolation
A workspace contract, not just a worktree: a git worktree OUTSIDE the repo
(~/.cache/fr/worktrees/<main-checkout>/<branch>), commands in the profile's
devcontainer, base repo untouched while the run is live. Plain shell, any agent or human.
Announce at start: "I'm using fr-isolation to run this work isolated."
Hard requirements
- Inside a git repo. devcontainer mode (default) needs ≥1 profile
(
.devcontainer/<profile>/devcontainer.json); missing → exit 2 pointing at
fr-init. NEVER proceed unisolated; offer the fr-init interview (pause, resume).
Modes (FR_ISOLATION_TARGET) — same contract, docker-less environment half
- host-worktree (
=worktree): fr worktree, the host process env as-is — NO
profile, no secrets provisioning. A host-level declaration, never a per-call flag.
- external (valid preparer-written
.fr-isolation marker, mode:external):
fr adopts the container's checkout — up --branch ensures the branch in place;
restart/stats refuse, gc reports (the container's owner runs both).
- Any other value fails closed naming
devcontainer|worktree.
Lifecycle
fr isolation up --branch <b> [--profile <name>] [--session <id>] [--print-path] # worktree + container; --print-path: last stdout line = path
fr isolation exec --branch <b> -- CMD ... # every build/test/run
fr isolation status [--branch ...] [--session <id>] [--format json] [--stats] [--push-check] # state + bound sessions
fr isolation attach|detach --session <id> [--repo <path>] [--branch ...] # bind/unbind a harness session
fr isolation restart [--branch ...] [--force] # bounce a wedged container, worktree kept
fr isolation down --branch <b> | --worktree <path> | --all [--force] # teardown (verifies + reaps); --all clears sentinel(s)
fr isolation gc [--repo <path>] [--dry-run] [--format json] # reconcile fr-owned workspaces, ALL three modes
up (devcontainer mode) resolves the profile (flag → repo default from
.devcontainer/fr-profiles.yaml → sole profile), creates the worktree under the
MAIN checkout's name (even from inside another worktree), ensures the host
secrets env-file, starts the container with the base repo's .git mounted at
the same absolute path. One profile per run — change = down --force + up.
- Cold-start base (#322): a NEW branch is cut from freshly-fetched
origin/<default>, never the base repo's HEAD; reuse keeps that branch's tip.
--base <ref> = <ref> verbatim, no fetch (--base HEAD forks the checkout);
--no-fetch = LOCAL origin/<default>. No remote / fetch fails / ref missing
→ local HEAD with a WARNING; the run never aborts.
Exec-bridge discipline
- EVERY build/test/lint/run command goes through
fr isolation exec -- ...; file
edits happen in the worktree directly (host-visible), execution in-container.
- Credential boundary (devcontainer mode): the container sees only the profile's
env-file (
~/.config/fr/secrets/<repo>/<profile>.env), NO SSH identity (#377).
ALL git-host I/O (push/fetch, PR/MR creation, gh/glab/tea reads in
status/down/gc) runs on the HOST outside exec; status --push-check previews.
- Pre-push guard (#320): a push to a branch whose PR is
MERGED/CLOSED is
denied by fr-merged-pr-push-guard.sh — cherry-pick onto main or a fresh PR.
- The harness resets cwd to base each call, so host-side git/gh is compound
cd <worktree> && …; the guard allows a leading cd under ~/.cache/fr/worktrees
/ temp (#279); /add-dir (#281) persists a bare cd. Never run base commands.
up writes a gitignored .fr-isolation marker (mode records the mode) that
the fr-isolation-required PreToolUse hook reads to ALLOW edits; tracked files
in an fr-enabled base clone are blocked (FR_BASE_OK=1 / .fr-isolation-allow
escape). down removes it. See that rule (#328).
Session bindings (traceability)
Which harness session holds which workspace — traceability only; the edit gate
never reads a binding. attach records {session_id, harness, attached_at} in
the workspace state (source of truth) plus a derived index
~/.cache/fr/sessions/<id>.json (FR_SESSIONS_DIR; keys session_id, harness, repo_root, branch, worktree, profile, attached_at), one binding per session.
up --session binds in the same call; down detaches everyone AFTER a successful
teardown (a refused down keeps bindings); gc prunes stale indexes. Claude
transports (plugin hooks, always exit 0): fr-session-bind.sh (PostToolUse Bash;
parses fr isolation up|exec|down …, leading cd folded), fr-session-unbind.sh
(SessionEnd), fr-worktree-create.sh (claude --worktree / EnterWorktree →
up --session --print-path, branch wt/<name>; agent-* keep Claude's default
<repo>/.claude/worktrees/ shape), fr-worktree-remove.sh (down --worktree; a
refusal keeps the workspace). Hermes has no bind transport yet. Status line: shell+jq+git,
never fr (100 ms; a real git first on PATH — the Apple shim doubles it). In
`/.claude/statusline.sh, after $branch_str/$cwd_str`:
seg=$(printf '%s' "$data" | bash ~/.claude/plugins/cache/derio-net--super-fr/super-fr/current/scripts/fr-statusline-segment.sh)
iso_line=$(printf '%s\n' "$seg" | sed -n '1p'); wt_line=$(printf '%s\n' "$seg" | sed -n '2p')
line2="${branch_str}${SEP}${cwd_str}"; [ -n "$iso_line" ] && line2="${line2}${SEP}${DIM}${iso_line}${RESET}"
echo -e "$line2"; [ -n "$wt_line" ] && echo -e "${DIM}${wt_line}${RESET}"
Cleanup contract
Worktree + container PERSIST after PR creation (back-loaded manual phases push there).
- gc auto-reconciles merged work, in every mode. Fires detached on every
up/down (host-wide, no daemon, ≤1 stale): tears down MERGED-PR and
content-merged workspaces, retires state records whose worktree is gone, removes
empty repo folders + stale session indexes, and (devcontainer only) reaps orphaned
containers / vsc-* images. Open-PR, dirty, no-PR work: never touched. external only reports.
- Ownership boundary. gc acts only where fr ownership is provable (state record,
fr worktree cache, devcontainer label); a foreign
git worktree add is invisible to it.
down is the immediate lever — verifies container + worktree are gone
before dropping state (never leaked) and refuses an open PR unless --force.
Recovery (#341) and failure handling
- Wedged container:
fr isolation restart [--force] bounces the devcontainer
WITHOUT dropping the worktree / installs — prefer it to down+up.
- Orphaned pipeline sentinel (every base command denied, no worktree to
cd
into): the guard self-heals (zero live worktrees → fails open); fr isolation down --all.
devcontainer up failures surface verbatim — missing Docker, a broken profile,
an absent secrets file are operator-environment issues: report and stop, never
work around isolation (no silent degradation to a weaker mode).
1---2name: fr-isolation3description: Run work in an isolated workspace: git worktree + devcontainer, driven through the `fr isolation` CLI (exec-bridge). Use when feature work must not touch the base repo, when fr-brainstorming or fr-goal needs a workspace, when the operator says "isolate this" / "run it in the container", or to clean up after a merged PR. devcontainer mode needs a profile; host/external modes are docker-less.4---56# fr-isolation78A workspace contract, not just a worktree: a git worktree OUTSIDE the repo9(`~/.cache/fr/worktrees/<main-checkout>/<branch>`), commands in the profile's10devcontainer, base repo untouched while the run is live. Plain shell, any agent or human.1112**Announce at start:** "I'm using fr-isolation to run this work isolated."1314## Hard requirements1516- Inside a git repo. **devcontainer mode** (default) needs ≥1 profile17 (`.devcontainer/<profile>/devcontainer.json`); missing → exit 2 pointing at18 fr-init. NEVER proceed unisolated; offer the fr-init interview (pause, resume).1920### Modes (`FR_ISOLATION_TARGET`) — same contract, docker-less environment half2122- **host-worktree** (`=worktree`): fr worktree, the host process env as-is — NO23 profile, no secrets provisioning. A host-level declaration, never a per-call flag.24- **external** (valid preparer-written `.fr-isolation` marker, `mode:external`):25 fr adopts the container's checkout — `up --branch` ensures the branch in place;26 restart/stats refuse, gc reports (the container's owner runs both).27- Any other value fails closed naming `devcontainer|worktree`.2829## Lifecycle3031```bash32fr isolation up --branch <b> [--profile <name>] [--session <id>] [--print-path] # worktree + container; --print-path: last stdout line = path33fr isolation exec --branch <b> -- CMD ... # every build/test/run34fr isolation status [--branch ...] [--session <id>] [--format json] [--stats] [--push-check] # state + bound sessions35fr isolation attach|detach --session <id> [--repo <path>] [--branch ...] # bind/unbind a harness session36fr isolation restart [--branch ...] [--force] # bounce a wedged container, worktree kept37fr isolation down --branch <b> | --worktree <path> | --all [--force] # teardown (verifies + reaps); --all clears sentinel(s)38fr isolation gc [--repo <path>] [--dry-run] [--format json] # reconcile fr-owned workspaces, ALL three modes39```4041- `up` (devcontainer mode) resolves the profile (flag → repo default from42 `.devcontainer/fr-profiles.yaml` → sole profile), creates the worktree under the43 MAIN checkout's name (even from inside another worktree), ensures the host44 secrets env-file, starts the container with the base repo's `.git` mounted at45 the same absolute path. One profile per run — change = `down --force` + `up`.46- **Cold-start base (#322):** a NEW branch is cut from freshly-fetched47 `origin/<default>`, never the base repo's HEAD; reuse keeps that branch's tip.48 `--base <ref>` = `<ref>` verbatim, no fetch (`--base HEAD` forks the checkout);49 `--no-fetch` = LOCAL `origin/<default>`. No remote / fetch fails / ref missing50 → local HEAD with a `WARNING`; the run never aborts.5152## Exec-bridge discipline5354- EVERY build/test/lint/run command goes through `fr isolation exec -- ...`; file55 edits happen in the worktree directly (host-visible), execution in-container.56- Credential boundary (devcontainer mode): the container sees only the profile's57 env-file (`~/.config/fr/secrets/<repo>/<profile>.env`), NO SSH identity (#377).58 ALL git-host I/O (push/fetch, PR/MR creation, `gh`/`glab`/`tea` reads in59 `status`/`down`/`gc`) runs on the HOST outside `exec`; `status --push-check` previews.60- Pre-push guard (#320): a push to a branch whose PR is `MERGED`/`CLOSED` is61 denied by `fr-merged-pr-push-guard.sh` — cherry-pick onto `main` or a fresh PR.62- The harness resets cwd to base each call, so host-side git/gh is compound `cd63 <worktree> && …`; the guard allows a leading `cd` under `~/.cache/fr/worktrees`64 / temp (#279); `/add-dir` (#281) persists a bare `cd`. Never run base commands.65- `up` writes a gitignored `.fr-isolation` marker (`mode` records the mode) that66 the `fr-isolation-required` PreToolUse hook reads to ALLOW edits; tracked files67 in an fr-enabled base clone are blocked (`FR_BASE_OK=1` / `.fr-isolation-allow`68 escape). `down` removes it. See that rule (#328).6970## Session bindings (traceability)7172Which harness session holds which workspace — traceability only; the edit gate73never reads a binding. `attach` records `{session_id, harness, attached_at}` in74the workspace state (source of truth) plus a derived index75`~/.cache/fr/sessions/<id>.json` (`FR_SESSIONS_DIR`; keys `session_id, harness,76repo_root, branch, worktree, profile, attached_at`), one binding per session.77`up --session` binds in the same call; `down` detaches everyone AFTER a successful78teardown (a refused `down` keeps bindings); `gc` prunes stale indexes. Claude79transports (plugin hooks, always exit 0): `fr-session-bind.sh` (PostToolUse Bash;80parses `fr isolation up|exec|down …`, leading `cd` folded), `fr-session-unbind.sh`81(SessionEnd), `fr-worktree-create.sh` (`claude --worktree` / `EnterWorktree` →82`up --session --print-path`, branch `wt/<name>`; `agent-*` keep Claude's default83`<repo>/.claude/worktrees/` shape), `fr-worktree-remove.sh` (`down --worktree`; a84refusal keeps the workspace). Hermes has no bind transport yet. Status line: shell+jq+git,85never fr (~100 ms; a real git first on PATH — the Apple shim doubles it). In86`~/.claude/statusline.sh`, after `$branch_str`/`$cwd_str`:8788 seg=$(printf '%s' "$data" | bash ~/.claude/plugins/cache/derio-net--super-fr/super-fr/current/scripts/fr-statusline-segment.sh)89 iso_line=$(printf '%s\n' "$seg" | sed -n '1p'); wt_line=$(printf '%s\n' "$seg" | sed -n '2p')90 line2="${branch_str}${SEP}${cwd_str}"; [ -n "$iso_line" ] && line2="${line2}${SEP}${DIM}${iso_line}${RESET}"91 echo -e "$line2"; [ -n "$wt_line" ] && echo -e "${DIM}${wt_line}${RESET}"9293## Cleanup contract9495Worktree + container PERSIST after PR creation (back-loaded manual phases push there).9697- **gc auto-reconciles merged work, in every mode.** Fires detached on every98 `up`/`down` (host-wide, no daemon, ≤1 stale): tears down MERGED-PR and99 content-merged workspaces, retires state records whose worktree is gone, removes100 empty repo folders + stale session indexes, and (devcontainer only) reaps orphaned101 containers / `vsc-*` images. Open-PR, dirty, no-PR work: never touched. **external** only reports.102- **Ownership boundary.** gc acts only where fr ownership is provable (state record,103 fr worktree cache, devcontainer label); a foreign `git worktree add` is invisible to it.104- **`down` is the immediate lever** — verifies container + worktree are gone105 before dropping state (never leaked) and refuses an open PR unless `--force`.106107## Recovery (#341) and failure handling108109- **Wedged container:** `fr isolation restart [--force]` bounces the devcontainer110 WITHOUT dropping the worktree / installs — prefer it to down+up.111- **Orphaned pipeline sentinel** (every base command denied, no worktree to `cd`112 into): the guard self-heals (zero live worktrees → fails open); `fr isolation down --all`.113- `devcontainer up` failures surface verbatim — missing Docker, a broken profile,114 an absent secrets file are operator-environment issues: report and stop, never115 work around isolation (no silent degradation to a weaker mode).