feat(phase-4): topic clustering with haiku tagging and hdbscan-lite re-cluster#698
feat(phase-4): topic clustering with haiku tagging and hdbscan-lite re-cluster#6984Gaige wants to merge 12 commits into
Conversation
agent playbook, master plan, six phase briefs, midnight design system copy, five orchestration shell scripts, three github actions (ci, upstream sync, auto-merge). no source changes.
watchdog.sh monitors each phase worktree's claude pid. intervenes only when cpu near zero, no child processes, and no recent file writes for 15 minutes. sigterm first, sigkill only if ignored after 30 seconds. run-phase.sh now retries up to 3 times. retry uses a recovery prompt that shows git status, recent commits, open prs so claude resumes from where it stopped instead of starting over. falls back to sonnet if opus is overloaded.
* feat(skin): install Midnight design system tokens and shadcn mapping Adds the Midnight v1.1 token set as src/styles/midnight.css and wires shadcn-style semantic vars (--background, --foreground, --primary, etc.) to resolve against Midnight tokens so legacy bg-background / text-muted- foreground utilities keep working. Tailwind config extends with Midnight palette, pastels, radii, shadows, and motion curves. HTML root now declares class=dark and preconnects to Inter + Geist Mono. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(skin): add mobile tab bar and persistent desktop left rail Introduces a five-slot primary navigation — Chat, Sessions, Preview, Browser, More — that renders as a bottom .ds-tabbar below 1024px and a 64px left rail at lg+. Sessions slot opens the sidebar as a Midnight .ds-sheet on mobile (swipe-down-to-dismiss) and selects the persistent second-column sidebar on desktop. Per-section data-accent wires focus rings and selection highlights to the section's pastel. Touch targets are 44x44px minimum on mobile; safe-area-inset-bottom is respected via .ios-bottom-safe on the tab bar container. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(skin): replace raw Tailwind color classes with Midnight semantics Rewrites the hardcoded color sites called out in the phase-1 brief so none rely on literal Tailwind palette classes. MCP provider buttons now use primary/card; kanban columns compose .ds-tile-inset with per- status pastel accents; TaskCard becomes a .ds-tile-hover with status- accented dots; the image viewer sits on a .ds-sheet-backdrop inside a .ds-tile; login submit becomes .btn-primary; Queue indicators use the pastel mint/sky tokens. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(skin): a11y polish — drop tablist role, make sheet keyboard-aware DesktopRail drops the role="tablist" / role="tab" pair since the component does not implement the roving-tabindex / arrow-key semantics those roles promise. Replaces with aria-current="page" on the active item, which is accurate for primary nav. MobileSidebarSheet now subtracts --keyboard-height from its 80vh/svh target so the iOS keyboard does not clip the session list when a text field inside focuses. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
orchestrator: pre-create worktrees serially to avoid git config lock race. check if phase pr is already merged and skip work if so, making reruns safe. run-phase: add 90 min wall clock deadline per phase. after attempt 1 with zero new commits, fail fast because retry is unlikely to help. watchdog: anchor on run-phase.sh wrapper pid instead of regex matching the claude argv which has embedded newlines that break pgrep.
…s-touched chips (#2) * feat(mcp): add bootstrap service for recommended MCP servers Registers codebase-memory-mcp and claude-code-mcp in ~/.claude.json on boot unless the user has dismissed them. Companion state file tracks dismissals so re-installs don't fight the toggle. Exposes HTTP endpoints for list/toggle, sub-agent spawn (SSE), and session files-touched derivation. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(mcp): add sub-agent composer button, recommended-mcps settings, files-touched chips - SpawnSubAgentButton in chat composer opens a mobile-first modal with agent type chips and a prompt textarea; submit streams SSE output from /api/mcp-bootstrap/spawn-sub-agent. - RecommendedMCPsTab in Settings lists the two Dispatch-recommended MCPs with toggles wired to /api/mcp-bootstrap/recommended; toggling writes ~/.claude.json and persists dismissals so the bootstrap does not fight the user. - SessionFilesTouchedChips under each sidebar session lazily fetches derived "files touched" from session JSONL tool uses; uses IntersectionObserver to defer work until the row scrolls into view. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: auto-create upstream-sync branch if missing The aormsby sync action requires the target branch to already exist. Dispatch the workflow on a fresh fork and it fails with "pathspec 'upstream-sync' did not match any file(s)". Create the branch from main up front so the first sync run (and any future re-creation) just works. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(mcp): harden path traversal, touch target, and config-parse safety - Reject '..', slashes, and null bytes in session-files-touched route params; defense-in-depth prefix-check on the resolved path so no response can read outside ~/.claude/projects. - Bump recommended-MCPs toggle to 44x44 with a 52px-wide track so the mobile touch target meets the phase's minimum. - If ~/.claude.json is unreadable/unparseable, ensureRecommendedMCPs and the toggle endpoint now abort instead of silently rewriting an empty config over the user's state. The toggle surfaces a 409 with the path so the user can fix their file manually. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(mcp): keep /recommended endpoint responsive when ~/.claude.json is corrupt describeRecommendedMCPs now treats an unparseable config the same as a missing one so the Settings page still renders (with items reported as not installed) instead of blowing up with a 500. Matches the guard already in place on the bootstrap and toggle paths. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat(sidebar): repo grouping service Wrap getProjects output with a new repo-grouper service so the sidebar can render a Repo → Topic → Conversation tree. The service walks up each project's fullPath looking for .git, reads remote.origin.url, normalizes the URL, and detects worktrees (.claude/worktrees path or .git pointer file). Results cached in ~/.cloudcli/project-config.json. Project type gains optional repoGroup, repoDisplayName, isWorktree, gitBranch, gitRoot, gitOrigin. server/projects.js gets one new import line and one new line at the tail of getProjects — no internal edits. Registers server/services/*.js as a new eslint boundaries element so the plugin classifies the new module. * feat(sidebar): topic tree render + drag-to-reassign Render projects as a Repo -> Topic -> Conversation tree via a new SidebarProjectTree orchestrator. Projects with the same repoGroup (from the server-side grouper) collapse into a single collapsible repo header so worktrees no longer appear as top-level entries. Each project is wrapped by SidebarTopicGroup, an additive wrapper around SidebarProjectItem. When expanded, it renders a horizontal Topic chip row (the All chip plus any user-created topics) and filters the sessions prop by the active topic before forwarding to SidebarProjectItem. The wrapped item itself is untouched. Drag-and-drop via @dnd-kit/core: SidebarSessionItem becomes a draggable (one-line root wrapper, activation constraints keep tap/click intact), topic chips are droppable. Dropping a session on a chip persists the assignment in localStorage under dispatch.sidebar.topics.v1; dropping on All unassigns. Pointer+touch sensors mean long-press-drag works on mobile too. New files under src/components/sidebar/topics/: TopicChip, SidebarTopicGroup, SidebarProjectTree, useTopicStorage. SidebarContent swaps SidebarProjectList for SidebarProjectTree. Adds @dnd-kit/core. * feat(sidebar): search scope relabel + disable-when-empty The Projects/Conversations tabs in the sidebar header were a search-scope toggle mislabeled as a primary navigation control. Reorder the search input above the scope row so the primary input becomes visually dominant, and disable the scope buttons (cursor-not-allowed + opacity-40) until the user types. Placeholder shown on an empty input is now a single "Type to search" string, switching to the scope label once text is entered. i18n: relabel search.modeProjects -> "Search projects" and search.modeConversations -> "Search conversations" across all 8 locales (en, de, it, ja, ko, ru, tr, zh-CN). Add two new keys in search (typeToSearch, scopeLabel) and a new topics section (all, createTopic, renamePrompt, deleteConfirm, namePlaceholder, chipRowLabel) used by SidebarTopicGroup. * fix(sidebar): restore document.title update in tree SidebarProjectList had a useEffect that kept document.title in sync with the selected project's displayName (e.g. tab title read "my-app - CloudCLI UI"). When SidebarProjectTree replaced it as the rendered tree the effect was lost. Port the same 8-line effect. * fix(sidebar): scope toggle active state + repo header polish Address visual-review findings: - Replace the ad-hoc flex/rounded-lg scope toggle with the Midnight .ds-segment + .ds-segment-item + .ds-segment-item-active catalog so the active scope reads white-on-black against the dark canvas instead of blending into bg-background (both desktop + mobile). - Ensure scope tabs hit the 44px touch-target minimum on both viewports by adding min-h-[44px] on each segment item. - Swap the ad-hoc rounded-full count badge next to repo group names for the Midnight .badge token. - Tint the origin-present FolderGit2 icon with the sidebar accent (var(--midnight-lavender)) to reinforce per-section accent discipline on the tree. Also adds Phase 2 visual artefacts under docs/screenshots/phase-2/: Playwright captures at 1440x900 + 375x812, a README explaining the acceptance-criteria evidence, and the regeneration helper _capture.mjs (auth-token seeding, onboarding skip, loading wait).
…s kanban (#4) * feat(phase-5): add backend routes for preview proxy, chrome screencast, worktrees, tasks Scaffolds four new Express/WS routes under server/routes/ and wires each one into server/index.js with a single import + mount line per feature. - preview-proxy: reverse-proxy /preview/:port/* to 127.0.0.1:{port} with WS upgrade passthrough for dev-server HMR. Strips CSP/X-Frame-Options and rewrites Set-Cookie domain to our host. - chrome-screencast: CDP Page.startScreencast over ws://host/ws/chrome-view. Forwards JPEG frames to client, acks back to Chrome, and translates client input messages to Input.dispatch* calls. Status + tab endpoints under /api/chrome-view for UI bootstrapping. - worktrees: list/create/delete under /api/worktrees plus a /spawn helper that launches 'claude' in tmux when available and falls back to a detached spawn. Worktrees live in <repo>/.claude/worktrees/<slug>. - tasks: reads the active claude session's JSONL and returns the latest TodoWrite tool_use bucketed into todo/in_progress/completed columns. server/index.js additions are additive only (4 imports, 4 app.use lines, 2 attach* calls for the WS upgrade handlers). No behavior of existing routes is changed. * feat(phase-5): add preview, browser, tasks, and worktree UI for phase 5 Frontend half of phase 5. Ships the four features via additive new component directories and a minimal set of wiring edits to non-restricted shared files. - preview: PreviewPane (iframe + URL bar + device presets) and PreviewModal (mobile fullscreen). Renders /preview/{port}{path} against the new server proxy. data-accent="mint" per nav discipline. - browser: BrowserPane (canvas renderer for CDP screencast frames, take-control toggle that forwards mouse/keyboard/scroll events via ws://host/ws/chrome-view) and BrowserModal. data-accent="peach". - tasks: TasksPane (3-column kanban on desktop, swipeable segmented on mobile with ds-segment), TaskCard (ds-tile), TasksModal (bottom sheet). Polls /api/tasks every 4s and refreshes on TodoWrite events seen on the chat WS. data-accent="butter". - sidebar/worktrees: WorktreeList rendered beneath each project's session list. Lets users list, create, spawn and delete worktrees under <repo>/.claude/worktrees/. Activity dots green/yellow/red/grey drive from the session protection state (wired-ready). Wiring edits (all to non-restricted files): - src/types/app.ts: AppTab union gains 'browser' variant - src/components/layout/useAppNavItems.ts: resolveActiveSlot routes browser → browser slot - src/components/app/AppContent.tsx: nav select handler opens the browser tab; main-column data-accent maps preview/browser/tasks - src/components/main-content/view/MainContent.tsx: preview/browser slots now render their panes; floating Tasks button + right drawer (desktop) or bottom-sheet modal (mobile) surface TasksPane - src/components/sidebar/view/subcomponents/SidebarProjectSessions.tsx: renders WorktreeList below the session list per project - .gitignore: carve-out so src/components/tasks/ escapes the tasks/ ignore that exists for TaskMaster storage * fix(phase-5): enforce ws auth on new upgrade paths and clean up dead code Review fixes from the fresh-eyes audit: - server/routes/preview-proxy.js: switch to prependListener on the http upgrade event so /preview/{port}/* wins the race against the root wss in server/index.js (which otherwise accepts and immediately closes unknown paths). Verify the JWT off the token query param / bearer header before forwarding upstream. Preserve upgrade and connection headers through to the dev-server. - server/routes/chrome-screencast.js: same prependListener + JWT verification at the /ws/chrome-view upgrade. - src/components/preview/PreviewPane.tsx: drop the dead iframeRef and the tautological DEVICE_PRESETS.find(... preset.id === preset.id) block that the reviewer flagged. - src/components/sidebar/worktrees/WorktreeList.tsx: fix the invisible delete button on mobile — the touch:opacity-100 pseudo-class isn't configured, and the row hover class was bg-midnight-surface-hover (undefined). Switch to a visible-on-mobile, reveal-on-hover-on-desktop pattern with a 44×44 touch target, and use hover:bg-accent/40 for the row hover state. * fix(phase-5): resolve ws double-upgrade crash and visual review polish Visual review found a reproducible backend crash plus two minor UI bugs. Screenshots and review notes are committed under docs/screenshots/phase-5/. - server/routes/chrome-screencast.js + server/routes/preview-proxy.js: accept the root WebSocketServer as a second argument and patch its shouldHandle() to reject paths we own. Without this the ws library's internal upgrade listener re-entered handleUpgrade on a socket we had already claimed, throwing "server.handleUpgrade() was called more than once with the same socket" on the second Browser-tab click. - server/index.js: pass the root wss (one-token argument change per existing attach call, no new lines). - src/components/browser/BrowserPane.tsx: add shrink-0 and whitespace-nowrap to the take-control button so its label does not clip on narrow desktop panels. - src/components/sidebar/worktrees/WorktreeList.tsx: swap the neutral branch label for a ds-chip-lavender pill so it reads as a proper Midnight badge rather than muted text. - docs/screenshots/phase-5/*.png + VISUAL-REVIEW.md: mobile (375x812) and desktop (1440x900) captures covering home, preview, browser, worktree list, tasks-drawer fallback, plus the midnight demo reference. Includes the written review notes.
brief instructs an opus session to review each merged phase against claude md checklist and phase briefs, fix critical issues in a dedicated review worktree, then poll for phase 3 + 4 to land and repeat. non-interfering with the running orchestrator.
* feat(titler): add haiku-based session auto-naming service Adds `server/services/session-titler.js` and `server/services/title-prompt.js`. The titler watches `~/.claude/projects/**/*.jsonl`, waits for 60s of idle per file, reads the first two non-system user messages, and asks Haiku for a 3-5 word title stored in the existing `session_names.custom_name` column. On boot it backfills any JSONL that has no custom name. Writes trigger the existing projects watcher via a blank-line append, so the sidebar updates live without touching server/index.js beyond a future single import line. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(titler): wire auto-namer into server boot and sidebar shimmer - server/index.js: single-line import of session-titler service so it self-starts at boot (matches the "no other edits" rule for churn files). - server/database/db.js: applyCustomSessionNames now marks Claude sessions without a custom_name with pendingTitle=true so the UI can flag them. - server/services/session-titler.js: prefer @anthropic-ai/sdk directly when an ANTHROPIC_API_KEY is available (env or settings.json); fall back to the Claude Agent SDK's query() for OAuth-only environments. - SidebarSessionItem.tsx: render the session name with the `.shimmer` Midnight utility class while pendingTitle is true; aria-busy for SR. - types/app.ts: add optional pendingTitle?: boolean to ProjectSession. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(titler): skip saving when no haiku path is available When both the direct Anthropic SDK and the Claude Agent SDK fail to produce a result (no key, offline, rate limited, ...), treat the title as unresolved and leave `pendingTitle=true` so the next boot or network recovery picks the session back up. Previously we wrote 'Untitled' which poisoned the cache. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix(review): harden mcp-bootstrap spawn and skip bootstrap when no claude config Two issues from the post-merge fresh-eyes review of phase 6: - The /spawn-sub-agent endpoint passed a user-supplied workingDir straight into child_process.spawn's cwd with no validation. An authenticated client could point claude at /etc, /var, or anywhere else the server uid can reach. Now we resolve the path, require it to live under $HOME (or DISPATCH_PROJECT_ROOTS), reject sensitive credential subdirs, and confirm it exists as a directory before spawning. - ensureRecommendedMCPs ran as a top-level module side-effect on import and would happily materialize a fresh ~/.claude.json containing only Dispatch's two MCPs for users who had never run claude. The bootstrap now refuses to create the file from nothing and defers via setImmediate so the import chain in server/index.js is not blocked by file IO. * fix(review): preview port allowlist, chrome viewer operator scoping, tasks path safety Three security fixes from the post-merge fresh-eyes review of phase 5: - Preview proxy used to forward any port from 1-65535 to 127.0.0.1, so any authenticated user could probe localhost services (mysql, postgres, redis, the cdp port itself, etc). Now defaults to a curated allowlist of common dev-server ports, blocks well-known infra ports outright, and exposes DISPATCH_PREVIEW_PORTS / DISPATCH_PREVIEW_ALLOW_ANY_HIGH_PORT for users who need something else. - Chrome screencast attaches to the operator's real browser over CDP and was reachable by any logged-in user. The Input.* dispatch path let any client drive the host browser (steal cookies via Runtime.evaluate, navigate to attacker URLs, etc). Now disabled by default; opt in via DISPATCH_CHROME_VIEW_ENABLED=true for view-only and additionally DISPATCH_CHROME_VIEW_ALLOW_INPUT=true for take-control. The HTTP /status and /tabs helpers report the disabled state cleanly so the client UI can render an explanation instead of a stack trace. - /api/tasks built the JSONL path from query params with no traversal guard. Now rejects path separators, null bytes, and '..' on projectName + sessionId, then prefix-checks the resolved path against ~/.claude/projects. * feat(review): wire mobile preview/browser modals and worktree activity context Two phase 5 frontend follow-ups from the post-merge review: - PreviewModal and BrowserModal shipped in phase 5 but were never imported anywhere, so mobile users on the preview/browser tabs got the desktop pane shoved into the tab slot with no fullscreen treatment or close button. MainContent now mounts both modals, gated by isMobile + activeTab; the inline panes stay desktop-only. Closing a modal returns to the chat tab. - WorktreeList accepted activeSessions/processingSessions/blockedSessions /worktreeSessionMap props but its parent (SidebarProjectItem) is on the no-edit churn list, so the props were never passed and every dot stayed grey. New SessionActivityContext lives at the AppContent root and delivers the same protection sets to WorktreeList without prop drilling. worktreeSessionMap is best-effort for now: only the currently selected worktree's session can be attributed; cross-worktree resolution needs a server endpoint, tracked in docs/follow-ups.md. * docs(review): track post-merge nice-to-haves in follow-ups.md Captures every NICE-TO-HAVE finding from the four phase reviews so the items aren't lost. Entries are grouped by phase with file:line citations; each is small enough to land in a polish PR or fold into the next phase.
polls origin/main every 2 minutes. on new commits: git pull, npm install if package.json changed, npm run build, launchctl kickstart the com.dispatch.forge service, health check, imessage the user with the commit subject. uses flock on /tmp/dispatch-main-worktree.lock to serialize with other main-worktree git ops.
…e-cluster Adds server-side automatic topic assignment (visible as pastel chip rows in the sidebar) on top of the Phase 2 client-only topic UI. Strategies: - Per-session Haiku tag, fired by the titler EventEmitter so a freshly-titled conversation gets its topic within seconds. - Nightly 3am full re-cluster: small projects (<20 convos) get a Haiku catch-up pass, large projects get Voyage embeddings (voyage-3-lite) + a single-linkage HDBSCAN-lite pass, with cluster names chosen by Haiku from each cluster's top-5 closest titles. Server: - migrations/001_conversation_topics.sql + topic-store.js (better-sqlite3 wrapper). - topic-prompt.js: tagging + cluster-naming prompts adapted from open-webui. - embeddings-client.js: Voyage HTTP wrapper, no SDK dep, gracefully no-ops when VOYAGE_API_KEY is unset. - topic-clusterer.js: both strategies + manual override entry point. - topic-clusterer-cron.js: self-arming setTimeout (no node-cron dep) + per-session debounce; subscribes to titlerEvents. - routes/topics.js: GET /api/topics, POST /assign, POST /cluster, etc. - session-titler.js: additive EventEmitter export so subscribers can react. - server/index.js: 3 lines (1 route import, 1 cron side-effect import, 1 mount). Client: - useServerTopics.ts replaces the local-only useTopicStorage; same shape, but fetched from /api/topics, refreshed on projects_updated WS events, with optimistic local updates for drag-drop assignment. - TopicChip now renders a count badge (server-provided sessionCount). - SidebarTopicGroup keys topics by project slug to align with server project_key. Manual drag-drop assignments persist with method='manual' and are preserved across automatic re-clustering. Manual flow continues to feel instant via optimistic update + post-hoc /api/topics refresh.
|
Wrong target — opening against the fork 4Gaige/Dispatch instead. |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (23)
📒 Files selected for processing (100)
📝 WalkthroughWalkthroughThis PR introduces a comprehensive fork-and-skin overhaul: GitHub Actions workflows for CI/auto-merge/upstream-sync; a complete MIDNIGHT design system with CSS/Tailwind config; server services for session auto-titling and conversation topic clustering; new UI components for task management, topic filtering, preview proxy, and chrome screencast; and extensive multi-phase documentation for implementation and review. Changes
Sequence Diagram(s)Not generated — the changes encompass diverse independent features (topic clustering, session titling, task polling, worktree management, preview proxy, chrome screencast) without a unified sequential control flow suitable for visualization. Each feature operates as a self-contained microservice or UI subsystem. Possibly related PRs
Suggested reviewers
✨ Finishing Touches🧪 Generate unit tests (beta)
|
Summary
titlerEventsEventEmitter).voyage-3-liteembeddings + single-linkage clustering + Haiku cluster naming) on projects with ≥20 conversations; small projects get a Haiku catch-up pass.method='manual'and survive automatic re-clustering.Changes
Server — all new files except one additive edit and three import lines
server/database/migrations/001_conversation_topics.sql— new table per brief.server/database/topic-store.js— better-sqlite3 wrapper; runs the migration on import (mirrors the inline-DDL pattern fromdb.js).server/services/topic-prompt.js— Haiku tagging + cluster-naming prompts (adapted from open-webui), plus title-case normalizer.server/services/embeddings-client.js— Voyage HTTP wrapper, no SDK dependency, gracefully no-ops withoutVOYAGE_API_KEY.server/services/topic-clusterer.js— both strategies + manual-override entry point; Haiku call mirrors Phase 3's Direct SDK → Agent SDK fallback.server/services/topic-clusterer-cron.js— self-arming setTimeout-at-3am (nonode-crondep); per-session debouncing; subscribes totitlerEvents.server/services/session-titler.js— additive only: adds atitlerEventsEventEmitter export and oneemitTitled(...)call after successful title writes.server/routes/topics.js—GET /api/topics,GET /api/topics/project/:slug,POST /assign,POST /cluster,POST /cluster/project/:slug,POST /tag/session.server/index.js— 3 new lines (1 route import, 1 cron side-effect import, 1app.usemount).Client
src/components/sidebar/topics/useServerTopics.ts(replacesuseTopicStorage.ts) — same API shape, backed by/api/topics; refreshes onprojects_updatedWebSocket events; optimistic updates for drag-drop assignment; preserves local ghost topics until they get a session.src/components/sidebar/topics/TopicChip.tsx— adds a session-count badge (server-provided).src/components/sidebar/topics/SidebarTopicGroup.tsx— keys active-topic state by project slug (matches serverproject_key); filters visible topics per project.src/components/sidebar/topics/SidebarProjectTree.tsx— switched touseServerTopics; drag-end now passesprojectKeythrough.Acceptance criteria (from phase-4-brief.md)
'Misc').findAccentForTopicbeforepickAccentForProject).method='manual'preserved byreplaceForProject).VOYAGE_API_KEYis set.Constraints
server/projects.jsorsrc/components/sidebar/subcomponents/SidebarProjectItem.tsx.server/index.jsgains exactly 3 lines (1 route import, 1 cron side-effect import, 1app.use)..ds-chip*component classes.min-h-[44px], chip row isoverflow-x-auto, root haspb-safe-area-inset-bottom.Verification
npm run build✅npm run typecheck✅npm run lint✅ (auto-fixed import-order warnings)npm test→ n/a (notestscript in package.json)index-*.js2,530,745 B → feat/topics 2,535,757 B = +0.20% (well under 5% budget)Test plan
conversation_topicstable gets created.VOYAGE_API_KEY, the nightly falls back to per-session Haiku tagging and logsskipped/no-voyage-keyfor large-project attempts.GET /api/topicsreturns{ byProject: { <slug>: { topics: [...], assignments: {...} } } }.Fresh-eyes review
A clean-context Opus reviewer ran through CLAUDE.md's full checklist + the phase-4 brief's acceptance criteria and flagged no required fixes. Optional follow-ups: add a dedicated
topics_updatedWS broadcast from the cron (today nightly updates surface on the nextprojects_updated), and a rate-limit onPOST /api/topics/cluster.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation