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:
- a
None sentinel (- None.) that explicitly declares no blockers, and
- 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
Blocked by
Environment
Context — field experience (this repo, orchestrate 0.13.0 + the #46 fix, 2026-07-20)
An
/orchestrate --yesrun over the 13 openready-for-agentissues failed at plan time with:Both #34 and #36 declare
Blocked by: None— there is no real cycle. The#Ntokens the planner turned into hard edges came from the parenthetical prose note that followsNone.on the same line:## Blocked by:- None. (Related: #35 / #36 — the incomplete-shared-seam-refactor class ...)## Blocked by:- None. (Related: #34, same-wave file-overlap scheduling; #35, the coding-standard companion.)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.pyparse_dependencies, the heading-form## Blocked bybranch:Every
#Nanywhere in the section body becomes a hard edge. There is no handling for:Nonesentinel (- None.) that explicitly declares no blockers, and(Related: ...)(or similar prose aside) clause, whose#Nreferences are non-directional context, not blockers — exactly theSOFT_NOTE_RE"coupling, never an edge" category the parser already recognizes elsewhere in the body, but not inside the## Blocked bysection.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 bysection is: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)
## Blocked bysection, if the section resolves to aNone/n/a/none.sentinel bullet, treat it as no blockers and do not scan it for#Nedges.## Blocked bysection too: a#Ninside 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.- Blocked by #N/- #Nbullet under the heading must still produce an edge (no regression to the existing behavior or its tests).Acceptance criteria
## Blocked bysection containing only aNone(-like) sentinel resolves to zero edges, even when the same line carries a(Related: #N)aside.#Nappearing solely inside a non-directional aside ((Related: ...),(See ...), etc.) under the## Blocked byheading is a soft note, not a hard edge.- Blocked by #N,- #N,- depends on #N) under the heading still yields a correctly-attributed edge — existing edge/attribution tests stay green.Blocked by: None. (Related: #A / #B)) plans to an empty edge set with no cycle.tests/test_orchestrate.pycovers the sentinel case, the(Related: #N)aside case, and the no-regression bullet case.CHANGELOG.mdupdated; the repo's gates (scripts/audit.py,pytest) green.Blocked by
Environment
orchestrate0.13.0 + the orchestrate: implementer branches fork from a stale worktree base, so merge-mode ff-only integration parks verified work en masse #46 stale-base fix; discovered by an/orchestrate --yesplanning pass on 2026-07-20.