gastownhall/beads · error

reparent: %s cannot be its own parent

Error message

reparent: %s cannot be its own parent

What it means

Guard error from reparent: the caller asked to make an issue its own parent (childID == newParentID). A self parent-child edge is invalid in the hierarchy model, so the use case rejects it before any lookup or write. This is analogous to the self-dependency guard in add().

Source

Thrown at internal/storage/domain/dependency.go:385

		return fmt.Errorf("remove dep %s -> %s: %w", sourceID, dependsOnID, err)
	}
	return nil
}

func (u *dependencyUseCaseImpl) Reparent(ctx context.Context, childID, newParentID, actor string) error {
	return u.reparent(ctx, childID, newParentID, actor, false)
}

func (u *dependencyUseCaseImpl) ReparentWisp(ctx context.Context, childWispID, newParentID, actor string) error {
	return u.reparent(ctx, childWispID, newParentID, actor, true)
}

func (u *dependencyUseCaseImpl) reparent(ctx context.Context, childID, newParentID, actor string, useWisp bool) error {
	if childID == "" {
		return fmt.Errorf("reparent: childID must not be empty")
	}
	if childID == newParentID {
		return fmt.Errorf("reparent: %s cannot be its own parent", childID)
	}

	opts := DepInsertOpts{UseWispsTable: useWisp}
	res, err := u.depRepo.ListByIssueIDs(ctx, []string{childID}, DepListOpts{
		Types:         []types.DependencyType{types.DepParentChild},
		Direction:     DepDirectionOut,
		UseWispsTable: useWisp,
	})
	if err != nil {
		return fmt.Errorf("reparent: list current parent: %w", err)
	}

	// A child can carry MORE THAN ONE parent-child edge — Create accepts
	// CreateRequest.ParentID and an explicit parent-child entry in
	// Dependencies in the same request — so this is a set replacement, not a
	// swap of one edge. Diffing the whole existing set against the target set
	// is the same rule the store-backed backends apply in
	// issueops.ApplyParentPatch; that body cannot be called from here because

View on GitHub (pinned to 71377f2769)

Solutions

  1. Compare childID and newParentID at the call site and skip no-op self-reparents before calling.
  2. Fix the parent-selection logic so a child is never offered/chosen as its own parent.
  3. If the intent was 'no change', skip the call entirely rather than passing equal IDs.
  4. Validate parent choice in scripts: reject any mapping where parent == child.

Example fix

// before
err := uc.Reparent(ctx, childID, parentID, actor)
// after
if childID == parentID {
    return nil // no-op reparent
}
err := uc.Reparent(ctx, childID, parentID, actor)
Defensive patterns

Strategy: validation

Validate before calling

if childID == newParentID {
    return nil // or return a descriptive no-op/validation error
}
err := uc.Reparent(ctx, childID, newParentID, actor)

Type guard

func isSelfParent(childID, newParentID string) bool {
    return childID != "" && childID == newParentID
}

Try / catch

if isSelfParent(childID, newParentID) {
    log.Printf("skipping self-parent for %s", childID)
    return nil
}
if err := uc.Reparent(ctx, childID, newParentID, actor); err != nil {
    return fmt.Errorf("reparent rejected: %w", err)
}

Prevention

When it happens

Trigger: Reparent(ctx, id, id, actor) or ReparentWisp(ctx, id, id, actor) — typically a UI/form default where the parent dropdown was pre-filled with the child itself, or a script that computes parentID from the same variable as childID.

Common situations: Bulk re-parent scripts mapping issues to 'themselves' as a default fallback; migration code that copies a flat list into parent-child form without excluding self-mappings; user selecting the current issue as its new parent.

Related errors


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