Skip to content

ADR 0059: [worktree.setup].ensure — a convergent setup bucket that re-runs every pass

Amendments.

  • Vocabulary: the current spelling is update (formerly integrate); the decision and reasoning are unchanged.
  • ADR 0153 — checkout scope: checkout-generic commands now belong in [repository].ensure, which shares this every-pass contract and also runs after landing; [worktree.setup].ensure is the linked-worktree-only bucket and may depend on worktree identity or resources.

Status: accepted. Realizes the deferred "Part B" of ADR 0055, and mirrors the resource create/ensure split of ADR 0025.

Context

§

[worktree.setup].steps run once, at worktree creation: they are sentinel-guarded and skipped on re-entry. Yet the schema told authors to write them "idempotent so a recovered partial setup re-runs safely." That contract was near-vestigial — the steps run once — and easy to miss. Idempotency was hung on the wrong hook.

ADR 0055 gave the worktree lifecycle its middle verb, discern update: it brings main into a branch and re-materializes the discern-owned artifacts (the generated agent files + the materialized skills). It does not keep the user-owned environment current. A merge that changes a lockfile leaves the worktree's dependencies stale, and the agent finds out only when the gate's tests fail on a missing module — exactly the late, slow failure update exists to prevent for agent files. ADR 0055 deferred "re-run the worktree's setup after a merge" as Part B.

The naive Part B — blindly re-run all setup steps on every update — was rejected. It forces global idempotency on every step to solve a local problem, and a one-shot step (create a database, append to a file) would break on every update, its "already exists" non-zero exit turning a good worktree red.

Decision

§

Give [worktree.setup] a second list, ensure, beside steps. steps stays one-shot (run once at creation, not re-run); ensure is convergent — it re-runs on every setup pass to keep the worktree current with the tree. The buckets are the same split discern already has for a resource:

bucket runs author it
steps once, at creation one-shot
ensure every pass (creation, re-entry, update) idempotent

This mirrors a resource's create (once) and ensure (reconcile at session start) from ADR 0025. The re-run contract becomes legible from the config shape, not a prose footnote: a command in ensure obviously re-runs; a command in steps obviously does not. A user who already understands resource ensure understands this immediately.

ensure runs at three points, through one shared runEnsureSteps helper (the single implementation):

  • creation — after the one-shot steps, as the final environment step before the agent-file refresh;
  • session-start re-entry — in worktree ensure's already-configured branch, alongside resource ensure, so a re-entered worktree re-converges (gated by the existing [worktree].enabled session-start mechanism);
  • discern update — after the merge + the agent-file refresh. This is the motivating case: a merge that changed a lockfile is followed by a reinstall, with no manual step.

Failure semantics track where the failure happens:

  • at a fresh creation, an ensure failure is fatal — it aborts setup, exactly like a steps failure. A worktree that cannot ready its environment is broken; fail loudly.
  • on re-entry or update, an ensure failure is non-fatal but recorded — a failed step plus a warning. It never undoes a completed merge or breaks session start over a convergence hiccup; the gate is the backstop. This mirrors how update already treats its post-merge refresh (recorded as failed, merge kept).

The reframe is the point: idempotency is expressed as config shape, not a prose contract and not a brittle shell-inspecting linter. So steps is reworded to drop its idempotency over-claim, and no per-step machinery is added.

ensure runs at session start too, not only at creation and update, for exact parity with resource ensure. The cost lever is the author's: a fast-when-current command (<install> check || <install>) over an always-clean one (<install> --clean). The deliberate trade is one cheap reconcile per session for a worktree that is never silently stale. (Running at session start also surfaced that a chatty ensure command leaked its output into the agent-context channel; ADR 0060 makes every worktree command quiet on success in response.)

Adding the key is backward-compatible: it is optional and defaults to [], so a config with only steps behaves exactly as before. No SCHEMA_VERSION bump or migration is needed (an additive optional key with a default). A new setup-ensure step kind lets the setup and update plans — and their --dry-run / --json — distinguish the two buckets.

Consequences

§
  • The staleness gap closes for the user-owned environment. A merge that changes a lockfile, followed by discern update, leaves dependencies current with no manual reinstall — the same gap update already closed for the discern-owned agent files, now closed for the environment.
  • The idempotency requirement lives where it is true. It moved off steps (which run once) onto ensure (which re-runs), so the schema no longer misdirects authors.
  • One more step kind to carry. setup-ensure joins the closed STEP_KINDS; the wire schema and its round-trip test follow from that one edit.
  • A per-pass cost at session start. The author controls it by writing a fast-when-current command; an always-clean install is a choice, not a default.
  • No silent middle ground. A failed ensure is either fatal (creation) or a recorded failed step (re-entry / update) — never swallowed.

Alternatives considered

§
  • Blindly re-run all setup.steps on every update (the naive Part B). Rejected — it forces global idempotency on every step, and a one-shot step breaks on every update.
  • Per-step config flags, watch/fingerprint globs, or idempotency wrappers. Rejected — the two-bucket split is the design. Per-step machinery re-hangs the same contract on a knob the shape already expresses.
  • A linter that inspects setup commands for idempotency. Rejected — idempotency of an arbitrary shell command is undecidable; a denylist is brittle and theatre. The config shape makes the contract legible without guessing at command semantics.
  • Run ensure only at creation + update, not session start. Rejected — it breaks parity with resource ensure, and session start is the natural reconcile point for an environment that drifted out-of-band (a host reboot, a pruned dependency cache).
choose openEsc close