Worktree and devcontainer are provisioned separately — concurrent features collide in one container #21
Labels
No labels
component:adapters
component:lifecycle
component:qa
component:setup
component:shared
component:worktrees
phase/accepted
phase/backlog
phase/deployed
phase/developing
phase/integrating
phase/planning
phase/qa
phase/requirements
phase/uat
priority:critical
priority:critical
priority:high
priority:high
priority:low
priority:low
priority:medium
priority:medium
type:bug
type:chore
type:docs
type:feature
type:infra
type:tech-debt
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
jbr870/devwork-skills#21
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
PREQ: Slot isolation unit — worktree + declared runtime provisioned as one act
Created: 2026-08-11
Problem
An operator running N concurrent features struggles with cross-feature interference because the
isolation unit is split across two layers that are provisioned independently:
.devcontainer/brings up a single Compose project; everyconcurrent feature shares its process table, port range, database server, globally-installed
tooling, and
~/.claude./git-worktreesthen creates a worktree per feature inside thatone container, provisioning exactly one isolation axis (a per-worktree database) of the four
that matter (database, ports, processes, config).
With more than one or two features in flight they collide, and no amount of care inside
/git-worktreesfixes it — the conflict is a layer below the worktree.Users:
per slot, driving
/developand QA runs concurrently).that a red suite tested their slot's code; and maintainers of projects adopting the suite,
who must be able to declare their stack's isolation recipe without the suite hardcoding it.
Current state (observed on the verity dogfood project, 6-slot pool):
server dies on
EADDRINUSEwhile the URL still answers — the suite then silently testsanother slot's app and database. Feature 164: 144 passed / 81 failed with three sibling
runners overlapping; 225/1/0 run alone.
.envstill sayingPORT=3000) with noallocation authority and no record of ownership.
pkill -fhazards routine (killing VSCode'sport forwarders drops the remote session; a pattern can hit a sibling slot's server).
_test/_e2edatabases areorphaned.
Proposed Solution
Provision the whole isolation unit as a single act per slot: branch + worktree + every
isolation axis the project declares — container runtime, port block, database(s). Slots form a
semi-permanent pool: claiming a slot for a new feature costs a branch switch plus an
incremental refresh; retiring a slot is a full teardown that removes everything the slot owns
(container, volumes, all derived databases, port reservations).
Projects declare their isolation recipe once in their CLAUDE.md (alongside the existing
## Worktreesdeclarations); the skill reads and provisions it. The container is an axis, nota prerequisite — a project with no container declaration still gets worktree + port + database
isolation, and the provisioning report says which axes are active.
Authorities: the project's recipe declares the scheme (port scheme shape/base, database
naming, container recipe); the skill is the sole allocation authority within that scheme; the
operator decides pool size per slot creation (no fixed N). Each slot's resulting identity —
assignments, not liveness — is recorded in a durable, skill-defined identity surface that
projects' own tooling can consume.
Scope: Full — the two provisioning layers merge (or one drives the other) so feature A and
feature B never share a runtime. Chosen over the ports-only middle step because the shared
process table and blast radius are also real failure sources, even though all observed failures
to date are port/DB-level.
User Stories
that I never hand-compose worktree + container + database + ports and silently miss an axis.
(processes, ports, databases) so that concurrent dev servers and test suites never interfere
with each other.
my slot's own server and database so that I never debug another feature's code as if it were
mine.
container, volume, database, or port reservation is orphaned.
once in my project's CLAUDE.md so that the skill provisions it without the suite hardcoding my
stack.
Acceptance Criteria
Provisioning:
then a single invocation provisions branch + worktree + every declared axis (container
runtime, port block, database(s)) and reports each axis it provisioned. Interactive
prompts/confirmations are allowed; the operator never runs provisioning commands or creates
resources by hand.
(a skill-defined, documented location), then it shows the slot's durable assignments — port
assignments, database names, runtime handle (if a container axis is active), claim state,
and the slot's resource inventory. It records assignments, not liveness, so it stays
truthful across host restarts (whether containers auto-start after reboot is the recipe's
choice).
port + database isolation is still provisioned, and the report explicitly marks the
container axis as not declared (never a silent failure and never a refusal).
operator re-runs it, then the slot resumes from its named partial state — the report says
exactly which axes are missing, and no duplicate resources are created.
provision or claim time, then the act stops loudly naming the unreachable axis — it never
silently degrades to fewer axes than declared.
readiness guard runs, then the defect is reported loudly with the offending field named —
provisioning refuses rather than guessing.
Isolation (verified on a reference project wired to the identity surface — a project whose own
tooling ignores the surface cannot receive these guarantees; wiring guidance is part of the
deliverable, per-project wiring is not):
full test suites (unit, integration, e2e), then each suite's results match its solo-run
results, and the slot identity is observable end-to-end — the running server/database can
be asserted to belong to the slot under test, so a passing run is distinguishable from a
lucky collision-free one. The guarantee covers the axes the project declared; full
"anything in the sibling slot" isolation requires the container axis.
e2e suite with no manual port override, then the sibling binds its own provisioned port —
no address-in-use failure, and no silent reuse of the first slot's already-answering URL.
processes via a representative battery (by recorded PID, by name pattern, by port), then
sibling slots' servers keep serving and the operator's editor connection to each slot
remains live.
Pool lifecycle:
feature, then the cost is a branch switch plus incremental refresh: the slot's container,
port assignments, and database identities persist unchanged, while derived data content
is reset for the new feature (schema/content refreshed; the previous feature's data does
not leak into the new feature's runs).
then it is refused with a report of what is running; an explicit force flag overrides.
then the drift is reported and a reconcile offered — the claim never silently proceeds on a
recipe that no longer matches the slot.
resource inventory remains — worktree, branch (via the cleanup skill's existing confirm
flow), container(s), volumes, every database in the inventory including derived ones
matching the documented derivation convention (e.g.
_test/_e2esuffixes), and portreservations — and a post-teardown listing verifies the inventory is empty. A partial
teardown failure leaves a named state and is resumable, like provisioning.
declared an isolation recipe, when the readiness guard runs, then the gap is reported
loudly and re-reported every run until the project declares one (no quiet default).
Out of Scope
migration is project-side work driven by its own tracker.
identity surface instead of a hardcoded default) — the skill provides the identity surface and
documents the consumption contract; each project wires its own tooling to it. The isolation
ACs are verified on a wired reference project.
declaration stays runtime-agnostic, but Docker + Compose is the validated path.
Dependencies
container/volume lifecycle, and image-reuse semantics must be verified against the real CLI at
plan time — not assumed.
rules constrain the slot pool's parking-branch model.
## WorktreesCLAUDE.md contract: thesetup(pre-install),install, andpost-installhook ordering (steps 4.7 → 5 → 5.5) and thedatabase:block already run atdefined points; container bring-up must slot into that ordering, not around it. The
.claudetracked-vs-symlinked ruling (
993855a) must be re-checked under a per-slot runtime.git-worktrees,worktree-cleanup,/setupreadiness(parallel-dev isolation item), and the
dev-server-lifecycleprocedure text.is a project declaration, never a suite-hardcoded stack opinion.
Timeline
Notes
any default it ships must be axis-generic, not stack-specific. Provisioning cost motivated the
slot pool; the design must not reintroduce a full install per feature.
Compose project names; the port allocation mechanism within the declared scheme (deterministic
offset vs. recorded allocation);
.claudemount semantics when each slot owns a runtime; exactcomposition with the 4.7/5/5.5 hook ordering; the concrete shape of the identity surface.
(shared process table, blast radius), acknowledged to have no cited incident of its own;
observed failures are port/DB-level.
stop, not a degrade.
atomic rollback).
naming-only alternative misses ad-hoc variants, e.g. the observed stray
_apival_testDB).reshaped by it; splitting builds them twice).
Pairs with #22 — both have to settle the same question: how
.claude/resolves when more than one candidate copy is in play. Here it is a per-worktree container, where "the main worktree's.claude" stops being a well-defined thing; there it is an installed plugin copy sitting alongside this repo's own.claude/skills/while we dogfood in here.993855aalready ruled once for the worktree case (checkgit ls-files .claudefirst; leave tracked paths to the checkout rather than symlinking over them). Whichever of these is planned first should extend that ruling to cover both, rather than each inventing its own resolution order.Test Plan: worktree-container-isolation (issue #21)
Prerequisites
State the scenarios need (not environment specifics — those are the UAT card's job):
their ports/database names from the slot identity surface (the PREQ's isolation ACs are
defined against a wired project).
database naming.
(for the unreachable-axis scenario).
axis.
provisioned (drift scenario).
Required Test Data
e2e) in the reference project.
into the slot's database(s), so carryover into the next feature is detectable.
one derived database (e.g. a test-suite database) beyond the one the act provisions.
Test Scenarios
Scenario 1: Single-act provisioning of every declared axis
Acceptance criterion: AC1 — single invocation provisions branch + worktree + every declared
axis and reports each axis.
for the slot is up, the slot's port assignments exist, and the slot's database(s) exist.
everything beyond answering prompts was done by the act.
Expected outcome: one invocation, a complete slot, an axis-by-axis report.
Scenario 2: The slot identity surface
Acceptance criterion: AC2 — identity surface shows durable assignments, claim state, and
inventory, from a documented location.
resource inventory.
actually exist, the container that actually runs).
Expected outcome: one readable, documented record of everything the slot owns.
Scenario 2b: Identity survives a host restart
Acceptance criterion: AC2 — records assignments, not liveness; stays truthful across restarts.
inventory) — nothing in it has become false.
that contradicts reality.
Expected outcome: the record never lies after a reboot.
Scenario 3: No container declaration — degrade by declaration
Acceptance criterion: AC3 — worktree + port + database isolation still provisioned; container
axis explicitly marked not declared.
silently nor refuses to provision.
Expected outcome: a degraded but honest slot: three axes provisioned, the fourth loudly absent.
Scenario 4: Partial provisioning failure is named and resumable
Acceptance criterion: AC4 — failure leaves a named partial state; re-run resumes; no duplicates.
provisioned and which are missing.
the missing axes are completed, and the final report shows a complete slot.
Expected outcome: an interrupted act is a resumable state, not a mess.
Scenario 5: Declared container runtime unreachable — loud stop
Acceptance criterion: AC5 — declared-but-unreachable axis stops the act loudly; never a
silent degrade.
Expected outcome: unreachable ≠ undeclared; the act refuses rather than quietly narrowing.
Scenario 6: Malformed recipe refused loudly
Acceptance criterion: AC6 — malformed/partial recipe reported loudly with the offending field
named, at provision time and by the readiness guard.
guess.
Expected outcome: a broken declaration is loud in both places a maintainer would look.
Scenario 7: Concurrent slots do not interfere — and can prove it
Acceptance criterion: AC7 — concurrent full runs match solo runs; slot identity observable
end-to-end; guarantee scoped to declared axes.
slot A's run you can assert the server and database being exercised belong to slot A (and
likewise for B), so the clean result is provably not a lucky collision-free overlap.
Expected outcome: concurrency changes nothing, and the runs can prove whose runtime they hit.
Scenario 8: Sibling e2e binds its own port — no silent takeover
Acceptance criterion: AC8 — sibling e2e with no manual override binds its own provisioned
port; no address-in-use failure; no silent reuse of the sibling's URL.
not against slot A's already-answering URL.
Expected outcome: the historical failure mode (loser dies, suite silently tests the sibling)
is impossible without anyone remembering a port flag.
Scenario 9: Kill blast radius stays inside the slot
Acceptance criterion: AC9 — a representative kill battery inside one slot leaves sibling
servers serving and editor connections live.
to each slot's runtime:
pattern, and by port.
Expected outcome: slot A can be as careless as it likes; the damage cannot leave slot A.
Scenario 10: Claiming a pooled slot — identities persist, data resets
Acceptance criterion: AC10 — claim = branch switch + incremental refresh; identities persist;
derived data content is reset.
that feature.
port assignments are unchanged, the database names are unchanged.
schema/content, not the old rows.
Expected outcome: cheap claim, no data ghosts.
Scenario 11: Busy slot refuses claim/retire unless forced
Acceptance criterion: AC11 — refusal with a report of what's running; explicit force overrides.
Expected outcome: stepping on a live slot requires saying so out loud.
Scenario 12: Recipe drift is reported at claim
Acceptance criterion: AC12 — drift reported, reconcile offered; never a silent claim on a
stale recipe.
offers to reconcile.
changed.
Expected outcome: a slot and its recipe cannot quietly disagree.
Scenario 13: Teardown empties the recorded inventory — verifiably
Acceptance criterion: AC13 — teardown removes everything in the inventory including derived
databases; post-teardown listing verifies; partial failure is named and resumable.
provisioned one (per Required Test Data).
container(s) and volumes gone, every database in the inventory gone — including the derived
one(s) matching the documented derivation convention — and port reservations released.
the act stops with a named remaining-state report, and a re-run completes the teardown.
Expected outcome: retiring a slot leaves nothing — and can prove it left nothing.
Scenario 14: Readiness guard blocks undeclared multi-slot layouts
Acceptance criterion: AC14 — multi-worktree layout without a recipe is reported loudly and
re-reported every run.
the readiness check.
default.
Expected outcome: parallel development without a declared recipe stays visibly blocked until
the project declares one.
Notes
precondition). Running them against an unwired project tests nothing the PREQ promises.
assignment (who executes which scenario) is
/technical-plan's call.no orphan scenarios.
Software Requirements: worktree-container-isolation
Context
Operators running N concurrent features today hand-compose two independently-provisioned layers —
one devcontainer per repo, N worktrees inside it — so every slot shares ports, processes, databases,
and config; sibling suites have silently tested each other's servers (144/81 concurrent vs 225/1
solo on verity #164). This feature makes the slot the isolation unit: one act provisions branch +
worktree + every axis the project declares (container runtime, port block, databases), slots pool
(cheap claim, full retire), and each slot's assignments live on a durable identity surface that
project tooling can consume. Constraint: the suite stays stack-agnostic — it orchestrates declared
recipes and never hardcodes a runner, framework, or container stack.
Approaches Considered
Approach A: Slot lifecycle inside the existing skills + canonical contract in
_sharedSummary:
git-worktreesgains the slot acts (provision/claim/release/status),worktree-cleanupgains inventory-driven retire (reusing its existing branch confirm flow, as AC13 requires), and a new
canonical doc in
_shared/procedures/owns the recipe schema, identity-surface schema, state machine,and verb contracts that both skills and all consumers (readiness, dev-server-lifecycle, QA) reference.
Pros: reuses the existing 4.7→5→5.5 hook machinery instead of duplicating it; retire lands where
cleanup already lives; canonical-doc pattern matches the repo's red-markers/observability rulings;
ad-hoc single-worktree path keeps working unchanged.
Cons: acts split across two skills (discoverability);
git-worktreesgrows.Effort: Medium
Approach B: New standalone
slotsskill owning the whole lifecycleSummary: A fresh skill with provision/claim/retire/status;
git-worktreesandworktree-cleanupdemoted to internal machinery.
Pros: one discoverable surface; clean-room state machine.
Cons: duplicates or awkwardly wraps the worktree-creation hook ordering; two skills must still be
kept coherent with it (the machinery cannot move wholesale — ad-hoc worktrees still need it);
worktree-cleanup's confirm flow would be reimplemented, which AC13 explicitly wants reused.
Effort: High
Approach C: Bolt axes onto the existing create/remove flows without a pool
Summary: Extend
git-worktreesStep 4.7 with container + port steps per feature-worktree;teardown stays per-feature.
Pros: smallest diff.
Cons: fails the slot-pool ACs outright (AC10–AC12: claim, busy refusal, drift), keeps
provisioning cost per-feature (the cost that motivated the pool), and leaves no durable identity
surface — allocation stays session-improvised.
Effort: Low, but does not meet the PREQ.
Decision
Selected: Approach A.
Rationale: The PREQ's "the two layers merge (or one drives the other)" is satisfied with one
driver: the slot acts drive the existing worktree machinery rather than reimplementing it. The
canonical-contract-doc pattern is this repo's proven answer to multi-consumer drift (red-markers,
observability-policy), and AC13's explicit reuse of the cleanup confirm flow points the retire act at
worktree-cleanup. UX discoverability cost is mitigated (status act footer-lists all acts and theirtrigger phrases; both skills' triggers name each other).
Architecture
Component Overview
Data Flow
Provision (one act): validate recipe (refuse on malformed, naming field) → take registry lock →
allocate lowest-free slot number + port block; write identity file (state
provisioning, all axespending) → release lock → worktree axis (worktree + parked-detached checkout at integration tip;existing Steps 3–4.5 incl. env copy and the tracked-wins
.clauderuling) → port axis (record +best-effort listener probe on the block) → container axis if declared (
runtime_checkfirst —unreachable = loud stop naming the axis; then
up) → database axis (create+ declaredmigrate/seed via the existing 4.7/5.5 hook split, against the slot's runtime) → install →
post-install → regenerate
.slot.env→ flip slot statefree(orclaimedif a feature was given)→ axis-by-axis report. Each axis flips
pending → done(orfailed:<reason>) in the identity fileonly after its verification step; a re-run resumes from the recorded axis states without duplicating
resources (a
faileddatabase axis is dropped and recreated, not trusted).Claim (feature → slot): lock → busy check (distinguishes live processes from a stale claim
record, and says which) → axis health check (
runtime_check, database existence — a dead declaredaxis is loudly named, AC5 beyond provision) → drift check (recipe hash) → branch create/switch in the
slot worktree → content reset: drop+create the same-named primary DB, drop derived DBs (exact
constructed names), re-run declared refresh hooks → regenerate
.slot.env→ update claim state →report. Identities (ports, DB names, container) never change at claim; the container is not rebuilt.
Retire: busy refusal unless
--force(force bypasses only the busy check) → validate theidentity file against the schema and the recipe's naming patterns before any destructive use →
teardown strictly from the recorded inventory: processes/container (
down+ volumes) → databases(recorded primary + exact
primary+suffixnames for each declared derived suffix — never substringmatches against
listoutput) → port reservations (registry record) → worktree (existing cleanupflow) → branch (cleanup skill's existing confirm flow) → post-teardown verification report: per
resource class, expected-empty vs observed (via declared
list/container list verbs +git worktree list) → identity file deleted last. Partial failure leaves named axis states; re-run resumes.External Data Contracts
-pnamespacing,down -vscoping, image reuse, label enumerationworktree add/list --porcelain/remove; branch-in-use, detach-to-park, dirty-remove rules--forcerecorded→external-contracts/git-worktree-semantics.provenance.jsonKey Decisions
{repo}-{slot}(repo identifier included)down -vcollisions. Docker semantics gated by the preconditionbase + N*blockfrom the declaredport_scheme) as the allocator's method; the identity record is the authority consumers read<slots_dir>/.slots/<slot>.json(outside every worktree) + derived flat<worktree>/.slot.env.slot.envis the zero-dependency project-tooling read. Write direction is one-way: JSON is sole source,.slot.envregenerated at provision/claim/reconcile, never read back, gitignored, 0600, carries ports + DB names (primary and derived) and never secretscreate; install/post-install ordering unchanged.claudemounts (PREQ open Q)993855a) unchanged; mounting~/.claude(user-level, may hold credentials) into slot containers is default-off, explicit recipe opt-inparallel_dev:block upgraded to a structured schema — this consciously supersedes worktree-discipline.md's "only presence is load-bearing, the suite never parses it" rulingport_scheme/database/containerfields; command values stay free-text project recipes. Legacy free-text blocks are a loud readiness gap with upgrade guidance, not grandfatheredmkdir-based lock (<slots_dir>/.slots/.lock/) around every registry mutation (allocate, claim, release, retire), with owner/timestamp metadata and stale-lock recovery guidancemkdiris atomic and POSIX;flockis not in the helper baseline (absent on macOS). Covers the allocation race and concurrent-claim raceprovisioning → free ⇄ claimed → retiring, axis statespending / done / failed:<reason>, a transition table naming which act may flip which fieldsh -cwith template vars substituted (not exported); a command containing any unresolved{var}refuses to run; substituted values validated against[A-Za-z0-9_-]{db_nmae}must fail loudly, not execute literallylistverbspsql -l-style decorated output must never feed a drop path; prefix collisions (slot1vsslot10) make pattern-drops unsafemain_dbguard kept from the old block and extended: applies to primary drops, derived-suffix drops, and claim-time drop+create; declareddropowns connection termination (documented per-engine, e.g. PostgresWITH (FORCE))statusrenders the pool (claim states, drifted slots flagged with decline date) and footer-lists the actsTechnical Risks
/dev/tcpconnect test detects listeners, cannot prove bindability)procedures/slot-lifecycle.md; SKILL.md gains only dispatchExpert Review
Reviewers
flockabsent on macOS) — specifymkdir-lock; the presence-onlyparallel_dev:ruling is silently reversed — supersede explicitly and update all consumers; declared-verb interpreter and substitution semantics undefined;listoutput contract unspecifiedlistcontract must be one-bare-name-per-line;main-dbguard missing from new schema; claim-time drop must own connection termination--forcescope must be busy-check-only; anchor derived-suffix matching; validate identity JSON before destructive use;~/.claudecontainer mount default-off; charset-validate substituted valuesChanges Made
mkdir-based registry lock with stale-lock recovery around all mutations (arch, backend, UX-claim-atomicity)slot-isolation.mdwith a transition/ownership table (arch)parallel_dev:ruling; legacy free-text blocks become a loud readiness gap with upgrade guidance; consumer updates (worktree-discipline, readiness-check, qa-playbook, setup) added to Implementation Scope (backend)sh -c, substituted vars, refusal on unresolved{var},[A-Za-z0-9_-]value validation (backend, security)listverbs: one-bare-name-per-line, verification-only; destructive ops use exact constructed names; per-engine example commands in the canonical doc (backend, DB, security)main_dbguard carried into the new schema and extended to derived drops and claim-time reset (DB)dropowns connection termination; documented per-engine (DB)--forcescope pinned to the busy check only; never widens the drop set; never overridesmain_db(security)~/.claudecontainer mount default-off with explicit opt-in;.slot.env/copied env hygiene (0600, gitignored) (security)jq -S -ccanonicalized extraction so cosmetic edits don't trip AC12 (arch, backend).slot.envone-way write direction; exposes derived DB names too (arch, backend, DB)dev_{feature}DBs handled by a documented orphan sweep at adoption (arch, DB)Noted (not actioned)
/dev/tcplistener detection plus the existing run-time QA ownership check.slot.envcarries only ports/names) insteadAcceptance Criteria
slot-flow.shscenario: provision against full fake recipe; assert resources + report lines per axisslot-flow.sh: assert JSON fields vs created resources; restart-sim (kill fake runtime, re-read: no liveness claims, assignments unchanged)slot-flow.shno-container fixture variant; assert report wordingslot-flow.sh: injectcreatefailure; assertfailed:<reason>state, resume, single set of resourcesslot-flow.sh:runtime_checkforced nonzero on both paths; assert stop + axis named, no degradeslot-flow.sh: malformed fixtures (missing field, non-numeric base) vsslot-recipe-validate.sh; guard half covered with AC14's dedicated runslot-flow.sh: marker data written, claim, assert same names/ports + marker gone--forceoverridesslot-flow.sh: fake live process → refusal text;--forceproceeds; stale-claim variant distinguishesslot-flow.sh: mutate fixture recipe post-provision; assert drift report; cosmetic-only edit asserts NO drift (hash canonicalization)slot-flow.sh: fixture with extra derived fake DB; retire; assert per-class empty report; failure-injection resume variantMechanical route-through rule: every identity-file mutation in skill text goes through the
slot-*.shhelpers — verified by grep over the shipped skill text for directjq ... > .slots/writes (none allowed) as part of the code QA domain.
Implementation Scope
Areas
_shared/procedures/slot-isolation.md_shared/procedures/bin/slot-lock.sh,slot-identity.sh(read/atomic-write),slot-allocate.sh,slot-recipe-validate.shgit-worktrees/SKILL.md(dispatch + triggers),git-worktrees/procedures/slot-lifecycle.md(provision/claim/release/status)worktree-cleanup/SKILL.md_shared/procedures/readiness-check.md,setup/SKILL.md_shared/procedures/worktree-discipline.md(supersede presence-only ruling, defer to canonical),_shared/procedures/dev-server-lifecycle.md(slot port sourcing + slot-scoped kill),_shared/procedures/qa-playbook.md(ownership check reads identity surface)_shared/procedures/test/slot-flow.sh+test/fixtures/slot/(fake recipes: full, no-container, malformed ×2)File Boundaries
Canonical doc + helpers are the foundation and touch nothing else. The two skill surfaces
(git-worktrees acts; worktree-cleanup retire) are independent of each other once the canonical doc
exists. Consumer edits (worktree-discipline, dev-server-lifecycle, qa-playbook, readiness/setup) are
each small, independent, and reference-only. The harness is authored red-first alongside the helpers.
Dependencies & Sequencing
slot-isolation.md+ helpers (+ harness red) — everything else references them.Constraints & Non-Goals
Constraints:
or an illustrative example; skill-emitted glue stays POSIX+jq; helpers stay bash≥3.2+jq+git.
readiness gap only on multi-slot layouts).
confirmedor validated at UAT (waive path).Non-goals (do NOT build):
Test Plan: worktree-container-isolation (issue #21)
Prerequisites
State the scenarios need (not environment specifics — those are the UAT card's job):
their ports/database names from the slot identity surface (the PREQ's isolation ACs are
defined against a wired project).
database naming.
(for the unreachable-axis scenario).
axis.
provisioned (drift scenario).
Required Test Data
e2e) in the reference project.
into the slot's database(s), so carryover into the next feature is detectable.
one derived database (e.g. a test-suite database) beyond the one the act provisions.
Test Scenarios
Scenario 1: Single-act provisioning of every declared axis
Acceptance criterion: AC1 — single invocation provisions branch + worktree + every declared
axis and reports each axis.
Lane: integration-covered —
_shared/procedures/test/slot-flow.sh(authored red-first in this slice; precedent + executor (bash+jq) confirmed via the existing flow tests in_shared/procedures/test/; real-Docker axis semantics additionally walked at UAT — see scenarios 7–9)for the slot is up, the slot's port assignments exist, and the slot's database(s) exist.
everything beyond answering prompts was done by the act.
Expected outcome: one invocation, a complete slot, an axis-by-axis report.
Scenario 2: The slot identity surface
Acceptance criterion: AC2 — identity surface shows durable assignments, claim state, and
inventory, from a documented location.
Lane: integration-covered —
_shared/procedures/test/slot-flow.shresource inventory.
actually exist, the container that actually runs).
Expected outcome: one readable, documented record of everything the slot owns.
Scenario 2b: Identity survives a host restart
Acceptance criterion: AC2 — records assignments, not liveness; stays truthful across restarts.
Lane: integration-covered —
_shared/procedures/test/slot-flow.sh(restart simulated by stopping the fixture's fake runtime, then re-reading the identity surface)inventory) — nothing in it has become false.
that contradicts reality.
Expected outcome: the record never lies after a reboot.
Scenario 3: No container declaration — degrade by declaration
Acceptance criterion: AC3 — worktree + port + database isolation still provisioned; container
axis explicitly marked not declared.
Lane: integration-covered —
_shared/procedures/test/slot-flow.sh(no-container fixture variant)silently nor refuses to provision.
Expected outcome: a degraded but honest slot: three axes provisioned, the fourth loudly absent.
Scenario 4: Partial provisioning failure is named and resumable
Acceptance criterion: AC4 — failure leaves a named partial state; re-run resumes; no duplicates.
Lane: integration-covered —
_shared/procedures/test/slot-flow.sh(database-create failure injection + resume)provisioned and which are missing.
the missing axes are completed, and the final report shows a complete slot.
Expected outcome: an interrupted act is a resumable state, not a mess.
Scenario 5: Declared container runtime unreachable — loud stop
Acceptance criterion: AC5 — declared-but-unreachable axis stops the act loudly; never a
silent degrade.
Lane: integration-covered —
_shared/procedures/test/slot-flow.sh(runtime_check forced failure, provision and claim paths)Expected outcome: unreachable ≠ undeclared; the act refuses rather than quietly narrowing.
Scenario 6: Malformed recipe refused loudly
Acceptance criterion: AC6 — malformed/partial recipe reported loudly with the offending field
named, at provision time and by the readiness guard.
Lane: integration-covered —
_shared/procedures/test/slot-flow.sh(provision refusal viaslot-recipe-validate.sh; the readiness-guard half runs in scenario 14's dedicated run)guess.
Expected outcome: a broken declaration is loud in both places a maintainer would look.
Scenario 7: Concurrent slots do not interfere — and can prove it
Acceptance criterion: AC7 — concurrent full runs match solo runs; slot identity observable
end-to-end; guarantee scoped to declared axes.
Lane: human-uat — operator; UAT walk on a Docker-capable host with a reference project wired to the identity surface (real-Docker semantics also gated by precondition D-PO-21-2-1)
slot A's run you can assert the server and database being exercised belong to slot A (and
likewise for B), so the clean result is provably not a lucky collision-free overlap.
Expected outcome: concurrency changes nothing, and the runs can prove whose runtime they hit.
Scenario 8: Sibling e2e binds its own port — no silent takeover
Acceptance criterion: AC8 — sibling e2e with no manual override binds its own provisioned
port; no address-in-use failure; no silent reuse of the sibling's URL.
Lane: human-uat — operator; same UAT walk as scenario 7
not against slot A's already-answering URL.
Expected outcome: the historical failure mode (loser dies, suite silently tests the sibling)
is impossible without anyone remembering a port flag.
Scenario 9: Kill blast radius stays inside the slot
Acceptance criterion: AC9 — a representative kill battery inside one slot leaves sibling
servers serving and editor connections live.
Lane: human-uat — operator; same UAT walk (needs real containers plus live editor connections)
to each slot's runtime:
pattern, and by port.
Expected outcome: slot A can be as careless as it likes; the damage cannot leave slot A.
Scenario 10: Claiming a pooled slot — identities persist, data resets
Acceptance criterion: AC10 — claim = branch switch + incremental refresh; identities persist;
derived data content is reset.
Lane: integration-covered —
_shared/procedures/test/slot-flow.sh(claim reset: marker content written, then asserted gone; identities asserted unchanged)that feature.
port assignments are unchanged, the database names are unchanged.
schema/content, not the old rows.
Expected outcome: cheap claim, no data ghosts.
Scenario 11: Busy slot refuses claim/retire unless forced
Acceptance criterion: AC11 — refusal with a report of what's running; explicit force overrides.
Lane: integration-covered —
_shared/procedures/test/slot-flow.sh(busy refusal incl. the stale-claim-record variant; explicit --force)Expected outcome: stepping on a live slot requires saying so out loud.
Scenario 12: Recipe drift is reported at claim
Acceptance criterion: AC12 — drift reported, reconcile offered; never a silent claim on a
stale recipe.
Lane: integration-covered —
_shared/procedures/test/slot-flow.sh(recipe mutation → drift report; cosmetic-only edit → no drift)offers to reconcile.
changed.
Expected outcome: a slot and its recipe cannot quietly disagree.
Scenario 13: Teardown empties the recorded inventory — verifiably
Acceptance criterion: AC13 — teardown removes everything in the inventory including derived
databases; post-teardown listing verifies; partial failure is named and resumable.
Lane: integration-covered —
_shared/procedures/test/slot-flow.sh(teardown of inventory incl. a derived fake DB; failure-injection resume variant)provisioned one (per Required Test Data).
container(s) and volumes gone, every database in the inventory gone — including the derived
one(s) matching the documented derivation convention — and port reservations released.
the act stops with a named remaining-state report, and a re-run completes the teardown.
Expected outcome: retiring a slot leaves nothing — and can prove it left nothing.
Scenario 14: Readiness guard blocks undeclared multi-slot layouts
Acceptance criterion: AC14 — multi-worktree layout without a recipe is reported loudly and
re-reported every run.
Lane: config-variant — needs a fixture workspace with 2+ worktrees and no declared recipe; standalone dedicated QA run executing
readiness-check.mdagainst it twice (also covers scenario 6's guard half)the readiness check.
default.
Expected outcome: parallel development without a declared recipe stays visibly blocked until
the project declares one.
Notes
precondition). Running them against an unwired project tests nothing the PREQ promises.
assignment (who executes which scenario) is
/technical-plan's call.no orphan scenarios.
Wave 1 builds the foundation: the canonical contract doc (artifact-shape, no Test Writer), then the TDD pair for the executable layer — the Test Writer authors slot-flow.sh + fake-recipe fixtures red, the Implementer writes the bin/slot-*.sh helpers and act engines green. Lead decision recorded: the deterministic lifecycle logic lives in executable bin/ engines (not prose-only skill text) so the integration-covered lane in the test plan is real; skill text wraps the engines for the operator. Wave 2 is three artifact-shape doc WUs consuming Wave 1. Single shared worktree, so waves execute sequentially (index-lock rule).
Dev Journal — WU-21-3-1 (canonical contract: slot-isolation.md)
2026-08-11T00:00 DISCOVERY: the recipe hash needs a declared algorithm, not just "sha256"
Context: AC12 wants a drift check via
recipe_hashthat cosmetic edits must not trip.The SREQ pins the canonicalization (
jq -S -cover the extracted block) but not the digest tool.Pattern: The helper baseline is bash≥3.2 + jq + git + POSIX utilities — and no digest tool is
POSIX-guaranteed (
sha256sumis GNU,shasumis perl-shipped,cksumis POSIX but weak). So therecord stores
recipe_hashas{algo, value}and the drift check re-computes with the recordedalgo; when that algo is unavailable the check reports undeterminable (loud) rather than passing.
Why it matters: A silent hash-algorithm swap would make every slot look drifted (false alarm) or,
worse, make the comparison meaningless. Same class as the
mkdir-vs-flockruling in the SREQ.2026-08-11T00:00 DISCOVERY:
/dev/tcpis legal in helpers, illegal in emitted glueContext: The SREQ specifies a best-effort listener probe over the allocated port block using
bash
/dev/tcp.Pattern:
/dev/tcpis a bash feature, not POSIX — so it lives in the helper tier (which carries#!/usr/bin/env bash) and must never appear in shell a skill emits into the operator's login shell.The doc states the tier explicitly at the probe rather than leaving a future reader to re-derive it.
Why it matters: CLAUDE.md's two-tier table is easy to violate in exactly this direction — a probe
"that works here" copied into skill text breaks under zsh/dash.
2026-08-11T00:00 DISCOVERY: the YAML the helper must parse is a schema constraint
Context:
parallel_dev:becomes a parsed block, but there is noyqin the baseline — thereader is bash+jq.
Pattern: Made the parseability a stated schema rule (scalars and flat string lists only, fixed
nesting depth, no anchors/aliases/multi-doc), so the schema itself keeps a jq-only reader viable.
Why it matters: Without it, a project could write legal YAML the helper tier structurally cannot
read, and the failure would surface as a bogus "malformed recipe" pointing at the wrong field.
2026-08-11T00:00 COMPLETE
Hardest part: Keeping every Docker/Postgres/MySQL command visibly an example while still making
the verb contract concrete enough to implement against — the doc leans on one repeated "examples,
never defaults" frame plus per-engine tables rather than prose disclaimers on each line.
If I did this again: I would write the transition table before the act narratives; the narratives
kept wanting to invent field writes the table had to then constrain.
Dev journal — WU-21-3-2 (Implementer): executable slot lifecycle engines
[2026-08-11T18:40] DISCOVERY: the harness's fixture env vars never reach the engines
What:
slot-flow.sh'snew_sandboxends withexport SLOT_FIXTURE_BIN=… SLOT_FIXTURE_STATE=…; echo "$sb", and every call site islocal sb; sb="$(new_sandbox full)". An export inside$(…)dies with the subshell, so both variables are unset in the harness process — and therefore in every
engine and every
sh -cverb. The fixture recipes resolve to/db-create.sh,/runtime-check.shand exit 127.
Why it matters here: no engine can compensate. Only the harness process knows the sandbox paths,
and inventing them (deriving
$sbfrom the repo root) would be fixture-aware production code — theopposite of what the contract asks for. Reported to the Test Writer with the repro and the one-line
fix rather than patching their file.
Second defect found the same way: AC10's
"$(grep -c 'feature-A-marker' … 2>/dev/null || echo 0)"—
grep -cexits 1 when the count is zero, so the fallback fires on top of grep's own0and thesubstitution yields the two-line string
0\n0, which can never equal"0". Both branches of thesurrounding
iftherefore FAIL. Fix:| head -1, or|| true.How I kept moving: developed against a scratchpad copy with exactly those two lines patched
(
slot-flow-envfixed.sh) so the engines were still measured against the real assertions, thenre-ran the real file once the Test Writer patched it. Outcome: patched in
129f256; the realharness now reports 92 ok / 0 FAIL, exit 0, with the engines unchanged from the copy-run.
[2026-08-11T18:20] DISCOVERY: "busy" means claimed, not "something is running"
Problem: §5.2.1 reads as an unconditional live-evidence check ("the slot's container project
appearing in
container.list⇒ refuse"). Taken literally that refuses the normal path: provisionbrings the container up, so the very next claim would see live evidence and refuse itself.
Resolution: a slot is BUSY iff it holds a claim. The live-evidence probe then only chooses which
refusal the operator gets — live ("this is running work") vs stale ("this is a leftover record") —
which is the distinction §5.2.1 actually exists to draw. The harness agrees: AC-13 retires a
freeslot whose container is up and expects exit 0, while AC-11 refuses a
claimedone on the sameevidence.
[2026-08-11T18:55] DISCOVERY: four sibling flow tests fail at pristine HEAD on this box
smoke.sh,disposition-flow.sh,promotion-flow.shandrelease-flow.shall die ontouch: Unable to parse date: @1786473805899219.616. Reproduced in a detached worktree at pristineHEAD (zero
slot-*.shfiles present), so it is not mine. Cause: this box'sdateisuutils coreutils 0.8.0, which ignores the width in%3Nand prints all nine nanosecond digits, solocal-fs/_lib.sh's_lfs_touch_msbuilds a malformed@epoch.frac. CLAUDE.md already declares GNUdate/touchas a local-fs-only requirement — uutils satisfies the preflight probe (the outputis all digits) without honoring the format. Worth a preflight that checks the LENGTH, not just the
charset. The slot engines are unaffected: they use only
date -u +%sanddate -u +%Y-%m-%dT%H:%M:%SZ.Dev Journal — WU-21-3-3 (git-worktrees slot acts)
2026-08-11T00:00 DISCOVERY: no reconcile engine ships in this slice, so the drift procedure had to route around it
Context: slot-isolation.md §5.5 defines reconcile as a first-class act, and
slot-claim.shtells the operator to "reconcile $SLOT" when drift is detected. But
bin/has noslot-reconcile.sh(grep for
reconcilehits only claim/provision refusal text), and the SREQ's Implementation Scopedoes not list one — nor does any engine ever set
drift.declined_at, whichslot-status.shreads.Pattern: the procedure presents reconcile-or-decline as §5.5 requires, but names the executable
route for reconcile today — retire (worktree-cleanup) + provision again, since provision refuses to
rewrite an existing slot's identities (
EXIT_STATE, "that is reconcile"). Decline is described aswhat it actually is: leave it,
statuskeeps thedetected <date>flag, every later claim stops thesame way, never clear the flag by editing the record.
Why it matters: the alternative was documenting a command that does not exist. A procedure that
tells an operator to run a missing engine fails at the exact moment they are already blocked.
2026-08-11T00:00 COMPLETE
Hardest part: keeping the file a workflow — the pull toward restating the axis table, the exit
codes' semantics and the report shape is strong, and all three are already normative elsewhere. The
exit table here says only "what you do next", not what the code means.
If I did this again: read the engines' failure paths (
fail_axis,stop_axis,_slot_diemessages) before the contract doc — the operator-facing wording the skill must not contradict is in
the engines, and it is more specific than §5.
WU-21-3-4 — worktree-cleanup slot retire
Non-obvious
The brief named
--keep-branch; the engine has no such flag.slot-retire.shtakes--delete-branch(opt-in), and retaining the branch is already the default (step 3e: "never deletedimplicitly"). So the skill text passes neither flag and says explicitly not to pass
--delete-branch, which is what leaves the branch to this skill's steps 2–4 confirm flow (AC-13).Documenting a
--keep-branchthat does not exist would have made the emitted command fail withunknown argument.Slot detection cannot start with
slot-status.sh. Every engine calls_slot_load, which runsslot-recipe-validate.shand exits 10 when the project declares noparallel_dev:block. So thedispatch (Step 1.5) checks for the block first and only probes with
slot-status.sh --slot … --jsonwhen one exists; otherwise a plain ad-hoc cleanup in a repo with no recipe would hit a spurious
refusal.
[]from the probe is the "no record → ad-hoc" signal.Which of the existing steps a slot retire skips. The engine already owns teardown and worktree
removal (step 3d), so a retire skips Steps 2.5/3.5 (teardown + database drop) and the worktree-removal
part of Step 4 — otherwise the skill would re-run a drop the engine performed from the record, which is
exactly the pattern-vs-record failure the feature exists to end.
WU-21-3-5 journal — consumer updates
Non-obvious decisions only.
/setuphad no parallel-dev handling to align — the onlyparallel_devhit was the readinessreport template line (Phase 1 report block). Phase 1's required-items table never carried the row,
so the report could print a verdict the phases never computed. Fixed by adding the table row and
a Phase 1.8 that mirrors 1.6/1.7's shape (detect → validate → AskUserQuestion, defer = critical gap).
Bigger than "align one line", but the alternative was a report field with no producer.
dev-server-lifecycle.mdStep 2 detected worktrees by the.claude/worktrees/path — a layoutconvention the slot recipe does not use (
slots_diris the project's to choose). Replaced with theidentity-record probe (
.slot.envpresent), which also gives the three-case split Step 3 needed:slot / non-slot worktree / main. The hash-offset improvisation survives only in the middle case.
Kill discipline was already 90% right (PID-first, never-by-port, pkill self-kill trap). The only
gap vs slot-isolation.md §7.3 was that its
pkill -f uvicornexample is the name-global patternthe slot rule forbids. Left the example in place for the non-slot case and added the slot-scoped
narrowing as a nested paragraph rather than rewriting the step.
Did not touch worktree-discipline.md's §ownership section even though qa-playbook cites it and
the citation is now partially superseded by slot-isolation §7.2 — the WU scoped that section as
untouched, and qa-playbook now cites both, so no reader is stranded.
Linked: this issue is sibling #23 (recorded by the devwork pipeline).
Linked: this issue is sibling #24 (recorded by the devwork pipeline).
Closing: the full devwork loop is complete and terminal. Merged to main (fast-forward 993855a..ae802e0, code-bundle comment 691), UAT walked and passed on a Docker host against verity (scenarios 7-9; waived precondition D-PO-21-2-1 validated against a real daemon), D-PO-21-8-1 resolved approve (comment 695), terminal PO-21-9 at accepted (comment 696).
This project declares no release: block and no deploy (uat.url_source: no deploy - the merged skill text on main IS the shipped product), so accepted is effectively terminal here and there is no /promote step to wait for. Closed deliberately by the operator.
Follow-up work lives in the sibling issues: #23 (local-fs on uutils-coreutils), #24 (slot-reconcile), #25 (/integrate in-flight guard). Verity-side slot wiring (feature/slot-container-wiring on verity) is tracked project-side there.