fix(kanban): make transition emit payload classifiable by the webhook adapter - #22
Conversation
… adapter
The 4c transition-emit bridge POSTs a kanban lifecycle transition to the
loopback webhook route so the orchestrator wakes as an agent run. The route
filters incoming events against its `events` allowlist, resolving the event
type from (in order) the X-GitHub-Event / X-GitLab-Event headers, then the
body fields `event_type` / `type`.
The emitted payload carried the transition kind only as `kind` and in the
X-Kanban-Event header — neither of which the adapter consults for
classification. On a loopback POST (no GitHub/GitLab header), extraction fell
through to "unknown", which is not in the route allowlist, so the adapter
returned {"status": "ignored"} with a 200 and never spawned the run. The
symptom was a healthy 200 on the emit with no orchestrator agent run.
Add `event_type` (mirroring `kind`) to the payload body so the adapter
classifies the transition and dispatches. Regression coverage:
- a unit test replicating the adapter's extraction precedence against the
built payload (was 'unknown', now the kind), and
- the E2E HTTP proof now classifies the received body the way the adapter
does, guarding the wire contract the previous permissive dummy handler
masked.
cwest
left a comment
There was a problem hiding this comment.
The bug is real and the fix is right. I checked the adapter at head SHA d034b26: gateway/platforms/webhook.py:533-538 resolves the event type as X-GitHub-Event header, then X-GitLab-Event header, then body event_type, then body type, then "unknown", and webhook.py:540-541 drops anything not in the route's events list. The loopback emitter sends X-Kanban-Event (not consulted) and no GitHub/GitLab header, so before this change the classifier landed on "unknown" and the route returned status ignored with a 200 and never spawned a run. Adding event_type mirroring kind fills the exact slot the adapter reads, so a headerless loopback POST now classifies as its transition kind.
Ran the emit suite in a throwaway clone against the base branch (10 passed) and against the PR head (11 passed), with the added test the only difference. Then deleted the event_type key from the source and reran: both the new unit test and the hardened HTTP test fail with "adapter classified the loopback POST as 'unknown'" — the production symptom — which confirms the guard is a genuine red-to-green, not a test that passes regardless. No regressions.
…(default-off) Optional, default-OFF bridge that lets a kanban lifecycle transition wake the orchestrator as an agent RUN (not merely a chat ping) by POSTing the transition to a loopback webhook route — mirroring how a GitHub pull_request event triggers a review run. New module gateway/kanban_transition_emit.py adds pure decision logic (should_emit_transition, build_transition_payload with a stable (board,task_id,kind,event_id) idempotency key) plus a fail-safe emit_transition coroutine that HMAC-signs and POSTs; it NEVER raises. The payload is classifiable by the webhook adapter, and transition wakes route back to the origin thread/session. Guarded by kanban.transition_emit.enabled (default OFF); when disabled the notifier path is byte-for-byte unchanged. No new core tool, no new model surface, no user-facing HERMES_* config var. upstream-pending: fork PR #21, #22, #23
…(default-off) Optional, default-OFF bridge that lets a kanban lifecycle transition wake the orchestrator as an agent RUN (not merely a chat ping) by POSTing the transition to a loopback webhook route — mirroring how a GitHub pull_request event triggers a review run. New module gateway/kanban_transition_emit.py adds pure decision logic (should_emit_transition, build_transition_payload with a stable (board,task_id,kind,event_id) idempotency key) plus a fail-safe emit_transition coroutine that HMAC-signs and POSTs; it NEVER raises. The payload is classifiable by the webhook adapter, and transition wakes route back to the origin thread/session. Guarded by kanban.transition_emit.enabled (default OFF); when disabled the notifier path is byte-for-byte unchanged. No new core tool, no new model surface, no user-facing HERMES_* config var. upstream-pending: fork PR #21, #22, #23 (cherry picked from commit d79d702)
Problem
The 4c transition-emit bridge POSTs a kanban lifecycle transition to the loopback webhook route (
/webhooks/kanban-transition) so the orchestrator wakes as an agent run. The webhook adapter filters incoming events against the route'seventsallowlist, resolving the event type from — in precedence order:The emitted payload carried the transition kind only as
kindand in theX-Kanban-Eventheader — neither of which the adapter consults for classification. On a loopback POST (no GitHub/GitLab header), extraction fell through to"unknown", which is not in the route allowlist ([blocked, completed]), so the adapter returned{"status": "ignored"}with a 200 and never spawned the run.Observed symptom (live)
— a healthy 200 on the emit, but no
inbound message: platform=webhook …/ agent run, unlike thegithub-prsroute.Root cause
Contract mismatch between the emitter and the adapter's event-extraction. The payload lacked a body field (
event_type/type) the adapter reads to classify a headerless loopback POST.Fix
Add
event_type(mirroringkind) to the payload body so the adapter classifies the transition and dispatches the run. One additive key; no adapter/core change.Tests (TDD)
'unknown', now the transition kind.event_type == 'blocked'— closing the gap the previous permissive dummy handler masked.Why the previous E2E test missed it
The E2E test used a dummy handler that accepted any event, so it proved the POST sent correctly but never proved the real adapter would classify and dispatch it. The hardened test now guards that wire contract.