Contracts

arbiter-override-classification.md

Contract: arbiter override classification

is_arbiter_override_history(events, wp_id, old_lane, target_lane, force) -> bool is pure.

It returns True iff all four hold:

  • force;
  • old_lane resolves to planned;
  • target_lane resolves to approved or done;
  • the WP's latest event with to_lane == planned has from_lane ∈ {for_review, in_review} and a non-None review_ref.

review/arbiter.py::_is_arbiter_override(feature_dir, wp_id, old_lane, target_lane, force) keeps its signature (it is pinned through the _detect_arbiter_override compat re-export) and delegates to this predicate. read_events returns lane transitions only, so with the WP in planned "the latest transition into planned" is the latest event. The rejection lookup is a private helper in arbiter.py. _run_arbiter_override (tasks_move_task.py:~3681) independently copies wp_events[-1].review_ref via read_events_transactional. For a WP in planned that is the same rejection event, so the two agree for LANES/flat missions, and the runner is left unchanged to keep file ownership disjoint. The read-path divergence under a coordination topology (post-plan D4) is recorded as a residual in research R-07.

approved → planned (a reopen) is deliberately excluded from the rejection sources, as on the base. It is not a review rejection of submitted rework.

Consumers are unchanged:

  • tasks_transition_core.arbiter_persist_signal
  • Emit.arbiter_forward
  • tasks_verdict_persistence.py:~842

Truth-table deltas versus the base:

CaseBaseNew
Forced planned → for_review after a for_review rejectionTrueFalse
Forced planned → claimed after a for_review rejectionTrueFalse
Forced planned → approved after a for_review rejectionTrueTrue
Forced planned → approved after an in_review rejectionFalseTrue
Forced planned → done after a rejection (with --done-override-reason, clearing _guard_done_ancestry)FalseTrue
Any unforced moveFalseFalse
Forced move after a rollback with no review_refFalseFalse

ownership-role-allowance.md

Contract: ownership role allowance

_ownership_role_allowance(req) -> bool is pure (no I/O) and is evaluated by _guard_agent_ownership only when the existing checks would refuse. The existing checks refuse when all of these hold: the slot occupant is not a generic actor, the tool keys differ, and --force is absent.

Returns True iff req.latest_implementer is known (not None/empty) and one of the following holds:

1. Reviewer arm: old_lane == for_review, target_lane ∈ {in_review, approved, planned}, and _actor_key(req.agent) != _actor_key(req.latest_implementer). 2. Implementer arm: old_lane ∈ {planned, claimed, in_progress}, target_lane ∈ {claimed, in_progress, for_review}, and _actor_key(req.agent) == _actor_key(req.latest_implementer).

Otherwise it returns False, and the refusal is byte-identical to today's: the Agent mismatch: … error; for rejection/approval saves under auto-commit, the ownership_refusal diagnostic; and the console warning, which may add one hint line.

Lanes are alias-normalized. in_review → * is never allowed by this function.

latest_implementer_actor(events, wp_id) (pure, status/review_roles.py, re-exported from specify_cli.status)

It returns the actor of the latest transition with to_lane ∈ {claimed, in_progress}, skipping two kinds of event:

  • a reviewer rework verdict: from_lane ∈ {for_review, in_review, approved} and a review_ref set. This covers in_review → in_progress rejections and the legacy review claim for_review → in_progress with review_ref="action-review-claim".
  • an event whose actor projects (_actor_key) to a generic actor (GENERIC_IMPLEMENTATION_ACTORS: user, implement-command, unknown, …).

It returns None when nothing qualifies, and the guard then allows nothing.

Notes (post-plan squad):

  • agent action implement rework emits for_review|in_review|approved → in_progress with the implementer as actor and no review_ref. That event counts, so a takeover via action implement moves the implementer role.
  • A forced planned → claimed on a legacy log may carry a copied rejection review_ref. Its from_lane is planned, so it still counts.
  • move-task hop expansion stamps whoever moves a WP forward from planned/claimed. Anyone who walks a WP through claimed/in_progress becomes the latest implementer, by definition. The acceptance tests must not route a reviewer through planned → in_review.
  • The residual is accepted and documented, not fixed: once a same-tool implementer resubmits, the slot holds that implementer. The existing same-key check then lets it approve its own work from for_review unforced. This is today's behaviour (pre-existing), and the guidance must not claim otherwise.