Skip to content

feat: scope linear watcher per team and pick ticket team in UI - #240

Merged
centdix merged 2 commits into
mainfrom
linear-team-scope-and-ticket-team-picker
May 18, 2026
Merged

feat: scope linear watcher per team and pick ticket team in UI#240
centdix merged 2 commits into
mainfrom
linear-team-scope-and-ticket-team-picker

Conversation

@hugocasa

Copy link
Copy Markdown
Contributor

Summary

Two improvements to the Linear integration:

  1. Multi-instance scoping: a new integrations.linear.watchTeams field in .webmux.yaml filters the auto-create watcher to specific Linear teams. Useful when the same Linear user runs webmux against multiple projects on one machine — without this, every instance would race to claim the same webmux-labeled issues across all teams.
  2. Cleaner ticket-creation UX: the legacy integrations.linear.teamId (UUID, opaque) was only consulted by the "Create Linear ticket" toggle in the new-worktree dialog. Replaced with a team-key input inside the dialog itself (validated like LinearPostDialog, last-used value persisted in localStorage). One webmux instance can now file tickets across multiple teams without editing yaml.

Changes

  • Config: drop integrations.linear.teamId from the schema, type, parser, init template, and tests.
  • Config: add integrations.linear.watchTeams: [ENG, OPS] (team keys, case-insensitive, normalized on parse). Wired through startLinearAutoCreateMonitor.
  • Watcher: filterAutoCreateIssues / filterAutoOneshotIssues now accept an optional watchTeamKeys arg; issues whose team.key is outside the list are dropped before label/dedup checks.
  • API contract: POST /api/worktrees body gains an optional linearTeamKey. Required when createLinearTicket: true; resolved server-side via fetchTeamByKey.
  • UI: CreateWorktreeDialog shows a "Team key" input inside the existing create-ticket panel (validated with parseLinearTarget). Submit is blocked until the key is valid. Last-used value persisted in localStorage["wt-linear-team-key"].
  • Init template: updated comments to reflect that the ticket team is picked in the dialog, and to document watchTeams.

Test plan

  • bun run --cwd backend check (tsc)
  • bun run --cwd frontend check (svelte-check)
  • bun test — backend 316/316, packages 5/5, bin 107/107, frontend 73/73
  • New unit tests cover the watchTeams filter (allow-all, case-insensitive match, oneshot variant)
  • Manual: open new-worktree dialog with createTicketOption: true, flip the toggle, confirm team-key input appears and validates; submit with a real team key and verify the ticket is filed
  • Manual: set watchTeams: [ENG] on a project where the user has issues across multiple teams, restart server, verify only ENG-labeled issues trigger auto-create

Generated with Claude Code

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 18, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
webmux cb9079b Commit Preview URL

Branch Preview URL
May 18 2026, 01:09 PM

@claude

claude Bot commented May 18, 2026

Copy link
Copy Markdown

Claude finished @hugocasa's task in 3m 14s —— View job


Code Review for PR #240

  • Read AGENTS.md and CLAUDE.md files
  • Read all changed files
  • Analyze backend changes (config, server, watcher)
  • Analyze frontend changes (dialog, API contract)
  • Analyze CLI changes (init template)
  • Verify test coverage
  • Post review findings

Overall this is a well-scoped, well-tested change. The decoupling of "which teams the watcher monitors" from "which team a new ticket files into" makes sense and matches the multi-instance scenario described in the PR. The watcher filter is composed cleanly via filterTriggerableIssues, the API contract change is minimal, and parse normalization (trim + uppercase + dedup) is correct. A few items worth addressing before merge.


Issues

1. Silent breakage for users with existing integrations.linear.teamId — medium

parseLinearIntegration (backend/src/adapters/config.ts:417) drops teamId without warning. Anyone on 0.32.0 who has teamId: ... in their .webmux.yaml will lose it on upgrade and the "Create Linear ticket" toggle will fail with a 400 until they discover the new team-key input. Worth a one-line warning when the legacy field is seen so it's discoverable in logs.

// in parseLinearIntegration, after deriving watchTeams:
if (typeof linear.teamId === "string") {
  log.warn("[config] integrations.linear.teamId is no longer used — pick the team at ticket-creation time in the dialog");
}

Fix this →

2. Server accepts any string for linearTeamKey — minor

backend/src/server.ts:887 upper-cases the trimmed key and forwards it straight to fetchTeamByKey. An invalid format ("!!", "ENG-123") round-trips to Linear and only gets rejected with a 404 ("team not found"). The contract already exposes LinearTeamKeySchema (packages/api-contract/src/schemas.ts:28) — validating with it server-side would return a 400 with a clearer message and avoid an unnecessary GraphQL call.

const linearTeamKey = body.linearTeamKey?.trim().toUpperCase() || undefined;
// ...
if (createLinearTicket) {
  if (!linearTeamKey || !LinearTeamKeySchema.safeParse(linearTeamKey).success) {
    return errorResponse("Invalid Linear team key (expected uppercase letters, e.g. \"ENG\")", 400);
  }
  // ...
}

Fix this →

3. Redundant .toUpperCase() in matchesTeamFilter — nit

backend/src/services/linear-auto-create-service.ts:46: watchTeamKeys.some((key) => key.toUpperCase() === issueKey) — the keys arriving here are already uppercased by parseTeamKeyList (adapters/config.ts:412). The defensive .toUpperCase() is harmless but obscures the invariant. Either drop it, or keep it and add a one-line comment noting the input is expected-uppercase.

4. bind:value on the team-key input doesn't normalize as the user types — minor UX

frontend/src/lib/CreateWorktreeDialog.svelte:484: the input is bind:value={linearTeamKey} with CSS uppercase, so the stored value can be "eng" while the rendered text reads "ENG". Submission still works (you derive linearTeamKeyTrimmed), but a user pasting and then editing may be confused by the visual/value mismatch. Easiest fix is to upper-case on input:

<input
  ...
  oninput={(e: Event) => { linearTeamKey = (e.currentTarget as HTMLInputElement).value.toUpperCase(); }}
  value={linearTeamKey}
/>

(Or keep bind:value and do the normalization in an oninput handler — either way the stored state matches what's rendered.)

5. parseTeamKeyList collapses [] to undefined — design choice worth confirming

backend/src/adapters/config.ts:414: keys.length > 0 ? ... : undefined. This means a user can't override an inherited watchTeams with an explicit empty list (to mean "watch everything") via .webmux.local.yaml — they'd have to delete the key. Probably fine since "unset" already means "all teams", but worth being explicit about that in the comment on LinearIntegrationConfig.watchTeams.


Nits

  • backend/src/server.ts:170...(watchTeamKeys && watchTeamKeys.length > 0 ? { watchTeamKeys } : {}) is redundant because parseTeamKeyList already guarantees undefined for empty. The spread already handles undefined cleanly, so just watchTeamKeys (when truthy) suffices.
  • frontend/src/lib/CreateWorktreeDialog.svelte is now 520 lines — per frontend/CLAUDE.md (~300 line threshold) extraction is overdue. Not this PR's job, but the create-ticket panel + team-key input + linear title input is a self-contained ~45-line block that could become LinearTicketCreationFields.svelte and would also help unit-test team-key validation in isolation.
  • The new watchTeamKeys filter describe block (linear-auto-create-service.test.ts:156) is inserted between the two existing describes; tucking it at the end (after filterAutoOneshotIssues) reads more naturally. Pure style, no functional impact.

Things that look good

  • Filter composition via filterTriggerableIssues avoids duplicating the team check across filterAutoCreateIssues / filterAutoOneshotIssues.
  • Test coverage for the new filter is solid: undefined/empty allowlist, case-insensitive match, oneshot variant.
  • Local-overlay support for watchTeams in parseLocalLinearOverlay (adapters/config.ts:448) — multi-instance users can configure scoping per-checkout without touching the shared project config.
  • Server returns 400 (not 503) when the key is missing — correct since it's now a client-input problem, not a server config problem.
  • LINEAR_TEAM_KEY_STORAGE_KEY is only written on a valid submit (CreateWorktreeDialog.svelte:262), so no junk persists.
  • parseLinearTarget reuse for client-side validation, including the friendly "Looks like an issue id" branch when someone pastes ENG-123 — nice touch.
    | Branch: linear-team-scope-and-ticket-team-picker

@hugocasa
hugocasa marked this pull request as ready for review May 18, 2026 11:53
@claude

claude Bot commented May 18, 2026

Copy link
Copy Markdown

Claude finished @hugocasa's task in 3m 6s —— View job


Code Review for PR #240

Solid PR overall — clean schema migration, sensible UX, and good test coverage. A handful of polish notes below; nothing blocking.

What works well

  • Layering is clean. watchTeamKeys is plumbed via dependency injection through startLinearAutoCreateMonitor, the filter is a pure function, and the new test cases (undefined/empty/case-insensitive/oneshot variant) exercise the contract directly.
  • Schema normalization is correct. parseTeamKeyList (backend/src/adapters/config.ts:408-415) trims, uppercases, drops empties, and dedupes — and is reused by both parseLinearIntegration and parseLocalLinearOverlay.
  • Shared validation in the dialog. CreateWorktreeDialog.svelte:150-152 reuses parseLinearTarget from @webmux/api-contract, which keeps the team-key validation consistent with LinearPostDialog (matches AGENTS.md §5 DRY).
  • Disambiguating "looks like issue id" message (CreateWorktreeDialog.svelte:487-490) is a nice UX touch — that mistake will be common.

Suggestions

  1. API contract: tighten linearTeamKey to the team-key regex. packages/api-contract/src/schemas.ts:153 currently uses z.string().optional(), but you already export LinearTeamKeySchema = /^[A-Z]+$/ two lines above. The server normalizes to uppercase but never re-validates format, so a request with linearTeamKey: "eng-1" becomes "ENG-1" and falls through to fetchTeamByKey, which returns a vague 404. Using LinearTeamKeySchema.optional() would reject this at the boundary with a clearer error. (Backend should still uppercase before sending to Linear, since some clients may send lowercase.) Fix this →

  2. matchesTeamFilter: redundant uppercase on the allowlist. backend/src/services/linear-auto-create-service.ts:46 does watchTeamKeys.some((key) => key.toUpperCase() === issueKey), but parseTeamKeyList already normalizes the input to uppercase before it reaches the watcher. The extra .toUpperCase() is wasted work and slightly obscures the invariant ("watchTeamKeys is normalized") — watchTeamKeys.includes(issueKey) is enough. Document the assumption on the watchTeamKeys?: string[] JSDoc and drop the call. Fix this →

  3. Server: linearTeamKey is not validated before fetchTeamByKey. backend/src/server.ts:887 only checks truthiness and uppercases. Same root cause as (1) — if the schema is tightened, this becomes a non-issue. Otherwise, consider an explicit LinearTeamKeySchema.safeParse(linearTeamKey) guard so a bad key returns 400 instead of "Linear team not found" 404.

  4. Frontend: linearTeamKey state holds the raw user case. CreateWorktreeDialog.svelte:484 uses bind:value={linearTeamKey} with CSS uppercase for visual feedback only. Validation and submission already use the derived linearTeamKeyTrimmed, so functionally this is fine — but if a user types eng, linearTeamKey === "eng" and the visual mismatch with the underlying value can be surprising during debugging. Optional polish: normalize on input (oninput={(e) => linearTeamKey = e.currentTarget.value.toUpperCase()}) so display, state, and validation agree. Not a bug.

  5. Test plan parity gap. AGENTS.md §3 explicitly requires CLI parity for new user-facing features. The init template documents watchTeams, but bin/src/worktree-commands.ts (the create flow) wasn't touched — users creating worktrees from the CLI cannot supply linearTeamKey, so createLinearTicket: true would fail server-side with the new 400. If CLI-side ticket creation isn't supported today, this is a no-op; if it is, the CLI needs an equivalent flag.

Minor / informational

  • parseTeamKeyList silently swallows all-empty arrays. watchTeams: ["", " "] collapses to undefined (no filter), which is the opposite of the user's intent. Edge case; a warning log might help future debugging but not worth holding the PR.
  • Filter ordering nit. PR description says team filter runs "before label/dedup checks" but filterTriggerableIssues runs matchesLabelRule before matchesTeamFilter (linear-auto-create-service.ts:59-62). Functionally identical; just a doc/code consistency note.
  • processedIssueIds is module-level state. Existing behavior, not introduced here — but worth flagging that changing watchTeams at runtime (via local overlay reload, if that ever lands) won't replay already-processed issues. Probably fine given current restart-on-config-change model.

Test coverage

  • Backend filter tests are thorough.
  • App.test.ts extends the integration assertion to include linearTeamKey: "ENG" — good.
  • No tests for the server-side 400 when createLinearTicket: true without linearTeamKey, or for the fetchTeamByKey 404 path. Not strictly required, but would catch regressions if the schema is tightened later.

  • Read AGENTS.md and CLAUDE.md files
  • Read all changed files
  • Analyze backend changes (config, watcher, server)
  • Analyze frontend changes (dialog, API contract)
  • Analyze CLI/init template changes
  • Verify test coverage
  • Post review findings
    | Branch: linear-team-scope-and-ticket-team-picker

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@centdix
centdix merged commit 729e51b into main May 18, 2026
2 checks passed
@centdix
centdix deleted the linear-team-scope-and-ticket-team-picker branch May 18, 2026 13:19
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.

2 participants