gastownhall/beads · error
edge %d %s->%s creates a blocking reverse of a parent-child
Error message
edge %d %s->%s creates a blocking reverse of a parent-child relationship
What it means
The plan has an edge in the reverse direction of an existing parent-child relationship, and the edge's dependency type is cycle-relevant (blocking). A blocking reverse of a parent-child edge would create a cycle in the blocking graph, so apply rejects it.
Source
Thrown at cmd/bd/graph_apply.go:1005
}
parentDepPairs := graphApplyParentDepPairs(plan.Nodes, keyToID)
newSchedulingEdges := make([][2]string, 0, len(plan.Nodes)+len(plan.Edges))
if err := validateGraphApplyPlannedParentBlockingPaths(ctx, tx, plan, keyToID, parentDepPairs); err != nil {
return err
}
if err := validateGraphApplyPlannedBlockingCycles(ctx, tx, plan, keyToID); err != nil {
return err
}
for i, edge := range plan.Edges {
fromID := resolveEdgeRef(edge.FromKey, edge.FromID, keyToID)
toID := resolveEdgeRef(edge.ToKey, edge.ToID, keyToID)
depType := graphApplyDependencyType(edge.Type)
if parentDepPairs[graphApplyDepPairKey(fromID, toID)] && depType != types.DepParentChild {
return fmt.Errorf("edge %d %s->%s duplicates a parent-child relationship with dependency type %q", i, fromID, toID, depType)
}
if parentDepPairs[graphApplyDepPairKey(toID, fromID)] && graphApplyCycleRelevantDependencyType(depType) {
return fmt.Errorf("edge %d %s->%s creates a blocking reverse of a parent-child relationship", i, fromID, toID)
}
}
// Add node parent-child dependencies first. The explicit and inline
// dependency sources below are also processed parent-first, so every
// blocking edge sees the plan's full hierarchy in storage.
for i, node := range plan.Nodes {
parentKey := node.effectiveParentKey()
parentID := node.ParentID
if parentKey != "" {
parentID = keyToID[parentKey]
}
if parentID != "" {
dep := &types.Dependency{
IssueID: issues[i].ID,
DependsOnID: parentID,
Type: types.DepParentChild,
}View on GitHub (pinned to 71377f2769)
Solutions
- Remove the reverse blocking edge; parent-child already encodes the relationship.
- Swap edge endpoints if the blocking direction was mistakenly inverted — but only if it does not duplicate the parent-child pair.
- Add cycle detection in the plan generator before emitting blocking edges.
- Run `bd graph validate` (if available) on the plan before apply.
Example fix
// before
node: {"key": "bd-1", "parents": ["bd-2"]}
edge: {"from": "bd-2", "to": "bd-1", "type": "blocks"}
// after
node: {"key": "bd-1", "parents": ["bd-2"]} Defensive patterns
Strategy: validation
Validate before calling
pairs := map[[2]string]bool{}
for _, n := range plan.Nodes {
for _, p := range n.Parents { pairs[[2]string{n.Key, p}] = true }
}
cycleRelevant := map[string]bool{"blocks": true}
for _, e := range plan.Edges {
if pairs[[2]string{e.To, e.From}] && cycleRelevant[e.Type] {
return fmt.Errorf("edge %s->%s is a blocking reverse of parent-child", e.From, e.To)
}
} Type guard
func isBlockingReverse(e Edge, pairs map[[2]string]bool) bool {
return pairs[[2]string{e.To, e.From}] && cycleRelevant[e.Type]
} Try / catch
if err := bd.GraphApply(ctx, plan); err != nil {
if strings.Contains(err.Error(), "blocking reverse of a parent-child") {
plan = dropReverseBlockingEdges(plan); return bd.GraphApply(ctx, plan)
}
} Prevention
- Run cycle detection on blocking edges before apply.
- Never emit blocking edges opposite to a parent-child pair.
- Validate merged plans from multiple sources for direction conflicts.
When it happens
Trigger: plan.Edges includes toID->fromID where fromID is already the parent of toID (pair present reversed in parentDepPairs) and graphApplyCycleRelevantDependencyType(depType) is true (e.g. blocks).
Common situations: Generators emitting both hierarchy and blocking edges without cycle checks; merging graphs where one direction is parent-child and the reverse is a blocking link; manually inverted edge endpoints.
Related errors
- edge %d %s->%s duplicates a parent-child relationship with d
- line %d: 'dep' requires a subcommand (add|remove)
- line %d: unknown dep subcommand %q (want add|remove)
- --deps cannot attach both %q and %q to the same target %q: a
- --deps target is empty
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/36758e308aa466a8.
Report an issue: GitHub.