gastownhall/beads · error

worktree containment observation is absent or invalid

Error message

worktree containment observation is absent or invalid

What it means

Prepare rejects the request because mode is Normal and facts.Containment is not one of Contained, NotContained, or ContainmentNotRequired. The containment observation (is the worktree HEAD an ancestor of the comparison target?) is missing or an invalid value, so unpushed-commit verification cannot run and the library fails closed.

Source

Thrown at internal/worktreeremove/policy.go:211

	}
	if facts.ManagedIgnore != IgnoreAbsent && facts.ManagedIgnore != IgnoreManaged {
		return Plan{}, fmt.Errorf("managed ignore observation is absent or invalid")
	}
	if (facts.ManagedIgnore == IgnoreManaged) != (facts.ManagedIgnoreEntry != "") {
		return Plan{}, fmt.Errorf("managed ignore cleanup identity is absent or invalid")
	}
	if request.Mode != Force {
		if facts.Status != Clean {
			return Plan{}, fmt.Errorf("worktree contains modified, untracked, or ignored files")
		}
		if facts.Comparator != ComparatorAvailable && facts.Comparator != ComparatorMissing && facts.Comparator != ComparatorNotRequired {
			return Plan{}, fmt.Errorf("worktree comparison target observation is absent or invalid")
		}
		if facts.Comparator != ComparatorAvailable {
			return Plan{}, fmt.Errorf("cannot verify unpushed commits: no comparison target is available")
		}
		if facts.Containment != Contained && facts.Containment != NotContained && facts.Containment != ContainmentNotRequired {
			return Plan{}, fmt.Errorf("worktree containment observation is absent or invalid")
		}
		if facts.Containment != Contained {
			return Plan{}, fmt.Errorf("worktree HEAD is not contained in the comparison target")
		}
	} else if facts.Comparator != ComparatorNotRequired || facts.Containment != ContainmentNotRequired {
		return Plan{}, fmt.Errorf("worktree force-mode comparison observations are inconsistent")
	}
	plan := Plan{approved: true, mode: request.Mode, targetPath: facts.RegisteredPath}
	if facts.ManagedIgnore == IgnoreManaged {
		plan.managedIgnoreEntry = facts.ManagedIgnoreEntry
	}
	return plan, nil
}

// InvariantState is the adapter's raw observation of one independently
// revalidated invariant.
type InvariantState uint8

View on GitHub (pinned to 71377f2769)

Solutions

  1. Set facts.Containment from an actual containment check (e.g. git merge-base --is-ancestor HEAD <comparator>)
  2. Ensure the adapter assigns Contained/NotContained rather than leaving the field zero
  3. In tests, pair ComparatorAvailable with Containment: worktreeremove.Contained

Example fix

// before
facts.Containment = 0 // invalid
// after
facts.Containment = worktreeremove.Contained // after verifying HEAD is an ancestor of comparator
Defensive patterns

Strategy: validation

Validate before calling

switch facts.Containment {
case worktreeremove.Contained, worktreeremove.NotContained, worktreeremove.ContainmentNotRequired:
default:
	return fmt.Errorf("containment not observed")
}

Type guard

func containmentObserved(c worktreeremove.Containment) bool {
	return c == worktreeremove.Contained || c == worktreeremove.NotContained || c == worktreeremove.ContainmentNotRequired
}

Try / catch

plan, err := worktreeremove.Prepare(req, facts)
if err != nil && strings.Contains(err.Error(), "containment observation is absent") {
	return fmt.Errorf("run the containment check (merge-base --is-ancestor) first: %w", err)
}

Prevention

When it happens

Trigger: Calling Prepare with Mode: Normal and a zero-value or out-of-range Containment, e.g. an adapter that resolved the comparator but never ran the containment check.

Common situations: Custom adapters skipping the merge-base/ancestor check; tests constructing partial facts; enum drift after adding new Containment values.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/4ffa3075c8aeef90. Report an issue: GitHub.