[F043] guard escalate_up against resurrecting terminal tasks

escalate_up had composes=() and no source-status guard, so a PM could
escalate a COMPLETED/CANCELLED task and apply_escalation set it back to
BLOCKED — bypassing the state machine's terminal-state invariant.

Defense in depth:
- spec: add PRECONDITION_NON_TERMINAL to escalate_up's extra_preconditions so
  the lifecycle gate rejects terminal tasks (invalid_state) before the
  journal:decision write fires; generalize _check_intent_preconditions to
  honor non-tracing rejection_kind (not_authorized / invalid_state).
- service: apply_escalation (the single write primitive) returns False and
  refuses to mutate a terminal task — covers the HTTP escalate route which
  bypasses the spec gate. escalate() / escalate_up_to_role() return None on
  refusal so the gateway emits a clean invalid_state envelope.
- route: the HTTP escalate route 409s a terminal task BEFORE sending the
  escalation notification (so a finished task isn't yanked back, PM not pinged).
This commit is contained in:
Renn F
2026-06-28 11:28:23 +02:00
parent 78272de4fc
commit 2f32228673
5 changed files with 229 additions and 15 deletions
+43
View File
@@ -566,6 +566,49 @@ def test_can_invoke_intent_developer_open_pr_no_commits_tracing_gap() -> None:
assert "commits>=1" in d.missing
def test_escalate_up_rejected_on_completed_task() -> None:
"""F043: a PM must not resurrect a COMPLETED task via escalate_up.
escalate_up has composes=() and historically no source-status guard, so the
spec gate accepted it on a terminal task and apply_escalation set it back to
BLOCKED — bypassing the state machine's terminal-state invariant. The spec
now rejects terminal tasks (completed / cancelled) before the journal:decision
write fires.
"""
d = spec.can_invoke_intent(
spec.Role.CELL_PM,
"escalate_up",
_stub_task(status="completed"),
context=spec.Context(notes="stuck on something"),
)
assert d.allowed is False
assert d.rejection_kind == "invalid_state"
def test_escalate_up_rejected_on_cancelled_task() -> None:
"""F043: cancelled is terminal — escalate_up must not resurrect it either."""
d = spec.can_invoke_intent(
spec.Role.MAIN_PM,
"escalate_up",
_stub_task(status="cancelled"),
context=spec.Context(notes="stuck on something"),
)
assert d.allowed is False
assert d.rejection_kind == "invalid_state"
def test_escalate_up_allowed_on_blocked_task() -> None:
"""F043: the terminal guard must not over-restrict — BLOCKED is the natural
escalation source and must still be allowed."""
d = spec.can_invoke_intent(
spec.Role.CELL_PM,
"escalate_up",
_stub_task(status="blocked"),
context=spec.Context(notes="stuck on something"),
)
assert d.allowed is True
def test_valid_next_verbs_developer_in_progress_includes_open_pr_and_i_am_done() -> (
None
):