Skip to content

planner: #N inside a "Blocked by: None. (Related: #N)" note is mis-read as a hard edge (false-positive cycle) #47

Description

@TBarregren

Context — field experience (this repo, orchestrate 0.13.0 + the #46 fix, 2026-07-20)

An /orchestrate --yes run over the 13 open ready-for-agent issues failed at plan time with:

error: dependency cycle among issues [34, 36]

Both #34 and #36 declare Blocked by: None — there is no real cycle. The #N tokens the planner turned into hard edges came from the parenthetical prose note that follows None. on the same line:

So the planner read #34 blocked_by {35, 36} and #36 blocked_by {34, 35}#34#36 mutual block → spurious cycle. Working around it (neutralizing just those two parentheticals in the planner input) unblocked the run, but the planner should not need hand-editing.

Root cause

scripts/orchestrate.py parse_dependencies, the heading-form ## Blocked by branch:

section_body = section["body"]
for number in ISSUE_REF_RE.findall(section_body):
    edges.setdefault(int(number), "Blocked by")

Every #N anywhere in the section body becomes a hard edge. There is no handling for:

  1. a None sentinel (- None.) that explicitly declares no blockers, and
  2. a (Related: ...) (or similar prose aside) clause, whose #N references are non-directional context, not blockers — exactly the SOFT_NOTE_RE "coupling, never an edge" category the parser already recognizes elsewhere in the body, but not inside the ## Blocked by section.

Fittingly, this is the mirror image of #34's own theme: #34 is about missing implicit edges; this is a false-positive edge manufactured from prose.

Reproduction

An issue whose ## Blocked by section is:

## Blocked by

- None. (Related: #99)

produces blocked_by = {99} instead of the correct empty set. Two such issues that name each other yield a hard-error cycle.

Proposed fix (options)

  • A (targeted). In the ## Blocked by section, if the section resolves to a None/n/a/none. sentinel bullet, treat it as no blockers and do not scan it for #N edges.
  • B (broader, preferred). Apply the existing soft-note / non-directional rule inside the ## Blocked by section too: a #N inside a parenthetical (Related: ...) / (See ...) aside (or any recognized non-directional phrase) becomes a soft note, never a hard edge — consistent with how the same references are treated in the rest of the body.
  • Either way, a genuine - Blocked by #N / - #N bullet under the heading must still produce an edge (no regression to the existing behavior or its tests).

Acceptance criteria

  • A ## Blocked by section containing only a None(-like) sentinel resolves to zero edges, even when the same line carries a (Related: #N) aside.
  • A #N appearing solely inside a non-directional aside ((Related: ...), (See ...), etc.) under the ## Blocked by heading is a soft note, not a hard edge.
  • A genuine bullet (- Blocked by #N, - #N, - depends on #N) under the heading still yields a correctly-attributed edge — existing edge/attribution tests stay green.
  • The real-world case that triggered this (issues declaring Blocked by: None. (Related: #A / #B)) plans to an empty edge set with no cycle.
  • tests/test_orchestrate.py covers the sentinel case, the (Related: #N) aside case, and the no-regression bullet case.
  • CHANGELOG.md updated; the repo's gates (scripts/audit.py, pytest) green.

Blocked by

Environment

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingready-for-agentFully specified, ready for an AFK agent

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions