diff --git a/.agents/skills/coven-cave/SKILL.md b/.agents/skills/coven-cave/SKILL.md new file mode 100644 index 0000000000..201295f10d --- /dev/null +++ b/.agents/skills/coven-cave/SKILL.md @@ -0,0 +1,186 @@ +--- +name: coven-cave +description: > + Default mandatory workspace discipline for all repository and development work in + OpenCoven Coven Cave (github.com/OpenCoven/coven-cave). Always activate when the + current working directory or target repository is Coven Cave, even if the user's + prompt does not name the project. Use for planning, status and diff inspection, Git + operations, setup, coding, reviewing, debugging, testing, building, running, release + work, and any task touching coven-cave / CovenCave / OpenCoven cave, ~/.coven, + COVEN_HOME, or the Cave Tauri/Next app. Enforces isolated-home VFS: never let the app + or tooling read the real user home. Triggers include a coven-cave path or remote, + Coven Cave, OpenCoven cave, "isolated cave", dev:app, pnpm dev for cave, ~/.coven, + COVEN_HOME, Tauri cave shell, and /coven-cave. This is the way. +--- + +# Coven Cave — isolated workspace (this is the way) + +When the current checkout or target repository is Coven Cave, **load this skill by +default and follow it before any other repository or development work**. The user does +not need to mention Coven Cave or invoke the skill explicitly. + +Coven Cave persists state under `~/.coven` / `os.homedir()` unless redirected. On this +machine the contract is: **development never reads or writes the real user home**. +Use the isolated tree + env-level fake home instead of a kernel VFS. + +## Detect that you are in coven-cave + +Any of these → this skill owns the session: + +- `cwd` or a parent is a `coven-cave` checkout (look for `src-tauri/`, `package.json` + name `coven-cave`, or `ai.opencoven.cave`) +- git remote contains `OpenCoven/coven-cave` +- User asks to run, fix, review, or develop Cave / CovenCave / coven-cave +- Any plan would touch `~/.coven`, `COVEN_HOME`, or install Cave tools into real `~` + +If you are **not** in that repo and the user did not ask for Cave, do not load this +workflow. + +## Non-negotiable rules + +1. **Never** run `pnpm dev`, `pnpm dev:app`, `scripts/dev-app.sh`, Tauri, or tests + against the real `$HOME` / real `~/.coven`. +2. **Always** use the isolation launcher so `HOME`, `COVEN_HOME`, `COVEN_CAVE_HOME`, + `XDG_*`, `CARGO_HOME`, and `CARGO_TARGET_DIR` sit under the isolated tree. +3. Prefer the existing isolated checkout over cloning into `~/…`. +4. If isolation is missing, recreate it with the skill script — do not “just use” + the real home for speed. +5. Real `~/.coven` may already exist from a production install. Treat it as **off + limits** unless the user explicitly asks to inspect production state. + +## Canonical isolated root + +| Item | Path | +|------|------| +| Checkout | `/tmp/coven-cave-isolated` | +| Fake home | `/tmp/coven-cave-isolated/.isolated-home` | +| `$COVEN_HOME` | `…/.isolated-home/.coven` | +| `$COVEN_CAVE_HOME` | `…/.isolated-home/.coven/cave` | +| Cargo home | `…/.isolated-home/.cargo` | +| Cargo target | `…/src-tauri/target` | +| Launcher | `…/dev-isolated.sh` | +| Toolchain | `…/.bin` (node + pnpm, auto-downloaded) | + +Override root with `COVEN_CAVE_ISOLATED_ROOT` if needed; keep it **outside** the +real user home. + +## Bootstrap (if missing) + +Resolve this skill’s directory (folder containing this `SKILL.md`), then: + +```bash +bash "/scripts/ensure-isolated.sh" +``` + +That clones `https://github.com/OpenCoven/coven-cave` into the isolated root if +needed, seeds empty cave state, installs `dev-isolated.sh`, and optionally seeds +the cargo registry **once** from the host (runtime still never points at real `~`). + +## Every command goes through the launcher + +```bash +ROOT=/tmp/coven-cave-isolated # or $COVEN_CAVE_ISOLATED_ROOT +cd "$ROOT" + +./dev-isolated.sh pnpm install +./dev-isolated.sh env PORT=3011 pnpm dev +./dev-isolated.sh env PORT=3011 bash scripts/dev-app.sh # Tauri shell +./dev-isolated.sh pnpm typecheck +./dev-isolated.sh pnpm test:app +``` + +`dev-isolated.sh` **exits non-zero** if `HOME` or `CARGO_HOME` still resolve under +the real user home. Do not bypass it. + +## Self-contained toolchain (`$ROOT/.bin`) + +The launcher does **not** rely on host homebrew, nvm, or corepack. On every run +it ensures a pinned toolchain under `$ROOT/.bin` and puts it first on `PATH`: + +- **node** — version from the checkout's `.nvmrc`, official tarball extracted + to `.bin/toolchain/`, with `node`/`npm`/`npx`/`corepack` symlinked into `.bin`. +- **pnpm** — standalone binary (no node needed to boot it), version from + `package.json` `"packageManager"`. +- **rust** — NOT downloaded by default (large). If `cargo` is missing the + launcher warns; run once with `COVEN_CAVE_ENSURE_RUST=1` to install rustup + into the isolated `CARGO_HOME`/`RUSTUP_HOME`. A host cargo (e.g. homebrew's) + is used as fallback when present. + +Provisioning is idempotent (fast no-op when versions match) and serialized via +`.bin/.provision.lock` (mkdir-atomic), so concurrent sessions can't corrupt a +download in flight — if the lock is stale (>5 min), the launcher says so; +remove the dir by hand. This means "pnpm/node: command not found" can no +longer happen from GUI-spawned shells, cron, or bare-PATH contexts: the +launcher bootstraps what it needs. Delete `.bin/` to force a re-download. + +## Dev server + Tauri + +- **Web only:** `./dev-isolated.sh env PORT=3011 pnpm dev` → `http://127.0.0.1:3011` +- **Native shell:** same `PORT`, then `bash scripts/dev-app.sh` (or `pnpm dev:app`). + The wrapper reuses a healthy server on that port; first cargo build is slow. +- Whisper runtime is bundled by `scripts/whisper-runtime-bundle.sh` on first + `dev-app` (lives under `src-tauri/resources/whisper`). +- Logs: `.isolated-home/Library/Logs/ai.opencoven.cave/CovenCave.log` +- App data: `.isolated-home/Library/Application Support/ai.opencoven.cave/` + +Pin a free port if 3011 is taken; stay consistent so web + Tauri share one origin. + +## Isolation model (why this works) + +App code honors: + +- `COVEN_HOME` / `COVEN_CAVE_HOME` (see `src/lib/coven-paths.ts`) +- `os.homedir()` for some paths that still join `~/.coven` without the env vars + +So isolation must set **both** explicit coven env vars **and** fake `HOME`. +Playwright e2e in-repo already uses temp `COVEN_HOME`; this skill is the same +idea for interactive/dev work, plus full home/XDG/cargo fencing. + +Not a FUSE VFS — env-root sandbox. Good enough when every process is started +through `dev-isolated.sh`. + +## Repo workflow (from upstream, still apply) + +While isolated for FS, still follow Cave’s engineering norms from the checkout’s +`AGENTS.md` / `CLAUDE.md`: + +- Branch from current `origin/main`; short-lived PR branches / worktrees +- Prefer managed worktrees via `pnpm beads:worktrees:create` when Beads is in play +- Verify with the suite that matches the change (`typecheck`, `test:app`, `test:api`, …) + +Run those commands **through** `./dev-isolated.sh` so beads/tests cannot write +real home by accident. + +## Quick health checks + +```bash +# Launcher points at fake home +./dev-isolated.sh node -e "const os=require('os'); console.log(os.homedir())" +# must print .../coven-cave-isolated/.isolated-home + +# Toolchain is self-contained (no host homebrew/nvm involved) +./dev-isolated.sh sh -c 'command -v node pnpm' +# both must print paths under $ROOT/.bin/ + +# Onboarding sees isolated coven home +curl -sS http://127.0.0.1:3011/api/onboarding/status | head -c 400 + +# Real production state untouched (mtime should not jump because of your session) +stat -f '%Sm %N' "$HOME/.coven" 2>/dev/null || true +``` + +## Do not + +- `cd` into a checkout under the real home and run bare `pnpm dev` +- Export `COVEN_HOME=~/.coven` “just this once” +- Point `CARGO_HOME` at `~/.cargo` for the running app (one-time rsync seed into + the isolated cargo dir is fine; live env is not) +- Commit the isolated tree under `/tmp` as if it were the user’s product clone + unless they asked + +## References + +- `references/layout.md` — path map and env vars +- `scripts/dev-isolated.sh` — launcher source of truth +- `scripts/ensure-isolated.sh` — bootstrap / repair +- Upstream: https://github.com/OpenCoven/coven-cave diff --git a/.agents/skills/coven-cave/agents/openai.yaml b/.agents/skills/coven-cave/agents/openai.yaml new file mode 100644 index 0000000000..ccc28a7d05 --- /dev/null +++ b/.agents/skills/coven-cave/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "Coven Cave" + short_description: "Keep Coven Cave development isolated" + default_prompt: "Use $coven-cave to work safely in the Coven Cave repository." + +policy: + allow_implicit_invocation: true diff --git a/.agents/skills/coven-cave/references/layout.md b/.agents/skills/coven-cave/references/layout.md new file mode 100644 index 0000000000..a560fd96ce --- /dev/null +++ b/.agents/skills/coven-cave/references/layout.md @@ -0,0 +1,70 @@ +# Isolated coven-cave layout + +Default root: `/tmp/coven-cave-isolated` (`$COVEN_CAVE_ISOLATED_ROOT`). + +## Tree + +``` +$ROOT/ + .git/ # clone of OpenCoven/coven-cave + node_modules/ + src/ src-tauri/ scripts/ … + dev-isolated.sh # required entrypoint for all commands + .bin/ # self-contained toolchain, first on PATH + node → toolchain/node-v…/bin/node + npm npx corepack # symlinks into the node dist + pnpm # standalone binary (packageManager pin) + toolchain/node-v…/ # extracted node dist (.nvmrc pin) + .provision.lock/ # transient; serializes concurrent provisioning + .pnpm-home/ + .isolated-home/ # fake $HOME + .coven/ # $COVEN_HOME + cave/ # $COVEN_CAVE_HOME + familiars.toml + memory/ prompts/ skills/ adapters/ workspaces/ + .cargo/ # $CARGO_HOME + .pnpm-store/ + .npm/ + .config/ .local/ .cache/ # XDG_* + Library/ + Application Support/ai.opencoven.cave/ + Logs/ai.opencoven.cave/ + src-tauri/target/ # $CARGO_TARGET_DIR +``` + +## Environment (set by `dev-isolated.sh`) + +| Variable | Value | +|----------|--------| +| `HOME` | `$ROOT/.isolated-home` | +| `COVEN_HOME` | `$HOME/.coven` | +| `COVEN_CAVE_HOME` | `$HOME/.coven/cave` | +| `XDG_CONFIG_HOME` | `$HOME/.config` | +| `XDG_DATA_HOME` | `$HOME/.local/share` | +| `XDG_STATE_HOME` | `$HOME/.local/state` | +| `XDG_CACHE_HOME` | `$HOME/.cache` | +| `CARGO_HOME` | `$HOME/.cargo` | +| `CARGO_TARGET_DIR` | `$ROOT/src-tauri/target` | +| `PNPM_STORE_PATH` | `$HOME/.pnpm-store` | +| `npm_config_cache` | `$HOME/.npm` | +| `npm_config_userconfig` | `$HOME/.npmrc` | +| `PATH` | `$ROOT/.bin` first, then `$CARGO_HOME/bin`, `$PNPM_HOME`, system dirs; homebrew last (fallback only) | + +## Toolchain pins + +- node — `.nvmrc` in the checkout (bare major resolves to newest release). +- pnpm — `"packageManager"` in the checkout's `package.json`. +- rust — not auto-installed; `COVEN_CAVE_ENSURE_RUST=1` installs rustup into + the isolated `CARGO_HOME`/`RUSTUP_HOME`; a host cargo is used as fallback. + +## Why both `HOME` and `COVEN_HOME` + +- `src/lib/coven-paths.ts` prefers `COVEN_HOME` / `COVEN_CAVE_HOME`. +- Some routes still use `homedir() + "/.coven"` (e.g. onboarding status, + github-subscriptions). Fake `HOME` covers those. + +## Ports + +Preferred isolated web port: **3011**. Reuse it for Tauri `devUrl` so the shell +and Next share one origin. `scripts/dev-app.sh` also auto-picks `3000..3010` if +`PORT` is unset — pin `PORT` when the isolated server is already up. diff --git a/.agents/skills/coven-cave/scripts/dev-isolated.sh b/.agents/skills/coven-cave/scripts/dev-isolated.sh new file mode 100755 index 0000000000..be5861e2bc --- /dev/null +++ b/.agents/skills/coven-cave/scripts/dev-isolated.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash +# Isolated dev launcher for coven-cave — never touches the real $HOME. +# Self-contained toolchain: node + pnpm are downloaded into $ROOT/.bin on +# first run (pinned by .nvmrc / package.json "packageManager"), so the +# launcher works from GUI-spawned shells, cron, CI, or any host without +# homebrew/nvm on PATH. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")" && pwd)" +export HOME="$ROOT/.isolated-home" +export USERPROFILE="$HOME" +export HOMEDRIVE="" +export HOMEPATH="" +export COVEN_HOME="$HOME/.coven" +export COVEN_CAVE_HOME="$HOME/.coven/cave" +export XDG_CONFIG_HOME="$HOME/.config" +export XDG_DATA_HOME="$HOME/.local/share" +export XDG_STATE_HOME="$HOME/.local/state" +export XDG_CACHE_HOME="$HOME/.cache" +export PNPM_HOME="$ROOT/.pnpm-home" +export npm_config_cache="$HOME/.npm" +export npm_config_userconfig="$HOME/.npmrc" +export npm_config_globalconfig="$HOME/.npmrc-global" +export PNPM_STORE_PATH="$HOME/.pnpm-store" +export CARGO_HOME="$HOME/.cargo" +export RUSTUP_HOME="$HOME/.rustup" +export CARGO_TARGET_DIR="$ROOT/src-tauri/target" +# macOS app support paths under fake home +export TMPDIR="$HOME/tmp" +unset CLAUDE_CONFIG_DIR 2>/dev/null || true + +BIN="$ROOT/.bin" +TOOLCHAIN="$BIN/toolchain" +# $BIN first so the pinned toolchain always wins; $CARGO_HOME/bin picks up an +# isolated rustup install; homebrew/usr-local sit at the END as fallback only — +# nothing here requires them. +export PATH="$BIN:$CARGO_HOME/bin:$PNPM_HOME:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/homebrew/bin:/opt/homebrew/sbin" + +cd "$ROOT" +mkdir -p \ + "$COVEN_HOME" "$COVEN_CAVE_HOME" \ + "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME" "$XDG_CACHE_HOME" \ + "$PNPM_STORE_PATH" "$npm_config_cache" "$PNPM_HOME" \ + "$CARGO_HOME" "$CARGO_TARGET_DIR" "$TMPDIR" \ + "$BIN" "$TOOLCHAIN" \ + "$HOME/Library/Application Support" \ + "$HOME/Library/Caches" \ + "$HOME/Library/Logs" \ + "$HOME/Library/Preferences" \ + "$HOME/Library/WebKit" + +fetch() { # fetch + local partial + partial="$(mktemp "$2.part.XXXXXX")" + curl -fsSL --retry 3 --proto '=https' -o "$partial" "$1" \ + || { echo "[isolated] FATAL: download failed: $1" >&2; rm -f "$partial"; exit 1; } + mv -f "$partial" "$2" +} + +ensure_toolchain() { + local node_os pnpm_os arch pnpm_arch + case "$(uname -s)" in + Darwin) node_os="darwin"; pnpm_os="macos" ;; + Linux) node_os="linux"; pnpm_os="linux" ;; + *) echo "[isolated] FATAL: unsupported OS: $(uname -s)" >&2; exit 1 ;; + esac + case "$(uname -m)" in + arm64|aarch64) arch="arm64"; pnpm_arch="arm64" ;; + x86_64|amd64) arch="x64"; pnpm_arch="x64" ;; + *) echo "[isolated] FATAL: unsupported arch: $(uname -m)" >&2; exit 1 ;; + esac + + # --- node: pinned by .nvmrc --- + local node_version="24.18.0" + if [ -f "$ROOT/.nvmrc" ]; then + node_version="$(head -1 "$ROOT/.nvmrc" | tr -d '[:space:]' | sed 's/^v//')" + fi + if ! printf '%s' "$node_version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then + # partial pin (e.g. "24") — resolve newest matching release + local major="${node_version%%.*}" resolved + resolved="$(curl -fsSL https://nodejs.org/dist/index.json \ + | grep -oE "\"v$major\.[0-9]+\.[0-9]+\"" | head -1 | tr -d '"' | sed 's/^v//')" || resolved="" + [ -n "$resolved" ] || { echo "[isolated] FATAL: cannot resolve node $node_version.x" >&2; exit 1; } + node_version="$resolved" + fi + if [ "$("$BIN/node" --version 2>/dev/null || true)" != "v$node_version" ]; then + local ndir="node-v$node_version-$node_os-$arch" + if [ ! -x "$TOOLCHAIN/$ndir/bin/node" ]; then + echo "[isolated] fetching node v$node_version → .bin/toolchain/$ndir" + fetch "https://nodejs.org/dist/v$node_version/$ndir.tar.gz" "$TOOLCHAIN/$ndir.tar.gz" + tar -xzf "$TOOLCHAIN/$ndir.tar.gz" -C "$TOOLCHAIN" + rm -f "$TOOLCHAIN/$ndir.tar.gz" + fi + ln -sfn "$TOOLCHAIN/$ndir/bin/node" "$BIN/node" + ln -sfn "$TOOLCHAIN/$ndir/bin/npm" "$BIN/npm" + ln -sfn "$TOOLCHAIN/$ndir/bin/npx" "$BIN/npx" + [ -e "$TOOLCHAIN/$ndir/bin/corepack" ] && ln -sfn "$TOOLCHAIN/$ndir/bin/corepack" "$BIN/corepack" + fi + + # --- pnpm: standalone binary, pinned by package.json "packageManager" --- + local pnpm_version="" + if [ -f "$ROOT/package.json" ]; then + pnpm_version="$(sed -n 's/.*"packageManager"[[:space:]]*:[[:space:]]*"pnpm@\([0-9][0-9.]*\).*/\1/p' "$ROOT/package.json" | head -1)" + fi + [ -n "$pnpm_version" ] || pnpm_version="10.34.0" + # --version from / so pnpm reports the binary's own version instead of + # self-switching to the repo's packageManager pin (a network fetch). + if [ "$(cd / && "$BIN/pnpm" --version 2>/dev/null | tail -1 || true)" != "$pnpm_version" ]; then + echo "[isolated] fetching pnpm v$pnpm_version → .bin/pnpm" + fetch "https://github.com/pnpm/pnpm/releases/download/v$pnpm_version/pnpm-$pnpm_os-$pnpm_arch" "$BIN/pnpm" + chmod +x "$BIN/pnpm" + fi + + # --- rust: opt-in (large download); installs into the ISOLATED cargo/rustup homes --- + if ! command -v cargo >/dev/null 2>&1; then + if [ "${COVEN_CAVE_ENSURE_RUST:-0}" = "1" ]; then + echo "[isolated] installing rust (rustup) into $RUSTUP_HOME / $CARGO_HOME" + fetch "https://sh.rustup.rs" "$BIN/rustup-init.sh" + sh "$BIN/rustup-init.sh" -y --no-modify-path --profile minimal --default-toolchain stable + rm -f "$BIN/rustup-init.sh" + else + echo "[isolated] WARN: cargo not found — Tauri dev needs it. Re-run with COVEN_CAVE_ENSURE_RUST=1 to install rustup into the isolated home." >&2 + fi + fi +} + +# Concurrent sessions are the norm in this repo — serialize provisioning so +# two launchers can't clobber each other's downloads (mkdir is atomic). +LOCKDIR="$BIN/.provision.lock" +waited=0 +until mkdir "$LOCKDIR" 2>/dev/null; do + if [ "$waited" -eq 0 ]; then + echo "[isolated] waiting for concurrent toolchain provisioning ($LOCKDIR)…" + fi + waited=$((waited + 1)) + if [ "$waited" -gt 300 ]; then + echo "[isolated] FATAL: provisioning lock held >5m — remove $LOCKDIR if stale" >&2 + exit 1 + fi + sleep 1 +done +trap 'rmdir "$LOCKDIR" 2>/dev/null || true' EXIT INT TERM +ensure_toolchain +rmdir "$LOCKDIR" 2>/dev/null || true +trap - EXIT INT TERM + +echo "[isolated] HOME=$HOME" +echo "[isolated] COVEN_HOME=$COVEN_HOME" +echo "[isolated] COVEN_CAVE_HOME=$COVEN_CAVE_HOME" +echo "[isolated] CARGO_HOME=$CARGO_HOME" +echo "[isolated] CARGO_TARGET_DIR=$CARGO_TARGET_DIR" +echo "[isolated] BIN=$BIN" +echo "[isolated] cwd=$(pwd)" +echo "[isolated] node=$(command -v node) ($(node --version 2>/dev/null || echo missing))" +echo "[isolated] pnpm=$(command -v pnpm) ($(pnpm --version 2>/dev/null || echo missing))" +echo "[isolated] cargo=$(command -v cargo) ($(cargo --version 2>/dev/null || echo missing))" + +REAL_HOME="$(dscl . -read /Users/"$(whoami)" NFSHomeDirectory 2>/dev/null | awk '{print $2}')" || REAL_HOME="/Users/$(whoami)" +if [ "$HOME" = "$REAL_HOME" ] || [ "$HOME" = "/Users/$(whoami)" ]; then + echo "[isolated] FATAL: HOME still points at real home: $HOME" >&2 + exit 1 +fi +if [[ "$CARGO_HOME" == "$REAL_HOME"/* ]]; then + echo "[isolated] FATAL: CARGO_HOME under real home: $CARGO_HOME" >&2 + exit 1 +fi +if [[ "$RUSTUP_HOME" == "$REAL_HOME"/* ]]; then + echo "[isolated] FATAL: RUSTUP_HOME under real home: $RUSTUP_HOME" >&2 + exit 1 +fi +if command -v node >/dev/null 2>&1 && [[ "$(command -v node)" != "$BIN/"* ]]; then + echo "[isolated] WARN: node resolves outside .bin: $(command -v node)" >&2 +fi + +exec "$@" diff --git a/.agents/skills/coven-cave/scripts/ensure-isolated.sh b/.agents/skills/coven-cave/scripts/ensure-isolated.sh new file mode 100755 index 0000000000..4200e0c06d --- /dev/null +++ b/.agents/skills/coven-cave/scripts/ensure-isolated.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# Ensure an isolated coven-cave workspace exists. Never uses the real $HOME for app state. +set -euo pipefail +ROOT="${COVEN_CAVE_ISOLATED_ROOT:-/tmp/coven-cave-isolated}" +REPO_URL="${COVEN_CAVE_REPO_URL:-https://github.com/OpenCoven/coven-cave}" +SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)" + +if [ ! -d "$ROOT/.git" ]; then + echo "[coven-cave] cloning $REPO_URL → $ROOT" + git clone "$REPO_URL" "$ROOT" +fi + +mkdir -p \ + "$ROOT/.isolated-home/.coven/cave" \ + "$ROOT/.isolated-home/.local/state" \ + "$ROOT/.isolated-home/.local/share" \ + "$ROOT/.isolated-home/.config" \ + "$ROOT/.isolated-home/.cache" \ + "$ROOT/.isolated-home/.pnpm-store" \ + "$ROOT/.isolated-home/.npm" \ + "$ROOT/.isolated-home/.cargo" \ + "$ROOT/.isolated-home/Library/Application Support" \ + "$ROOT/.isolated-home/Library/Caches" \ + "$ROOT/.isolated-home/Library/Logs" \ + "$ROOT/.isolated-home/Library/Preferences" \ + "$ROOT/.isolated-home/tmp" \ + "$ROOT/.pnpm-home" \ + "$ROOT/.bin" \ + "$ROOT/src-tauri/target" + +# Install / refresh the isolation launcher from this skill +cp "$SKILL_DIR/scripts/dev-isolated.sh" "$ROOT/dev-isolated.sh" +chmod +x "$ROOT/dev-isolated.sh" + +# Provision the self-contained toolchain (node + pnpm → $ROOT/.bin) now so the +# first real dev command doesn't pay the download mid-task. +"$ROOT/dev-isolated.sh" true + +# Minimal seed state (empty familiars; onboarding free to run) +if [ ! -f "$ROOT/.isolated-home/.coven/familiars.toml" ]; then + printf '# isolated dev familiars registry\n' > "$ROOT/.isolated-home/.coven/familiars.toml" +fi +if [ ! -f "$ROOT/.isolated-home/.coven/cave/config.json" ]; then + cat > "$ROOT/.isolated-home/.coven/cave/config.json" <<'JSON' +{ + "version": 1, + "profile": { "displayName": "Isolated Dev" }, + "onboarding": { "dismissed": true } +} +JSON +fi + +# Seed cargo registry once from the real user cache if present (one-time host seed; +# runtime never points CARGO_HOME at real ~). +if [ -d "${REAL_CARGO_HOME:-$HOME/.cargo}/registry" ] && [ ! -d "$ROOT/.isolated-home/.cargo/registry" ]; then + # Only seed when our isolation HOME is not already the process HOME + REAL_HOME_PROBE="$(dscl . -read /Users/"$(whoami)" NFSHomeDirectory 2>/dev/null | awk '{print $2}')" || REAL_HOME_PROBE="/Users/$(whoami)" + if [ -d "$REAL_HOME_PROBE/.cargo/registry" ]; then + echo "[coven-cave] seeding isolated cargo registry from host (one-time)" + rsync -a "$REAL_HOME_PROBE/.cargo/registry" "$ROOT/.isolated-home/.cargo/" || true + [ -d "$REAL_HOME_PROBE/.cargo/git" ] && rsync -a "$REAL_HOME_PROBE/.cargo/git" "$ROOT/.isolated-home/.cargo/" || true + fi +fi + +echo "[coven-cave] ready: $ROOT" +echo "[coven-cave] run via: $ROOT/dev-isolated.sh " +echo "$ROOT"