Skip to content

kew delegate — Assign-to-kew delegation watcher

kew delegate lets a human hand off a Linear issue to kew by assigning it to the kew seat (or adding a configured label). kew delegate watch runs a foreground loop that detects the assignment, claims the issue, and dispatches it as a one-node ephemeral epic — reusing the same claim → dispatch → close path as kew epic run.

Status — supervised use is safe; unattended use is close. As of #966 the delegation reservation lifecycle is sound (terminal outcomes release their reservation, so the fleet no longer deadlocks after [parallel] max_workers completions), and #971 hardens the refusal path for long unattended runs: kew comments once per refusal episode (not on every cooldown expiry), a failed unclaim leaves a reconcile retry surface instead of stranding the item, and an expired/revoked LINEAR_API_KEY terminates the watcher loudly (exit 2) rather than retrying forever. Running kew delegate watch under supervision is safe today; fully unattended operation is pending the remainder of the delegation-followups epic (driver-side reservation reconcile and epic-driver lock liveness). --dry-run is always safe.

Refusal-episode comment semantics (#971)

When a budget/preflight pre-check refuses a delegated issue, kew writes a terminal refusal row and posts one Linear comment naming the cap that bound and the knob to raise. While the issue stays over budget the watcher keeps re-surfacing it (its trigger label survives the unclaim), but kew comments only once per episode: a repeat refusal updates state silently until the episode ends (a successful dispatch or any non-refusal outcome resets it). Raise the named budget.*_usd limit and the next re-claim proceeds. See [delegation].refusal_cooldown_s.

Assign means go

There is no confirm step. The moment an issue is assigned to the kew seat (or carries the trigger label) and the watcher sees it, kew claims it and spends money on a contained agent run. Nothing asks you first — that is the point of the feature.

The guards are the budget caps, not a prompt:

  • [budget].per_issue_usd, daily_usd, and weekly_usd are checked in the claim preflight through the shared resolver (env > DB override > kew.toml > default), exactly as for an epic node. Over cap, kew refuses, comments once naming the cap that bound, and leaves the issue alone.
  • [parallel].max_workers is the shared fleet ceiling — delegated work and kew epic run --all draw from the same pool, so delegation cannot outrun it.
  • The breaker and sandbox containment apply unchanged.

Set those caps to numbers you are willing to lose before you assign anything to kew, and use --dry-run first if you want to see what would be picked up.

Requirements

  • [tracker].type = "linear" — delegation is a Linear-only feature.
  • [delegation].assignee set to the kew Linear member name — the watcher resolves this to a member ID at startup and refuses to start if it is blank.
  • LINEAR_API_KEY environment variable set to a Linear personal API key.

Setting up the kew member seat

kew is a real Linear member seat, not an integration or an OAuth app — its identity is a personal API key belonging to that seat. Single-tenant and firm-operated: one seat, one key.

  1. Invite a seat with a plus-addressed email. Most mail providers (Gmail among them) deliver you+kew@gmail.com to you@gmail.com, so you can create a second Linear member without a second mailbox. In Linear: Settings → Members → Invite and send the invite to you+kew@gmail.com.
  2. Accept the invite in an incognito/private window. Your normal browser is already signed in as you; accepting there would just re-authenticate your own account. Open the invite link in an incognito window and complete signup there.
  3. Set the member's name to kew (or whatever you put in [delegation].assignee — the watcher resolves that display name to a member ID at startup and refuses to start if it cannot). Keep the two in sync.
  4. Create the API key as kew. Still in the incognito session: Settings → Security & access → Personal API keys → New key. Export it as LINEAR_API_KEY where you run the watcher. Everything kew does — comments, state transitions, the post-merge close — is attributed to that seat, so the audit trail in Linear reads as "kew did this", not "you did this".
  5. Create the trigger label (kew:ready by default) in the workspace, if you intend to delegate by label as well as by assignment.

Anyone who can assign an issue to the kew seat — or apply the trigger label — can spend your budget. Treat seat membership and the ability to apply that label as spend permissions — see Assign means go above.

kew delegate watch [--dry-run]

Usage: kew delegate watch [OPTIONS]

Options:
  --dry-run    Log 'would claim / would dispatch' without reserving or spending.
kew delegate watch                # foreground watcher — Ctrl-C to stop
kew delegate watch --dry-run      # preview mode: no claims, no spend

Starts a foreground polling loop (like kew ingest serve — no daemon, no autostart). On each tick it reads the local event cursor from the run store (every [delegation].fast_interval_s seconds) and polls Linear (at most once per [delegation].poll_interval_s seconds) to find issues that are either assigned to the kew member or carry the configured delegation label — either trigger alone surfaces a candidate. For each candidate it:

  1. Re-verifies against the live Linear API (not a snapshot): the issue must still be assigned to the kew member and still be in a claimable state. A label-only candidate is surfaced by the poll but released here until the issue is also assigned to the kew seat.
  2. Claims the issue — transitions it to In Progress and reserves a fleet slot.
  3. Dispatches it as a one-node ephemeral epic under the "delegation" pseudo-epic, sharing the [parallel] max_workers fleet cap with kew epic run --all.

Ctrl-C shuts down cleanly.

Configuration

The [delegation] block in kew.toml:

[tracker]
type = "linear"               # delegation is Linear-only

[delegation]
assignee = "kew"              # the kew Linear member name (required at watch start)
label = "kew:ready"           # Linear label that signals a delegated issue is ready
poll_interval_s = 20          # throttled Linear API poll floor (seconds, >= 1)
fast_interval_s = 2           # local tracker-events read cadence (seconds, >= 1)
refusal_cooldown_s = 300      # skip re-claiming a budget-refused item this long (0 = off)
Field Default Description
assignee "" The kew Linear member name. Required at watch start.
label "kew:ready" Linear label that signals a delegated issue is ready.
poll_interval_s 20 Throttled Linear API poll floor (seconds).
fast_interval_s 2 Local tracker-events read cadence (seconds).
refusal_cooldown_s 300 Skip re-claiming a budget-refused item for this long, damping the refuse→comment loop (0 disables).

Field-by-field notes live in [delegation] in the configuration reference. Precedence is the shared resolver's: env > DB override > kew.toml > default — so a budget override set in the kew serve Budget panel governs a delegation run exactly as it governs an epic-driver run.

Optional: the webhook tunnel

Without a webhook, the watcher's local tracker_events read has nothing new to find and pickup latency is bounded by [delegation].poll_interval_s (20s by default) — which is fine, and needs no inbound network at all.

Wiring the Linear webhook receiver makes assignment events land locally within fast_interval_s instead. It needs a public URL pointing at kew ingest serve, so in development that means a tunnel. The full recipe — creating the webhook in Linear, the signing secret, and the cloudflared tunnel — is in Linear webhooks — dev-tunnel recipe.

Delegation is correct either way: the throttled poll is the floor, the webhook is only an accelerator, and the claim reservation (not the event stream) is what prevents a double dispatch.

Exit codes

Code Meaning
0 Clean shutdown (Ctrl-C).
1 Misconfiguration (tracker.type not linear, delegation.assignee not set).
2 LINEAR_API_KEY not set at startup, or Linear authentication failed at runtime (expired/revoked key) — the watcher terminates loudly rather than retrying forever (#971).

See also

Configuration — [delegation] — full field reference.

Configuration — override precedence — how the [budget] caps that guard an assign=go dispatch are resolved.

Linear webhooks — the optional receiver + dev-tunnel recipe that shortens pickup latency.

kew epic — the epic driver that delegated work shares its fleet cap and claim/dispatch/close path with.