You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(notifications): Linear dispatcher in fanout consumer (cost/turns/duration) (#243)
* feat(notifications): platform-side Linear final-status comment with cost/turns/duration
Closes#239.
Adds a Linear dispatcher to the fanout consumer alongside the existing
slack/github/email dispatchers. Posts a deterministic final-status
comment on terminal task events for Linear-origin tasks, with cost,
turns/max_turns, duration, and PR link rendered.
Three framing modes by (event_type, pr_url):
✅ task_completed → 'Task completed'
⚠️ error_max_turns + pr_url → 'Shipped a PR but stopped early'
❌ all other terminal states → 'Task <subtype>: <classifier title>'
The ⚠️ case is the motivating ABCA-91 scenario: agent hit max_turns on
turn 101 after shipping PR #35; previous behaviour was a silent ❌
reaction with no metrics surfaced to the requester. The platform-side
comment fires deterministically even when the agent crashes (OOM,
SDK buffer overflow, max_turns) before reaching its own step-3
completion comment.
Architecture:
- One new entry in NotificationChannel + CHANNEL_DEFAULTS + DISPATCHERS
in fanout-task-events.ts. Dispatcher gates on channel_source ===
'linear' so non-Linear tasks short-circuit after one DDB Get.
- Reuses the existing postIssueComment helper from
shared/linear-feedback.ts (already in use by the screenshot pipeline
+ orchestrator failure-reporting paths).
- New construct props linearWorkspaceRegistryTable + linearOauthSecretArnPattern
guard the IAM grants the same way slackSecretArnPattern does — a
deployment without LinearIntegration gets no dangling permission to
bgagent-linear-oauth-*.
- FanOutConsumer instantiation moved below LinearIntegration in agent.ts
so it can receive the registry table reference.
Tests: 92 passing in fanout-task-events.test.ts (1816 across full
CDK suite). New Linear-dispatcher describe block covers happy path,
failed without PR, ABCA-91 max_turns-with-PR, channel_source short-
circuit, missing metadata, and postIssueComment-returning-false
graceful no-op.
* fix(linear-dispatcher): krokoko PR-243 review nits + test coverage
Addresses the non-blocking nits from #239 review:
- JSDoc on renderLinearFinalStatusComment now describes the actual
(eventType, prUrl) discriminator rather than 'error_max_turns' as
an event type. The agent_status discrimination lives in the error
classifier, not in the dispatcher's framing logic.
- Inline comment on classifyError result corrected: returns null only
for empty error_message, UNKNOWN_CLASSIFICATION (title 'Unexpected
error') for any non-empty unmatched message.
- ⚠️ frame now appends classifier title — for ABCA-91 the requester
sees 'Shipped a PR but stopped early — Exceeded max turns', not
just the bare PR-shipped frame.
- missing_env and post_failed log paths bumped from INFO to WARN
with error_id tags so missing-env / post-failure are alarmable.
The Linear comment is the only completion signal for the
agent-crash case, so silent drops defeat the dispatcher's purpose.
- Stale 'at most 3 channels' comment in routeEvent updated to 4.
Test coverage:
- New test: LINEAR_WORKSPACE_REGISTRY_TABLE_NAME unset → WARN + skip
(the deploy-misconfig safety valve was unexercised).
- New test: error_max_turns WITHOUT pr_url renders ❌, not ⚠️
(the ⚠️↔❌ boundary the other direction).
- New describe block: 8 direct tests of renderLinearFinalStatusComment
covering null-metric fallbacks, formatDuration boundaries (<60s,
exact-minute Nm, mixed Nm Ss), classifier-title rendering on ⚠️ and
❌ frames, and the no-trailing-colon when errorTitle is null.
Total: 102 tests in fanout-task-events.test.ts (was 92), 1826
passing across the full CDK suite.
* fix(linear): drop redundant PR url + agent step-3 comment after first dev smoke
After the first dev deploy of the Linear dispatcher (#239), two
near-duplicate things showed up on the Linear thread:
1. The platform's ✅ comment carried PR: <url> while the agent's
step-2 'PR opened' comment had already posted the same link one
slot earlier. Two clickable copies of the same URL adds noise.
2. The agent's step-3 'task completed' free-form comment stacked
right next to the platform's ✅ structured comment with full
metrics. Two completion comments back-to-back with overlapping
intent — the platform one is strictly more informative.
Changes:
- renderLinearFinalStatusComment: render PR url ONLY on the ⚠️
shipped-but-stopped-early path. On ✅, the agent's step-2 comment is
guaranteed to have fired with the PR link; on ⚠️ the agent may have
crashed before step-2 (e.g. ABCA-91 max-turns on turn 101), so the
platform comment is the backup signal and the PR url has to be
there.
- Updated the corresponding test to assert not.toContain on the ✅
fixture's PR URL.
- Removed step 3 from the Linear-channel prompt contract in
prompt_builder.py. Replaced with an explicit prohibition against
posting a final 'task completed/failed' comment, with a sentence
pointing the agent at the platform fan-out plane (#239) as the
source of truth for terminal status.
Net Linear thread shape post-task: agent posts start (1) + PR-opened
(2); platform posts terminal ✅/⚠️/❌ (3). One PR url, one completion
comment. Krokoko predicted this exact migration in their PR-243
review — 'the agent prompt can drop step 3 once the platform side is
reliable.'
Targeted suite still 102 passing.
0 commit comments