feat: session ↔ ticket linking (Linear / Jira / GitHub Issues)#73
Merged
Conversation
Design for linking Claude Code sessions to Linear/Jira/GitHub tickets in karma. Read-only: karma stores links and caches metadata fetched by the agent via MCP at link time. Three link paths (branch auto-detect, slash command, dashboard). Many-to-many with no primary. New tables in schema v11. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address all blockers/majors from oh-my-claudecode:critic and feature-dev:code-reviewer: - Split POST (link, idempotent) from PUT (metadata refresh) so each endpoint is properly idempotent in the HTTP sense - Add session_slug column to session_tickets plus partial unique index to dedupe resumed-session links (sessions get new UUIDs per resume but share a slug) - Define link_source precedence (slash_command > dashboard > branch) via CASE on conflict - Wrap upsert in single transaction with RETURNING; cap metadata_json at 64 KB via CHECK constraint - Reference correct write pattern: get_writer_db() from api/db/connection.py:45, not the nonexistent sync_queries.py - Mandate adding tables to SCHEMA_SQL (fresh install) AND the v11 migration block; v10 → v11 covers both paths - Lock down slash-command session-UUID acquisition: agent reads ~/.claude_karma/live-sessions/ filtered by cwd + recency - Remove impossible server-side git remote fallback; callers must fully qualify GitHub refs (owner/repo#N) - Switch frontend routes to /tickets/[provider]/[external_key] so URLs survive a DB rebuild; aligns with repo's semantic-slug pattern - Define ~/.claude_karma/config.json loader inline with opt-in default (branch_detect_enabled=False) - Cite session_title_generator.post_title() as prior art for hook-to- karma POST pattern - Add orphan cleanup policy, scaling notes, accepted-risks section - Move hook tests to hooks/tests/ where the code lives Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slice (a) of the session-ticket-linking feature (see
docs/superpowers/specs/2026-05-13-session-ticket-linking-design.md):
schema, models, parser, queries, router, and orphan-cleanup task.
Schema v11 (api/db/schema.py):
tickets: (provider, external_key) unique registry with cached title/
status and a 64 KB CHECK on metadata_json. session_tickets:
many-to-many link table with partial unique index on (session_slug,
ticket_id) WHERE session_slug NOT NULL — dedupes links across
resumed sessions that share a slug but have different UUIDs. Added
to both SCHEMA_SQL (fresh install) and the v10 → v11 incremental
migration block.
Router (api/routers/tickets.py): 8 endpoints, mounted with no prefix
so it spans /sessions/{uuid}/tickets and /tickets/{provider}/{key}
roots. POST creates/upgrades the link (link_source precedence:
slash_command > dashboard > branch; never downgrades). PUT/PATCH
refresh cached metadata with COALESCE preservation so a degraded
MCP fetch never wipes prior data. external_key uses :path so
github-style 'owner/repo#42' routes correctly.
Queries (api/db/ticket_queries.py): upsert helpers handle both
unique constraints — same-UUID re-link AND same-slug resume — by
finding the existing row and upgrading link_source / filling slug,
rather than racing on INSERT OR IGNORE which couldn't disambiguate
the two unique indexes.
Parser (api/services/ticket_parser.py): pure URL/ref → TicketRef
function. Recognized: Linear/Jira/GitHub URLs, github short
owner/repo#N, bare ALPHA-N with explicit provider hint. Bare '#N'
deliberately unsupported (server has no git context).
Orphan cleanup (api/services/ticket_cleanup.py + api/main.py
lifespan): 6h background task removes session_tickets rows whose
session_uuid never materialized after 7 days, mirroring the
existing session_reconciler asyncio.create_task pattern.
Tests: 57 new tests across parser (26), schema (9), endpoints (22).
All 1474 existing tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slice (b) of the session-ticket-linking feature: minimal dashboard
linking flow on the session detail page.
Adds:
- Ticket TS types in api-types.ts (Provider, LinkSource, Ticket,
SessionTicketLink, SessionTicketRow, TicketListItem,
CreateLinkRequest, CreateLinkResponse)
- <TicketBadge> — three variants (inline/card/pill) with provider
color tokens, external-link icon, optional remove button
- <TicketLinkInput> — accepts URL or bare ref; auto-shows provider
dropdown only when the input is an ambiguous bare key. Posts to
/sessions/{uuid}/tickets and surfaces API hints on 400
- <SessionTicketsSection> — wraps badges + input; local state seeded
from initial prop via $state.snapshot to silence the reactive-prop
warning (matches the SkillList idiom). Optimistic update on
create; idempotent — re-linking same ticket replaces the row in
place, otherwise prepends
- Index barrel at lib/components/tickets/index.ts
Wiring:
- +page.server.ts adds an 8th parallel fetch to
/sessions/{uuid}/tickets via fetchWithFallback (empty array fallback)
- +page.svelte renders <SessionTicketsSection> above ConversationView
using session.uuid + session.slug for slug-based dedup
npm run check: 0 errors. npm run lint: no new warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slice (c) of the session-ticket-linking feature: ticket-centric views
in the dashboard.
Backend:
- GET /tickets gains a ?project=<encoded_name> query parameter. The
list_tickets query adds a join to sessions when filtered so the
project filter excludes orphan links (session_uuid not yet
indexed). Endpoint test added covering project A vs project B
isolation.
Frontend routes:
- /tickets — index page. Table of tickets with provider · key ·
title · status · session_count · last linked. Filter by provider
via dropdown, search by key/title via URL-state-driven form,
project filter shown as a removable chip when present.
- /tickets/[provider]/[external_key] — detail page. Card view of
the ticket metadata + linked-sessions list. Cross-links to
/projects/{encoded_name}/{slug} for indexed sessions; marks
orphans clearly when the session_uuid isn't in the sessions
index yet.
Header:
- "Tickets" link added to both desktop and mobile nav, between
Sessions and Plans.
Verification:
- 58 backend tests pass (incl. new project-filter test using a
real sessions row inserted via the writer connection).
- npm run check: 0 errors. New warnings are the same
state_referenced_locally pattern already accepted in SkillList.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slice (d) of the session-ticket-linking feature: SessionStart hook
that auto-links sessions to tickets when the current git branch
matches a configured pattern.
hooks/ticket_branch_detector.py — opt-in, silent on every failure.
- Reads stdin JSON (session_id, cwd) from the SessionStart event
- Loads ~/.claude_karma/config.json (defaults to disabled)
- Runs `git symbolic-ref --short HEAD` in cwd
- Matches branch against configured regexes (named 'key' group or
full match)
- Best-effort slug lookup via ~/.claude_karma/live-sessions/ —
picks the entry with matching cwd and most-recent timestamp,
feeds slug to the API for slug-based dedup across resumes
- POSTs to /sessions/{uuid}/tickets via urllib.request, mirroring
the prior-art pattern from session_title_generator.post_title()
- All errors are caught and appended to
~/.claude_karma/logs/ticket_branch_detector.log; hook always
exits 0 so it never blocks SessionStart
Opt-in by default: branch_detect_enabled=False until the user
explicitly opts in via config. Prevents surprise links on
personal-projects directories and contains regex blast radius.
hooks/tests/test_ticket_branch_detector.py — 17 tests across
subprocess integration (silent-exit scenarios) and direct-module
unit tests for pure helpers (git_current_branch, match_pattern,
lookup_slug_from_live_sessions, load_config).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slice (e) — final piece of the session-ticket-linking feature.
commands/link-ticket-to-session.md: agent-driven slash command. Uses
${CLAUDE_SESSION_ID} directly (confirmed during the planning spike to
be a documented custom-command substitution), so no need to look up
the session UUID from ~/.claude_karma/live-sessions/. The agent
parses the user's ref, uses the appropriate MCP server (Linear,
Atlassian, GitHub) to pull title/status, then POSTs the link and
PUTs the metadata in two clean idempotent calls.
SETUP.md: new Tier 4 section walking users through installing the
slash command (symlink or copy into ~/.claude/commands/) and the
optional branch-detect hook with its ~/.claude_karma/config.json
opt-in pattern. Distinguishes karma's read-only role from the agent's
MCP-driven metadata fetch.
docs/superpowers/specs/...design.md: spec section on Path 1 updated
to drop the live-sessions UUID lookup that was needed before the
spike — ${CLAUDE_SESSION_ID} replaces it. Frontmatter and curl
examples match the shipped commands/link-ticket-to-session.md.
Verification across the full feature: 75 ticket tests passing
(22 endpoint + 26 parser + 9 schema + 17 hook + 1 backend filter),
ruff clean on all new files, frontend type-check 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per design discussion: skills are the right shape because they can
be invoked two ways — via the slash form (/link-ticket-to-session)
AND via natural language ("link this session to LINEAR-123") — while
commands are only user-typed slash invocations. The narrow
description prevents auto-firing on passing ticket-key mentions
in normal conversation (we explicitly rejected fuzzy prompt
detection during brainstorming).
Changes:
- commands/link-ticket-to-session.md → skills/link-ticket-to-session/SKILL.md
(git rename; contents updated with skill-shaped frontmatter:
`name`, narrower `description`, `argument-hint`, plus
`allowed-tools: Bash, mcp__linear, mcp__claude_ai_Linear,
mcp__plugin_github_github, mcp__atlassian` so the agent can't
reach into unrelated tooling)
- SETUP.md Tier 4 install instructions updated:
symlink/copy is now directory-shaped under ~/.claude/skills/
- Design spec updated: section heading and prose now say
"skill" instead of "slash command". link_source enum value
in the DB stays as `slash_command` (stable API contract; the
skill is still invoked via the slash mechanism).
Verification:
- 75 ticket tests still pass (no DB enum changes)
- npm run check: 0 errors
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug found by the live meta-test of the session-ticket-linking feature (GH issue #72). On a machine where the karma DB had been used on a parallel branch whose linear SCHEMA_VERSION had advanced past ours (observed: v22), the early-return version gate in ensure_schema() if current_version >= SCHEMA_VERSION: return skipped our v11 migration block entirely. Result: `tickets` and `session_tickets` tables never created → every ticket endpoint returned 500. Fix: extract the two ticket-table DDL blocks into a separate constant `_TICKETS_SCHEMA_SQL` and run it UNCONDITIONALLY at the top of ensure_schema() via `conn.executescript()`. All statements use CREATE TABLE IF NOT EXISTS / CREATE INDEX IF NOT EXISTS so this is a no-op when the tables already exist. SCHEMA_SQL also still contains the ticket blocks (via string concat) for the fresh-install path, so callers that only invoke the full SCHEMA_SQL script continue to get every table. Why this design instead of bumping SCHEMA_VERSION past the observed maximum: that fixes one user but not the general case of cross- branch DB drift. The unconditional idempotent approach makes the ticket feature resilient to ANY future version drift, and the same pattern can be applied to other features that need to survive cross-branch DB sharing. Adds api/tests/test_schema_tickets.py::test_ensure_schema_creates_ ticket_tables_on_cross_branch_higher_version — sets schema_version to 99 directly, calls ensure_schema, asserts the ticket tables and the partial-unique index exist afterwards. The recorded version stays at 99 because we deliberately don't pretend we ran a migration — we just ensure tables exist. Verification: 76/76 ticket tests pass (75 + 1 new), 1474/1474 existing tests still pass, ruff clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Briefing document to hand off to claude.ai/design for a visual pass
on the ticket-linking surfaces shipped in this branch.
Contents:
- Self-contained context (problem, goals, in-scope surfaces)
- Real JSON data samples + edge cases (long titles, missing
metadata, orphan links, mixed providers)
- Design tokens already in use (from frontend/src/app.css)
- Constraint list (Svelte 5, Tailwind 4, bits-ui, lucide; no new
deps; WCAG AA; light + dark)
- 8 open questions for design (provider visual language, empty
state, detail-page hierarchy, input micro-interactions, section
placement, unlink button weight, status display, mobile layout)
- Non-goals (don't redesign nav, routes, data model, brand assets)
- Deliverable format I want back (variants + rationale + Tailwind
classes + tokens explicitly named + dark-mode parity + system
spec pages for cross-cutting decisions)
- Round-trip plan: what code returns to design after impl, then
critique + polish + ship
Paired companion `2026-05-18-ticket-linking-ui-log.md` will be
created lazily as decisions land — one section per loop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements UI surfaces 5a + 5b from the design discussion on
project↔ticket relationship. Data model stays Option A (derived
through sessions) — no schema change, karma stays a passive
observer. The relationship is m:n by nature (a ticket can touch
many projects via its linked sessions); these two surfaces make it
navigable from both directions.
5a. <ProjectTicketsCard> on /projects/[project_slug] Overview tab
New `frontend/src/lib/components/tickets/ProjectTicketsCard.svelte`,
sits between ActiveBranches and the Sessions section. Client-side
$effect fetch against the existing `/tickets?project=<encoded>`
endpoint (already shipped in slice c). Renders up to 8 ticket
pills with click-through, an overflow chip ("+N more") that links
to the filtered `/tickets?project=` view, and "View all" header
link. Loading + empty + error states all handled. No change to
+page.server.ts — kept the load function untouched.
5b. Sessions grouped by project on /tickets/[provider]/[external_key]
Reshapes the existing sessions list with a $derived grouping by
`project_encoded_name`. When a ticket touches ≥2 projects, a
"Touched <proj A> (3) · <proj B> (1)" summary line appears next
to the section heading. Each group has its own header (folder
icon + project name + session count) above its sessions. Orphan
links (session_uuid not yet indexed) bucket under a separate
"Not yet indexed" group, always rendered last. Group order:
most-sessions-first for real projects, orphan last.
Brief amendment
§7.9 added to docs/design-briefs/2026-05-18-ticket-linking-ui.md
asking design for two-or-three variants of (a) the project-page
ticket surface and (b) the ticket-detail project breakdown, plus
a cross-cutting question about hierarchy on the ticket detail
page. Framed explicitly as a UI question — the data model
question was resolved (derived stays).
Verification: `npm run check` 0 errors, all 23 ticket endpoint
tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…te input, undo toast, Q9 tabs
Drop-in replacement of the ticket UI per the design deliverables from
the 2026-05-18 iteration (see docs/design-briefs/iterations/2026-05-18/
for screenshots + the README from design's patch).
Decisions locked (full rationale in docs/design-briefs/2026-05-18-
ticket-linking-ui-log.md):
Q1 Provider language — Direction C (industry color + LIN/JIR/GH letter-mark)
Q2 Empty state — Variant A (terminal-flavored, teaches 3 link paths)
Q3 Detail hierarchy — V2 (stats-first hero, sessions main content)
Q4 Link input — full 5-state machine (idle/typing/validating/error/success)
Q5 Session-section — Variant A ($ tickets [N linked] terminal header)
Q6 Unlink — kebab menu (Open/Copy/Unlink) + 5s undo toast
Q7 Status display — normalized dot + verbatim label
Q9a Project ticket surface — TAB on /projects/[slug] (Variant A)
Q9b Sessions by project — TAB row on detail page (Variant A, conditional ≥2 projects)
Deferred to next iteration: Q8 (mobile/responsive) — index still uses
overflow-x; not in critical path.
Code changes:
frontend/src/app.css
+11 new tokens × 2 (light + dark) — --provider-{linear,jira,github}{,-subtle}
and --status-{todo,active,review,done,closed}. All other tokens
design's patch uses (--success, --info, --shadow-md, .focus-ring,
etc.) already existed.
frontend/src/lib/ticket-helpers.ts (new)
Single source of truth for PROVIDER_META, normalizeStatus,
detectProviderFromRef (URL/short-ref auto-detection),
isAmbiguousKey, formatRelative, projectDisplayName.
frontend/src/lib/components/tickets/
TicketBadge.svelte — full rewrite: provider chip + status
dot + kebab menu (pill variant)
TicketLinkInput.svelte — full rewrite: 5-state machine with
rich below-input feedback. Local
var renamed `state` → `phase` to
avoid shadowing the $state rune.
SessionTicketsSection.svelte — full rewrite: terminal header,
optimistic remove + 5s undo toast
with countdown
ProjectTicketsCard.svelte — removed (superseded by tab)
ProjectTicketsTab.svelte — new (Q9a A): project-scoped Tickets
tab with search + empty state +
table reused from /tickets design
frontend/src/routes/tickets/+page.svelte
Full rewrite — segmented provider filter with counts, grid table,
Q2 A empty state.
frontend/src/routes/tickets/[provider]/[external_key]/+page.svelte
Full rewrite — stats-first hero (Projects / Sessions / First seen /
Metadata), Q9b A project tabs conditional on ≥2 projects, source
tags (via /link / via branch / via paste).
frontend/src/routes/projects/[project_slug]/+page.svelte
Added Tickets tab between Memory and Analytics; removed the
inline ProjectTicketsCard from Overview. validTabs updated.
.gitignore
Negation rule: !docs/design-briefs/**/*.png so design iteration
artifacts stay versioned with the feature even though *.png is
ignored at the repo root.
docs/design-briefs/
2026-05-18-ticket-linking-ui-log.md (new) — full decision log
iterations/2026-05-18/ (new) — 13 screenshots from
design + ticket-ui-
review.html shell.
~12 MB.
Local fixes I made to design's patch during application:
• TicketBadge: menuEl typed as HTMLSpanElement (was HTMLDivElement,
type-check rejected the bind:this on the <span>).
• TicketLinkInput: renamed `state` → `phase` (Svelte 5 rune name
collision — `$state` rune shadowed by local var declaration).
Verification:
npm run check 0 errors, no new warnings
pytest ticket tests 76/76 pass (no backend changes)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… loosen pill, fix contrast
After live-screenshot critique against impeccable:frontend-design
guidelines (see screenshots in docs/design-briefs/iterations/), six
calibration fixes:
F1. Rebuild ticket detail page hero (highest-leverage)
Drop the 4-up metric grid (Projects/Sessions/First seen/Metadata)
— the impeccable DON'T list calls out exactly this "big number,
small label, supporting stats" template as AI-slop. Title is now
the protagonist: 24-28px display, generous tracking. Small
identifier row above (provider chip + monospace key + Open in
GitHub button). One subtitle line below with the rollup —
`• open · 1 session · 1 project · first seen just now · synced
just now`. Sessions list takes over as main content immediately.
F2. Loosen pill cramping in TicketBadge.svelte
Drop the max-w-[42ch] outer cap and max-w-[20ch] title truncation.
Title moves to a hover `title=` tooltip on the pill wrapper —
the kebab menu already provides "Open in Linear/Jira/GitHub" so
the inline external-link icon next to the key is redundant when
onRemove is set; hidden in that case. Pill is now: provider chip
+ monospace key + optional status + kebab. Three clean tokens.
F3. (Voice consistency) — verified no change needed.
The terminal "$ tickets [N linked]" voice is already correctly
scoped to in-session contexts (SessionTicketsSection) and empty
states (/tickets when no rows). Catalog and detail pages use the
quiet "Sentence [N]" voice. Rule is intentional.
F4. Replace OCC-1284 ghost data with generic placeholders
OCC-1284 is a real-company project key — leaking it through
empty-state examples implied tenant data. Replaced with:
- `/link-ticket-to-session ABC-123`
- `git checkout -b feat/ABC-123-…`
- `https://linear.app/team/issue/ABC-123`
In three files: /tickets+page.svelte, ProjectTicketsTab.svelte,
TicketLinkInput.svelte.
F5. Provider chip foreground tokens + contrast audit
Added --provider-{linear,jira,github}-fg tokens to app.css
(3 mode blocks × 3 providers = 9 token additions). Light mode
keeps white text everywhere. Dark mode flips:
- --provider-linear-fg: #1a1c4e (dark navy on lightened violet)
- --provider-jira-fg: #001e4d (dark navy on lightened blue)
- --provider-github-fg: #0f172a (dark slate on silver)
Previous dark-mode GitHub chip was white-on-silver — 1.4:1
contrast, critical AA fail. Now all three chips clear AA at
10px small text in both modes.
ProviderMeta in ticket-helpers.ts gains fgVar field. TicketBadge
providerChip snippet + 2 inline chip renders (index table, project
tab table) updated to use `color: var({meta.fgVar})`.
F6. Map "open" status → "active" dot color (was "todo" / gray)
normalizeStatus in ticket-helpers.ts: split `open` out of the
todo cluster. GitHub `open` issues are actively trackable work,
not stale backlog — render with the active (blue) dot, not the
todo (gray) one. Backlog/triage/to-do still map to todo.
Verification:
npm run check 0 errors, no new warnings
59 ticket tests still pass
Live screenshots at 1440x900 against running uvicorn+vite
Round 2 wins (visual):
- Detail page reads as "designed by someone who thought about it"
instead of "AI dashboard template"
- Session-page pill is breathable, scannable
- All ghost data replaced with canonical placeholders
- Dark-mode chips legible at AA across all three providers
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final polish pass after re-screenshotting and re-critiquing against the
impeccable:frontend-design guidelines. All issues found in R2 are now
NIT-level resolved.
1. Detail page subtitle no longer doubles "just now"
Added isSyncedNearFirstSeen() helper — when metadata_updated_at is
within 60s of first_seen_at (i.e., we just got both from MCP at link
time), suppress the "synced X" half of the subtitle. Subtitle now
reads "first seen just now" for a freshly-linked ticket instead of
the redundant "first seen just now · synced just now".
2. Session-page pill now shows status inline
SessionTicketsSection passes showStatus={true} to TicketBadge. Pill
becomes [GH] [key] [• open] [⋯] — status visible without click-through.
Pill stays one comfortable line; the loosened max-width from R2 holds.
3. Project Tickets tab heading demoted
The redundant "Tickets touched by this project" h2 (tab is already
labeled "Tickets") is replaced with a quiet muted text line. Count
moved inline. Tab label is the heading; the body just gives context.
4. "Open in /tickets" reworded to "View all"
Clearer affordance — the user knows they're on the project's tickets
surface; "View all" promises the global view. ExternalLink icon
preserved.
5. Detail-page sessions sort caption tightened
"· sorted by most recently linked" → "· sorted by most recently
linked" (single space). Cosmetic.
Plus: dropped unused TicketIcon import from ProjectTicketsTab.
Verification:
npm run check 0 errors, no new warnings
Live screenshots r3-* confirm each fix
This closes the design iteration loop. The branch is at the state
documented in iterations/2026-05-18 plus two polish rounds (R2 commit
1e78d03 and this R3). PR #73 is ready to merge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes two real bugs surfaced by live use of the ticket linking feature: 1. **Active sessions no longer show as orphans.** Linking ticket #72 to this Claude Code session used to render as "(orphan — not yet indexed)" because the `sessions` SQLite table is only populated by the JSONL indexer at SessionEnd. We now fall through to the live-sessions JSON files (written by hooks/live_session_tracker.py) for any row missing an indexed sessions entry. Active sessions render with their actual slug, project, started time, and a distinct status badge (ACTIVE / WAITING / STARTING / STALE). 2. **Tickets tab on session page** for parity with the project page. The strip above ConversationView is removed; Tickets is now a tab inside ConversationView, sitting between Commands and Analytics — matching the project page's tab order (Memory · Tickets · Analytics). Gated on main sessions only. ## Backend NEW: `api/services/ticket_session_enrichment.py` (148 LoC) - `enrich_sessions_with_live(rows)` — mutates ticket-session-row dicts in place, adds a `live` block and backfills missing sessions-table fields from live state. - `_build_uuid_index` — two-pass: pass 1 indexes historical `session_ids[]` via setdefault; pass 2 overwrites with current `session_id`. Guarantees "current wins" regardless of input order, addressing a fragility flagged in code review. - `_live_block` — narrow projection of LiveSessionState into the response shape. - One filesystem scan per request via `load_all_live_sessions()`; O(N) lookup over result rows. Error-isolated — if live-sessions read fails, rows are returned unmodified. MOD: `api/routers/tickets.py` - `list_sessions_for_ticket` now passes `get_ticket_sessions` output through `enrich_sessions_with_live`. MOD: `api/models/live_session.py` - `load_all_live_sessions` now catches `OSError` and `UnicodeDecodeError` in addition to JSON errors, so a single corrupt or partially-written file no longer aborts the whole batch — important for the enrichment service's best-effort coverage. NEW: `api/tests/test_ticket_session_enrichment.py` (11 tests) - Indexed row preserved, live block None. - Orphan row backfilled with live data; live block populated. - Resumed-session lookup via `session_ids[]` membership. - True orphan stays orphan. - Read failure doesn't break the pipeline. - Fast path (no missing rows) skips the directory scan entirely. - Indexed start_time not overwritten by live data. - Cross-state UUID collision: "current wins" in forward AND reverse input order (regression test for the ordering invariant). ## Frontend NEW types in `api-types.ts`: - `LiveSessionMeta` — `status: LiveSessionState` (reuses existing union type), `started_at`, `last_updated`, `cwd`. - `TicketDetailSessionRow` — full row shape returned by `GET /tickets/.../sessions`, including the `live` field. Centralizes what was a local inline type in +page.server.ts. MOD: `frontend/src/lib/components/conversation/ConversationView.svelte` - New `tickets: SessionTicketRow[]` prop (defaults to `[]`). - Tickets TabsTrigger between Commands and Analytics, count badge matches the Skills/Commands count-span pattern. Inside the `isMainSession` gate. - Tickets Tabs.Content mounts `<SessionTicketsSection>` with the seeded `initial` prop. Sessions without a UUID see a fallback "Session UUID unavailable" message instead of mounting the section (avoids undefined-cascade). - `validTabs` $derived now includes `'tickets'` for main sessions so `?tab=tickets` deep-links correctly. MOD: `frontend/src/routes/projects/[project_slug]/[session_slug]/+page.svelte` - Removed the `<SessionTicketsSection>` strip that previously sat above ConversationView (Q5 A → tab-only consistency). - Passes `tickets` + `sessionUuid` through to ConversationView. MOD: `frontend/src/routes/tickets/[provider]/[external_key]/+page.svelte` - New `liveBadge()` helper maps `LiveSessionState` → label+color. - Each-row block: new derived flags `isLive`, `isActive`, `isOrphan`, `slugLabel`, `navIdentifier`. Click-through href now falls back to UUID-prefix when slug is unavailable (live sessions early in their lifecycle don't have slugs assigned yet). - `activeCount` is now derived from the same live-aware logic as the per-row `isLive`, so the subtitle stat doesn't disagree with the visible list. STALE/STOPPED live sessions correctly count as ended. - True orphans render with "no data" suffix (different from "ACTIVE" / "live-but-no-slug"). MOD: `frontend/src/routes/tickets/[provider]/[external_key]/+page.server.ts` - Imports `TicketDetailSessionRow` from `api-types.ts` instead of re-declaring inline. ## Verification - 70 ticket tests pass (parser 26, schema 10, endpoints 23, enrichment 11). Adds 11 new tests. - npm run check: 0 errors. - ruff: clean. - Live smoke screenshot: session #72 (this one) renders with ACTIVE badge, "1 session (1 active) · 1 project · first seen just now" subtitle, click-through href. ## Process This commit was produced via the feature-dev:feature-dev skill loop: discovery → 2 parallel explorer agents (live-sessions infra + ConversationView tab system) → 3 clarifying questions → architecture proposal + sign-off → implementation → 3 parallel reviewer agents (simplicity, correctness, conventions) → 5 review fixes applied → verify → commit. See PR #73 description for the full thread. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8 tasks
The homepage navigation grid had a low-traffic Archived card. Tickets
is a primary feature in this branch and deserves a home-row slot.
Swap one card in place:
• Archived (History icon, gray) → Tickets (Ticket icon, amber)
• Archived stays accessible via the header nav for users who want it.
• Amber for the warm "active feature" signal; semantically tied to
ticket / sticky-note convention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two small unrelated improvements:
1. /tickets page header
Was: a plain h1 + monospace count + paragraph subtitle, inconsistent
with the rest of the app (Sessions and Projects use <PageHeader>
with icon-on-left + breadcrumb + monospace subtitle pill).
Now: <PageHeader title="Tickets" icon={TicketIcon}
iconColor="--nav-amber" breadcrumbs=[Dashboard / Tickets]
subtitle="Linked across sessions · click through for the
cross-project rollup"/>.
The amber matches the homepage NavigationCard color so the user's
visual handoff from "click Tickets card on home" → "land on
/tickets" is consistent.
2. docs/design-briefs/2026-05-19-nav-header-redesign.md
New brief for claude.ai/design covering the crowded top nav (12
text labels at 1440px). Includes: problem statement, semantic
grouping proposal (Work / Knowledge / Infra / Meta), 7 open
design questions, constraints, and the existing token list. Same
workflow pattern as the ticket-linking design brief.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the design returned from claude.ai/design for the crowded
navigation header (12 link items → wall of labels at 1440px). See
docs/design-briefs/iterations/2026-05-19/C-icon-first-compact.{png,html}
for the source artifacts.
The pattern, per design:
- Inactive items: icon-only (4×4 lucide glyph), text-muted, `title`
attribute provides the tooltip on hover. Halves horizontal footprint.
- Active item: expands to icon + label inside a brand-tinted pill —
background uses `--nav-{color}-subtle`, icon takes `--nav-{color}`,
label is `--text-primary` for legibility. The pill is `px-2.5 py-1.5`
while inactive items are `p-2`.
The brand colors per item mirror the homepage NavigationCard color
exactly, so a user clicking "Tickets" on the home grid → landing on
/tickets sees the same amber tint propagate to the active nav pill.
Continuity of identity across surfaces.
Implementation:
- Refactored Header.svelte from 12 hand-copied <a> blocks to a
single `NAV_ITEMS` array iterated with {#each}. ~140 lines saved.
- Both desktop nav AND mobile drawer use the same array; mobile keeps
text labels alongside icons (vertical space isn't constrained).
- `aria-label` on every item for screen readers; `aria-current="page"`
preserved on the active item; `title` attribute = tooltip text.
- `focus-ring` class on the desktop pill so keyboard nav is visible.
Tradeoff (per design notes): steeper learning curve — new users have
to hover to learn glyphs. Cable/Webhook/Puzzle in particular are not
universally recognisable. Mitigation: tooltips kick in on hover; the
existing ⌘K command palette covers power-user search; the homepage
NavigationCard grid still shows full labels for first-time orientation.
Tokens used: `--nav-{color}`, `--nav-{color}-subtle`, `--text-muted`,
`--text-primary`, `--bg-muted`. No new tokens added.
Verification: `npm run check` 0 errors. Live screenshots on /sessions
(teal active pill) and /tickets (amber active pill) match the design
mock pixel-perfectly except for minor spacing differences explained by
the gap-0.5 we chose vs the gap-1 in the mock — kept tighter to give
even more breathing room.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues reported on /commands:
- The page-header icon was --nav-blue but the homepage card and
new nav pill were both red → cross-surface inconsistency.
- Even after fixing that, Tools/Sessions both used teal and
Hooks/Tickets both used amber — colors weren't distinct.
Now every route has a unique color, and the homepage card ↔ nav pill
↔ page-header icon all match for every section.
Color logic (semantic clusters):
WORK (cool, foundational) Projects=blue · Sessions=teal · Analytics=green
ASSETS (characterful) Tickets=amber · Plans=yellow · Agents=purple · Skills=orange
ACTION (runtime/exec) Commands=red · Hooks=cyan (NEW)
INFRA (plumbing) Tools=indigo · Plugins=violet
META Archived=gray
Changes:
1. Add --nav-cyan + --nav-cyan-subtle to app.css (3 mode blocks: :root,
:root[data-theme='dark'], prefers-color-scheme:dark fallback).
Light: #0e7490 (dark cyan). Dark: #22d3ee (Tailwind cyan-400).
2. NavigationCard.svelte gains 'cyan' in the color union + a
colorConfig entry (gradient + glow tuned for cyan).
3. Routes/+page.svelte (homepage):
Tools: color="teal" → color="indigo"
Hooks: color="amber" → color="cyan"
4. Header.svelte NAV_ITEMS array:
Tools: color: 'teal' → 'indigo'
Hooks: color: 'amber' → 'cyan'
5. Page headers (PageHeader iconColor):
/commands: --nav-blue → --nav-red (the original bug fix)
/tools: --nav-teal → --nav-indigo
/hooks: --nav-amber → --nav-cyan
Visual verification on /, /commands, /tools, /hooks: every active nav
pill matches its page-header icon AND its homepage card color. Zero
duplicates across the 12 routes.
Type-check clean. No new dependencies.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tickets linked from a session in one local project now also appear in
the tickets tab of every sibling project that shares the same git
remote (worktrees, subdir CWDs, multiple clones). Previously each
encoded_name was its own island even when they all pointed at the
same repo on github.
- schema v12: adds projects.git_identity TEXT + idx, idempotent ALTER
(handles the phantom column some DBs already have), nudges
sessions.jsonl_mtime so the periodic indexer backfills on next pass
- services/git_identity.py: pure URL normalizer + `git remote get-url
origin` shellout with 2s timeout, same defensive pattern as
hooks/ticket_branch_detector.py
- indexer wires read_git_identity into _update_project_summaries
- ticket_queries.list_tickets resolves project -> git_identity once,
then takes one of three paths:
A) sibling aggregation: p.git_identity = target
B) github heuristic: external_key LIKE '{identity}#%' surfaces
tickets linked on peer machines even with no local link
C) per-encoded fallback when target git_identity is NULL
- link-ticket-to-session skill: hardcoded localhost:8000 replaced with
inline \${KARMA_API_URL:-http://localhost:8000} per curl (bash state
doesn't persist across Claude Code Bash calls, so per-call is correct)
- tests: 23 unit (URL parser + real-repo integration) + 4 endpoint
(paths A/B/C + sibling count aggregation). Full suite: 1572 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The dashboard had two competing project identifiers (slug, encoded_name)
used inconsistently across the stack: ProjectCard linked via slug,
SessionCard via encoded_name, several components inlined a
`slug || encoded_name` fallback, and 12 API endpoints did raw exact-
match on encoded_name. Bug surface: /projects -> card -> tickets tab
showed empty because the URL carried a slug ('claude-karma-1044')
which the tickets API couldn't match.
Architectural principle now applied: slug at the boundary,
encoded_name inside.
API:
- New `safely_resolve_project()` helper in routers/projects.py — filter-
friendly variant of `resolve_project_identifier` that returns None on
None, returns input verbatim on unknown (so filter endpoints yield
empty lists instead of 404), else returns canonical encoded_name.
- Applied to all 12 endpoints that take a project identifier:
tickets (1), skills (3 + FS fallback), commands (5), agents (2),
live_sessions (1).
- Module-level imports throughout (verified no circular path).
Frontend:
- Route renamed `[project_slug]` -> `[project_id]` to reflect honest
"either form accepted" semantics. All `params.project_slug`,
`data.project_slug`, and route-id strings updated.
- ProjectTicketsTab now receives `data.project.encoded_name` (resolved)
instead of `$page.params.project_id` (raw URL form) — prop matches
its name again.
- Session and agent pages thread `sessionLookup.project_encoded_name`
through to `ConversationView.encodedName` (fixes a parallel bug
where the session page was propagating a slug as 'encoded name').
- Fixed pre-existing typo: SessionLookupResult.project_project_slug
was unreachable; renamed to project_encoded_name to match the API.
- New helper `frontend/src/lib/utils/project-url.ts` —
`projectHref()` + `projectHrefFromSession()` centralize the
`slug || encoded_name` policy. Replaced 4 inline fallbacks.
Tests:
- `tests/test_project_identity_helpers.py` (new): 7 unit tests for
both resolution helpers covering None, empty, encoded_name,
known slug, and unknown identifier branches.
- `tests/api/test_tickets.py`: regression test
`test_project_filter_accepts_slug_or_encoded_name` locking in the
contract that ?project= accepts either form.
- Full suite: 1580 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace 5 inline `/projects/{encoded_name}/{session_slug}` template
strings with `projectHrefFromSession()` from $lib/utils/project-url.
Brings the plans page and ConversationOverview's continuation link in
line with the policy already applied to GlobalSessionCard, LiveSessions*,
and CommandPalette.
Files:
- src/routes/plans/[slug]/+page.svelte: 4 sites (header link + 3 in
Related Sessions list)
- src/lib/components/conversation/ConversationOverview.svelte: 1 site
(continuation session link)
Left as-is:
- src/routes/agents/[name]/+page.svelte:624 — loop variable is a raw
encoded_name string from a dict key, no session object available;
the route accepts either form so `/projects/{project}` works.
- Prop-passing components (SessionCard, ConversationView,
ConversationHeader, TimelineRail) — parent already resolves.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two design briefs that have been sitting in docs/features/ untracked. Both pre-date the session-ticket linking work but were never committed. Pattern follows existing docs/superpowers/specs/ briefs (e.g. the ticket-linking design that landed earlier in this branch). - agent-walkie-talkie.md (828 lines): feature scope for inter-agent communication during long-running tasks. - sync-v4.md (603 lines): Session Sync v4 design. Note: the sync code itself is NOT in this branch — design only. Sync prototype artifacts (sync_* tables, sync-imported project rows) were cleaned out of the local karma DB in this PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promote the session ↔ ticket linking work to a tagged release.
Versions:
- api/pyproject.toml: 0.1.0 → 0.2.0 (minor — new feature, no breaking API)
- frontend/package.json: 0.0.1 → 0.2.0 (parity with api)
New files:
- CHANGELOG.md: Keep-a-Changelog format. v0.2.0 entry documents Added /
Changed / Fixed / Internal sections plus an "Upgrading from 0.1.x"
section that explains the v11 → v12 schema migration is automatic
and idempotent, with a one-shot SQL snippet for users cleaning up
syncthing-prototype leftovers.
Doc updates:
- README.md: new "Ticket Linking" section in Features. Lists the three
link paths (paste / slash command / branch auto-detect) and what users
see on /tickets, the ticket detail page, and the project Tickets tab.
- FEATURES.md: full "Tickets" section under the TOC. Documents both
routes, the three link paths in a table, link-source precedence
(slash_command > dashboard > branch), the git_identity aggregation
semantics, the GitHub-key heuristic, schema, and orphan cleanup.
- SETUP.md:
* Reframed Tier 4 from "Ticket Linking" to "Auto-Link Tickets" — the
/tickets page and dashboard linking UX are Tier 1, only the
workflows (skill + branch hook) are Tier 4.
* Added /tickets to the Tier 1 features table.
* Tier 4 now covers KARMA_API_URL, the "works without MCP" fallback,
a verification checklist, branch detector entry in the All-Tiers
hook config reference, and a removal recipe.
* Updating section now documents the schema-version table (v7 → v12)
and includes the syncthing-leftover cleanup SQL.
Verification:
- pytest from api/: 1580 passed
- npm run check from frontend/: 0 errors
- Live DB upgrade-from-v11 path tested manually: ensure_schema correctly
bumps to v12, recreates idx_projects_git_identity, and ticket
aggregation continues to return the same results.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitHub uses one numbering namespace for issues and pull requests but two URL paths (/issues/N vs /pull/N) and two semantic models — PRs carry draft/merged state that issues don't. The parser was collapsing both to /issues/N at write time, silently telling users that their PRs were issues. The /tickets UI then had no way to distinguish them. Root cause (not the surface): ticket_parser.py treated all three providers asymmetrically — Linear and Jira branches preserve the input URL verbatim (`url=s`), but the GitHub branch rebuilt the URL from regex captures and hard-coded `/issues/` in the format string. The regex correctly accepted `(issues|pull)` but discarded which one matched. Restoring the parser's own preservation pattern is the fix; the bug-locking test (`test_github_url_returns_canonical_form_even_for_pull_url`) is replaced with tests that encode the correct behavior. Parser changes: - `_GITHUB_URL` regex now captures `(?P<kind>issues|pull)` as a named group; the canonical URL is built using that capture instead of hard-coded `/issues/`. Noise (query, fragment, trailing path) is still stripped. - `_GITHUB_SHORT` (bare `owner/repo#N`) continues to default to `/issues/N` because the input doesn't tell us the kind — GitHub auto-redirects /issues/N to /pull/N when N is a PR, so the link still resolves. Comment makes this explicit. - Replaced 1 misleading test, added 4 new tests covering both forms + query stripping + the bare-key default. Frontend changes: - New helper `githubKindFromUrl(url)` derives `'issue' | 'pull_request'` from a github URL. Docstring explains why we don't add a schema column (Linear/Jira have no subtype concept; the URL itself is sufficient; if future kanban status taxonomy needs queryability, that PR can add a column). - New component `ProviderChip.svelte` is the single source of truth for the provider letter-mark (LIN/JIR/GH) and optional `🔀 PR` indicator. Extracted because the chip was inlined at four sites — the original bug would have to be patched at each, with high risk of the indicator being silently omitted at one. Now there's one component to keep honest. - Migrated TicketBadge, ProjectTicketsTab, /tickets index, and ticket detail header to use ProviderChip. Inline chip markup removed. - Added 5 vitest cases for `githubKindFromUrl` covering /pull/, /issues/, query+fragment, null/empty/unrecognized URLs, and a false-match guard. Existing #36 row repair: A pre-existing row in the local DB had `url=/issues/36` but `status=MERGED` (internally inconsistent — MERGED is unique to PRs). Repaired manually via UPDATE statement; backup at ~/.claude_karma/backups/tickets-pre-pr-url-fix-*.sql. Not part of this commit because it's a one-shot data fix, not code. Verified: - api: 1583 passed (was 1580) - frontend: 266 passed (was 261), 0 type errors - Playwright: /tickets index shows `🔀 PR` next to GH chip for #36 and #73 (the two PRs), plain GH for #40-#42 and #72 (issues), unchanged for Linear DE-246 / INF-83 / INF-88 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion brief to the 2026-05-18 ticket-linking UI brief. Documents the intended kanban-shaped board view for /tickets, pre-resolves the contested decisions (toggle not replace; 5 canonical columns matching existing StatusKey; no drag-and-drop because karma is read-only), and leaves seven variant questions for Claude Design to answer with rendered HTML/Tailwind alternatives. Includes ASCII mockups for three direction options (Terminal Kanban, Editorial Inbox, Status Strip). Self-contained: pasteable into a fresh claude.ai/design conversation as the sole context. No code changes — implementation follows once variants are picked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 6 review surfaced four cleanups on commit 4597711; addressing all of them so the fix isn't itself patchy. 1. Drop vestigial `providerChip` snippet in TicketBadge.svelte. After ProviderChip was extracted, the `{#snippet providerChip()}` wrapper was left in place, taking no parameters and doing nothing except relay to `<ProviderChip {ticket} />`. Snippets earn their keep when they take params or have conditional structure — this one had neither. Removed the snippet; inlined `<ProviderChip {ticket} />` at the three callsites (card, inline, pill variants). 2. Harden `githubKindFromUrl` against query-string false-positives. The old regex `/\/pull\/\d+(?:[/?#]|$)/i` ran against the raw URL string, so a URL like `…/issues/1?file=/pull/9` would have incorrectly returned 'pull_request'. The test claimed broader safety than the implementation provided. Switched to `new URL(url).pathname` and tightened the regex to require the `pull/N` after owner/repo segments. Added a dedicated regression test for the query-string case and a malformed-URL test (helper now catches and falls back to 'issue' instead of throwing). 3. Test file conventions: added the `// ===` banner before the `describe` block to match every other test in `frontend/src/tests/` (api-fetch, grouped-projects, utils, tasks all use the same shape). 4. Document the backward-compat story for stale PR rows in pre-v0.2.0 DBs (CHANGELOG) + provide a self-service repair: New endpoint `POST /admin/repair-github-urls`. Conservatively rewrites `/issues/N` URLs to `/pull/N` for github tickets whose status is `MERGED` (a state unique to PRs — issues only have open/closed). Open or closed-unmerged PRs stay ambiguous from cached data alone and self-heal on next re-link. The endpoint is idempotent and reports `{"rewritten": N}`. Backed by 4 endpoint tests covering the happy path, the "leave unambiguous issues alone" guarantee, idempotency, and the github-provider scoping. Verified: - api: 1587 passed (was 1583 → +4 admin tests) - frontend: 268 passed (was 266 → +2 helper tests, false-positive + malformed-URL regressions) - type-check: 0 errors Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two CI breakages introduced by the earlier root-cause fix work: 1. Python Lint (ruff I001) — module-level imports of `safely_resolve_project` in routers/agents.py, routers/commands.py, and routers/skills.py were inserted without re-sorting the surrounding import block. Ruff's import-sort rule flagged them. Fix: `ruff check --fix` reordered the imports. Affects agents.py, commands.py, skills.py, plus a stray blank-line cleanup in test_git_identity.py. 2. Python Tests (3.10 / 3.11 / 3.12) — `tests/test_project_identity_helpers.py` imports `fastapi` at module top to test `resolve_project_identifier` which raises HTTPException. But the unit-test CI job runs with `pytest tests/ --ignore=tests/api/` AND doesn't install fastapi (only the models package's deps). Collection failed with `ModuleNotFoundError: No module named 'fastapi'`. Fix: moved the test to `tests/api/test_project_identity_helpers.py`. The router-layer code it tests (resolve_project_identifier, safely_resolve_project from routers/projects.py) genuinely belongs under tests/api/ — the file's placement at the tests/ root was a judgment slip during the original commit, not a deliberate choice. Verified locally with the exact CI commands: - `ruff check models/ routers/ tests/` → All checks passed - `pytest tests/ --ignore=tests/api/` → 1161 passed (unit-test job) - `pytest tests/` → 1587 passed (full integration job) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous CI fix addressed `ruff check` (the linter) but not `ruff format --check` (the formatter), which is a separate step in `.github/workflows/api-tests.yml`. CI reported "Python Lint: fail" despite linter passing because the formatter found 9 files that needed reformatting. The reformatted files include four written/modified during the recent ticket-linking work plus five untouched-by-me files that were already formatter-divergent in main. Running `ruff format` once normalizes the lot. Verified locally with the exact CI commands: - `ruff check models/ routers/ tests/` → All checks passed - `ruff format --check models/ routers/ tests/` → 91 files already formatted - `pytest tests/ --ignore=tests/api/` → 1161 passed - `pytest tests/` → 1587 passed Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
the-non-expert
approved these changes
May 19, 2026
…kill
The /tickets and project Tickets-tab empty states told first-time users
"the skill is installed by default" — which is false. The skill ships
in the karma repo at skills/link-ticket-to-session/ but only becomes
discoverable to Claude Code after Tier 4 symlinks it into
~/.claude/skills/. Users who saw that copy would type
`/link-ticket-to-session ABC-123` in a session and get nothing.
Three changes:
1. New shared component TicketEmptyState.svelte. Both empty states
(frontend/src/routes/tickets/+page.svelte and
frontend/src/lib/components/tickets/ProjectTicketsTab.svelte) were
inlining the same ~40 lines of markup with one-word copy
differences. Extracted to a single component with a `scope` prop
('global' | 'project') that toggles the small per-context copy
variations. Removes ~80 lines of duplication.
2. Card order is now friction-ascending. Dashboard paste — the only
path that works out of the box with zero setup — leads. Slash
command (one-time install) is second. Branch hook (opt-in, requires
config) is last. Previously the order led with the highest-friction
path (slash command labelled "Fastest"), giving new users the worst
first-run UX.
3. Card #2 (slash command) now carries an inline install block with
both the symlink and the cp -R variants from SETUP.md → Tier 4.
Users see the exact commands they need without leaving the empty
state. Card badges (WORKS OUT OF THE BOX / ONE-TIME SETUP / OPT-IN
HOOK) make the setup expectation explicit at a glance, and a footer
link points to SETUP.md → Tier 4 for the full doc.
Verified:
- frontend type-check: 0 errors
- frontend tests: 268 passed
- Playwright: empty state renders correctly with all three cards,
badges, install commands, and SETUP.md link — screenshot at
docs/screenshots/2026-05-18-git-identity-tickets/16-empty-state-project-tab.png
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Real-world usage of the skill in this branch exposed three gaps that the previous SKILL.md didn't address: 1. **GitHub URL kind not surfaced.** The skill listed GitHub URLs as /issues/N only. PRs use /pull/N, and karma's parser (fixed in 4597711) now preserves that distinction. If the agent doesn't know to keep the URL kind, it sends a /issues/ URL for a PR and the UI shows it as an issue. Step 1 now has a 4-row table making both issue and PR URL forms explicit, with a note that the URL kind is the only signal between the two. 2. **Which GitHub MCP tool to call wasn't specified.** The MCP exposes `issue_read` and `pull_request_read` as separate tools. The agent had to guess. Step 3 now has a provider · kind → MCP tool table so the agent picks correctly. Calling the wrong one returns the wrong shape silently. 3. **PR status semantics were hidden.** GitHub PRs have draft + merged flags that don't map cleanly to issue-style open/closed. Step 3 now includes a small flag-truth-table mapping `(state, draft, merged)` triples to the canonical status string to cache (draft / open / MERGED / closed). Linear and Jira keep their workspace-defined workflow state names verbatim. Other tightenings: - Description includes "GitHub Pull Requests" explicitly so it triggers on PR-flavored requests, not just issues. - The 64KB metadata cap warning now lists which fields are big per provider (PRs' commits/files/reviewers arrays are the usual culprits). - Step 4 explicitly says "don't rewrite the URL kind when POSTing" — important because karma's backend now preserves it. - Step 6 examples include both [issue] and [PR] confirmation lines so the user sees the kind in the slash-command output. Name kept as `link-ticket-to-session`: - "Ticket" is the umbrella term that already covers Linear's issues, Jira's stories/tasks/epics, and both GitHub issues and PRs. - Renaming to `link-issue-or-pr` would over-index on GitHub vocabulary and break every existing symlinked install. - Karma's data model is `tickets`, not `issues_or_prs` — name should align with storage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When GitHub is the active provider on /tickets, a secondary sub-pill
row appears showing `[All N] [Issues M] [PRs K]`. The kind sub-filter
narrows the visible tickets to issues, PRs, or both. URL state lives
at `?kind=issue|pull_request`. The sub-pill row is hidden when any
other provider (or All) is active — kind is a GitHub-specific concept
and surfacing it elsewhere would be dishonest.
Design choice: Design A from the brief — progressive disclosure under
GitHub. Rejected B (five top-level chips, asymmetric, long row) and C
(independent Type dimension, conceptually muddy because "Issues" would
have to span Linear/Jira/GH-issues).
Filter logic is client-side, deliberately. The backend doesn't gain a
new query param — kind is derivable from the ticket URL via the
existing `githubKindFromUrl()` helper, and the ticket list is short
enough that a `.filter()` per render is free. This keeps the diff to
one frontend file (plus a one-line addition to +page.server.ts to
surface kind as URL state), preserves the cross-machine sync property
of all filter state living in the URL, and matches how `q` / project
/ provider already filter via `data.tickets` post-load.
Implementation details:
- New URL param `kind` ∈ '' | 'issue' | 'pull_request'. Added to
+page.server.ts under `filters` so links remain shareable.
- `setProvider(next)` helper replaces the old inline onclick. When the
user switches away from GitHub, it drops `kind` from both local
state and the URL — kind only applies under GitHub, leaving it
stuck in the URL would be a latent bug.
- `setKind(next)` is a small mirror for the sub-pill clicks.
- `visibleTickets` is the new `$derived` that filters `data.tickets`
client-side when kind is set. The `{#each}` loop and the
no-results state both switch to `visibleTickets`.
- `githubKindCounts` is a separate `$derived` for the sub-pill badges,
computed against the full `data.tickets` so the counts stay sane
even when filtering is active.
- The sub-pill row uses a smaller chip size (px-2.5 py-0.5 vs
px-3 py-1, 11px vs 12px) and a `CornerDownRight` glyph for the
visual indent — matches the "under GitHub" semantic without being a
full second row.
- hasFilters now includes `kind` so the empty-state heuristic still
works.
Verified:
- frontend type-check: 0 errors
- frontend tests: 268 passed (unchanged)
- Playwright: All / Linear / Jira / GitHub / GH+Issues / GH+PRs flows
render correctly; sub-pills appear only under GitHub; counts match.
Screenshots at docs/screenshots/2026-05-18-git-identity-tickets/17-19.
Deferred (per design brief, follow-up PR):
- Same sub-filter on the ProjectTicketsTab. Project tab today only
has a search box, no provider chips, so adding kind there would
also add a provider filter and grows scope. Worth doing once the
filter pattern is proven on the global view.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Links Claude Code sessions to tickets (Linear, Jira, GitHub Issues) and surfaces them across the dashboard. Tickets attach to sessions via three paths — branch name auto-detect, slash command, and dashboard paste — and aggregate across all checkouts of the same git repo via a new
git_identitycolumn.Closes #72.
What's in this PR
Backend (FastAPI)
tickets+session_ticketstables (idempotent CREATE, FK with cascade, partial unique index on slug, 64KB metadata cap)projects.git_identitycolumn populated at index time fromgit remote get-url origin, normalized to lowercaseowner/repoPOST/GET/DELETE /sessions/{uuid}/tickets,GET/PUT/PATCH /tickets/{provider}/{external_key},GET /tickets,GET /tickets/{provider}/{external_key}/sessionslist_tickets(project=…)resolves project'sgit_identityand aggregates tickets across all sibling encoded_names sharing it (e.g., main clone + frontend subdir + worktrees all show the same tickets)owner/repo#42surface under projects withgit_identity=owner/repoeven without a local link — supports cross-machine syncsafely_resolve_project()lets every endpoint accept either slug or encoded_name uniformly; applied to 12 endpoints (tickets, skills × 3, commands × 5, agents × 2, live_sessions)cleanup_orphan_session_ticketsFrontend (SvelteKit / Svelte 5)
/tickets: global tickets index (filterable, searchable)/tickets/{provider}/{external_key}: ticket detail with linked sessionsProjectTicketsTab: new tab on every project page showing tickets touched by sessions in that project (with cross-encoded aggregation)SessionTicketsSection: ticket links inline on session pages with link/unlink UITicketBadge,TicketLinkInput: 5-state input component for paste/link UX[project_slug]→[project_id]— honest naming for the dual-form identifierprojectHref()+projectHrefFromSession()centralize theslug || encoded_namepolicy (used by GlobalSessionCard, LiveSessions*, CommandPalette)Hooks (Python / captain-hook)
ticket_branch_detector.py: SessionStart hook auto-detects ticket refs from git branch names (feat/ABC-123-foo→ links toLINEAR-123)Skill
link-ticket-to-session: slash-command-as-skill for manual linking via MCP-provided title/status. HonorsKARMA_API_URLenv var for non-default ports.Tests
test_tickets.py(40 cases),test_schema_tickets.py(8 cases),test_ticket_parser.py,test_ticket_session_enrichment.py,test_git_identity.py(23 cases),test_project_identity_helpers.py(7 cases)test_ticket_branch_detector.py(299 LOC)Bug fix included (b68e7fd)
After the initial implementation, surfaced a routing bug: navigating
/projects → card → tickets tabshowed empty becauseProjectCardlinked via slug while the tickets API only matched on encoded_name. The fix unifies the data flow with a clean architecture — slug at the boundary, encoded_name inside — covered by 7 new unit tests and a Playwright walk-through of both navigation paths.Test plan
pytestfromapi/— full suite passesnpm run checkfromfrontend/— type-check clean/projects→ click a project card → Tickets tab loads (slug URL form)/tickets→ click a ticket → click a linked session → Tickets tab on the project page loads (encoded_name URL form)claude-karma/frontend) show the same tickets as the main project (cross-encoded aggregation)/link-ticket-to-session LINEAR-123in a Claude Code session creates a link visible in karmafeat/ABC-123-fooand verify the hook auto-creates the link🤖 Generated with Claude Code