Use when the user runs multiple git worktrees of the same repo in parallel and hits docker-compose container_name / port / volume collisions OR has stateful singletons (Slack/Discord bots, schedulers, webhook receivers, host crontabs) that must not run concurrently across worktrees. Singleton-aware in v0.2. Wraps inspector + override emitter + worktrunk hook.
78
—
Does it follow best practices?
Impact
100%
2.63xAverage score across 1 eval scenario
Passed
No known issues
v0.2 migration (schema 1 → 2): Existing repos on v0.1 must run
x-worktree-isolate init --rescanfrom the main checkout, hand-merge the.newfile, and commit the updated profile.applyhard-rejects schema:1 profiles with a precise migration message. There is no soft fallback — schema:2 introducessingletons[]anddetection_guardrails{}that the rest of v0.2 depends on.
v0.3 migration (soft, self-healing): Singleton locks are now enforced — only one worktree may
enablea given singleton (Slack/Telegram/WhatsApp/etc.) at a time. The per-repo registry'ssingleton_ownersis enriched to{id: {worktree_path, branch, claimed_at}}with aregistry_schema: 2marker and self-heals lazily on the nextapply/enable/list/doctor— no manual step required. Runx-worktree-isolate migratefor the one-shot upgrade view (heal + pre-existing-conflict report + rescan/x-qa updatepointers). Unlike v0.2, there is no hard reject: every new field is additive. Pick up the new WhatsApp patterns viainit --rescan. Profileschemastays 2.
Two-phase model:
profile.json capturing hardcoded container_names, fixed ports, identity-mount data dirs, cross-worktree footguns, AND stateful singletons across three tiers (compose-service / env-flag / host). User reviews interactively + commits.compose.override.yml (with !reset null + !override ports + singleton profiles: [xwi-disabled] / deploy.replicas: 0) and .env.worktree (COMPOSE_PROJECT_NAME + ports + data dir + singleton env-flags). Hard-blocks on host-tier singletons until acknowledged. Triggered by worktrunk's wt post-create hook.The skill never runs docker compose up, never reads or writes the user's .env, and never auto-patches their Makefile.
MANDATORY first step — do this BEFORE anything else:
../x-shared/capability-loading.md. Relevant capabilities: agy_cli is irrelevant; this skill is a self-contained CLI. Skip the multi-model dispatch lanes.command -v x-worktree-isolate. If absent, point the user at the repo root: /Users/randytran/Codes/x-skills/bin/setup (the setup script symlinks bin/x-worktree-isolate into ~/.local/bin/).gotchas.md — known failure patterns the apply step won't catch.config.json for the version, schema version, stack identity, minimum Compose version, and dependency list. Per-repo runtime values (port range, registry root, lock retry counts) are owned by port_strategy in each profile and the constants in scripts/allocate-ports.sh.| Signal | Subcommand |
|---|---|
User asks to "isolate worktrees", "make compose work in parallel worktrees", "container name conflict in second worktree", or types init worktree-isolate | init (interactive; pass --non-interactive for CI) |
Just ran wt switch -c <branch> (or git worktree add) and started seeing port/name collisions | apply (auto via wt post-create hook; manual run also valid) |
docker compose up in a second worktree binds to the same host ports as the first / silently uses the same container_name | doctor |
wt remove was just run and the registry needs cleanup | release (auto via wt pre-remove hook) |
| User wants to see all currently-claimed slots | list |
| User wants to inspect singletons profiled for this worktree | features |
| User wants the Slack bot ON in this worktree | enable <id> |
| User wants to revert a singleton to default-disabled in this worktree | disable <id> |
| Host-tier singleton (crontab/systemd) blocks apply — user disabled the host state manually | ack-host-singletons |
| User upgraded the skill and wants the one-shot "what do I need to do" view | migrate |
| User wants to steal a live singleton lock from another worktree | enable <id> --force / apply --force |
container_name: entries AND no Makefile/script global --filter label= filters AND only ${VAR} host ports → use wtc or rft instead. They handle just COMPOSE_PROJECT_NAME + port renaming and require less ceremony. See references/existing-tools.md for the comparison matrix.The dispatch.sh router exposes one subcommand per workflow stage:
init — scan repo, write .worktree-isolate/profile.json, patch .gitignore. Must run from the main checkout (not a linked worktree). Idempotent: re-runs only append missing .gitignore lines.init --dry-run — print the draft profile JSON to stdout, do not touch disk.init --rescan — re-detect, write profile.json.new next to existing, exit 1 with the exact diff command. The user merges by hand (no auto-merge in v1).apply (--force/--steal) — Phase 2. Reads profile, acquires registry lock, allocates next slot, picks ports (default + slot * 1000 deterministic-first, then lsof collision scan inside scan_range), writes override + env file + state. Hard-blocks on any severity: blocker warning unless --ignore-warnings. Re-applying a worktree that already has a registry entry reuses its existing ports byte-for-byte. apply now claims every enabled singleton before writing any file; a refused claim leaves the worktree untouched and exits non-zero. --force/--steal steals a live lock.apply --quiet — same, suppress success summary. Used by the wt hook.apply --if-profile-exists — exit 0 silently when no profile. Safe for global hooks that may run on repos that haven't opted in.apply --ignore-warnings — explicit footgun acknowledgement. Skips the blocker gate.apply --dry-run — render override + env to stdout, do not touch disk, do not claim slot.release — drop this worktree's registry slot. Removes generated files only when their first line still matches the auto-generated header.doctor — validation suite. Asserts docker compose --env-file .env.worktree config (with --env-file .env stacked first when a base .env exists) actually exposes overridden host ports.list — print all slots claimed in the per-repo registry.version — print version (currently 0.3.1).features — list profiled singletons + per-worktree state (disabled / enabled / acknowledged).enable <id> [--force] — claim the singleton lock for this worktree, then mark it enabled and regenerate override + .env.worktree. Refuses if another live worktree owns it (SINGLETON_CONFLICT); auto-steals a dead owner (SINGLETON_LOCK_STOLEN); --force/--steal steals a live owner. Reaching enabled therefore guarantees this worktree owns the singleton.disable <id> — revert to disabled (default). Same regeneration.ack-host-singletons — write acknowledged for every host-tier singleton in feature-overrides.local.json. Required (per-worktree) when host singletons present, otherwise apply blocks.migrate — convenience upgrade view: heal registry → report pre-existing conflicts → prompt init --rescan → point x-qa users at x-qa update. The heal runs automatically on apply/enable/list/doctor regardless; migrate is just the single summary.compose.yml — apply only writes compose.override.yml (auto-merged by Compose) and .env.worktree (passed via stacked --env-file)..env — .env.worktree contains only the override keys. When a base .env is present, Compose merges via docker compose --env-file .env --env-file .env.worktree up (later file wins, Compose v2.24+). When there is no base .env, the launch command drops to a single --env-file .env.worktree.docker compose up from this skill — that's the user's Makefile / launch tooling. Apply only writes files and prints the launch command.severity: blocker warnings; the user fixes them by hand. Out of scope for v1.x-worktree-isolate (symlinked by bin/setup).feature-overrides.local.json to enabled — the "enabled ⇒ owned" guarantee holds only because enable wins the registry claim first. A hand-edited dual-enable surfaces as SINGLETON_CONFLICT_PREEXISTING on the next claim and blocks until resolved.Stateful features that must not run concurrently across parallel worktrees. Three tiers:
| Tier | What's detected | Disable mechanism (in worktree) |
|---|---|---|
| 1 — compose-service | Compose service env contains Slack/Discord/Telegram/Stripe/GitHub-App/WhatsApp token, or image matches ngrok/watchtower | services.<svc>.profiles: [xwi-disabled] (default) or deploy.replicas: 0 (Swarm only) — written into compose.override.yml |
| 2 — env-flag | Source code matches node-cron/bullmq/celery beat/slack-bolt/telegraf/agenda/chokidar/Procfile worker line/@whiskeysockets/baileys/whatsapp-web.js | <VAR>=false line appended to .env.worktree (app code reads the flag) |
| 3 — host | Repo-tracked *.crontab, crontab, *.service, *.timer | NONE — apply hard-blocks until x-worktree-isolate ack-host-singletons (per-worktree) |
init scans + prompts (interactive by default; --non-interactive for CI accepts all candidates as disabled). Detection is bounded by profile.detection_guardrails — scan_max_depth, scan_max_file_bytes, exclude_dirs, exclude_globs. The pattern catalog lives in scripts/singleton-patterns.py; see references/singleton-patterns.md for per-tier rationale and the "why no host auto-disable" answer.
Per-worktree overrides via features / enable <id> / disable <id> write to <worktree>/.worktree-isolate/feature-overrides.local.json (gitignored). apply re-renders both files honoring those overrides.
Enforcement (v0.3): the per-repo registry records each owned singleton as
singleton_owners[id] = {worktree_path, branch, claimed_at} (top-level registry_schema: 2). enable and apply run an enforced claim inside the registry lock: unowned/owned-by-self → claim; another live owner → refuse (SINGLETON_CONFLICT=<id> owner=<branch>@<path>) unless --force; a dead owner → auto-steal (SINGLETON_LOCK_STOLEN=<id> from=<branch>). A lock is dead when the owner's worktree_path is gone from disk OR it no longer holds a registry slot (all tiers), OR — compose-tier only — its COMPOSE_PROJECT_NAME has zero running containers. On upgrade, two live worktrees that both already enabled the same singleton surface SINGLETON_CONFLICT_PREEXISTING=<id> owners=<b1>@<p1>,<b2>@<p2> and the id stays unowned until the loser runs disable <id>.
Honest guarantee per tier. The registry makes the claim exclusive — two worktrees can never both believe they own a platform. Runtime exclusivity is only as strong as the tier:
profiles: [xwi-disabled]) — a real gate; the disabled service does not start. Note: a compose lock only reads as live once the owner's stack is up — a stopped stack is reclaimable (R3).<VAR>=false and short-circuit. The skill cannot enforce app code.The lock cannot reach into application code; it prevents the dual-ownership belief, not every possible dual execution.
See gotchas.md. Highlights:
container_name: !reset null is the only incantation that removes the field at merge time. null, "", and field-omission all silently retain the base value.${V}:${V}) require an absolute host path that equals the container path — apply produces an absolute path; the user MUST keep the mount line as ${V}:${V} in the base compose.container_name only when env values reference the service name (almost always true). The inspector flags any environment value that hard-codes a stripped container name.apply hard-blocks on global Makefile label filters (--filter label=app.sandbox=1) because they cross-tear-down parallel worktrees' containers. Resolve by scoping the filter to COMPOSE_PROJECT_NAME.wt switch -c worktreesinit worktree-isolate, apply worktree-isolate, worktree doctorgit ≥ 2.5, python3 + PyYAML (for parse-compose.py), docker compose ≥ 2.24 (for --env-file stacking and !reset / !override merge tags), lsof (port collision check), openssl or python3 hashlib (sha1 of repo identity).wt CLI for automatic post-create / pre-remove hooks. Without it, the user invokes apply manually after git worktree add.bin/setup symlinks bin/x-worktree-isolate to ~/.local/bin/x-worktree-isolate. The wt.toml hook resolves the launcher via PATH, never the plugin cache./x-skills:x-worktree (sibling skill) automatically calls apply --quiet --if-profile-exists after creating a linked worktree, so users only run ONE command (/x-skills:x-worktree main feat/x) and isolation fires when a profile is present. The integration contract:
timeout 5 x-worktree-isolate apply --quiet --if-profile-exists from the new worktree's cwd.<worktree>/.worktree-isolate/profile.json OR <main-checkout>/.worktree-isolate/profile.json (apply.sh:58–65). x-worktree never replicates this detection.state.local.json is written under <worktree>/.worktree-isolate/. x-worktree emits ISOLATE_APPLIED=true in its envelope.--if-profile-exists short-circuit (no profile, OR cwd is the main checkout): apply.sh exits 0 silently with no state file written. x-worktree emits ISOLATE_APPLIED=skipped, ISOLATE_REASON=no-profile.ISOLATE_APPLIED=false with sanitized ISOLATE_REASON + ISOLATE_HINT, removes any orphan state.local.json, and runs release --quiet to unclaim a possibly-claimed slot.<worktree>/.worktree-isolate/state.local.json directly — that file is the single source of truth (see schema below). The envelope deliberately does NOT cache port lists or launch hints, because the user may add or remove a base .env after apply runs.Detailed envelope semantics + caller read pattern: ../x-worktree/references/auto-isolation.md.
The wt.toml post-create hook documented below remains supported as a parallel path for raw-wt users who never go through the skill. Both paths are safe — apply.sh acquires a registry lock so first-writer wins; a second concurrent apply on the same worktree no-ops with the existing slot reused byte-for-byte.
Written by apply.sh to <worktree>/.worktree-isolate/state.local.json after every successful apply. Single source of truth for callers needing port / project-name / data-dir info.
| Field | Type | Description |
|---|---|---|
schema | int | Schema version. Currently 1. Callers MUST refuse on mismatch. (Note: same convention as profile.json which also uses schema.) |
slot | int | Slot index allocated for this worktree (0-based, monotonically assigned per repo). |
branch | string | Branch name at apply time (from git rev-parse --abbrev-ref HEAD). |
compose_project_name | string | Sanitized <repo-slug>-<branch-slug>. Use as COMPOSE_PROJECT_NAME env var. |
allocated_ports | object | Map of <VAR> → host port (int). E.g. {"POSTGRES_PORT": 18432, "REDIS_PORT": 19000}. |
data_dir_var | string | First per-worktree data-dir env-var name. Empty string when no data dirs are profiled. |
data_dir_path | string | Absolute path of the first per-worktree data dir on the host. Empty string when none. |
applied_at | string | ISO8601 UTC timestamp (e.g. 2026-05-09T12:34:56Z). |
Re-applying the same worktree produces a byte-identical file modulo applied_at (port reuse via registry lookup at apply.sh:135–164).
The wt.toml hook snippet below is optional — only needed for raw-wt users who never invoke /x-skills:x-worktree. Skill callers don't need it; the skill calls apply --quiet --if-profile-exists for them. The wt.toml-only path is on the v2 deprecation track; new repos should adopt /x-skills:x-worktree.
Written by enable / disable / ack-host-singletons to <worktree>/.worktree-isolate/feature-overrides.local.json (gitignored). Per-worktree state for each profiled singleton:
{
"schema": 1,
"overrides": [
{"id": "slack-listener", "state": "enabled"},
{"id": "host-crontab", "state": "acknowledged"}
],
"updated_at": "2026-05-11T12:00:00Z"
}State values: enabled | disabled | acknowledged (host-tier only). Absent entry = use default_in_worktree from profile.singletons[] (always disabled for non-host tiers; host requires explicit acknowledged). apply reads this file before rendering override + env.
Loaded only when the workflow needs them:
references/detection-heuristics.md — what the inspector probes for and why (incl. v0.2 singleton probes)references/singleton-patterns.md — singleton pattern catalog, evidence formats, severity assignment, ID contractreferences/compose-override-cookbook.md — !reset null empirical verificationreferences/port-strategies.md — slot formula, collision behavior, rangereferences/worktrunk-integration.md — wt.toml snippet, manual fallbackreferences/existing-tools.md — wtc / rft / grove comparisonTask: {{ARGUMENTS}}
6381b15
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.