Contracts

is-review-rejection-edge.md

Contract (FROZEN) — is_review_rejection_edge

WP01 introduces this; WP02 and all FIVE rejection-family call sites consume it. Frozen so WP02 and reviewers can rely on the signature and semantics before WP01 lands. (The predicate signature is frozen; the five-site enumeration below is a factual call-site correction, not an interface change.)

Signature

# src/specify_cli/cli/commands/agent/tasks_transition_core.py
def is_review_rejection_edge(old_lane: str, target_lane: str) -> bool: ...

Semantics

Returns True iff the edge must durably record reviewer feedback (persist a review-cycle record, populate the feedback location, emit a resolvable pointer, and refuse a no-rationale rejection):

resolve_lane_alias(old_lane)  == IN_REVIEW and
resolve_lane_alias(target_lane) in (PLANNED, IN_PROGRESS)
    OR
resolve_lane_alias(target_lane) == PLANNED     # preserve any-source ->planned rollback (non-regression)
  • Pure; no I/O; alias-normalizing (doing → in_progress).
  • Returns False for arbiter-forward edges in_review → {approved, done} (NFR-003 — untouched).
  • Returns True for every * → planned edge that returns True today (behavior preserved).

Call sites replaced (WP01)

GateSiteWasBecomes
content readtasks_move_task.py:615st.target_lane == Lane.PLANNEDis_review_rejection_edge(st.old_lane, st.target_lane)
persist gatetasks_move_task.py:2334 (via Emit)decision.planned_rollbackdecision.is_review_rejection
emit reftasks_transition_core.py:296target_lane == Lane.PLANNED and review_feedback_pointeris_review_rejection_edge(old_lane, target_lane) and review_feedback_pointer
no-rationale refusaltasks_transition_core.py:596req.target_lane != Lane.PLANNEDnot is_review_rejection_edge(req.old_lane, req.target_lane)
emitted review_resulttasks_move_task.py:2539 (_mt_hop_review_result)target == Lane.PLANNED and rejected is not Noneis_review_rejection_edge(st.old_lane, target) and rejected is not None

The 5th site (_mt_hop_review_result) selects the emitted event's review_result; if left keyed on == Lane.PLANNED the re-implement edge falls to the approval-shaped branch (verdict=APPROVED, reference="auto-forward:<WP>") and the emitted event no longer matches the resolvable pointer emitted by the emit ref site — so SC-001 field-for-field VALUE parity is unprovable with only the first four routed.

Emit flag generalization

Emit.planned_rollback (tasks_transition_core.py:829) → renamed is_review_rejection, set from is_review_rejection_edge(req.old_lane, req.target_lane). Consumers: persist-call gate (tasks_move_task.py:2334) and plan-rebuild trigger (tasks_move_task.py:2371, whose now-redundant explicit in_review → {planned,in_progress} clause is removed).

Emitted reference shape (consumed downstream)

On a rejection edge, event.review_ref MUST be a resolvable review-cycle://… pointer (from st.review_feedback_pointer), never a synthetic review:<WP> marker. is_non_resolvable_review_ref(ref) == False for the emitted pointer.

render-feedback-contract.md

Contract — render-side feedback surfacing (WP02, #5024)

1. Visible fall-through warning (FR-006, SC-003)

implement_try_render_fix_mode_prompt (workflow_executor.py:1084), except at :1161:

console.print("[bold red]⚠️ …[/bold red]")), naming the WP and the cause — NOT a log-only logger.warning.

substitute a feedback-less prompt with no visible signal.

  • MUST surface the failure on the operator-visible surface the implementer reads (e.g.
  • MAY still return None (the full prompt is a legitimate, now-announced fallback); MUST NOT silently
  • The except block retains concrete recovery logic (no effect-free handler — Sonar).

2. build_implement_prompt_lines test (FR-005 positive control)

build_implement_prompt_lines (workflow_executor.py:1302) currently has NO test. WP02 adds tests/agent/test_build_implement_prompt_lines.py covering:

  • feedback-present → feedback text appears in the produced lines;
  • feedback-absent → no crash, no spurious feedback.

3. Event-log partition re-route (FR-007, SC-004 — F4 independent-red)

The event-log read behind the render/feedback resolution MUST resolve through the STATUS_STATE placement seam, not the WORK_PACKAGE_TASK feature_dir:

read_dir(WORK_PACKAGE_TASK) dir to resolve_review_feedback_context.

(workflow_cores.py:311,394) MUST read events from placement_seam(repo_root, mission_slug).read_dir(STATUS_STATE) (mirroring the write-side _resolve_verdict_read_feature_dir, tasks_verdict_persistence.py:694).

  • implement_resolve_feedback_and_gate (workflow_executor.py:667) currently passes a
  • The event-log-reading helpers latest_review_feedback_reference / has_prior_rejection
  • The review-cycle ARTIFACT read stays on WORK_PACKAGE_TASK (_review_cycle_wp_dir) — unchanged.
  • Single-branch: PRIMARY == COORD == repo_root, so the re-route is a no-op (no single-branch regression).

Consumed FROZEN dependencies (NFR-001 — do NOT modify)

SYNTHETIC_REVIEW_REF_PREFIXES, synthetic_review_ref.

  • is_review_rejection_edge — see is-review-rejection-edge.md.
  • Grammar in review/cycle.py: is_non_resolvable_review_ref, is_synthetic_review_ref,
  • ReviewCycleArtifact (review/artifacts.py) and generate_fix_prompt (review/fix_prompt.py).