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_laneresolves toplanned;target_laneresolves toapprovedordone;- the WP's latest event with
to_lane == plannedhasfrom_lane ∈ {for_review, in_review}and a non-Nonereview_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_signalEmit.arbiter_forwardtasks_verdict_persistence.py:~842
Truth-table deltas versus the base:
| Case | Base | New |
|---|---|---|
Forced planned → for_review after a for_review rejection | True | False |
Forced planned → claimed after a for_review rejection | True | False |
Forced planned → approved after a for_review rejection | True | True |
Forced planned → approved after an in_review rejection | False | True |
Forced planned → done after a rejection (with --done-override-reason, clearing _guard_done_ancestry) | False | True |
| Any unforced move | False | False |
Forced move after a rollback with no review_ref | False | False |
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 areview_refset. This coversin_review → in_progressrejections and the legacy review claimfor_review → in_progresswithreview_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 implementrework emitsfor_review|in_review|approved → in_progresswith the implementer as actor and noreview_ref. That event counts, so a takeover viaaction implementmoves the implementer role.- A forced
planned → claimedon a legacy log may carry a copied rejectionreview_ref. Itsfrom_laneisplanned, so it still counts. move-taskhop expansion stamps whoever moves a WP forward fromplanned/claimed. Anyone who walks a WP throughclaimed/in_progressbecomes the latest implementer, by definition. The acceptance tests must not route a reviewer throughplanned → 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_reviewunforced. This is today's behaviour (pre-existing), and the guidance must not claim otherwise.