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.
83%
Does it follow best practices?
Impact
100%
2.63x1 of 3 eval scenarios. Add 2 more for a full score.
Passed
No findings from the security scan
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}}
quangtran88/x-skills
daac48d
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.