Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions hermes_cli/kanban_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -2849,6 +2849,29 @@ def decompose_triage_task(
if p == idx:
raise ValueError(f"child[{idx}] cannot list itself as a parent")

# Detect cycles in the sibling parent graph (Kahn's topological sort).
# link_tasks() calls _would_cycle() for every new edge; here we check
# the entire sibling graph before touching the DB. A cycle silently
# deadlocks every involved child in 'todo' because recompute_ready()
# can never promote them.
_in_deg = [0] * len(children)
_adj: list[list[int]] = [[] for _ in range(len(children))]
for _i, _c in enumerate(children):
for _p in (_c.get("parents") or []):
_adj[_p].append(_i)
_in_deg[_i] += 1
_queue = [_i for _i in range(len(children)) if _in_deg[_i] == 0]
_seen = 0
while _queue:
_node = _queue.pop()
_seen += 1
for _nb in _adj[_node]:
_in_deg[_nb] -= 1
if _in_deg[_nb] == 0:
_queue.append(_nb)
if _seen != len(children):
raise ValueError("cyclic dependency detected in decomposed children list")

# We do the full decomposition in a SINGLE write_txn so it's
# atomic: either every child is created AND the root flips to
# ``todo``, or nothing changes. We deliberately do NOT call any
Expand Down
16 changes: 16 additions & 0 deletions tests/hermes_cli/test_kanban_decompose_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,22 @@ def test_decompose_rejects_out_of_range_parent(kanban_home):
)


def test_decompose_rejects_cyclic_parents(kanban_home):
with kb.connect() as conn:
tid = _create_triage(conn)
with pytest.raises(ValueError, match="cyclic dependency"):
kb.decompose_triage_task(
conn,
tid,
root_assignee="orch",
children=[
{"title": "A", "parents": [1]},
{"title": "B", "parents": [0]},
],
author="me",
)


def test_decompose_records_audit_comment_and_event(kanban_home):
with kb.connect() as conn:
tid = _create_triage(conn)
Expand Down
Loading