Skip to content

Linear agent integration: delegate issues to h2#28

Open
vihaanshahh wants to merge 10 commits into
dcosson:mainfrom
vihaanshahh:feature/linear-attachment
Open

Linear agent integration: delegate issues to h2#28
vihaanshahh wants to merge 10 commits into
dcosson:mainfrom
vihaanshahh:feature/linear-attachment

Conversation

@vihaanshahh

Copy link
Copy Markdown

Adds a native Linear agent integration: delegate (or @mention) an issue to h2 in Linear, and h2 spawns an agent to work it in your repo, streaming progress back to the issue's agent-session activity feed. Same UX model as Cursor/Devin/Charlie.

Branch name is historical — this started as an "attachment" idea (see docs/plan-linear-attachment.md, now marked superseded) and evolved into agent delegation (docs/plan-linear-agent-delegation.md).

What it does

  • Register h2 as a Linear OAuth app (actor=app); delegate an issue → AgentSessionEvent webhook → h2 acks (thought), spawns an agent, streams action/thought activities, and posts a final response.
  • Follow-up comments route to the running agent; permission-blocks surface as elicitation. Sessions persist so follow-ups survive a restart.

Architecture

  • internal/linear — GraphQL client (OAuth + agent-session activity mutations).
  • internal/linearagentSource/Reporter interfaces, the agent service (ack → spawn → stream → respond), webhook source (HMAC-verified), and a persistent session map.
  • internal/linearrelay — optional hosted relay for one-click plug-and-play install: single public endpoint that does the OAuth install, holds each workspace's token, and routes webhooks to each user's local daemon over an outbound long-poll (no inbound port/tunnel; token never leaves the relay). deploy/linear-relay/ has a Dockerfile + compose.
  • CLI: h2 linear serve (relay or webhook mode) and h2 linear relay.

Two transports

  • Relay (recommended): users set only a pairing_token.
  • Webhook (dev/self-host): local receiver + tunnel.

Testing

  • Unit-tested throughout (client, webhook signature, service ack/spawn/stream/follow-up, relay install→webhook→poll→activity, persistence); make check passes.
  • Verified live end-to-end against a real Linear workspace: delegated a real issue → h2 spawned an agent → it finished the task (committed on a branch) → status posted back to the agent session.

Deferred (documented in the plan)

  • Verbatim agent response capture (currently a summary pointing back to the agent), per-team role mapping, agent-guidance ingestion.

🤖 Generated with Claude Code

vihaanshahh and others added 10 commits June 21, 2026 23:21
Add an outbound, opt-in integration that mirrors an agent's live state to
a Linear attachment on a linked issue. Fully side-channel: it observes the
existing monitor state machine off the agent's hot path and swallows all
Linear errors, so it never affects the agent or the daemon.

- internal/linear: self-contained GraphQL client (resolve issue, upsert/
  update attachment) + Watcher that maps agent state to a coarse, deduped,
  rate-capped status. No dependency on the session/monitor packages.
- config: top-level `linear.api_token`; per-session `LinearIssue` on
  RuntimeConfig set via `h2 run --linear LIN-123` (rejected with
  --pod/--resume).
- daemon: one guarded call (startLinearWatcher) wires it in; inert unless
  an issue is linked and a token is configured.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pivot the Linear integration from outbound attachments to native agent
delegation: delegate/@mention an issue to h2 → Linear webhooks h2 → h2 acks,
spawns an agent to work it, and reports the result to the agent session.

This is Phase 1 (webhook-only, for dev). The transport is abstracted behind
an inbound.Source so a hosted outbound-dialed relay can replace it later
without touching the service.

- internal/linear: OAuth (Bearer) client variant + agent-session mutations
  (Viewer, CreateAgentActivity for thought/action/response/elicitation/error).
- internal/linearagent: AgentSessionEvent types, webhook Source with HMAC
  signature verification, and the Service (ack within ~10s → spawn via an
  injected AgentRunner → response on completion). Fully unit-tested with fakes.
- cmd: `h2 linear serve` wires config → webhook source → OAuth reporter →
  a real AgentRunner that spawns an h2 agent, delivers the issue prompt, and
  detects completion by polling agent state.
- config: LinearConfig extended with oauth_token, app_id, inbound (webhook
  mode/address/path/secret), and agent.role.
- docs: plan-linear-agent-delegation.md; attachment plan marked superseded.

Known MVP limit: Result() points back to the agent rather than capturing its
verbatim final message (deferred to the live-activity/interactivity phases).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ence

Build out the agent-delegation integration through Phases 2-4:

- Phase 2 (live activity): AgentHandle.Activities() streams thoughts/actions
  derived from the agent's state transitions; the service forwards each to the
  Linear agent session during the run.
- Phase 3 (interactivity): a session map routes follow-up ("prompted") events
  to the running agent via Deliver; permission-blocks surface as elicitation
  activities so the human is prompted to approve.
- Phase 4 (persistence/cleanup): FileStore persists sessionID->agent so
  follow-ups resume after a linear-service restart (DeliverTo by name); removed
  the superseded attachment watcher (internal/session/linear.go,
  internal/linear/watcher.go) and the now-dead attachment client methods.

The inbound transport stays behind Source so the future relay drops in without
touching the service. All layers unit-tested with fakes; make check passes.
Live smoke test confirms signature verification, ack, spawn, and fail-safe
error handling end to end.

Known limits (documented in the plan): verbatim response capture (currently a
summary pointing back to the agent), per-team role mapping, and the relay
transport remain for a later pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d debug log

Two changes to de-risk the first live test:

- Prefer Linear's pre-formatted `promptContext` when seeding the spawned agent,
  falling back to the triggering comment, then issue title/description.
- Add `h2 linear serve --debug` which logs each raw inbound webhook payload, so
  the exact AgentSessionEvent shape can be confirmed against a real delegation
  and any parsing mismatch fixed immediately. Off by default (payloads may carry
  issue content).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the relay so other users plug into Linear with a single "Authorize h2"
click — no OAuth app creation, no webhook config, no tunnel, and no Linear token
on their machine. Keeps the full native agent experience.

- internal/linearrelay: the hosted relay server (run via `h2 linear relay`).
  Single public endpoint for a published OAuth app: handles the OAuth install,
  holds each workspace's actor=app token, verifies + receives all agent-session
  webhooks, and routes each event to the paired user's daemon over an
  HTTP long-poll hub keyed by organizationId. Activity posts are proxied to
  Linear with the stored token (the token never leaves the relay).
- internal/linearagent: RelaySource (outbound long-poll with reconnect/backoff,
  implements Source) and RelayReporter (posts activities via the relay,
  implements Reporter). Extracted VerifySignature so the relay edge and the
  direct webhook share one impl.
- cmd: `h2 linear serve` now defaults to relay mode (pairing_token only);
  webhook mode retained for dev/self-host. New `h2 linear relay` runs the server.
- config: inbound.relay_url/pairing_token; linear.relay server block.
- linear.Client: Organization() lookup (relay routes webhooks by org id).

Transport is HTTP long-poll (zero new deps, firewall-friendly). Full
install->webhook->poll->activity flow is unit-tested with fakes; make check
passes. Live verification still needs a published Linear app + deployed relay.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the relay deployable and durable:

- Persistence: relay now persists workspace tokens + pairings to StatePath
  (atomic write, 0600) and reloads them on start, so a restart no longer drops
  connected workspaces. Tested across a simulated restart.
- Env config: `h2 linear relay` reads H2_RELAY_* env vars (BASE_URL, CLIENT_ID,
  CLIENT_SECRET, WEBHOOK_SECRET, ADDR, STATE_PATH), so a container deploy needs
  no h2 directory; falls back to the linear.relay config block otherwise.
- Deploy scaffolding under deploy/linear-relay/: distroless Dockerfile,
  docker-compose with a persisted volume, .env.example, and an ops README.

Verified live: env-configured relay starts with no h2 dir, /healthz 200,
/oauth/authorize redirects to Linear with actor=app + scopes, bad webhook
signature 401. make check passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two fixes found while testing the delegation flow end-to-end:
- Set Priority "normal" on the delivered message. Without it the message had an
  empty priority and was never delivered to the agent (manual `h2 send`, which
  defaults to "normal", worked — the auto-delivery path did not).
- Wait for the agent to reach idle (TUI booted, ready for input) before
  delivering, instead of sending as soon as the socket opens, which raced the
  harness's startup screens and dropped the task.

Verified: a simulated delegation now spawns an agent and the task is delivered
and completed with no manual intervention.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tuned from the live delegation run, where the agent finishing its turn took
~9s+ to be detected and reported back:
- completion detection: 3s poll x 3 idle confirmations (~9s) -> 2s poll x 2
  (~4s), still long enough to ride out brief idle between tool calls.
- spawn-time task delivery: poll for the agent's idle/ready state every 500ms
  instead of 1s, so the task lands as soon as the TUI is ready.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant