Skip to content

Phase 2 blocker: cold-journal trigger attribution degrades to 'unknown' (affects #481) #485

@michael-wojcik

Description

@michael-wojcik

Context

Surfaced in PR #477 (#401 teachback gate) round-6 blind architectural review. Must be resolved before Phase 2 flip ships (issue #481).

The gap

_trigger_for_transition at pact-plugin/hooks/teachback_gate.py:492-564 requires both from_state AND to_state to be non-empty to return a named trigger. On cold-journal scenarios — where no prior teachback_state_transition event exists for the task_id in the current session journal — from_state = "" is passed. No named branch matches, so the trigger falls through to "unknown".

Affected trigger attributions (all silently degrade to "unknown" on cold-journal first observation):

  • lead_approve (cycle-4 R2-A2): under_review → active
  • content_fixed (cycle-5 M-R4-3): correcting → active
  • auto_downgrade (cycle-6 M-R5-A): under_review → correcting with unaddressed_items reason
  • lead_correct (cycle-6 M-R5-A): under_review → correcting with corrections_pending reason
  • content_invalid (cycle-4 T10): active → teachback_pending

When cold-journal happens

  • Session boundary (claude --resume where task was created in prior session)
  • Fresh teammate spawn on existing task
  • Any scenario where _emit_state_transition_if_changed fires before any prior transition was journaled for this task_id

Why this is a Phase 2 gate (not merge gate for #477)

Phase 1 is advisory-mode: the gate doesn't block tool calls based on trigger attribution. Observability is degraded; behavior is unaffected.

Phase 2 flip (issue #481) is where this matters. The Phase-2 auditor's forgery detection depends on distinguishing lead-authored approvals from teammate-authored resubmissions. If cold-journal emits trigger="unknown" instead of the attribution-carrying values, the auditor can't distinguish them from state_transition events alone.

Partial mitigation exists: teachback_gate_advisory.reason carries reason_code via a separate event type. If the Phase-2 auditor correlates across event types ({state_transition.from_state/to_state, advisory.reason, state_transition.task_id}), this is workable. If it relies on state_transition.trigger as self-contained, attribution is lost on session boundaries.

Resolution paths

Three options (decide during Phase 2 design):

Option 1 — explicit cold triggers: extend _trigger_for_transition to accept reason_code on empty-from_state fallback and return reason-specific triggers (e.g., cold_unaddressed, cold_invalid, cold_approve). Preserves attribution on first-observation.

Option 2 — suppress cold emission: don't emit teachback_state_transition at all on cold-journal. Let teachback_gate_advisory be the sole attribution source for first-observation. Simpler journal shape; requires auditor to correlate.

Option 3 — auditor-level correlation: accept current behavior; design Phase-2 auditor to correlate state_transition + advisory by task_id. Zero code change here; audit design burden.

Blocker rationale

Issue #481 (Phase 2 blocking-mode flip) should reference this issue as a prerequisite. Whichever of the 3 options is chosen, the Phase-2 auditor design must accommodate cold-journal attribution.

Related observability

Round-6 reviewers also flagged a convergent MEDIUM: integration-test gap on the auto_downgrade emission wire-up at teachback_gate.py:269. Being addressed in cycle-7 remediation on PR #477 separately.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions