mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(tasks): sequence is the bar — strict sibling ordering at the claim chokepoint (#452)
* feat(tasks): enforce sibling sequence order at the claim chokepoint A task with a parent and effective sequence N (COALESCE(sequence, 0)) can no longer be claimed while any sibling with a strictly lower effective sequence is non-terminal — assignee-blind, independent of and stricter than dependency_ids, enforced in _validate_claim_preconditions so both claim paths (gateway verbs and the dispatcher's raw REST claim) cross it. Ties run parallel; cancelled siblings never block; sequence 0 and parentless tasks are unaffected. Live failure this guards: a PM delegated revision subtasks sequenced 0..3 with no dependency edges and seq 2 started alongside seq 0 — sequence was advisory-only. set_sequence's contract updated accordingly. New e2e smoke case drives the refusal and the post-completion claim through the real gateway. * chore(scripts): skip .uv-cache and .claude in the prose scanner Repo-local tool dirs (private uv cache, agent worktrees) carry vendored and generated markdown that tripped make reflow-check. * fix(tasks): wave-derived delegation sequences + claim-gate hardening Three fixes from the adversarial review of the sequence claim gate: Delegation no longer stamps a raw per-sibling ordinal (deterministic merge-order bookkeeping) as sequence — under the strict gate that serialized ALL delegated work, including fully independent cross-dev and cross-cell siblings. Sequences are now wave-derived post-wiring (stamp_wave_sequence: 1 + max same-parent dependency sequence, 0 when independent), so independent siblings tie and run parallel while colliding/ordered work ascends. The cross-cell UX wiring restamps instead of writing relative ux+1 values (a relative write could invert a collision-derived stamp), and the dispatch merge/lane barriers gain a created_at tiebreak for wave-tied siblings so shared-branch merge order stays deterministic. PM-authored sequences are never rewritten. The guard now also fires on reclaims from needs_revision (a lower- sequence sibling delegated after the first claim was invisible), and tasks.parent_task_id gains an index (migration 069) — the guard's sibling probe ran as a Seq Scan on the hottest verb. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -1352,9 +1352,12 @@ async def test_unclaimed_parent_acs_counts_live_children_not_just_completed() ->
|
||||
|
||||
|
||||
def _svc_with_sibling_status_seq(rows: list[tuple]) -> TaskService:
|
||||
"""TaskService whose execute() yields (status, sequence) sibling rows."""
|
||||
"""TaskService whose execute() yields (status, sequence[, created_at])
|
||||
sibling rows; 2-tuples are padded with created_at=None (only compared on
|
||||
a sequence tie)."""
|
||||
row_width = 3 # (status, sequence, created_at)
|
||||
res = MagicMock()
|
||||
res.all.return_value = rows
|
||||
res.all.return_value = [r if len(r) == row_width else (*r, None) for r in rows]
|
||||
return TaskService(MagicMock(execute=AsyncMock(return_value=res)))
|
||||
|
||||
|
||||
@@ -1386,6 +1389,25 @@ async def test_earlier_incomplete_code_sibling_false_when_earlier_terminal() ->
|
||||
assert await svc.has_earlier_incomplete_code_sibling(task) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_earlier_incomplete_code_sibling_tie_breaks_by_created_at() -> None:
|
||||
# Wave ties (equal sequence) order by created_at: the earlier-created
|
||||
# tied sibling holds the later one; a later-created one does not.
|
||||
task = _build_task(
|
||||
task_type=TaskType.CODE.value,
|
||||
parent_task_id=uuid4(),
|
||||
assigned_to=uuid4(),
|
||||
sequence=1,
|
||||
created_at=datetime(2026, 7, 10, 12, 0, tzinfo=UTC),
|
||||
)
|
||||
earlier = datetime(2026, 7, 10, 11, 0, tzinfo=UTC)
|
||||
later = datetime(2026, 7, 10, 13, 0, tzinfo=UTC)
|
||||
svc = _svc_with_sibling_status_seq([(TaskStatus.IN_PROGRESS, 1, earlier)])
|
||||
assert await svc.has_earlier_incomplete_code_sibling(task) is True
|
||||
svc = _svc_with_sibling_status_seq([(TaskStatus.IN_PROGRESS, 1, later)])
|
||||
assert await svc.has_earlier_incomplete_code_sibling(task) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_earlier_incomplete_code_sibling_false_for_higher_seq_only() -> None:
|
||||
# A LATER sibling (seq 3) does not hold an earlier leaf (seq 2).
|
||||
|
||||
Reference in New Issue
Block a user