gastownhall/beads · error

worktree force-mode comparison observations are inconsistent

Error message

worktree force-mode comparison observations are inconsistent

What it means

Prepare validates that a force-mode worktree removal request carries consistent comparison facts. In force mode the policy skips HEAD-containment comparison entirely, so the facts must explicitly record ComparatorNotRequired AND ContainmentNotRequired. If the caller passes force mode with any comparison observation set (or leaves one of the two 'not required' markers unset), the observations contradict the force contract and Prepare refuses to build an approved Plan.

Source

Thrown at internal/worktreeremove/policy.go:217

	}
	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

const (
	// InvariantUnknown means the adapter could not complete the observation.
	InvariantUnknown InvariantState = iota
	// InvariantStable means the current observation matches preparation.
	InvariantStable
	// InvariantChanged means the current observation differs from preparation.

View on GitHub (pinned to 71377f2769)

Solutions

  1. Set facts.Comparator = ComparatorNotRequired and facts.Containment = ContainmentNotRequired when request.Mode is force.
  2. Build the Facts struct per-request rather than reusing one across mode changes.
  3. If containment genuinely matters, drop force mode and let Prepare run the normal containment check.

Example fix

// before
facts := Facts{RegisteredPath: p, ManagedIgnore: IgnoreManaged, Comparator: ComparatorHeadInTarget, Containment: Contained}
plan, err := Prepare(Request{Mode: ModeForce, WorktreePath: p}, facts)

// after
facts := Facts{RegisteredPath: p, ManagedIgnore: IgnoreManaged, Comparator: ComparatorNotRequired, Containment: ContainmentNotRequired}
plan, err := Prepare(Request{Mode: ModeForce, WorktreePath: p}, facts)
Defensive patterns

Strategy: validation

Validate before calling

if request.Mode == ModeForce {
	if facts.Comparator != ComparatorNotRequired || facts.Containment != ContainmentNotRequired {
		// fix facts before calling Prepare
	}
}

Prevention

When it happens

Trigger: Calling Prepare with request.Mode == force while facts.Comparator != ComparatorNotRequired or facts.Containment != ContainmentNotRequired — e.g. a caller that populated containment facts from a prior non-force inspection and then flipped the mode to force without clearing them.

Common situations: Reusing a populated Facts struct across Prepare calls with different modes; a caller that partially upgraded to force mode but still ran the containment comparator; test scaffolding that fills in realistic containment data for a force request.

Related errors


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