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(linear): comment + react on pre-container task-creation failures
Closes the silent-drop UX gap that appeared whenever a Linear-triggered task
was rejected before the agent container started — the user would apply the
trigger label, see nothing happen, and have no way to know why. Reactions
and progress comments are emitted by the agent container; nothing fired
until that point, so all upstream rejections were invisible on the Linear
side.
This commit wires a best-effort GraphQL feedback path covering all six
distinct rejection points:
In `linear-webhook-processor.ts` (pre-`createTaskCore`):
1. Issue has no projectId → "isn't in a project" comment
2. Project not onboarded / removed → "isn't onboarded; admin can run
`bgagent linear onboard-project`" comment
3. Webhook missing organization or actor → diagnostic comment
4. Linear actor has no linked platform user → "v1 only the API-token
owner can submit; multi-user OAuth is on the v3 roadmap" comment
5. `createTaskCore` returns non-201 → message branched on status:
guardrail/validation block surfaces the user-facing error string;
503 prompts the user to re-apply the label; other 4xx/5xx falls
through to a generic message.
In `orchestrate-task.ts` (post-201, in admission control):
6. User concurrency cap rejection → "concurrency limit; wait for one
to finish, then re-apply the label" comment.
All five processor paths and the orchestrator path call a shared helper,
`reportIssueFailure(secretArn, issueId, message)`, that runs the comment
and ❌ reaction in parallel via `Promise.allSettled`. The helper:
- Reuses the existing 5-minute `getLinearSecret` cache from
`linear-verify.ts` (no extra Secrets Manager hits on warm Lambdas).
- Swallows network, auth, and GraphQL errors with WARN logs — Linear
feedback is advisory and must never gate the rejection path.
- Posts to Linear's hosted GraphQL endpoint; mutation shapes match
`agent/src/linear_reactions.py` (`commentCreate`, `reactionCreate`).
CDK plumbing:
- `linear-integration.ts` — wires `LINEAR_API_TOKEN_SECRET_ARN` into
the webhook processor and grants read on the existing
`LinearIntegration.apiTokenSecret`.
- `agent.ts` — grants the same secret to `orchestrator.fn` and
populates the env var. The grant is unconditional; the orchestrator
only invokes the helper when `task.channel_source === 'linear'`.
The non-Linear case is a hard no-op at the call site — `notifyLinear-
OnConcurrencyCap` early-returns on `channel_source !== 'linear'`, and the
processor only handles Linear payloads. Slack/API/webhook tasks are
unaffected.
Tests (28 new; 1240 → 1268, all green):
- `cdk/test/handlers/shared/linear-feedback.test.ts` (13 tests):
mutation shape, auth header, error swallowing in 4 distinct failure
modes (secret-resolution null, non-2xx, GraphQL `errors`, network
throw), `Promise.allSettled` partial-success semantics.
- `cdk/test/handlers/linear-webhook-processor.test.ts` (10 new tests
in a `user-visible feedback` describe block): one assertion per
rejection path + happy-path-doesn't-fire + filter-rejection-doesn't-
fire (the latter is intentional UX — the processor sees many events
that aren't tasks, and dropping a comment on each would be noisy).
- `cdk/test/handlers/orchestrate-task-feedback.test.ts` (5 tests):
new file; covers `notifyLinearOnConcurrencyCap` directly with
`withDurableExecution` mocked. Asserts the linear path fires; the
api/webhook/slack paths no-op; missing metadata, missing env, and
undefined `channel_metadata` all no-op cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(linear): finish v1.1 polish — state-on-start + Alain #63 nits
Wraps the v1.1 polish theme from PR #87. Five small additions, all
agent-side or docs:
State-on-start (the user-visible one):
- prompt_builder._channel_prompt_addendum now instructs the agent to
transition the originating Linear issue to `In Progress` (or `Todo`
fallback) at agent-start, mirroring the existing `In Review` chain
fired at PR-open. Closes the gap where the issue stayed at `Backlog`
during real agent work — only the 👀 reaction and "🤖 Starting"
comment signaled progress, while humans-using-Linear expect the state
column to reflect "being worked." Skips if the issue is already in
`In Progress` or any later state; doesn't loop on list_issue_statuses.
Alain #63 review nits (4 small surgical changes):
- linear_reactions.py: auth-failure circuit breaker. Track consecutive
401/403s; after 3 strikes, log ERROR once and short-circuit all later
_graphql calls (return None) until the container restarts. Resets on
any 2xx response. Replaces the prior behaviour where revoked tokens
flooded CloudWatch with WARNs and wasted Linear API quota indefinitely.
- pipeline.py: declare `linear_eyes_reaction_id: str | None = None`
explicitly before the try block instead of relying on
`locals().get("linear_eyes_reaction_id")` in the crash handler.
Functionally identical; survives refactors and reads cleanly.
- config.py::resolve_linear_api_token: narrow `except Exception` to
`(BotoCoreError, ClientError)` from botocore.exceptions. Switch
`print()` to `shell.log("WARN", ...)` so warnings join the structured
log stream the rest of the agent uses.
- LINEAR_SETUP_GUIDE.md + cli/src/commands/linear.ts: stop telling
users to run `bgagent linear link <code>` when auto-link fails — the
code generator is a v3 feature that doesn't ship in v1, so the
suggestion was misleading. Replaced with explicit admin-assisted
fallback (DynamoDB put-item with steps to find workspaceId, viewerId,
Cognito sub) and a clear "this command exists but is non-functional
in v1" note.
Tests: 532 agent + 1268 cdk + 196 cli, all green. Deployed to
backgroundagent-dev. Smoke-tested 👀-on-start (156ms, agent unblocked)
in the prior commit; state-on-start smoke is the next manual step.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(format): apply ruff format
Whitespace-only changes flagged by CI's self-mutation guard. No behaviour
change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(linear): address PR #87 must-fix review items
- linear_reactions: guard auth-circuit globals with `_auth_state_lock`
so the daemon sweep thread and the main thread can't race the
read-modify-write on `_consecutive_auth_failures` /
`_auth_circuit_open`.
- linear_reactions: wrap the daemon sweep target in
`_sweep_stale_reactions_safe` so an unexpected exception logs at
ERROR instead of dying silently (stderr from a daemon thread doesn't
reliably reach CloudWatch).
- linear_reactions: only increment the sweep delete counter when
`_graphql(_DELETE_MUTATION, ...)` actually returns a non-None
response — previously the summary log overstated success.
- config: hoist `import boto3` out of the catch-narrowed try/except
so an `ImportError` (boto3 missing from the image) degrades to a
WARN log instead of crashing the agent.
- orchestrate-task: wrap `notifyLinearOnConcurrencyCap` in a
defensive try/catch — durable-execution retries the entire
admission-control step on throw, which would re-fire `failTask` +
`emitTaskEvent` and produce duplicate events.
- tests: 1 new throw-propagation test for `notifyLinearOnConcurrencyCap`,
3 new tests for `resolve_linear_api_token` (cached env, no-arn,
ImportError fallback). Auto-reset fixture in
`test_linear_reactions.py` now also resets the circuit-breaker
globals between tests so future cases don't leak state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(linear): address PR #87 nice-to-have review items
- linear_reactions: log a single DEBUG line when the auth circuit
breaker short-circuits a call, so the path isn't zero-trace once
open.
- config: split the `(BotoCoreError, ClientError)` catch so
`AccessDeniedException` logs at ERROR instead of WARN — IAM
misconfig is persistent and should page someone, not blend into
transient warnings. Also drop the personal name from the inline
reference to the #63 review.
- linear-webhook-processor: tighten `buildCreateTaskFailureMessage`
param types to `number` / `string` (no `| undefined`) — the only
caller passes `APIGatewayProxyResult` fields which are always
defined. Removes dead fallback-to-`'unknown'` branches.
- test_config: 2 new tests covering the split exception path
(AccessDenied → ERROR; ResourceNotFound → WARN).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(linear): address PR #87 re-review must-fix items
- linear-webhook-processor: extracted `safeReportIssueFailure` helper
and routed all 5 bare `await reportIssueFailure(...)` call sites
through it. The helper is uniformly non-throwing — wraps the call
in try/catch to defend against a future signature refactor that
could break the helper's `Promise.allSettled` never-throw contract.
A synchronous throw would otherwise propagate, fail the Lambda,
and trigger SQS retries on a poison message.
- linear-webhook-processor: dropped the `!` non-null assertion on
`process.env.LINEAR_API_TOKEN_SECRET_ARN` at module scope. The
helper now guards on `!API_TOKEN_SECRET_ARN` and logs a single
clear `Skipping Linear feedback: LINEAR_API_TOKEN_SECRET_ARN not
set` diagnostic per skip — matches the orchestrator pattern in
`notifyLinearOnConcurrencyCap`.
- test_linear_reactions: new `TestAuthCircuitBreaker` class with 5
tests covering the previously-untested circuit:
* 3 consecutive 401/403s open the circuit
* Once open, calls short-circuit without hitting the network
* A 2xx between failures resets the counter
* Non-auth status (500) doesn't increment the counter
* 401 and 403 are both treated as auth failures
- test_linear-webhook-processor: 2 new tests assert
`safeReportIssueFailure` swallows both synchronous throws and
async rejections from the underlying helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(linear): address PR #87 re-review nice-to-have items
- test_config: cover the BotoCoreError branch of
`resolve_linear_api_token` with an `EndpointConnectionError`
case. The PR-#87 split into ClientError + BotoCoreError branches
previously had no test on the BotoCoreError path.
- test_linear_reactions: new
`test_sweep_preserves_just_posted_eyes_via_exclude_id` exercises
the `exclude_id` filter — the existing sweep test never collided
prior reaction ids with the newly posted one, so the branch was
effectively dead code in tests. The new test plants the just-
posted 👀 in the prior reactions list and asserts it survives the
sweep while an older ❌ is deleted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(linear): annotate circuit breaker globals so ty doesn't narrow
`ty check` infers `_consecutive_auth_failures = 0` as `Literal[0]` and
`_auth_circuit_open = False` as `Literal[False]`, which then rejects
the legitimate runtime flips (and the test fixture that resets them
between cases). Adding explicit `int` / `bool` annotations widens the
inferred type and fixes the CI typecheck failure introduced in
`f4633be`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: bgagent <bgagent@noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Alain Krok <alkrok@amazon.com>
0 commit comments