[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6C7H2QVVKPRZED1K609WWWM":3,"skill-same:01M6C7H2QVVKPRZED1K609WWWM":67},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":25,"publishedAt":35,"linkOnly":36,"markdownContent":37,"markdownBody":38,"frontMatter":39,"refName":40,"commitSha":41,"skillDirectory":42,"forksCount":43,"repositoryHtmlUrl":44,"repositoryHomepageUrl":6,"ownerHtmlUrl":45,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":46,"install":47,"resources":60},"01M6C7H2QVVKPRZED1K609WWWM","ui-data-layer",null,"Svelte + TanStack Query v6 data fetching patterns. Use when building or migrating web UI data fetching, cache management, WebSocket invalidation, or query\u002Fmutation patterns.","lepahc\u002Fkynetic-spec","lepahc","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F255431765?v=4","01M6C7E6Z0C8SE1X6PJ2F9Q1CX","01M6C7E744QKNYBVZ7KV7Z69GF",1,"MIT","https:\u002F\u002Fgithub.com\u002Flepahc\u002Fkynetic-spec\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fui-data-layer\u002FSKILL.md",[17,22],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000032","设计师","PROFESSION","适用职业",{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000033","数据分析师",[26,27,28,29,30,31,32,33,34],"ui","data","ui data layer","web","cache","websocket","tanstack","management","use","2026-11-10 10:12:30",false,"---\nname: ui-data-layer\ndescription: Svelte + TanStack Query v6 data fetching patterns. Use when\n  building or migrating web UI data fetching, cache management, WebSocket\n  invalidation, or query\u002Fmutation patterns.\n---\n\u003C!-- kspec-managed -->\n\n# UI Data Layer — Svelte + TanStack Query\n\nPatterns and conventions for the web UI data fetching layer. Covers TanStack Query v6 setup, query factories, WebSocket-driven cache invalidation, mutations, and migration from manual fetch patterns.\n\n**Companion to:** `\u002Fsvelte-5` skill (reactivity, SSR, state management). This skill covers the data layer specifically.\n\n## Technology Choice\n\n**@tanstack\u002Fsvelte-query v6** — Svelte 5 runes-native (~13 kB gzip).\n\nKey capabilities used in this project:\n- Automatic caching with stale-while-revalidate\n- Request deduplication (concurrent identical requests share one fetch)\n- Background revalidation on navigation\n- Query invalidation via `queryClient.invalidateQueries()`\n- Direct cache updates via `queryClient.setQueryData()`\n- Infinite queries for paginated data (`createInfiniteQuery`)\n\n**Requires:** Svelte >= 5.25.0\n\n### References\n\n- [TanStack Query Svelte Overview](https:\u002F\u002Ftanstack.com\u002Fquery\u002Fv5\u002Fdocs\u002Fframework\u002Fsvelte\u002Foverview)\n- [v5 → v6 Migration Guide](https:\u002F\u002Ftanstack.com\u002Fquery\u002Fv5\u002Fdocs\u002Fframework\u002Fsvelte\u002Fmigrate-from-v5-to-v6)\n- [createQuery API Reference](https:\u002F\u002Ftanstack.com\u002Fquery\u002Fv5\u002Fdocs\u002Fframework\u002Fsvelte\u002Freference\u002Ffunctions\u002Fcreatequery)\n- [createInfiniteQuery API Reference](https:\u002F\u002Ftanstack.com\u002Fquery\u002Fv5\u002Fdocs\u002Fframework\u002Fsvelte\u002Freference\u002Ffunctions\u002FcreateInfiniteQuery)\n- [createMutation API Reference](https:\u002F\u002Ftanstack.com\u002Fquery\u002Fv5\u002Fdocs\u002Fframework\u002Fsvelte\u002Freference\u002Ffunctions\u002Fcreatemutation)\n- [SSR & SvelteKit Guide](https:\u002F\u002Ftanstack.com\u002Fquery\u002Fv5\u002Fdocs\u002Fframework\u002Fsvelte\u002Fssr)\n\n## Quick Reference\n\n| Need | Pattern |\n|------|---------|\n| Fetch and cache data | `createQuery(() => ({ queryKey, queryFn }))` |\n| Paginated \u002F infinite scroll | `createInfiniteQuery(() => ({ queryKey, queryFn, getNextPageParam, initialPageParam }))` |\n| Write operation with cache update | `createMutation(() => ({ mutationFn, onSuccess: invalidate }))` |\n| Invalidate after WS event | `queryClient.invalidateQueries({ queryKey: ['tasks'] })` |\n| Update cache without refetch | `queryClient.setQueryData(['tasks', ref], newData)` |\n| Clear all cache (project switch) | `queryClient.clear()` |\n| Access QueryClient in component | `useQueryClient()` |\n\n## Setup\n\n### QueryClient Configuration\n\n```typescript\n\u002F\u002F src\u002Flib\u002Fquery\u002Fclient.ts\nimport { QueryClient } from '@tanstack\u002Fsvelte-query';\n\nexport function createAppQueryClient(): QueryClient {\n  return new QueryClient({\n    defaultOptions: {\n      queries: {\n        \u002F\u002F Localhost daemon — data changes via WS events, not staleness\n        staleTime: 30_000,        \u002F\u002F 30s — lists and frequently changing data\n        gcTime: 10 * 60_000,      \u002F\u002F 10min — keep in memory for session\n        retry: 1,                 \u002F\u002F Localhost: one retry is enough\n        retryDelay: 500,          \u002F\u002F Short delay — daemon is local\n        refetchOnWindowFocus: false, \u002F\u002F WS events handle freshness\n      },\n    },\n  });\n}\n```\n\n### Provider in Root Layout\n\n```svelte\n\u003C!-- src\u002Froutes\u002F+layout.svelte -->\n\u003Cscript lang=\"ts\">\n  import { QueryClientProvider } from '@tanstack\u002Fsvelte-query';\n  import { createAppQueryClient } from '$lib\u002Fquery\u002Fclient';\n\n  const queryClient = createAppQueryClient();\n\u003C\u002Fscript>\n\n\u003CQueryClientProvider client={queryClient}>\n  \u003Cslot \u002F>\n\u003C\u002FQueryClientProvider>\n```\n\n## v6 API — Thunk Pattern (Critical)\n\nIn v6, **all query\u002Fmutation functions take a thunk** (function returning options), not a plain object. This enables Svelte 5 runes reactivity without stores.\n\n```typescript\n\u002F\u002F v5 (WRONG in v6)\nconst query = createQuery({\n  queryKey: ['tasks'],\n  queryFn: fetchTasks,\n});\n\n\u002F\u002F v6 (CORRECT) — options wrapped in () => ({...})\nconst query = createQuery(() => ({\n  queryKey: ['tasks'],\n  queryFn: fetchTasks,\n}));\n```\n\n**Why:** The thunk is re-executed when reactive dependencies inside it change. This is how reactive query keys work — Svelte's fine-grained reactivity tracks which `$state`\u002F`$derived` values the thunk reads.\n\n### Accessing Query Results\n\nv6 returns a plain object (not a Svelte store). No `$` prefix needed:\n\n```svelte\n\u003Cscript lang=\"ts\">\n  const tasks = createQuery(() => ({\n    queryKey: ['tasks'],\n    queryFn: () => fetchTasks(),\n  }));\n\u003C\u002Fscript>\n\n\u003C!-- Direct property access — no $tasks needed -->\n{#if tasks.isPending}\n  \u003CLoading \u002F>\n{:else if tasks.isError}\n  \u003CError message={tasks.error.message} \u002F>\n{:else}\n  {#each tasks.data.items as task}\n    \u003CTaskRow {task} \u002F>\n  {\u002Feach}\n{\u002Fif}\n```\n\n### Reactive Query Keys\n\nWhen the query key depends on reactive state, include the state in the thunk:\n\n```svelte\n\u003Cscript lang=\"ts\">\n  import { page } from '$app\u002Fstores';\n\n  let status = $derived($page.url.searchParams.get('status') ?? 'all');\n\n  const tasks = createQuery(() => ({\n    queryKey: ['tasks', { status }],\n    queryFn: () => fetchTasks({ status }),\n    enabled: true,\n  }));\n\u003C\u002Fscript>\n```\n\nWhen `status` changes (via URL param), the thunk re-runs, producing a new queryKey. TanStack Query detects the key change and fetches fresh data.\n\n## Query Key Conventions\n\n### Naming Pattern\n\nUse hierarchical arrays with entity type first, then qualifiers:\n\n```typescript\n\u002F\u002F Entity lists\n['tasks']\n['tasks', { status: 'pending', tag: 'web-ui' }]\n['items']\n['items', { type: 'feature' }]\n['inbox']\n['sessions', { status: 'active' }]\n\n\u002F\u002F Entity detail\n['tasks', ref]          \u002F\u002F e.g. ['tasks', '@task-add-auth']\n['items', ref]\n['sessions', sessionId]\n\n\u002F\u002F Aggregations \u002F summaries\n['tasks', 'summary']   \u002F\u002F Status counts\n['validation']\n['alignment']\n\n\u002F\u002F Lightweight indexes\n['refs', 'index']       \u002F\u002F Ref → title\u002Ftype\u002Fstatus map\n\n\u002F\u002F Counts (sidebar badges)\n['inbox', 'count']\n['observations', 'count']\n['tasks', 'pending-review-count']\n```\n\n### Query Key Factory\n\nCentralize keys to prevent typos and enable targeted invalidation:\n\n```typescript\n\u002F\u002F src\u002Flib\u002Fquery\u002Fkeys.ts\nexport const queryKeys = {\n  tasks: {\n    all: ['tasks'] as const,\n    lists: () => [...queryKeys.tasks.all] as const,\n    list: (filters: TaskFilters) => [...queryKeys.tasks.all, filters] as const,\n    detail: (ref: string) => [...queryKeys.tasks.all, ref] as const,\n    summary: () => [...queryKeys.tasks.all, 'summary'] as const,\n    pendingReviewCount: () => [...queryKeys.tasks.all, 'pending-review-count'] as const,\n  },\n  items: {\n    all: ['items'] as const,\n    lists: () => [...queryKeys.items.all] as const,\n    list: (filters: ItemFilters) => [...queryKeys.items.all, filters] as const,\n    detail: (ref: string) => [...queryKeys.items.all, ref] as const,\n  },\n  inbox: {\n    all: ['inbox'] as const,\n    list: (filters?: InboxFilters) => [...queryKeys.inbox.all, filters] as const,\n    count: () => [...queryKeys.inbox.all, 'count'] as const,\n  },\n  sessions: {\n    all: ['sessions'] as const,\n    list: (filters: SessionFilters) => [...queryKeys.sessions.all, filters] as const,\n    detail: (id: string) => [...queryKeys.sessions.all, id] as const,\n  },\n  agents: {\n    all: ['agents'] as const,\n    status: () => [...queryKeys.agents.all, 'status'] as const,\n    definitions: () => [...queryKeys.agents.all, 'definitions'] as const,\n  },\n  refs: {\n    index: () => ['refs', 'index'] as const,\n  },\n  validation: () => ['validation'] as const,\n  alignment: () => ['alignment'] as const,\n  observations: {\n    all: ['observations'] as const,\n    list: (filters?: ObservationFilters) => [...queryKeys.observations.all, filters] as const,\n    count: () => [...queryKeys.observations.all, 'count'] as const,\n  },\n} as const;\n```\n\n**Usage:**\n\n```typescript\n\u002F\u002F In a query\nconst tasks = createQuery(() => ({\n  queryKey: queryKeys.tasks.list({ status: 'pending' }),\n  queryFn: () => fetchTasks({ status: 'pending' }),\n}));\n\n\u002F\u002F For invalidation — invalidate all tasks queries\nqueryClient.invalidateQueries({ queryKey: queryKeys.tasks.all });\n\n\u002F\u002F For targeted invalidation — only list queries\nqueryClient.invalidateQueries({ queryKey: queryKeys.tasks.lists() });\n```\n\n## Cache Timing Conventions\n\n| Data Type | staleTime | gcTime | Rationale |\n|-----------|-----------|--------|-----------|\n| Entity lists (tasks, items) | 30s | 10 min | Frequently changing, WS handles freshness |\n| Entity detail | 30s | 10 min | Same as lists |\n| Ref index (title resolution) | 5 min | 30 min | Rarely changes, expensive to compute |\n| Sidebar badge counts | 30s | 5 min | Small payload, WS-invalidated |\n| Validation \u002F alignment | 60s | 10 min | Expensive to compute, rarely polled |\n| Agent status | 10s | 5 min | Needs near-real-time feel |\n| Session context | 5 min | 30 min | Changes on project switch only |\n\nOverride per-query in the factory:\n\n```typescript\nconst refIndex = createQuery(() => ({\n  queryKey: queryKeys.refs.index(),\n  queryFn: () => fetchRefIndex(),\n  staleTime: 5 * 60_000,  \u002F\u002F 5 minutes — titles rarely change\n  gcTime: 30 * 60_000,    \u002F\u002F 30 minutes\n}));\n```\n\n## Creating a New Query — Step by Step\n\n### 1. Add query key to factory\n\n```typescript\n\u002F\u002F src\u002Flib\u002Fquery\u002Fkeys.ts\nexport const queryKeys = {\n  \u002F\u002F ...existing keys...\n  workflows: {\n    all: ['workflows'] as const,\n    list: () => [...queryKeys.workflows.all] as const,\n  },\n};\n```\n\n### 2. Create the query in the component\n\n```svelte\n\u003Cscript lang=\"ts\">\n  import { createQuery } from '@tanstack\u002Fsvelte-query';\n  import { fetchWorkflows } from '$lib\u002Fapi';\n  import { queryKeys } from '$lib\u002Fquery\u002Fkeys';\n  import { isStaticMode } from '$lib\u002Fstores\u002Fmode.svelte';\n  import { isProjectInitialized, getProjectVersion } from '$lib\u002Fstores\u002Fproject.svelte';\n\n  const workflows = createQuery(() => ({\n    queryKey: queryKeys.workflows.list(),\n    queryFn: () => fetchWorkflows(),\n    enabled: isProjectInitialized(),\n  }));\n\u003C\u002Fscript>\n\n{#if workflows.isPending}\n  \u003CLoadingSkeleton \u002F>\n{:else if workflows.isError}\n  \u003CErrorMessage error={workflows.error} \u002F>\n{:else}\n  {#each workflows.data.items as workflow}\n    \u003CWorkflowCard {workflow} \u002F>\n  {\u002Feach}\n{\u002Fif}\n```\n\n### 3. Wire WS invalidation (if applicable)\n\n```typescript\n\u002F\u002F In the WebSocket → query invalidation wiring\ncase 'workflows':\n  queryClient.invalidateQueries({ queryKey: queryKeys.workflows.all });\n  break;\n```\n\n## Creating a Mutation\n\nMutations handle write operations (start task, add note, create inbox item, etc.).\n\n```svelte\n\u003Cscript lang=\"ts\">\n  import { createMutation, useQueryClient } from '@tanstack\u002Fsvelte-query';\n  import { startTask } from '$lib\u002Fapi';\n  import { queryKeys } from '$lib\u002Fquery\u002Fkeys';\n\n  const queryClient = useQueryClient();\n\n  const startTaskMutation = createMutation(() => ({\n    mutationFn: (ref: string) => startTask(ref),\n    onSuccess: () => {\n      \u002F\u002F Invalidate task queries so lists and details refresh\n      queryClient.invalidateQueries({ queryKey: queryKeys.tasks.all });\n    },\n    onError: (error: Error) => {\n      \u002F\u002F Show error toast\n      addToast({ type: 'error', message: error.message });\n    },\n  }));\n\n  function handleStartTask(ref: string) {\n    startTaskMutation.mutate(ref);\n  }\n\u003C\u002Fscript>\n\n\u003Cbutton\n  onclick={() => handleStartTask(task._ref)}\n  disabled={startTaskMutation.isPending}\n>\n  {startTaskMutation.isPending ? 'Starting...' : 'Start Task'}\n\u003C\u002Fbutton>\n```\n\n### Optimistic Updates\n\nFor mutations where you want the UI to update instantly:\n\n```typescript\nconst addNoteMutation = createMutation(() => ({\n  mutationFn: ({ ref, content }: { ref: string; content: string }) =>\n    addTaskNote(ref, content),\n  onMutate: async ({ ref, content }) => {\n    \u002F\u002F Cancel outgoing refetches\n    await queryClient.cancelQueries({ queryKey: queryKeys.tasks.detail(ref) });\n\n    \u002F\u002F Snapshot previous value\n    const previous = queryClient.getQueryData(queryKeys.tasks.detail(ref));\n\n    \u002F\u002F Optimistically update\n    queryClient.setQueryData(queryKeys.tasks.detail(ref), (old: TaskDetail) => ({\n      ...old,\n      notes: [...old.notes, { content, created_at: new Date().toISOString() }],\n    }));\n\n    return { previous };\n  },\n  onError: (_err, _vars, context) => {\n    \u002F\u002F Roll back on error\n    if (context?.previous) {\n      queryClient.setQueryData(queryKeys.tasks.detail(ref), context.previous);\n    }\n  },\n  onSettled: () => {\n    \u002F\u002F Always refetch after mutation\n    queryClient.invalidateQueries({ queryKey: queryKeys.tasks.all });\n  },\n}));\n```\n\n## WebSocket → Query Invalidation\n\nThe WebSocket manager receives broadcast events. Wire these to query invalidation so active views revalidate automatically.\n\n### Wiring Pattern\n\n```typescript\n\u002F\u002F src\u002Flib\u002Fquery\u002Fws-invalidation.ts\nimport type { QueryClient } from '@tanstack\u002Fsvelte-query';\nimport type { BroadcastEvent } from '@kynetic-ai\u002Fshared';\nimport { queryKeys } from '.\u002Fkeys';\n\nexport function handleBroadcastInvalidation(\n  queryClient: QueryClient,\n  event: BroadcastEvent,\n): void {\n  switch (event.topic) {\n    case 'tasks':\n      queryClient.invalidateQueries({ queryKey: queryKeys.tasks.all });\n      queryClient.invalidateQueries({ queryKey: queryKeys.tasks.pendingReviewCount() });\n      \u002F\u002F If enriched payload includes the full task, update cache directly\n      if (event.data?.task) {\n        queryClient.setQueryData(\n          queryKeys.tasks.detail(event.data.ref),\n          event.data.task,\n        );\n      }\n      break;\n\n    case 'inbox':\n      queryClient.invalidateQueries({ queryKey: queryKeys.inbox.all });\n      break;\n\n    case 'agents':\n      queryClient.invalidateQueries({ queryKey: queryKeys.agents.all });\n      break;\n\n    case 'files':\n      \u002F\u002F Settings\u002Fconfig changes\n      queryClient.invalidateQueries({ queryKey: queryKeys.validation() });\n      queryClient.invalidateQueries({ queryKey: queryKeys.alignment() });\n      queryClient.invalidateQueries({ queryKey: queryKeys.items.all });\n      queryClient.invalidateQueries({ queryKey: queryKeys.tasks.all });\n      break;\n  }\n}\n```\n\n### Integration with WebSocket Manager\n\n```typescript\n\u002F\u002F In root layout or connection setup\nimport { on } from '$lib\u002Fstores\u002Fconnection.svelte';\nimport { handleBroadcastInvalidation } from '$lib\u002Fquery\u002Fws-invalidation';\n\n\u002F\u002F Subscribe to all relevant topics\nsubscribe(['tasks', 'agents', 'inbox', 'files']);\n\n\u002F\u002F Single handler for all invalidations\nfunction handleBroadcast(event: BroadcastEvent) {\n  handleBroadcastInvalidation(queryClient, event);\n}\n\non('tasks', handleBroadcast);\non('agents', handleBroadcast);\non('inbox', handleBroadcast);\non('files', handleBroadcast);\n```\n\n**Key principle:** After wiring WS → invalidation, individual pages no longer need `on('tasks', reloadEverything)` handlers. TanStack Query handles refetching active queries automatically.\n\n## Ref Index for Title Resolution\n\nInstead of `fetchTasks({ limit: 1000 })` to get task titles for display, use the lightweight ref index endpoint:\n\n```typescript\n\u002F\u002F Query for the ref index (long staleTime — titles rarely change)\nconst refIndex = createQuery(() => ({\n  queryKey: queryKeys.refs.index(),\n  queryFn: () => fetchRefIndex(),\n  staleTime: 5 * 60_000,\n  gcTime: 30 * 60_000,\n}));\n\n\u002F\u002F Usage in template\nfunction resolveTitle(ref: string): string {\n  return refIndex.data?.[ref]?.title ?? ref;\n}\n```\n\nAfter the server-side resolution work is done, most title resolution will happen inline in API responses (e.g., `spec_title` alongside `spec_ref`), reducing the need for client-side lookups.\n\n## Project Switch — Cache Clearing\n\nWhen the user switches projects, all cached data must be discarded:\n\n```typescript\n\u002F\u002F In project switch handler\nimport { useQueryClient } from '@tanstack\u002Fsvelte-query';\n\nfunction handleProjectSwitch(newPath: string) {\n  const queryClient = useQueryClient();\n\n  \u002F\u002F Clear ALL cached data\n  queryClient.clear();\n\n  \u002F\u002F Set new project and trigger reloads\n  selectProject(newPath);\n}\n```\n\n`queryClient.clear()` removes all queries from the cache. New queries will fetch fresh data with the updated `X-Kspec-Dir` header.\n\n## Static Mode Compatibility\n\nQuery factories must respect static mode. The existing `fetchTasks()`, `fetchItems()`, etc. in `api.ts` already dispatch to static implementations when `isStaticMode()` is true — so queries \"just work\" in static mode.\n\nKey considerations:\n\n```typescript\n\u002F\u002F Mutations should be disabled in static mode\nconst startTaskMutation = createMutation(() => ({\n  mutationFn: (ref: string) => startTask(ref),  \u002F\u002F Throws ReadOnlyModeError in static mode\n  \u002F\u002F ...\n}));\n\n\u002F\u002F Agent-specific queries should be disabled\nconst agentStatus = createQuery(() => ({\n  queryKey: queryKeys.agents.status(),\n  queryFn: () => fetchAgentStatus(),\n  enabled: !isStaticMode() && isProjectInitialized(),  \u002F\u002F Skip in static mode\n}));\n```\n\n**WebSocket connections are not established in static mode**, so WS-driven invalidation is naturally disabled. The `subscribe()` calls in the connection store are no-ops when static.\n\n## Error Handling and Retry Config\n\nThe daemon runs on localhost — network flakiness is rare. Connection failures usually mean the daemon isn't running.\n\n```typescript\n\u002F\u002F Default query client config (see Setup section)\n{\n  retry: 1,              \u002F\u002F One retry — if daemon is down, more retries won't help\n  retryDelay: 500,       \u002F\u002F Short delay — it's localhost\n  refetchOnWindowFocus: false,  \u002F\u002F WS events handle freshness\n}\n```\n\n### Error Display Pattern\n\n```svelte\n{#if query.isError}\n  {#if query.error.message.includes('fetch')}\n    \u003CAlert type=\"warning\">\n      Daemon not reachable. Start it with \u003Ccode>kspec serve\u003C\u002Fcode>.\n    \u003C\u002FAlert>\n  {:else}\n    \u003CAlert type=\"error\">{query.error.message}\u003C\u002FAlert>\n  {\u002Fif}\n{\u002Fif}\n```\n\n### Invalid Project Recovery\n\nThe existing `handleResponseError()` in `api.ts` detects invalid project responses (400\u002F404) and calls `clearInvalidSelection()`. This works the same through TanStack Query — the error propagates to the query's `error` state.\n\n## Sessions Infinite Scroll — createInfiniteQuery\n\nThe sessions page uses `createInfiniteQuery` for paginated data with scroll-to-load-more:\n\n```svelte\n\u003Cscript lang=\"ts\">\n  import { createInfiniteQuery } from '@tanstack\u002Fsvelte-query';\n  import { queryKeys } from '$lib\u002Fquery\u002Fkeys';\n\n  const PAGE_SIZE = 25;\n\n  const sessions = createInfiniteQuery(() => ({\n    queryKey: queryKeys.sessions.list({ status: filterStatus }),\n    queryFn: ({ pageParam }) =>\n      fetchSessions({ offset: pageParam, limit: PAGE_SIZE, status: filterStatus }),\n    initialPageParam: 0,\n    getNextPageParam: (lastPage, allPages) => {\n      const totalFetched = allPages.reduce((sum, p) => sum + p.items.length, 0);\n      return totalFetched \u003C lastPage.total ? totalFetched : undefined;\n    },\n  }));\n\n  \u002F\u002F Flatten pages for rendering\n  let allSessions = $derived(sessions.data?.pages.flatMap(p => p.items) ?? []);\n\u003C\u002Fscript>\n\n{#each allSessions as session}\n  \u003CSessionRow {session} \u002F>\n{\u002Feach}\n\n{#if sessions.hasNextPage}\n  \u003Cdiv use:intersectionObserver={() => sessions.fetchNextPage()}>\n    {#if sessions.isFetchingNextPage}\n      \u003CLoadingMore \u002F>\n    {\u002Fif}\n  \u003C\u002Fdiv>\n{\u002Fif}\n```\n\n### Key Properties\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `data.pages` | `Page[]` | Array of fetched pages |\n| `data.pageParams` | `number[]` | Array of page params used |\n| `fetchNextPage()` | `() => void` | Trigger next page fetch |\n| `hasNextPage` | `boolean` | Whether more pages exist |\n| `isFetchingNextPage` | `boolean` | Currently loading next page |\n\n## Migration Guide — Page from Manual Fetch to Queries\n\n### Before (Manual Fetch + $state + $effect)\n\n```svelte\n\u003Cscript lang=\"ts\">\n  import { fetchTasks, fetchAgentStatus } from '$lib\u002Fapi';\n  import { getProjectVersion, isProjectInitialized } from '$lib\u002Fstores\u002Fproject.svelte';\n  import { isStaticMode } from '$lib\u002Fstores\u002Fmode.svelte';\n  import { subscribe, on, off } from '$lib\u002Fstores\u002Fconnection.svelte';\n\n  let tasks = $state\u003CTaskSummary[]>([]);\n  let agentStatus = $state\u003CAgentStatus | null>(null);\n  let loading = $state(true);\n  let error = $state\u003Cstring | null>(null);\n\n  \u002F\u002F Fetch on mount + project change\n  $effect(() => {\n    const version = getProjectVersion();\n    if (!isProjectInitialized()) return;\n    loadData();\n  });\n\n  async function loadData() {\n    loading = true;\n    error = null;\n    try {\n      const [tasksRes, statusRes] = await Promise.all([\n        fetchTasks({ limit: 1000 }),\n        isStaticMode() ? null : fetchAgentStatus().catch(() => null),\n      ]);\n      tasks = tasksRes.items;\n      agentStatus = statusRes;\n    } catch (e) {\n      error = e.message;\n    } finally {\n      loading = false;\n    }\n  }\n\n  \u002F\u002F WebSocket reload\n  onMount(() => {\n    if (!isStaticMode()) {\n      subscribe(['tasks', 'agents']);\n      on('tasks', loadData);      \u002F\u002F Full reload on any task event\n      on('agents', loadData);     \u002F\u002F Full reload on any agent event\n    }\n  });\n\n  onDestroy(() => {\n    off('tasks', loadData);\n    off('agents', loadData);\n  });\n\u003C\u002Fscript>\n```\n\n### After (TanStack Query)\n\n```svelte\n\u003Cscript lang=\"ts\">\n  import { createQuery } from '@tanstack\u002Fsvelte-query';\n  import { fetchTasks, fetchAgentStatus } from '$lib\u002Fapi';\n  import { queryKeys } from '$lib\u002Fquery\u002Fkeys';\n  import { isProjectInitialized } from '$lib\u002Fstores\u002Fproject.svelte';\n  import { isStaticMode } from '$lib\u002Fstores\u002Fmode.svelte';\n\n  \u002F\u002F Queries — caching, dedup, and revalidation handled automatically\n  const tasks = createQuery(() => ({\n    queryKey: queryKeys.tasks.lists(),\n    queryFn: () => fetchTasks(),\n    enabled: isProjectInitialized(),\n  }));\n\n  const agentStatus = createQuery(() => ({\n    queryKey: queryKeys.agents.status(),\n    queryFn: () => fetchAgentStatus(),\n    enabled: !isStaticMode() && isProjectInitialized(),\n  }));\n\n  \u002F\u002F No manual loading\u002Ferror state — query objects provide it\n  \u002F\u002F No onMount\u002FonDestroy — query lifecycle is automatic\n  \u002F\u002F No WS handlers here — centralized WS → invalidation wiring handles it\n\u003C\u002Fscript>\n\n{#if tasks.isPending}\n  \u003CLoadingSkeleton \u002F>\n{:else if tasks.isError}\n  \u003CErrorState error={tasks.error} \u002F>\n{:else}\n  \u003CTaskList tasks={tasks.data.items} \u002F>\n{\u002Fif}\n```\n\n### What Changed\n\n| Before | After |\n|--------|-------|\n| `let data = $state([])` | `createQuery()` manages state |\n| `let loading = $state(true)` | `query.isPending` |\n| `let error = $state(null)` | `query.isError` \u002F `query.error` |\n| `$effect(() => loadData())` | `enabled` option + reactive queryKey |\n| `on('tasks', loadData)` | Centralized WS → `invalidateQueries` |\n| `Promise.all([fetch1, fetch2])` | Multiple independent `createQuery` calls (auto-parallel) |\n| Manual try\u002Fcatch\u002Ffinally | Query error\u002Floading states |\n| `fetchTasks({ limit: 1000 })` | Proper pagination or ref index |\n\n### Migration Checklist\n\n1. Replace `$state` variables for API data with `createQuery()`\n2. Remove manual `loading` \u002F `error` state variables\n3. Remove `$effect(() => loadData())` — use `enabled` + reactive keys\n4. Remove per-page `on('topic', handler)` \u002F `off('topic', handler)` — rely on centralized WS wiring\n5. Remove `onMount` \u002F `onDestroy` for WS subscription when page no longer needs custom event handling\n6. Update template to use `query.isPending`, `query.isError`, `query.data`\n7. Remove `fetchTasks({ limit: 1000 })` calls for title lookup — use ref index or server-resolved titles\n\n## Testing Patterns\n\n### QueryClientProvider Wrapper\n\nEvery component using queries needs a QueryClientProvider in tests:\n\n```typescript\n\u002F\u002F tests\u002Fhelpers\u002Fquery.ts\nimport { QueryClient, QueryClientProvider } from '@tanstack\u002Fsvelte-query';\n\nexport function createTestQueryClient(): QueryClient {\n  return new QueryClient({\n    defaultOptions: {\n      queries: {\n        retry: false,         \u002F\u002F No retries in tests\n        staleTime: Infinity,  \u002F\u002F Prevent background refetches\n        gcTime: Infinity,     \u002F\u002F Keep data for test duration\n      },\n    },\n  });\n}\n```\n\n### Mocking queryFn\n\n```typescript\nimport { render } from '@testing-library\u002Fsvelte';\nimport { vi } from 'vitest';\n\n\u002F\u002F Mock the API module\nvi.mock('$lib\u002Fapi', () => ({\n  fetchTasks: vi.fn().mockResolvedValue({\n    items: [{ _ulid: '01ABC', title: 'Test task', status: 'pending' }],\n    total: 1,\n  }),\n}));\n\n\u002F\u002F Render with provider\nconst { getByText } = render(TasksPage);\nawait waitFor(() => expect(getByText('Test task')).toBeInTheDocument());\n```\n\n### Testing Mutations\n\n```typescript\nconst { getByRole } = render(TaskActions, { props: { taskRef: '@test-task' } });\n\n\u002F\u002F Trigger mutation\nawait fireEvent.click(getByRole('button', { name: 'Start Task' }));\n\n\u002F\u002F Verify API called\nexpect(startTask).toHaveBeenCalledWith('@test-task');\n\n\u002F\u002F Verify cache invalidated (check via query client spy or re-render)\n```\n\n## Anti-Patterns\n\n### 1. Manual fetch + $state for API data\n\n```typescript\n\u002F\u002F BAD — bypasses cache, dedup, and revalidation\nlet tasks = $state\u003CTask[]>([]);\n$effect(() => {\n  fetchTasks().then(res => tasks = res.items);\n});\n\n\u002F\u002F GOOD — uses query cache\nconst tasks = createQuery(() => ({\n  queryKey: queryKeys.tasks.lists(),\n  queryFn: () => fetchTasks(),\n}));\n```\n\n### 2. fetchTasks({ limit: 1000 }) for title lookup\n\n```typescript\n\u002F\u002F BAD — fetches all tasks just to show a title\nconst allTasks = await fetchTasks({ limit: 1000 });\nconst title = allTasks.items.find(t => t._ulid === ref)?.title ?? ref;\n\n\u002F\u002F GOOD — use ref index (lightweight endpoint)\nconst refIndex = createQuery(() => ({\n  queryKey: queryKeys.refs.index(),\n  queryFn: () => fetchRefIndex(),\n  staleTime: 5 * 60_000,\n}));\nconst title = refIndex.data?.[ref]?.title ?? ref;\n\n\u002F\u002F BEST — use server-resolved title from API response\n\u002F\u002F task.spec_title is already in the response — no separate lookup needed\n```\n\n### 3. Polling intervals for data freshness\n\n```typescript\n\u002F\u002F BAD — wasteful, laggy, and doesn't scale\nsetInterval(loadCounts, 30_000);\n\n\u002F\u002F GOOD — event-driven invalidation via WebSocket\n\u002F\u002F WS event → queryClient.invalidateQueries() → active queries refetch\n```\n\n### 4. Per-page WebSocket reload handlers\n\n```typescript\n\u002F\u002F BAD — every page manages its own WS handlers\nonMount(() => {\n  on('tasks', () => { loadTasks(); loadSummary(); loadAgentStatus(); });\n});\nonDestroy(() => {\n  off('tasks', handler);\n});\n\n\u002F\u002F GOOD — centralized WS → invalidation wiring\n\u002F\u002F Pages don't need WS handlers — queries auto-refetch when invalidated\n```\n\n### 5. Unbounded limit fetches\n\n```typescript\n\u002F\u002F BAD — fetches everything, O(n) memory, slow on large projects\nfetchTasks({ limit: 999 })\n\n\u002F\u002F GOOD — paginate with limit\u002Foffset, or use summary endpoints\n\u002F\u002F For counts: use summary endpoint or limit: 0 with total\n\u002F\u002F For display: paginate with createInfiniteQuery\n```\n\n## Boundaries\n\n### What Goes Through TanStack Query\n\nAll **request-response** data fetching:\n- Entity lists and details (tasks, items, inbox, sessions, etc.)\n- Aggregation endpoints (counts, validation, alignment)\n- Ref index for title resolution\n- Agent status and definitions\n- Settings and configuration\n\n### What Stays Outside TanStack Query\n\n- **WebSocket text streaming** (agent text chunks) — These are real-time streaming data, not request-response. Continue using the existing WS handler + `$state` accumulation pattern for `agent_text_chunk` events.\n- **WebSocket connection management** — The connection store handles connect\u002Fdisconnect\u002Freconnect lifecycle.\n- **Local UI state** — Filter selections, panel open\u002Fclose, dialog state. These are not server data.\n- **Static snapshot loading** — Initial mode detection and snapshot loading happens once at startup, before QueryClientProvider is available.\n\n## Project Architecture\n\n### File Layout\n\n```\nsrc\u002Flib\u002F\n├── query\u002F\n│   ├── client.ts              # QueryClient factory with default config\n│   ├── keys.ts                # Query key factory (centralized)\n│   └── ws-invalidation.ts     # WebSocket → query invalidation mapping\n├── api.ts                     # HTTP fetch functions (unchanged)\n├── api-static.ts              # Static mode fetch implementations (unchanged)\n├── stores\u002F\n│   ├── connection.svelte.ts   # WebSocket connection store (unchanged)\n│   ├── mode.svelte.ts         # Static\u002Fdaemon mode detection (unchanged)\n│   └── project.svelte.ts     # Project selection store (unchanged)\n└── websocket\u002F\n    ├── manager.ts             # WebSocket manager (unchanged)\n    └── types.ts               # WebSocket types (unchanged)\n```\n\n### Data Flow\n\n```\nUser navigates to page\n  → createQuery() checks cache\n  → Cache hit + fresh? → Render immediately\n  → Cache hit + stale? → Render from cache, refetch in background\n  → Cache miss? → Show loading, fetch from daemon\n\nWebSocket event arrives\n  → handleBroadcastInvalidation() maps topic → query keys\n  → queryClient.invalidateQueries() marks queries stale\n  → Active queries refetch in background\n  → If enriched payload: queryClient.setQueryData() updates cache directly\n\nUser performs write (mutation)\n  → createMutation() calls API\n  → onSuccess: invalidateQueries() for affected keys\n  → Active queries refetch, UI updates\n\nUser switches project\n  → queryClient.clear() discards all cache\n  → New queries fetch with updated X-Kspec-Dir header\n```\n","\u003C!-- kspec-managed -->\n\n# UI Data Layer — Svelte + TanStack Query\n\nPatterns and conventions for the web UI data fetching layer. Covers TanStack Query v6 setup, query factories, WebSocket-driven cache invalidation, mutations, and migration from manual fetch patterns.\n\n**Companion to:** `\u002Fsvelte-5` skill (reactivity, SSR, state management). This skill covers the data layer specifically.\n\n## Technology Choice\n\n**@tanstack\u002Fsvelte-query v6** — Svelte 5 runes-native (~13 kB gzip).\n\nKey capabilities used in this project:\n- Automatic caching with stale-while-revalidate\n- Request deduplication (concurrent identical requests share one fetch)\n- Background revalidation on navigation\n- Query invalidation via `queryClient.invalidateQueries()`\n- Direct cache updates via `queryClient.setQueryData()`\n- Infinite queries for paginated data (`createInfiniteQuery`)\n\n**Requires:** Svelte >= 5.25.0\n\n### References\n\n- [TanStack Query Svelte Overview](https:\u002F\u002Ftanstack.com\u002Fquery\u002Fv5\u002Fdocs\u002Fframework\u002Fsvelte\u002Foverview)\n- [v5 → v6 Migration Guide](https:\u002F\u002Ftanstack.com\u002Fquery\u002Fv5\u002Fdocs\u002Fframework\u002Fsvelte\u002Fmigrate-from-v5-to-v6)\n- [createQuery API Reference](https:\u002F\u002Ftanstack.com\u002Fquery\u002Fv5\u002Fdocs\u002Fframework\u002Fsvelte\u002Freference\u002Ffunctions\u002Fcreatequery)\n- [createInfiniteQuery API Reference](https:\u002F\u002Ftanstack.com\u002Fquery\u002Fv5\u002Fdocs\u002Fframework\u002Fsvelte\u002Freference\u002Ffunctions\u002FcreateInfiniteQuery)\n- [createMutation API Reference](https:\u002F\u002Ftanstack.com\u002Fquery\u002Fv5\u002Fdocs\u002Fframework\u002Fsvelte\u002Freference\u002Ffunctions\u002Fcreatemutation)\n- [SSR & SvelteKit Guide](https:\u002F\u002Ftanstack.com\u002Fquery\u002Fv5\u002Fdocs\u002Fframework\u002Fsvelte\u002Fssr)\n\n## Quick Reference\n\n| Need | Pattern |\n|------|---------|\n| Fetch and cache data | `createQuery(() => ({ queryKey, queryFn }))` |\n| Paginated \u002F infinite scroll | `createInfiniteQuery(() => ({ queryKey, queryFn, getNextPageParam, initialPageParam }))` |\n| Write operation with cache update | `createMutation(() => ({ mutationFn, onSuccess: invalidate }))` |\n| Invalidate after WS event | `queryClient.invalidateQueries({ queryKey: ['tasks'] })` |\n| Update cache without refetch | `queryClient.setQueryData(['tasks', ref], newData)` |\n| Clear all cache (project switch) | `queryClient.clear()` |\n| Access QueryClient in component | `useQueryClient()` |\n\n## Setup\n\n### QueryClient Configuration\n\n```typescript\n\u002F\u002F src\u002Flib\u002Fquery\u002Fclient.ts\nimport { QueryClient } from '@tanstack\u002Fsvelte-query';\n\nexport function createAppQueryClient(): QueryClient {\n  return new QueryClient({\n    defaultOptions: {\n      queries: {\n        \u002F\u002F Localhost daemon — data changes via WS events, not staleness\n        staleTime: 30_000,        \u002F\u002F 30s — lists and frequently changing data\n        gcTime: 10 * 60_000,      \u002F\u002F 10min — keep in memory for session\n        retry: 1,                 \u002F\u002F Localhost: one retry is enough\n        retryDelay: 500,          \u002F\u002F Short delay — daemon is local\n        refetchOnWindowFocus: false, \u002F\u002F WS events handle freshness\n      },\n    },\n  });\n}\n```\n\n### Provider in Root Layout\n\n```svelte\n\u003C!-- src\u002Froutes\u002F+layout.svelte -->\n\u003Cscript lang=\"ts\">\n  import { QueryClientProvider } from '@tanstack\u002Fsvelte-query';\n  import { createAppQueryClient } from '$lib\u002Fquery\u002Fclient';\n\n  const queryClient = createAppQueryClient();\n\u003C\u002Fscript>\n\n\u003CQueryClientProvider client={queryClient}>\n  \u003Cslot \u002F>\n\u003C\u002FQueryClientProvider>\n```\n\n## v6 API — Thunk Pattern (Critical)\n\nIn v6, **all query\u002Fmutation functions take a thunk** (function returning options), not a plain object. This enables Svelte 5 runes reactivity without stores.\n\n```typescript\n\u002F\u002F v5 (WRONG in v6)\nconst query = createQuery({\n  queryKey: ['tasks'],\n  queryFn: fetchTasks,\n});\n\n\u002F\u002F v6 (CORRECT) — options wrapped in () => ({...})\nconst query = createQuery(() => ({\n  queryKey: ['tasks'],\n  queryFn: fetchTasks,\n}));\n```\n\n**Why:** The thunk is re-executed when reactive dependencies inside it change. This is how reactive query keys work — Svelte's fine-grained reactivity tracks which `$state`\u002F`$derived` values the thunk reads.\n\n### Accessing Query Results\n\nv6 returns a plain object (not a Svelte store). No `$` prefix needed:\n\n```svelte\n\u003Cscript lang=\"ts\">\n  const tasks = createQuery(() => ({\n    queryKey: ['tasks'],\n    queryFn: () => fetchTasks(),\n  }));\n\u003C\u002Fscript>\n\n\u003C!-- Direct property access — no $tasks needed -->\n{#if tasks.isPending}\n  \u003CLoading \u002F>\n{:else if tasks.isError}\n  \u003CError message={tasks.error.message} \u002F>\n{:else}\n  {#each tasks.data.items as task}\n    \u003CTaskRow {task} \u002F>\n  {\u002Feach}\n{\u002Fif}\n```\n\n### Reactive Query Keys\n\nWhen the query key depends on reactive state, include the state in the thunk:\n\n```svelte\n\u003Cscript lang=\"ts\">\n  import { page } from '$app\u002Fstores';\n\n  let status = $derived($page.url.searchParams.get('status') ?? 'all');\n\n  const tasks = createQuery(() => ({\n    queryKey: ['tasks', { status }],\n    queryFn: () => fetchTasks({ status }),\n    enabled: true,\n  }));\n\u003C\u002Fscript>\n```\n\nWhen `status` changes (via URL param), the thunk re-runs, producing a new queryKey. TanStack Query detects the key change and fetches fresh data.\n\n## Query Key Conventions\n\n### Naming Pattern\n\nUse hierarchical arrays with entity type first, then qualifiers:\n\n```typescript\n\u002F\u002F Entity lists\n['tasks']\n['tasks', { status: 'pending', tag: 'web-ui' }]\n['items']\n['items', { type: 'feature' }]\n['inbox']\n['sessions', { status: 'active' }]\n\n\u002F\u002F Entity detail\n['tasks', ref]          \u002F\u002F e.g. ['tasks', '@task-add-auth']\n['items', ref]\n['sessions', sessionId]\n\n\u002F\u002F Aggregations \u002F summaries\n['tasks', 'summary']   \u002F\u002F Status counts\n['validation']\n['alignment']\n\n\u002F\u002F Lightweight indexes\n['refs', 'index']       \u002F\u002F Ref → title\u002Ftype\u002Fstatus map\n\n\u002F\u002F Counts (sidebar badges)\n['inbox', 'count']\n['observations', 'count']\n['tasks', 'pending-review-count']\n```\n\n### Query Key Factory\n\nCentralize keys to prevent typos and enable targeted invalidation:\n\n```typescript\n\u002F\u002F src\u002Flib\u002Fquery\u002Fkeys.ts\nexport const queryKeys = {\n  tasks: {\n    all: ['tasks'] as const,\n    lists: () => [...queryKeys.tasks.all] as const,\n    list: (filters: TaskFilters) => [...queryKeys.tasks.all, filters] as const,\n    detail: (ref: string) => [...queryKeys.tasks.all, ref] as const,\n    summary: () => [...queryKeys.tasks.all, 'summary'] as const,\n    pendingReviewCount: () => [...queryKeys.tasks.all, 'pending-review-count'] as const,\n  },\n  items: {\n    all: ['items'] as const,\n    lists: () => [...queryKeys.items.all] as const,\n    list: (filters: ItemFilters) => [...queryKeys.items.all, filters] as const,\n    detail: (ref: string) => [...queryKeys.items.all, ref] as const,\n  },\n  inbox: {\n    all: ['inbox'] as const,\n    list: (filters?: InboxFilters) => [...queryKeys.inbox.all, filters] as const,\n    count: () => [...queryKeys.inbox.all, 'count'] as const,\n  },\n  sessions: {\n    all: ['sessions'] as const,\n    list: (filters: SessionFilters) => [...queryKeys.sessions.all, filters] as const,\n    detail: (id: string) => [...queryKeys.sessions.all, id] as const,\n  },\n  agents: {\n    all: ['agents'] as const,\n    status: () => [...queryKeys.agents.all, 'status'] as const,\n    definitions: () => [...queryKeys.agents.all, 'definitions'] as const,\n  },\n  refs: {\n    index: () => ['refs', 'index'] as const,\n  },\n  validation: () => ['validation'] as const,\n  alignment: () => ['alignment'] as const,\n  observations: {\n    all: ['observations'] as const,\n    list: (filters?: ObservationFilters) => [...queryKeys.observations.all, filters] as const,\n    count: () => [...queryKeys.observations.all, 'count'] as const,\n  },\n} as const;\n```\n\n**Usage:**\n\n```typescript\n\u002F\u002F In a query\nconst tasks = createQuery(() => ({\n  queryKey: queryKeys.tasks.list({ status: 'pending' }),\n  queryFn: () => fetchTasks({ status: 'pending' }),\n}));\n\n\u002F\u002F For invalidation — invalidate all tasks queries\nqueryClient.invalidateQueries({ queryKey: queryKeys.tasks.all });\n\n\u002F\u002F For targeted invalidation — only list queries\nqueryClient.invalidateQueries({ queryKey: queryKeys.tasks.lists() });\n```\n\n## Cache Timing Conventions\n\n| Data Type | staleTime | gcTime | Rationale |\n|-----------|-----------|--------|-----------|\n| Entity lists (tasks, items) | 30s | 10 min | Frequently changing, WS handles freshness |\n| Entity detail | 30s | 10 min | Same as lists |\n| Ref index (title resolution) | 5 min | 30 min | Rarely changes, expensive to compute |\n| Sidebar badge counts | 30s | 5 min | Small payload, WS-invalidated |\n| Validation \u002F alignment | 60s | 10 min | Expensive to compute, rarely polled |\n| Agent status | 10s | 5 min | Needs near-real-time feel |\n| Session context | 5 min | 30 min | Changes on project switch only |\n\nOverride per-query in the factory:\n\n```typescript\nconst refIndex = createQuery(() => ({\n  queryKey: queryKeys.refs.index(),\n  queryFn: () => fetchRefIndex(),\n  staleTime: 5 * 60_000,  \u002F\u002F 5 minutes — titles rarely change\n  gcTime: 30 * 60_000,    \u002F\u002F 30 minutes\n}));\n```\n\n## Creating a New Query — Step by Step\n\n### 1. Add query key to factory\n\n```typescript\n\u002F\u002F src\u002Flib\u002Fquery\u002Fkeys.ts\nexport const queryKeys = {\n  \u002F\u002F ...existing keys...\n  workflows: {\n    all: ['workflows'] as const,\n    list: () => [...queryKeys.workflows.all] as const,\n  },\n};\n```\n\n### 2. Create the query in the component\n\n```svelte\n\u003Cscript lang=\"ts\">\n  import { createQuery } from '@tanstack\u002Fsvelte-query';\n  import { fetchWorkflows } from '$lib\u002Fapi';\n  import { queryKeys } from '$lib\u002Fquery\u002Fkeys';\n  import { isStaticMode } from '$lib\u002Fstores\u002Fmode.svelte';\n  import { isProjectInitialized, getProjectVersion } from '$lib\u002Fstores\u002Fproject.svelte';\n\n  const workflows = createQuery(() => ({\n    queryKey: queryKeys.workflows.list(),\n    queryFn: () => fetchWorkflows(),\n    enabled: isProjectInitialized(),\n  }));\n\u003C\u002Fscript>\n\n{#if workflows.isPending}\n  \u003CLoadingSkeleton \u002F>\n{:else if workflows.isError}\n  \u003CErrorMessage error={workflows.error} \u002F>\n{:else}\n  {#each workflows.data.items as workflow}\n    \u003CWorkflowCard {workflow} \u002F>\n  {\u002Feach}\n{\u002Fif}\n```\n\n### 3. Wire WS invalidation (if applicable)\n\n```typescript\n\u002F\u002F In the WebSocket → query invalidation wiring\ncase 'workflows':\n  queryClient.invalidateQueries({ queryKey: queryKeys.workflows.all });\n  break;\n```\n\n## Creating a Mutation\n\nMutations handle write operations (start task, add note, create inbox item, etc.).\n\n```svelte\n\u003Cscript lang=\"ts\">\n  import { createMutation, useQueryClient } from '@tanstack\u002Fsvelte-query';\n  import { startTask } from '$lib\u002Fapi';\n  import { queryKeys } from '$lib\u002Fquery\u002Fkeys';\n\n  const queryClient = useQueryClient();\n\n  const startTaskMutation = createMutation(() => ({\n    mutationFn: (ref: string) => startTask(ref),\n    onSuccess: () => {\n      \u002F\u002F Invalidate task queries so lists and details refresh\n      queryClient.invalidateQueries({ queryKey: queryKeys.tasks.all });\n    },\n    onError: (error: Error) => {\n      \u002F\u002F Show error toast\n      addToast({ type: 'error', message: error.message });\n    },\n  }));\n\n  function handleStartTask(ref: string) {\n    startTaskMutation.mutate(ref);\n  }\n\u003C\u002Fscript>\n\n\u003Cbutton\n  onclick={() => handleStartTask(task._ref)}\n  disabled={startTaskMutation.isPending}\n>\n  {startTaskMutation.isPending ? 'Starting...' : 'Start Task'}\n\u003C\u002Fbutton>\n```\n\n### Optimistic Updates\n\nFor mutations where you want the UI to update instantly:\n\n```typescript\nconst addNoteMutation = createMutation(() => ({\n  mutationFn: ({ ref, content }: { ref: string; content: string }) =>\n    addTaskNote(ref, content),\n  onMutate: async ({ ref, content }) => {\n    \u002F\u002F Cancel outgoing refetches\n    await queryClient.cancelQueries({ queryKey: queryKeys.tasks.detail(ref) });\n\n    \u002F\u002F Snapshot previous value\n    const previous = queryClient.getQueryData(queryKeys.tasks.detail(ref));\n\n    \u002F\u002F Optimistically update\n    queryClient.setQueryData(queryKeys.tasks.detail(ref), (old: TaskDetail) => ({\n      ...old,\n      notes: [...old.notes, { content, created_at: new Date().toISOString() }],\n    }));\n\n    return { previous };\n  },\n  onError: (_err, _vars, context) => {\n    \u002F\u002F Roll back on error\n    if (context?.previous) {\n      queryClient.setQueryData(queryKeys.tasks.detail(ref), context.previous);\n    }\n  },\n  onSettled: () => {\n    \u002F\u002F Always refetch after mutation\n    queryClient.invalidateQueries({ queryKey: queryKeys.tasks.all });\n  },\n}));\n```\n\n## WebSocket → Query Invalidation\n\nThe WebSocket manager receives broadcast events. Wire these to query invalidation so active views revalidate automatically.\n\n### Wiring Pattern\n\n```typescript\n\u002F\u002F src\u002Flib\u002Fquery\u002Fws-invalidation.ts\nimport type { QueryClient } from '@tanstack\u002Fsvelte-query';\nimport type { BroadcastEvent } from '@kynetic-ai\u002Fshared';\nimport { queryKeys } from '.\u002Fkeys';\n\nexport function handleBroadcastInvalidation(\n  queryClient: QueryClient,\n  event: BroadcastEvent,\n): void {\n  switch (event.topic) {\n    case 'tasks':\n      queryClient.invalidateQueries({ queryKey: queryKeys.tasks.all });\n      queryClient.invalidateQueries({ queryKey: queryKeys.tasks.pendingReviewCount() });\n      \u002F\u002F If enriched payload includes the full task, update cache directly\n      if (event.data?.task) {\n        queryClient.setQueryData(\n          queryKeys.tasks.detail(event.data.ref),\n          event.data.task,\n        );\n      }\n      break;\n\n    case 'inbox':\n      queryClient.invalidateQueries({ queryKey: queryKeys.inbox.all });\n      break;\n\n    case 'agents':\n      queryClient.invalidateQueries({ queryKey: queryKeys.agents.all });\n      break;\n\n    case 'files':\n      \u002F\u002F Settings\u002Fconfig changes\n      queryClient.invalidateQueries({ queryKey: queryKeys.validation() });\n      queryClient.invalidateQueries({ queryKey: queryKeys.alignment() });\n      queryClient.invalidateQueries({ queryKey: queryKeys.items.all });\n      queryClient.invalidateQueries({ queryKey: queryKeys.tasks.all });\n      break;\n  }\n}\n```\n\n### Integration with WebSocket Manager\n\n```typescript\n\u002F\u002F In root layout or connection setup\nimport { on } from '$lib\u002Fstores\u002Fconnection.svelte';\nimport { handleBroadcastInvalidation } from '$lib\u002Fquery\u002Fws-invalidation';\n\n\u002F\u002F Subscribe to all relevant topics\nsubscribe(['tasks', 'agents', 'inbox', 'files']);\n\n\u002F\u002F Single handler for all invalidations\nfunction handleBroadcast(event: BroadcastEvent) {\n  handleBroadcastInvalidation(queryClient, event);\n}\n\non('tasks', handleBroadcast);\non('agents', handleBroadcast);\non('inbox', handleBroadcast);\non('files', handleBroadcast);\n```\n\n**Key principle:** After wiring WS → invalidation, individual pages no longer need `on('tasks', reloadEverything)` handlers. TanStack Query handles refetching active queries automatically.\n\n## Ref Index for Title Resolution\n\nInstead of `fetchTasks({ limit: 1000 })` to get task titles for display, use the lightweight ref index endpoint:\n\n```typescript\n\u002F\u002F Query for the ref index (long staleTime — titles rarely change)\nconst refIndex = createQuery(() => ({\n  queryKey: queryKeys.refs.index(),\n  queryFn: () => fetchRefIndex(),\n  staleTime: 5 * 60_000,\n  gcTime: 30 * 60_000,\n}));\n\n\u002F\u002F Usage in template\nfunction resolveTitle(ref: string): string {\n  return refIndex.data?.[ref]?.title ?? ref;\n}\n```\n\nAfter the server-side resolution work is done, most title resolution will happen inline in API responses (e.g., `spec_title` alongside `spec_ref`), reducing the need for client-side lookups.\n\n## Project Switch — Cache Clearing\n\nWhen the user switches projects, all cached data must be discarded:\n\n```typescript\n\u002F\u002F In project switch handler\nimport { useQueryClient } from '@tanstack\u002Fsvelte-query';\n\nfunction handleProjectSwitch(newPath: string) {\n  const queryClient = useQueryClient();\n\n  \u002F\u002F Clear ALL cached data\n  queryClient.clear();\n\n  \u002F\u002F Set new project and trigger reloads\n  selectProject(newPath);\n}\n```\n\n`queryClient.clear()` removes all queries from the cache. New queries will fetch fresh data with the updated `X-Kspec-Dir` header.\n\n## Static Mode Compatibility\n\nQuery factories must respect static mode. The existing `fetchTasks()`, `fetchItems()`, etc. in `api.ts` already dispatch to static implementations when `isStaticMode()` is true — so queries \"just work\" in static mode.\n\nKey considerations:\n\n```typescript\n\u002F\u002F Mutations should be disabled in static mode\nconst startTaskMutation = createMutation(() => ({\n  mutationFn: (ref: string) => startTask(ref),  \u002F\u002F Throws ReadOnlyModeError in static mode\n  \u002F\u002F ...\n}));\n\n\u002F\u002F Agent-specific queries should be disabled\nconst agentStatus = createQuery(() => ({\n  queryKey: queryKeys.agents.status(),\n  queryFn: () => fetchAgentStatus(),\n  enabled: !isStaticMode() && isProjectInitialized(),  \u002F\u002F Skip in static mode\n}));\n```\n\n**WebSocket connections are not established in static mode**, so WS-driven invalidation is naturally disabled. The `subscribe()` calls in the connection store are no-ops when static.\n\n## Error Handling and Retry Config\n\nThe daemon runs on localhost — network flakiness is rare. Connection failures usually mean the daemon isn't running.\n\n```typescript\n\u002F\u002F Default query client config (see Setup section)\n{\n  retry: 1,              \u002F\u002F One retry — if daemon is down, more retries won't help\n  retryDelay: 500,       \u002F\u002F Short delay — it's localhost\n  refetchOnWindowFocus: false,  \u002F\u002F WS events handle freshness\n}\n```\n\n### Error Display Pattern\n\n```svelte\n{#if query.isError}\n  {#if query.error.message.includes('fetch')}\n    \u003CAlert type=\"warning\">\n      Daemon not reachable. Start it with \u003Ccode>kspec serve\u003C\u002Fcode>.\n    \u003C\u002FAlert>\n  {:else}\n    \u003CAlert type=\"error\">{query.error.message}\u003C\u002FAlert>\n  {\u002Fif}\n{\u002Fif}\n```\n\n### Invalid Project Recovery\n\nThe existing `handleResponseError()` in `api.ts` detects invalid project responses (400\u002F404) and calls `clearInvalidSelection()`. This works the same through TanStack Query — the error propagates to the query's `error` state.\n\n## Sessions Infinite Scroll — createInfiniteQuery\n\nThe sessions page uses `createInfiniteQuery` for paginated data with scroll-to-load-more:\n\n```svelte\n\u003Cscript lang=\"ts\">\n  import { createInfiniteQuery } from '@tanstack\u002Fsvelte-query';\n  import { queryKeys } from '$lib\u002Fquery\u002Fkeys';\n\n  const PAGE_SIZE = 25;\n\n  const sessions = createInfiniteQuery(() => ({\n    queryKey: queryKeys.sessions.list({ status: filterStatus }),\n    queryFn: ({ pageParam }) =>\n      fetchSessions({ offset: pageParam, limit: PAGE_SIZE, status: filterStatus }),\n    initialPageParam: 0,\n    getNextPageParam: (lastPage, allPages) => {\n      const totalFetched = allPages.reduce((sum, p) => sum + p.items.length, 0);\n      return totalFetched \u003C lastPage.total ? totalFetched : undefined;\n    },\n  }));\n\n  \u002F\u002F Flatten pages for rendering\n  let allSessions = $derived(sessions.data?.pages.flatMap(p => p.items) ?? []);\n\u003C\u002Fscript>\n\n{#each allSessions as session}\n  \u003CSessionRow {session} \u002F>\n{\u002Feach}\n\n{#if sessions.hasNextPage}\n  \u003Cdiv use:intersectionObserver={() => sessions.fetchNextPage()}>\n    {#if sessions.isFetchingNextPage}\n      \u003CLoadingMore \u002F>\n    {\u002Fif}\n  \u003C\u002Fdiv>\n{\u002Fif}\n```\n\n### Key Properties\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `data.pages` | `Page[]` | Array of fetched pages |\n| `data.pageParams` | `number[]` | Array of page params used |\n| `fetchNextPage()` | `() => void` | Trigger next page fetch |\n| `hasNextPage` | `boolean` | Whether more pages exist |\n| `isFetchingNextPage` | `boolean` | Currently loading next page |\n\n## Migration Guide — Page from Manual Fetch to Queries\n\n### Before (Manual Fetch + $state + $effect)\n\n```svelte\n\u003Cscript lang=\"ts\">\n  import { fetchTasks, fetchAgentStatus } from '$lib\u002Fapi';\n  import { getProjectVersion, isProjectInitialized } from '$lib\u002Fstores\u002Fproject.svelte';\n  import { isStaticMode } from '$lib\u002Fstores\u002Fmode.svelte';\n  import { subscribe, on, off } from '$lib\u002Fstores\u002Fconnection.svelte';\n\n  let tasks = $state\u003CTaskSummary[]>([]);\n  let agentStatus = $state\u003CAgentStatus | null>(null);\n  let loading = $state(true);\n  let error = $state\u003Cstring | null>(null);\n\n  \u002F\u002F Fetch on mount + project change\n  $effect(() => {\n    const version = getProjectVersion();\n    if (!isProjectInitialized()) return;\n    loadData();\n  });\n\n  async function loadData() {\n    loading = true;\n    error = null;\n    try {\n      const [tasksRes, statusRes] = await Promise.all([\n        fetchTasks({ limit: 1000 }),\n        isStaticMode() ? null : fetchAgentStatus().catch(() => null),\n      ]);\n      tasks = tasksRes.items;\n      agentStatus = statusRes;\n    } catch (e) {\n      error = e.message;\n    } finally {\n      loading = false;\n    }\n  }\n\n  \u002F\u002F WebSocket reload\n  onMount(() => {\n    if (!isStaticMode()) {\n      subscribe(['tasks', 'agents']);\n      on('tasks', loadData);      \u002F\u002F Full reload on any task event\n      on('agents', loadData);     \u002F\u002F Full reload on any agent event\n    }\n  });\n\n  onDestroy(() => {\n    off('tasks', loadData);\n    off('agents', loadData);\n  });\n\u003C\u002Fscript>\n```\n\n### After (TanStack Query)\n\n```svelte\n\u003Cscript lang=\"ts\">\n  import { createQuery } from '@tanstack\u002Fsvelte-query';\n  import { fetchTasks, fetchAgentStatus } from '$lib\u002Fapi';\n  import { queryKeys } from '$lib\u002Fquery\u002Fkeys';\n  import { isProjectInitialized } from '$lib\u002Fstores\u002Fproject.svelte';\n  import { isStaticMode } from '$lib\u002Fstores\u002Fmode.svelte';\n\n  \u002F\u002F Queries — caching, dedup, and revalidation handled automatically\n  const tasks = createQuery(() => ({\n    queryKey: queryKeys.tasks.lists(),\n    queryFn: () => fetchTasks(),\n    enabled: isProjectInitialized(),\n  }));\n\n  const agentStatus = createQuery(() => ({\n    queryKey: queryKeys.agents.status(),\n    queryFn: () => fetchAgentStatus(),\n    enabled: !isStaticMode() && isProjectInitialized(),\n  }));\n\n  \u002F\u002F No manual loading\u002Ferror state — query objects provide it\n  \u002F\u002F No onMount\u002FonDestroy — query lifecycle is automatic\n  \u002F\u002F No WS handlers here — centralized WS → invalidation wiring handles it\n\u003C\u002Fscript>\n\n{#if tasks.isPending}\n  \u003CLoadingSkeleton \u002F>\n{:else if tasks.isError}\n  \u003CErrorState error={tasks.error} \u002F>\n{:else}\n  \u003CTaskList tasks={tasks.data.items} \u002F>\n{\u002Fif}\n```\n\n### What Changed\n\n| Before | After |\n|--------|-------|\n| `let data = $state([])` | `createQuery()` manages state |\n| `let loading = $state(true)` | `query.isPending` |\n| `let error = $state(null)` | `query.isError` \u002F `query.error` |\n| `$effect(() => loadData())` | `enabled` option + reactive queryKey |\n| `on('tasks', loadData)` | Centralized WS → `invalidateQueries` |\n| `Promise.all([fetch1, fetch2])` | Multiple independent `createQuery` calls (auto-parallel) |\n| Manual try\u002Fcatch\u002Ffinally | Query error\u002Floading states |\n| `fetchTasks({ limit: 1000 })` | Proper pagination or ref index |\n\n### Migration Checklist\n\n1. Replace `$state` variables for API data with `createQuery()`\n2. Remove manual `loading` \u002F `error` state variables\n3. Remove `$effect(() => loadData())` — use `enabled` + reactive keys\n4. Remove per-page `on('topic', handler)` \u002F `off('topic', handler)` — rely on centralized WS wiring\n5. Remove `onMount` \u002F `onDestroy` for WS subscription when page no longer needs custom event handling\n6. Update template to use `query.isPending`, `query.isError`, `query.data`\n7. Remove `fetchTasks({ limit: 1000 })` calls for title lookup — use ref index or server-resolved titles\n\n## Testing Patterns\n\n### QueryClientProvider Wrapper\n\nEvery component using queries needs a QueryClientProvider in tests:\n\n```typescript\n\u002F\u002F tests\u002Fhelpers\u002Fquery.ts\nimport { QueryClient, QueryClientProvider } from '@tanstack\u002Fsvelte-query';\n\nexport function createTestQueryClient(): QueryClient {\n  return new QueryClient({\n    defaultOptions: {\n      queries: {\n        retry: false,         \u002F\u002F No retries in tests\n        staleTime: Infinity,  \u002F\u002F Prevent background refetches\n        gcTime: Infinity,     \u002F\u002F Keep data for test duration\n      },\n    },\n  });\n}\n```\n\n### Mocking queryFn\n\n```typescript\nimport { render } from '@testing-library\u002Fsvelte';\nimport { vi } from 'vitest';\n\n\u002F\u002F Mock the API module\nvi.mock('$lib\u002Fapi', () => ({\n  fetchTasks: vi.fn().mockResolvedValue({\n    items: [{ _ulid: '01ABC', title: 'Test task', status: 'pending' }],\n    total: 1,\n  }),\n}));\n\n\u002F\u002F Render with provider\nconst { getByText } = render(TasksPage);\nawait waitFor(() => expect(getByText('Test task')).toBeInTheDocument());\n```\n\n### Testing Mutations\n\n```typescript\nconst { getByRole } = render(TaskActions, { props: { taskRef: '@test-task' } });\n\n\u002F\u002F Trigger mutation\nawait fireEvent.click(getByRole('button', { name: 'Start Task' }));\n\n\u002F\u002F Verify API called\nexpect(startTask).toHaveBeenCalledWith('@test-task');\n\n\u002F\u002F Verify cache invalidated (check via query client spy or re-render)\n```\n\n## Anti-Patterns\n\n### 1. Manual fetch + $state for API data\n\n```typescript\n\u002F\u002F BAD — bypasses cache, dedup, and revalidation\nlet tasks = $state\u003CTask[]>([]);\n$effect(() => {\n  fetchTasks().then(res => tasks = res.items);\n});\n\n\u002F\u002F GOOD — uses query cache\nconst tasks = createQuery(() => ({\n  queryKey: queryKeys.tasks.lists(),\n  queryFn: () => fetchTasks(),\n}));\n```\n\n### 2. fetchTasks({ limit: 1000 }) for title lookup\n\n```typescript\n\u002F\u002F BAD — fetches all tasks just to show a title\nconst allTasks = await fetchTasks({ limit: 1000 });\nconst title = allTasks.items.find(t => t._ulid === ref)?.title ?? ref;\n\n\u002F\u002F GOOD — use ref index (lightweight endpoint)\nconst refIndex = createQuery(() => ({\n  queryKey: queryKeys.refs.index(),\n  queryFn: () => fetchRefIndex(),\n  staleTime: 5 * 60_000,\n}));\nconst title = refIndex.data?.[ref]?.title ?? ref;\n\n\u002F\u002F BEST — use server-resolved title from API response\n\u002F\u002F task.spec_title is already in the response — no separate lookup needed\n```\n\n### 3. Polling intervals for data freshness\n\n```typescript\n\u002F\u002F BAD — wasteful, laggy, and doesn't scale\nsetInterval(loadCounts, 30_000);\n\n\u002F\u002F GOOD — event-driven invalidation via WebSocket\n\u002F\u002F WS event → queryClient.invalidateQueries() → active queries refetch\n```\n\n### 4. Per-page WebSocket reload handlers\n\n```typescript\n\u002F\u002F BAD — every page manages its own WS handlers\nonMount(() => {\n  on('tasks', () => { loadTasks(); loadSummary(); loadAgentStatus(); });\n});\nonDestroy(() => {\n  off('tasks', handler);\n});\n\n\u002F\u002F GOOD — centralized WS → invalidation wiring\n\u002F\u002F Pages don't need WS handlers — queries auto-refetch when invalidated\n```\n\n### 5. Unbounded limit fetches\n\n```typescript\n\u002F\u002F BAD — fetches everything, O(n) memory, slow on large projects\nfetchTasks({ limit: 999 })\n\n\u002F\u002F GOOD — paginate with limit\u002Foffset, or use summary endpoints\n\u002F\u002F For counts: use summary endpoint or limit: 0 with total\n\u002F\u002F For display: paginate with createInfiniteQuery\n```\n\n## Boundaries\n\n### What Goes Through TanStack Query\n\nAll **request-response** data fetching:\n- Entity lists and details (tasks, items, inbox, sessions, etc.)\n- Aggregation endpoints (counts, validation, alignment)\n- Ref index for title resolution\n- Agent status and definitions\n- Settings and configuration\n\n### What Stays Outside TanStack Query\n\n- **WebSocket text streaming** (agent text chunks) — These are real-time streaming data, not request-response. Continue using the existing WS handler + `$state` accumulation pattern for `agent_text_chunk` events.\n- **WebSocket connection management** — The connection store handles connect\u002Fdisconnect\u002Freconnect lifecycle.\n- **Local UI state** — Filter selections, panel open\u002Fclose, dialog state. These are not server data.\n- **Static snapshot loading** — Initial mode detection and snapshot loading happens once at startup, before QueryClientProvider is available.\n\n## Project Architecture\n\n### File Layout\n\n```\nsrc\u002Flib\u002F\n├── query\u002F\n│   ├── client.ts              # QueryClient factory with default config\n│   ├── keys.ts                # Query key factory (centralized)\n│   └── ws-invalidation.ts     # WebSocket → query invalidation mapping\n├── api.ts                     # HTTP fetch functions (unchanged)\n├── api-static.ts              # Static mode fetch implementations (unchanged)\n├── stores\u002F\n│   ├── connection.svelte.ts   # WebSocket connection store (unchanged)\n│   ├── mode.svelte.ts         # Static\u002Fdaemon mode detection (unchanged)\n│   └── project.svelte.ts     # Project selection store (unchanged)\n└── websocket\u002F\n    ├── manager.ts             # WebSocket manager (unchanged)\n    └── types.ts               # WebSocket types (unchanged)\n```\n\n### Data Flow\n\n```\nUser navigates to page\n  → createQuery() checks cache\n  → Cache hit + fresh? → Render immediately\n  → Cache hit + stale? → Render from cache, refetch in background\n  → Cache miss? → Show loading, fetch from daemon\n\nWebSocket event arrives\n  → handleBroadcastInvalidation() maps topic → query keys\n  → queryClient.invalidateQueries() marks queries stale\n  → Active queries refetch in background\n  → If enriched payload: queryClient.setQueryData() updates cache directly\n\nUser performs write (mutation)\n  → createMutation() calls API\n  → onSuccess: invalidateQueries() for affected keys\n  → Active queries refetch, UI updates\n\nUser switches project\n  → queryClient.clear() discards all cache\n  → New queries fetch with updated X-Kspec-Dir header\n```\n",{"name":5,"description":7},"main","c4b0de3035f8512bba7eb0a9b4e39ea2fbd3f08e",".claude\u002Fskills\u002Fui-data-layer",2,"https:\u002F\u002Fgithub.com\u002Flepahc\u002Fkynetic-spec","https:\u002F\u002Fgithub.com\u002Flepahc","2026-07-22 18:57:40",{"source":8,"skillName":5,"commands":48,"prompt":58,"zipAvailable":59},[49,52,55],{"tool":50,"command":51},"npx","npx skills add lepahc\u002Fkynetic-spec --skill ui-data-layer",{"tool":53,"command":54},"bunx","bunx skills add lepahc\u002Fkynetic-spec --skill ui-data-layer",{"tool":56,"command":57},"pnpm","pnpm dlx skills add lepahc\u002Fkynetic-spec --skill ui-data-layer","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Flepahc\u002Fkynetic-spec\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fui-data-layer\u002FSKILL.md",true,[61],{"path":62,"resourceType":63,"resourceTypeDesc":64,"sizeBytes":65,"sourceHtmlUrl":66},"indexer.py","SCRIPT","脚本",926,"https:\u002F\u002Fgithub.com\u002FNVIDIA\u002FSkillSpector\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fsdi\u002Fsdi_clean\u002Findexer.py",[68,84,90,98,109,117],{"skillNo":69,"name":70,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":71,"categories":72,"keywords":81,"publishedAt":35},"01M6C7KCZE43MDZ74FV3A5TDCQ","writing-specs","https:\u002F\u002Fgithub.com\u002Flepahc\u002Fkynetic-spec\u002Fblob\u002Fmain\u002Ftemplates\u002Fskills\u002Fwriting-specs\u002FSKILL.md",[73,78],{"categoryNo":74,"categoryName":75,"taxonomyType":76,"taxonomyTypeDesc":77},"01JBSEEDCAT000000000000018","写作与文案","SCENARIO","使用场景",{"categoryNo":79,"categoryName":80,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000036","内容创作者",[82,83],"writing","writing specs",{"skillNo":85,"name":86,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":87,"categories":88,"keywords":89,"publishedAt":35},"01M6C7KAK9CBJ87PX3BCW7B7YG","triage","https:\u002F\u002Fgithub.com\u002Flepahc\u002Fkynetic-spec\u002Fblob\u002Fmain\u002Ftemplates\u002Fskills\u002Ftriage\u002FSKILL.md",[],[86],{"skillNo":91,"name":92,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":93,"categories":94,"keywords":95,"publishedAt":35},"01M6C7K89VN6FJCT8M86K56EE8","triage-inbox","https:\u002F\u002Fgithub.com\u002Flepahc\u002Fkynetic-spec\u002Fblob\u002Fmain\u002Ftemplates\u002Fskills\u002Ftriage-inbox\u002FSKILL.md",[],[86,96,97],"inbox","triage inbox",{"skillNo":99,"name":100,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":101,"categories":102,"keywords":106,"publishedAt":35},"01M6C7K6F2342AC1TJCN9PM0ZB","triage-automation","https:\u002F\u002Fgithub.com\u002Flepahc\u002Fkynetic-spec\u002Fblob\u002Fmain\u002Ftemplates\u002Fskills\u002Ftriage-automation\u002FSKILL.md",[103],{"categoryNo":104,"categoryName":105,"taxonomyType":76,"taxonomyTypeDesc":77},"01JBSEEDCAT000000000000023","Skill与智能体开发",[86,107,108],"automation","triage automation",{"skillNo":110,"name":111,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":112,"categories":113,"keywords":114,"publishedAt":35},"01M6C7K3TFQ88QQBW3PXYDHQG2","task-work","https:\u002F\u002Fgithub.com\u002Flepahc\u002Fkynetic-spec\u002Fblob\u002Fmain\u002Ftemplates\u002Fskills\u002Ftask-work\u002FSKILL.md",[],[115,116],"work","task work",{"skillNo":118,"name":119,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":120,"categories":121,"keywords":122,"publishedAt":35},"01M6C7K168CH3XJCC3PDGA9H6C","review","https:\u002F\u002Fgithub.com\u002Flepahc\u002Fkynetic-spec\u002Fblob\u002Fmain\u002Ftemplates\u002Fskills\u002Freview\u002FSKILL.md",[],[119]]