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
Falsefor arbiter-forward edgesin_review → {approved, done}(NFR-003 — untouched). - Returns
Truefor every* → plannededge that returnsTruetoday (behavior preserved).
Call sites replaced (WP01)
| Gate | Site | Was | Becomes |
|---|---|---|---|
| content read | tasks_move_task.py:615 | st.target_lane == Lane.PLANNED | is_review_rejection_edge(st.old_lane, st.target_lane) |
| persist gate | tasks_move_task.py:2334 (via Emit) | decision.planned_rollback | decision.is_review_rejection |
| emit ref | tasks_transition_core.py:296 | target_lane == Lane.PLANNED and review_feedback_pointer | is_review_rejection_edge(old_lane, target_lane) and review_feedback_pointer |
| no-rationale refusal | tasks_transition_core.py:596 | req.target_lane != Lane.PLANNED | not is_review_rejection_edge(req.old_lane, req.target_lane) |
emitted review_result | tasks_move_task.py:2539 (_mt_hop_review_result) | target == Lane.PLANNED and rejected is not None | is_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
exceptblock 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— seeis-review-rejection-edge.md.- Grammar in
review/cycle.py:is_non_resolvable_review_ref,is_synthetic_review_ref, ReviewCycleArtifact(review/artifacts.py) andgenerate_fix_prompt(review/fix_prompt.py).