diff --git a/.antigravitycli/1da03e32-5da9-4ec4-b377-33029cda1bb1.json b/.antigravitycli/1da03e32-5da9-4ec4-b377-33029cda1bb1.json new file mode 120000 index 000000000..7b19d04aa --- /dev/null +++ b/.antigravitycli/1da03e32-5da9-4ec4-b377-33029cda1bb1.json @@ -0,0 +1 @@ +/home/tstapler/.gemini/config/projects/1da03e32-5da9-4ec4-b377-33029cda1bb1.json \ No newline at end of file diff --git a/.asdf-install.stamp b/.asdf-install.stamp new file mode 100644 index 000000000..e69de29bb diff --git a/.claude/brand-guidelines.md b/.claude/brand-guidelines.md new file mode 100644 index 000000000..06d2499be --- /dev/null +++ b/.claude/brand-guidelines.md @@ -0,0 +1,312 @@ +# Seneschal Brand Guidelines + +> Condensed brand guide — specific enough that a contractor who has never worked with this brand can produce on-brand work from this document alone. +> +> For foundational strategy, positioning, and the estate metaphor vocabulary, see `product-marketing-context.md`. + +--- + +## 1. Color System + +### Semantic Palette + +All color decisions should reference semantic roles, not raw hex. Match these tokens to the CSS variables in `globals.css`. + +#### Foundation (Backgrounds) + +| Role | Token | Hex | Usage | +|------|-------|-----|-------| +| Canvas | `--background` | `#0f1117` | Page background, deepest layer | +| Surface | `--card-background` | `#161b22` | Cards, panels, drawers | +| Surface raised | `--hover-background` | `#1e2530` | Hover states on surfaces | +| Border | `--border-color` | `#1e293b` | Dividers, input borders | + +#### Terminal Layer + +| Role | Token | Hex | Usage | +|------|-------|-----|-------| +| Terminal canvas | `--terminal-background` | `#1e1e1e` | xterm.js background | +| Terminal chrome | `--terminal-surface-header` | `#252526` | Terminal header/tab bar | +| Terminal text | `--terminal-foreground` | `#d4d4d4` | Terminal output text | +| Terminal accent | `--terminal-accent` | `#4d8bf7` | Highlights, cursor | + +#### Text + +| Role | Token | Hex | Usage | +|------|-------|-----|-------| +| Primary | `--text-primary` | `#e2e8f0` | Body text, headings | +| Secondary | `--text-secondary` | `#94a3b8` | Labels, metadata, captions | +| Muted | `--text-muted` | `#7d8ea8` | Placeholders, disabled labels | +| Disabled | `--text-disabled` | `#9a9a9a` | Inactive controls | + +#### Action + +| Role | Token | Hex | Usage | +|------|-------|-----|-------| +| Primary | `--primary` | `#8B1A3B` | Buttons, icons, active indicators — deep burgundy | +| Primary hover | `--primary-hover` | `#A82248` | Button hover | +| Primary active | `--primary-active` | `#6D1530` | Button pressed | +| Gold accent | `--gold` | `#C9A84C` | Taglines, decorative accents, blade bits, seal rim | +| Gold hover | `--gold-hover` | `#D4B866` | Gold element hover states | +| Purple whisper | `--purple` | `#5B3678` | Used sparingly — one accent per composition | +| Primary text | `--primary-text` | `#EDE0D0` | Text on dark — warm cream, not pure white | + +#### Status + +| Role | Token | Hex | Usage | +|------|-------|-----|-------| +| Success | `--success` | `#22c55e` | Completed, approved, healthy | +| Warning | `--warning` | `#f59e0b` | Attention needed, uncommitted changes | +| Error | `--error` | `#ef4444` | Failed, denied, critical | +| Processing | `--processing` | `#4338ca` | In-progress, running | + +#### Session Status Badges + +These are the inline status chips on session cards. Use the full bg + fg pair — never mix them. + +| Status | Background | Foreground | When | +|--------|-----------|-----------|------| +| Approval pending | `#fecaca` | `#991b1b` | Agent has a petition awaiting assent — highest urgency | +| Input needed | `#dbeafe` | `#1e40af` | Agent is waiting on user input | +| Complete | `#dcfce7` | `#166534` | Task finished cleanly | +| Uncommitted | `#fef3c7` | `#92400e` | Worktree has unmerged changes | +| Idle | `#f3f4f6` | `#374151` | Running, no action needed | + +### Usage Rules + +- **Never use raw hex in component code.** Always reference the CSS variable. This makes theme changes propagate correctly. +- **Dark-first.** There is no light theme. Do not design for it. +- **Indigo is authority, not decoration.** Use `--primary` for interactive elements and the active state of selected items only. Do not use it for decorative highlights. +- **Red means stop.** `--error` and the approval-pending badge are the only red elements in the UI. Do not use red for anything non-critical. +- **Terminal surfaces are their own world.** Do not mix terminal surface colors (`#1e1e1e`) with UI surface colors (`#161b22`). They are intentionally distinct layers. + +--- + +## 2. Typography + +### Typeface Stack + +The project uses four fonts loaded via `next/font`. Each has a defined role — do not substitute. + +| Typeface | Variable | Role | Why | +|----------|----------|------|-----| +| **Cinzel** | `--font-cinzel` | Display / Logo / Estate headings | Modeled on classical Roman monumental inscriptions — directly evokes the Seneschal name's historical roots. Authority without ornamentation. | +| **Inter** | `--font-inter` | UI body / labels / descriptions | The standard for legible, dense information UI. Neutral enough to carry any content. | +| **Rajdhani** | `--font-rajdhani` | Technical section headings | Geometric sans with a slightly industrial edge — appropriate for labeling technical structures (sessions, branches, tags). | +| **JetBrains Mono** | `--font-mono` | Code / terminal / paths / commands | Best-in-class developer monospace. All code-adjacent content uses this. | + +### Type Scale + +| Role | Font | Size | Weight | Line-height | Usage | +|------|------|------|--------|-------------|-------| +| Display | Cinzel | 32px | 700 | 1.2 | Logo wordmark, hero headline, marketing h1 | +| Heading 1 | Cinzel | 24px | 700 | 1.3 | Page titles, modal titles | +| Heading 2 | Rajdhani | 18px | 600 | 1.4 | Section headings, panel headers | +| Heading 3 | Rajdhani | 15px | 600 | 1.4 | Subsection labels, grouped item headers | +| Body | Inter | 14px | 400 | 1.6 | All descriptive text, form labels, README body | +| Body strong | Inter | 14px | 600 | 1.6 | Emphasis within body text | +| Small | Inter | 12px | 400 | 1.5 | Captions, metadata, timestamps, tag labels | +| Small strong | Inter | 12px | 600 | 1.5 | Status labels, badge text | +| Code inline | JetBrains Mono | 13px | 400 | 1.6 | Inline `code`, paths, commands | +| Code block | JetBrains Mono | 13px | 400 | 1.7 | Code blocks, terminal output | +| Terminal | JetBrains Mono | 13px | 400 | 1.5 | Live terminal content in xterm pane | + +### Typography Rules + +- **Cinzel is for the estate, not the engine.** Use it for display text, brand headings, and estate-vocabulary callouts. Do not use it for form labels, error messages, or dense UI text. +- **All code-adjacent content is JetBrains Mono.** This includes: inline code, command examples, file paths, branch names, session IDs, git hashes, and terminal output. +- **Minimum body size is 12px.** Nothing smaller in production UI. +- **No italic in the UI.** Italics are reserved for marketing copy and the estate metaphor voice examples. Use weight variation (400→600) instead of italic for emphasis in the product. +- **Letter-spacing on Cinzel display**: add `letter-spacing: 0.05em` at 24px+ for proper monumental spacing. + +--- + +## 3. Logo System + +### Mark Components + +**Wordmark**: `SENESCHAL` in Cinzel Bold (700), uppercase, letter-spacing 0.08em. The full caps and spacing evoke Roman inscription — intentional. + +**Icon**: A skeleton key with a grid of three parallel horizontal bars in the bow (the ring at the top of the key). The bars represent parallel agent sessions under unified authority. The key represents the seneschal's literal role — keeper of the keys to the estate. + +**Combination mark**: Icon to the left of wordmark, separated by 1× the icon height in space. Icon and wordmark optically centered on the horizontal axis. + +### Sizing & Clear Space + +- **Minimum size**: 120px wide for the combination mark; 24px for icon-only +- **Clear space**: Equal to the cap-height of the `S` on all four sides of the full mark +- **Don't place the logo on a background lighter than `#1e293b`** — the mark is designed for dark surfaces + +### Approved Variations + +| Variant | When to use | +|---------|-------------| +| Full mark on dark (`#0f1117`) | Primary — README, website, presentations | +| Full mark on indigo (`#6366f1`) | Secondary — badges, social cards | +| Wordmark only (no icon) | Tight horizontal spaces, breadcrumbs | +| Icon only | Favicons, app icons, 32px and under | +| Mono white | Single-color contexts (embroidery, engraving) | + +### Prohibited Uses + +- No color gradients on the mark or icon +- No drop shadows, glows, or bloom effects +- No stretching or non-uniform scaling +- No placement on backgrounds with insufficient contrast +- No recoloring the icon to anything except `#6366f1`, `#818cf8`, or white +- No outlines or strokes added to the letterforms +- No AI-generated or playful variations of the icon (no robots, brains, sparkles, circuit boards) + +--- + +## 4. Voice & Tone Matrix + +### Personality Anchors + +**Pragmatic** — Says exactly what is needed. No filler. +**Technically precise** — Uses the right word, not the accessible word. +**Quietly intellectual** — The estate metaphor is present but never forced. +**Unpretentious** — Expert-first without condescension. + +### By Context + +#### README / GitHub Hero +*Goal: Convince a developer in 60 seconds.* +- Declarative statements, not questions +- Estate metaphor is welcome here — this is the one place it gets full expression +- Numbers over adjectives ("42 built-in rules" beats "powerful approval engine") +- The aha moment is always the approval gate + +> ✅ "Your agentic estate, managed. Run five AI coding agents in parallel — every petition heard, every holding visible." +> +> ❌ "Seneschal is a powerful AI agent management platform that helps developers streamline their workflow." + +#### Onboarding / First Run +*Goal: Orient without hand-holding.* +- Warmer than the README, not warm +- Acknowledge the user knows what they're doing +- Frame first steps as taking possession of the estate + +> ✅ "Your first holding is ready. Seneschal manages the routine — you'll only be summoned when a decision matters." +> +> ❌ "Welcome! Let's get you started with your first session! Here's how it works..." + +#### Approval / Petition Interface +*Goal: Fast, unambiguous decision-making.* +- Formal and precise — this is the court +- Agent name + tool name + what it will do. Nothing more. +- The two options are always "Assent" and "Deny" (or "Approve" / "Deny" in technical labels) + +> ✅ "claude-code is petitioning to delete `dist/` (3 files). Assent or deny?" +> +> ❌ "Your AI assistant would like permission to perform a file deletion operation. Would you like to allow this?" + +#### Error Messages +*Goal: State what happened and what to do.* +- No apology. No "oops" or "uh oh". +- Cause first, remedy second. +- One sentence each. + +> ✅ "Session failed to start. Verify tmux is installed and the path exists." +> +> ❌ "Oops! We couldn't start your session. Please make sure tmux is installed correctly and try again." + +#### Empty States +*Goal: Acknowledge absence without drama.* +- Brief. Matter-of-fact. One line when possible. +- A dry hint is acceptable; never cheerful. + +> ✅ "No holdings. Start a session to put your first agent to work." +> +> ❌ "You don't have any sessions yet! Click the button below to create your first one 🚀" + +#### Notifications / Alerts +*Goal: Interrupt only when necessary; be scannable.* +- Session name first, status second, action third +- Never alert for informational events — only for petitions, failures, and completions that require human attention + +> ✅ "Petition pending — `feat/payments` is waiting on your assent." +> +> ❌ "You have a new notification from one of your AI coding agent sessions!" + +### Words to Use +`session`, `holding`, `petition`, `assent`, `deny`, `standing order`, `review queue`, `worktree`, `isolated`, `real-time`, `approval rule`, `coverage`, `estate` + +### Words to Avoid +`AI-powered`, `revolutionize`, `seamlessly`, `effortlessly`, `unlock`, `supercharge`, `cutting-edge`, `empower`, `smart`, `intelligent`, `magic`, `oops`, `uh oh`, `please` + +--- + +## 5. Imagery & Screenshot Standards + +### What to Show +- **Real UI at full resolution** — screenshots of the actual Seneschal dashboard, terminal panes, approval drawer +- **Dense, active states** — show 3–5 sessions running simultaneously; empty states are not marketing material +- **The approval gate in action** — the petition interface is the product's most differentiating moment; it should appear in the README hero area +- **Terminal content** — real code output, real diffs, real agent interactions (sanitized, no credentials) + +### What Not to Show +- No stock photography +- No abstract "AI" imagery (neural nets, glowing brains, circuit board patterns) +- No light-mode screenshots — the product is dark-first; always capture in dark theme +- No castle, estate, or medieval imagery — the estate metaphor lives in copy, not visuals +- No AI-generated illustrations + +### Demo GIF Standards +- Maximum 30 seconds +- Show the aha moment: agent running → petition appears → user assents → agent continues +- Record at 1440×900 minimum +- Optimize to under 5MB (use `gifsicle` or equivalent) +- Caption the key moments with Rajdhani 15px labels + +--- + +## 6. Layout & Spacing Principles + +### Density Philosophy +Seneschal is a professional tool for developers who have many sessions running at once. **Information density is a feature, not a design failure.** Do not add whitespace to "breathe" — add it only to create clear visual hierarchy. + +### Grid +- Base unit: 4px +- Common spacings: 4, 8, 12, 16, 24, 32, 48px +- Session cards: minimum 280px wide, no fixed maximum +- Sidebar: 240px fixed; 56px collapsed +- Terminal pane: fills available space; minimum 400px wide + +### Hierarchy Rules +- Use weight (400→600) to create emphasis within a type role before changing size +- Use color (`--text-primary` → `--text-secondary` → `--text-muted`) to de-emphasize supporting content +- Status badges are the loudest element on a session card — they should be immediately scannable without reading anything else + +### Borders & Radius +- Card borders: 1px `--border-color` (`#1e293b`) +- Border radius: 6px for cards, 4px for inputs, 2px for badges, 0 for terminal panes +- No rounded corners on terminal panes — the terminal is a raw surface + +--- + +## 7. Quick Audit Checklist + +Use this before shipping any external-facing asset: + +- [ ] Colors reference CSS tokens, not raw hex +- [ ] No color lighter than `#1e293b` used as a background +- [ ] Cinzel is used only for display text and estate-vocabulary headings +- [ ] All code, paths, and commands are in JetBrains Mono +- [ ] No "oops", "seamlessly", "AI-powered", or emoji in UI copy +- [ ] Approval interface uses "Assent" / "Deny" language +- [ ] Empty states are one line, matter-of-fact +- [ ] Screenshots are dark-theme, real UI, active state with multiple sessions +- [ ] Logo has proper clear space; no drop shadows or gradients +- [ ] Red (`--error`, approval-pending badge) is used only for genuinely critical states + +--- + +## Related Skills + +| Skill | When to apply | +|-------|-------------| +| `pm-brand-strategy` | Source of truth for positioning, estate metaphor, and name vetting | +| `ui-frontend-design` | Executing these guidelines in React/vanilla-extract components | +| `ui-logo-designer` | Generating logo concepts from the mark description above | +| `ui-design-system` | Building or auditing the design token system | diff --git a/.claude/commands/perf/make-it-faster.md b/.claude/commands/perf/make-it-faster.md new file mode 100644 index 000000000..b772a3408 --- /dev/null +++ b/.claude/commands/perf/make-it-faster.md @@ -0,0 +1,368 @@ +--- +description: Connect to the running stapler-squad pprof endpoint, interpret the profiles, identify the top performance bottlenecks, propose concrete improvements ranked by impact, and codify each fix with a test or lint rule so regressions cannot silently reappear. +prompt: | + # perf:make-it-faster — Profiling → Proposal → Enforcement + + You are performing a live performance audit of the running stapler-squad process. + Work through four phases in order and produce concrete, actionable output. + + --- + + ## Phase 0 — Connect and Capture + + The server must be running with `--profile` to expose pprof. Check first: + + ```bash + curl -s http://localhost:6060/debug/pprof/ | head -5 + ``` + + If it returns HTML, capture all four profiles: + + ```bash + # Goroutine states (qualitative: what are all goroutines doing right now?) + curl -s "http://localhost:6060/debug/pprof/goroutine?debug=2" > /tmp/goroutines.txt + + # Mutex contention (quantitative: which mutexes are hot?) + curl -s "http://localhost:6060/debug/pprof/mutex?debug=1" > /tmp/mutex.txt + + # Scheduler blocking (quantitative: which goroutines block the scheduler longest?) + curl -s "http://localhost:6060/debug/pprof/block?debug=1" > /tmp/block.txt + + # In-use heap allocations (what is alive right now?) + curl -s "http://localhost:6060/debug/pprof/heap?debug=1" > /tmp/heap.txt + + # Allocation rate (what allocates most often, even if short-lived?) + curl -s "http://localhost:6060/debug/pprof/allocs?debug=1" > /tmp/allocs.txt + ``` + + If the server is not running with `--profile`, restart it: + ```bash + make restart-web PROFILE_FLAGS="--profile" + ``` + + --- + + ## Phase 1 — Read the Profiles + + ### How to interpret each profile + + **mutex** — the most actionable for latency. + - Format: ` @ ` + - `cycles` = total CPU cycles spent waiting on this lock (higher → more contention) + - `count` = how many times goroutines waited (high count at low cycles = many short waits; low count at high cycles = long waits) + - Look for: your own packages (`github.com/tstapler/stapler-squad`) in the stack, especially inside loops or hot-path handlers. + - Red flag: `log.(*Logger).output` in the stack — stdlib log holds a mutex per write; any hot-path debug `Printf` call serializes every goroutine that hits it. + + **block** — scheduler delays from channel/select operations. + - Same format as mutex. + - High `cycles` on `runtime.selectgo` inside event loops is normal (timer fires). Abnormally high `count` (>10K) on a per-connection goroutine is a sign of excessive goroutine wake-ups. + - Red flag: >10K blocks on a `streamVia*` or `handleClient` goroutine with a short lifetime. + + **allocs** — allocation rate (lifetime may be short). + - Format: `: [: ]` + - Second pair `[total-count: total-bytes]` is the rate metric — even if objects are freed quickly, allocating millions of them adds GC pressure. + - Red flag: proto `Marshal`/`Unmarshal` allocating on every streaming frame, or ORM queries returning full rows when only one field is needed. + + **heap** — live allocations at snapshot time. + - Same format; first pair is the rate metric here. + - Red flag: compression encoder `blockEnc.init` without a `sync.Pool` — should show pool-resident objects, not fresh allocations. + + **goroutines** — qualitative health check. + - Count goroutines by state with: + ```bash + grep "^goroutine" /tmp/goroutines.txt | sed 's/goroutine [0-9]* //' | sort | uniq -c | sort -rn + ``` + - Normal states: `[select]`, `[chan receive]`, `[IO wait]` + - Red flags: many goroutines in `[semacquire]` (lock contention) or `[sleep, X minutes]` (goroutine leak) + + --- + + ## Phase 2 — Rank Bottlenecks + + Extract the top-5 stacks from mutex and block profiles, filtering to stapler-squad frames: + + ```bash + grep -E "^[0-9]+ [0-9]+ @|#.*github.com/tstapler" /tmp/mutex.txt | head -60 + grep -E "^[0-9]+ [0-9]+ @|#.*github.com/tstapler" /tmp/block.txt | head -60 + grep -E "^[0-9]+: [0-9]+ \[[0-9]+:|#.*github.com/tstapler" /tmp/allocs.txt | head -60 + ``` + + Fill in this table (sort by cycles × count for mutex; by count for block): + + | Rank | Profile | Location | cycles | count | Root cause hypothesis | + |------|---------|----------|--------|-------|-----------------------| + | 1 | mutex | file:line | ... | ... | ... | + | 2 | block | file:line | ... | ... | ... | + | … | … | … | … | … | … | + + ### Known recurring hotspots in this codebase (as of 2026-05-02 profiling session) + + | Issue | Location | Profile signal | Impact | + |-------|----------|----------------|--------| + | `log.DebugLog.Printf` in hot poll loop | `session/instance_status.go:78` (`GetStatus`) | mutex: 2.2B cycles, 5094 events | Every review queue tick serializes on log mutex | + | `log.DebugLog.Printf` in content cache hot path | `session/review_queue_poller.go:557,574,581` | mutex: 1.4B cycles, 2607 events | Same pattern — no `DebugLog != nil` guard | + | `log.DebugLog.Printf` on every `%output` event | `session/tmux/control_mode.go:331` | mutex: 2.7B cycles, 94 events | tmux output path — fires on every terminal byte | + | `log.DebugLog.Printf` inside streaming send loop | `server/services/connectrpc_websocket.go:629` | block: 23T cycles, 26437 events | Per-frame log call in WebSocket stream goroutine | + | `EntRepository.Get` before every field update | `session/ent_repository.go:622` via `storage.go:285` | allocs: full row read per update | Should be a direct `UPDATE … WHERE id=?` | + + --- + + ## Phase 3 — Propose Improvements + + For each bottleneck, propose a concrete fix at the **earliest achievable enforcement level**: + + ``` + 1. Compile time → type change, interface constraint + 2. Lint rule → custom golangci-lint rule, existing staticcheck rule + 3. Benchmark → must regress detectably if the fix is reverted + 4. Unit test → asserts correct behavior before/after + 5. CLAUDE.md → only when 1–4 are genuinely unreachable + ``` + + ### Template for each proposal + + ``` + ### [PerfFix-N] Short title + + **Profile signal**: mutex / block / allocs — file:line — X cycles, Y events + **Root cause**: one sentence + **Fix**: what to change and where + **Enforcement**: lint rule name / benchmark name / test name that would have caught it + **Estimated impact**: low / medium / high — why + ``` + + --- + + ## Phase 4 — Codify (Reflect & Fix) + + Apply the Reflect & Fix framework to every fix you propose. + + For **mutex contention from hot-path logging**: + - Category: **Semantic/Intent** — the debug log is syntactically valid but semantically wrong in a tight loop + - Enforcement: lint rule that flags `log.DebugLog.Printf` calls not guarded by `if log.DebugLog != nil` inside functions whose names match `*poll*`, `*check*`, `*stream*`, `*handle*` + - Write the rule in `buildSrc/` or as a golangci-lint custom check; add a test that fires on the bad pattern and is silent on the guarded form + - Add to `.golangci.yml` under `custom-gcl` or `revive` rules + + For **allocation-per-frame in streaming paths**: + - Category: **Integration Gap** — proto allocation per frame is correct in isolation but adds up at stream throughput + - Enforcement: benchmark `BenchmarkStreamViaControlMode` that asserts `allocs/op == 0` for the hot path (use `testing.AllocsPerRun`) + - Must fail before the fix (pooled protos not yet introduced) and pass after + + For **read-before-write in ORM updates**: + - Category: **API Contract Gap** — the update method's interface doesn't signal that it does a read first + - Enforcement: integration test `TestUpdateFieldInRepo_UsesDirectUpdate` that counts SQL statements and asserts `SELECT` count == 0 for a field update + + ### Verification table + + | Fix | Enforcement | Pre-fix behaviour | Verdict | + |----|------------|------------------|---------| + | Remove hot-path `DebugLog.Printf` | lint rule | fires on pre-fix code ✓ | catches it | + | Pool proto in stream loop | `BenchmarkStream_AllocsPerOp` | allocs > 0 ✓ | catches it | + | Direct SQL update | `TestUpdateFieldInRepo_NoSelect` | sees SELECT ✓ | catches it | + + --- + + ## Output Format + + Produce: + 1. The filled-in Phase 2 ranking table + 2. One `### [PerfFix-N]` block per proposed fix (minimum 3, maximum 10) + 3. The Phase 4 verification table + 4. A prioritised "what to tackle first" recommendation (2–3 sentences) + + Do **not** implement the fixes — this command produces proposals for agent hand-off. + Do **not** add a CLAUDE.md note unless every other enforcement level is unreachable. + + --- + + ## Phase 5 — Browser / React Profiling + + Run this phase in parallel with or after Go profiling. The app runs at `http://localhost:8543`. + Playwright is available at `tests/e2e/node_modules/.bin/playwright`. + + ### 5a — Capture numeric baseline via Playwright + + Write and run `/tmp/ss-browser-baseline.js`: + + ```javascript + const { chromium } = require('/Users/tylerstapler/IdeaProjects/stapler-squad/tests/e2e/node_modules/playwright-core'); + + async function captureBaseline(label, scenarioFn) { + const browser = await chromium.launch(); + const page = await browser.newPage(); + + await page.addInitScript(() => { + window.__perfData__ = { longTasks: [] }; + new PerformanceObserver(list => { + list.getEntries().forEach(e => window.__perfData__.longTasks.push({ + duration: e.duration, startTime: e.startTime + })); + }).observe({ entryTypes: ['longtask'] }); + }); + + await browser.startTracing(page, { + path: `/tmp/trace-${label}.json`, + screenshots: false, + categories: ['devtools.timeline', 'v8', 'blink.user_timing', 'disabled-by-default-v8.cpu_profiler'], + }); + + const before = await page.metrics(); + await page.goto('http://localhost:8543', { waitUntil: 'networkidle' }); + await scenarioFn(page); + const after = await page.metrics(); + await browser.stopTracing(); + + const longTasks = await page.evaluate(() => window.__perfData__.longTasks); + console.log(`\n=== ${label} ===`); + console.log({ + scriptDuration: (after.ScriptDuration - before.ScriptDuration).toFixed(3) + 's', + layoutCount: after.LayoutCount - before.LayoutCount, + recalcStyleCount: after.RecalcStyleCount - before.RecalcStyleCount, + heapGrowthMB: ((after.JSHeapUsedSize - before.JSHeapUsedSize) / 1024 / 1024).toFixed(2) + 'MB', + nodes: after.Nodes - before.Nodes, + }); + console.log(`Long tasks (>50ms): ${longTasks.length}`, longTasks.map(t => Math.round(t.duration) + 'ms')); + console.log(`Trace saved: /tmp/trace-${label}.json`); + await browser.close(); + } + + captureBaseline('initial-load', async (page) => { + await page.waitForSelector('body'); + await page.waitForTimeout(1000); + }).then(() => + captureBaseline('session-list-scroll', async (page) => { + await page.waitForSelector('body'); + for (let i = 0; i < 5; i++) { + await page.keyboard.press('ArrowDown'); + await page.waitForTimeout(100); + } + })).catch(console.error); + ``` + + ```bash + node /tmp/ss-browser-baseline.js + ``` + + ### 5b — Interpret results + + **Long tasks (>50ms)**: each one blocks user input and shows up as red-flagged bars in the Performance panel. + Load `/tmp/trace-initial-load.json` into Chrome DevTools → Performance tab for the flamechart. + + **Key metrics to flag**: + | Metric | Warning threshold | Critical threshold | + |--------|------------------|--------------------| + | `scriptDuration` on initial load | > 0.5s | > 1.0s | + | `layoutCount` per interaction | > 10 | > 50 | + | `heapGrowthMB` after 10 interactions | > 5MB | > 20MB | + | Long task count on load | > 3 | > 10 | + | Single long task duration | > 100ms | > 500ms | + + ### 5c — React-specific checks + + Add a temporary `` wrapper in the dev build around the sessions list: + + ```tsx + import { Profiler, type ProfilerOnRenderCallback } from 'react'; + + const onRender: ProfilerOnRenderCallback = (id, phase, actualDuration, baseDuration) => { + if (actualDuration > 16) + console.warn(`[Profiler] ${id} (${phase}): ${actualDuration.toFixed(1)}ms ratio: ${(actualDuration/baseDuration).toFixed(2)}`); + }; + + + + + ``` + + Key ratio: `actualDuration / baseDuration` → near 1.0 = memoization absent; near 0.1 = working. + + ### 5d — Bundle size check + + ```bash + cd web-app && npm run build 2>/dev/null | tail -20 + # Then inspect the largest chunks + ls -lah web-app/.next/static/chunks/*.js 2>/dev/null | sort -k5 -rh | head -10 + # or for Vite/CRA: + ls -lah web-app/dist/assets/*.js 2>/dev/null | sort -k5 -rh | head -10 + ``` + + ### 5e — Browser fix proposals (same template as Phase 3) + + For each browser bottleneck found, produce a `### [PerfFix-Browser-N]` block: + - **Signal**: metric name + value + - **Root cause**: one sentence + - **Fix**: what component/hook to change + - **Enforcement**: Jest/RTL test or Playwright perf assertion that would catch regression + +--- + +# perf:make-it-faster + +Connect to the live pprof endpoint, read all five profiles, rank hotspots by CPU cycles +and allocation rate, produce numbered fix proposals with enforcement stubs, and verify +each proposal would have caught the regression via the Reflect & Fix ladder. + +## Quick start + +```bash +# Server must be running with --profile +make restart-web PROFILE_FLAGS="--profile" + +# Capture all profiles in one shot +for p in goroutine mutex block heap allocs; do + curl -s "http://localhost:6060/debug/pprof/${p}?debug=1" > /tmp/ss-${p}.txt +done + +# Then invoke this command — it reads the files and does the rest +``` + +## Profile quick-reference + +| Profile | Primary metric | What to look for | +|---------|---------------|-----------------| +| `mutex` | cycles waiting for a lock | stdlib `log.Printf` in hot paths; RWMutex on read-heavy paths | +| `block` | cycles blocked in select/chan | abnormally high `count` on per-connection goroutines | +| `allocs` | total-bytes column `[N: X]` | proto Marshal per frame, ORM full-row reads | +| `heap` | in-use objects | large objects without pool; compress encoder per request | +| `goroutine` | goroutine count and state | leaks (`[sleep, X minutes]`), lock storms (`[semacquire]`) | + +## Enforcement ladder + +``` +1. Compile time → type / interface change +2. Lint rule → golangci-lint custom check or existing rule +3. Benchmark → AllocsPerRun or ns/op regression gate +4. Unit test → asserts pre-fix code fails +5. CLAUDE.md → last resort only +``` + +## Browser quick-reference + +```bash +# Run browser baseline (app must be on localhost:8543) +node /tmp/ss-browser-baseline.js + +# Inspect bundle chunks by size +ls -lah web-app/.next/static/chunks/*.js 2>/dev/null | sort -k5 -rh | head -10 + +# JS coverage (unused code) +# Run captureBaseline with page.coverage.startJSCoverage() — see browser-profiling skill +``` + +| Signal | Tool | Where to look | +|--------|------|---------------| +| Long tasks on load | Playwright `page.metrics()` + `PerformanceObserver longtask` | > 3 tasks or > 100ms each | +| React re-render cascade | `` | `actualDuration / baseDuration` near 1.0 | +| Layout thrashing | Performance panel → "Forced reflow" | `layoutCount` > 10 per interaction | +| Memory leak | Playwright heap delta across 10 cycles | > 5MB growth | +| Oversized bundle | `source-map-explorer` or `.next/static/chunks` | chunk > 500KB unparsed | + +## Known hotspots (as of 2026-05-02) + +| Location | Profile | Cycles | Count | Fix direction | +|----------|---------|--------|-------|---------------| +| `session/instance_status.go:78` | mutex | 2.2B | 5094 | remove debug Printf from GetStatus hot path | +| `session/review_queue_poller.go:557` | mutex | 1.4B | 2607 | gate behind `DebugLog != nil` or remove | +| `session/tmux/control_mode.go:331` | mutex | 2.7B | 94 | remove Printf from %output hot path | +| `server/services/connectrpc_websocket.go:629` | block | 23T | 26437 | remove per-frame debug log from stream goroutine | +| `session/ent_repository.go:622` via `storage.go:285` | allocs | — | — | direct UPDATE instead of Get + update | diff --git a/.claude/docs/benchmarks.md b/.claude/docs/benchmarks.md new file mode 100644 index 000000000..df800e8db --- /dev/null +++ b/.claude/docs/benchmarks.md @@ -0,0 +1,52 @@ +# Benchmark Reference + +**CRITICAL: Benchmarks take 5-30 minutes. Always run with `&` to avoid blocking your terminal.** + +## Core Benchmarks + +```bash +go test -bench=. -benchmem ./app -timeout=30m & + +# Makefile shortcuts +make benchmark-tier1 # Tier 1 critical-path benchmarks (~5 min) +make benchmark-baseline # Capture baseline for comparison +make benchmark-compare # Compare against saved baseline +make benchmark # Full benchmark suite (background) +make benchmark-quick # Fast subset +make benchmark-navigation +make profile-cpu +``` + +## Specific Benchmark Categories + +```bash +go test -bench=BenchmarkNavigation -benchmem ./app -timeout=10m & +go test -bench=BenchmarkLargeSessionNavigation -benchmem ./app -timeout=20m & +go test -bench=BenchmarkAttachDetachPerformance -benchmem ./app -timeout=15m & +go test -bench=BenchmarkFilteringPerformance -benchmem ./app -timeout=10m & +go test -bench=BenchmarkRenderingPerformance -benchmem ./app -timeout=15m & +go test -bench=BenchmarkMemoryUsage -benchmem ./app -timeout=15m & +go test -bench=BenchmarkStartupPerformance -benchmem ./app -timeout=10m & +go test -bench=BenchmarkRealtimeUpdates -benchmem ./app -timeout=10m & + +# Overlay benchmarks +go test -bench=BenchmarkGitRepositoryDiscovery -benchmem ./ui/overlay -timeout=5m & +go test -bench=BenchmarkContextualDiscovery -benchmem ./ui/overlay -timeout=5m & +go test -bench=BenchmarkValidatePath -benchmem ./ui/overlay -timeout=2m & +``` + +## Profiling with Benchmarks + +```bash +# CPU profile +go test -bench=BenchmarkLargeSessionNavigation -benchmem -cpuprofile=cpu.prof ./app -timeout=20m +go tool pprof cpu.prof + +# Memory profile +go test -bench=BenchmarkMemoryUsage -benchmem -memprofile=mem.prof ./app -timeout=15m +go tool pprof mem.prof + +# Execution trace +go test -bench=BenchmarkAttachDetachPerformance -trace=trace.out ./app -timeout=15m +go tool trace trace.out +``` diff --git a/.claude/docs/codesigning.md b/.claude/docs/codesigning.md new file mode 100644 index 000000000..cfe441b05 --- /dev/null +++ b/.claude/docs/codesigning.md @@ -0,0 +1,102 @@ +# macOS Code Signing for TCC Permission Persistence + +## What this does + +stapler-squad signs its binary with a self-signed certificate (`StaplerSquadDev`) on every `make install-service`. The certificate anchors the Designated Requirement (DR), so macOS TCC grants (Full Disk Access, Apple Events) survive rebuilds without re-prompting. + +Without a stable cert-anchored DR, every rebuild changes the binary's cdhash and macOS treats it as a new app — requiring a fresh round of permission dialogs. + +## One-time setup + +Run once per developer machine: + +```bash +# Install OpenSSL if needed (macOS ships LibreSSL, which won't work) +brew install openssl + +# Create and trust the self-signed cert +OPENSSL_BIN=$(brew --prefix openssl)/bin/openssl make setup-codesign +``` + +If `openssl version` shows `OpenSSL` (not `LibreSSL`), you can omit `OPENSSL_BIN`: + +```bash +make setup-codesign +``` + +The script is idempotent — running it a second time prints "StaplerSquadDev cert already present." and exits without creating a duplicate. + +## Verifying the setup + +After `make install-service`, run: + +```bash +make verify-codesign +``` + +Pass criteria: +- `Authority=StaplerSquadDev` in the Code Signature section +- `Identifier=com.stapler-squad` in the Code Signature section +- DR section contains `anchor H""` or `anchor trusted` (cert-anchored, not `cdhash`) +- `CFBundleIdentifier => "com.stapler-squad"` in the Embedded Info.plist section +- `com.apple.security.automation.apple-events` in the Entitlements section + +## Exporting the cert for backup + +If you need to move the cert to another machine or back it up: + +1. Open **Keychain Access** (`/Applications/Utilities/Keychain Access.app`) +2. Select the **login** keychain +3. Find **StaplerSquadDev** in the My Certificates category +4. Right-click → **Export "StaplerSquadDev"** +5. Save as `.p12`, set a password when prompted + +## Re-importing on a new machine + +```bash +security import StaplerSquadDev.p12 \ + -k ~/Library/Keychains/login.keychain-db \ + -T /usr/bin/codesign +``` + +Then run `make setup-codesign` (it will detect the cert exists and skip creation, but the set-key-partition-list step ensures codesign can access the key without prompting). + +Actually for a clean import, just run the full setup again — the script checks for the cert and exits early if found. You may need to delete the old cert entry first in Keychain Access if re-importing on the same machine. + +## Diagnosing TCC issues + +### Check current grants + +```bash +# List Apple Events grants for stapler-squad +sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db \ + "SELECT client, auth_value, auth_reason FROM access WHERE client = 'com.stapler-squad';" +``` + +Note: reading TCC.db directly may require Full Disk Access for your terminal. + +### Reset grants (development only) + +```bash +make tcc-reset +``` + +This resets all TCC grants for `com.stapler-squad`. Use during development to reproduce the first-launch permission prompt experience. Requires `sudo`. + +### Re-grant after reset + +1. Run `make install-service` +2. Open System Settings → Privacy & Security → Full Disk Access +3. Enable the toggle for `stapler-squad` +4. Trigger FocusWindow in the UI to re-approve Apple Events for each target app + +## How it works + +The `make install-service` flow on macOS: + +1. `go build` with `CGO_LDFLAGS="-sectcreate __TEXT __info_plist Info.plist"` embeds the plist into the binary's `__TEXT/__info_plist` Mach-O section +2. `otool` assertion verifies the plist was actually embedded (catches silent `CGO_ENABLED=0` failures) +3. `codesign --sign "StaplerSquadDev" --entitlements entitlements.plist` signs the binary +4. `install-service.sh` stops, installs, and restarts the LaunchAgent + +The `CFBundleIdentifier = com.stapler-squad` in the embedded plist causes TCC to track the app by bundle ID (`client_type=0`) rather than path+hash. The cert-anchored DR means the TCC row's `csreq` field matches every rebuild as long as the same cert is used. diff --git a/.claude/docs/nil-safety.md b/.claude/docs/nil-safety.md new file mode 100644 index 000000000..c12d18dba --- /dev/null +++ b/.claude/docs/nil-safety.md @@ -0,0 +1,48 @@ +# Nil Safety Analysis + +Comprehensive nil safety analysis tools to prevent panic-causing nil pointer dereferences. + +## Running Analysis + +```bash +make nil-safety # Run all nil safety tools +make nilaway # Advanced nil flow analysis (Uber NilAway) +go vet -nilness ./... # Built-in Go nilness analyzer +go-nilcheck ./... # Function pointer validation +``` + +## Tool Installation + +```bash +make install-tools +# Or manually: +go install go.uber.org/nilaway/cmd/nilaway@latest +go install honnef.co/go/tools/cmd/staticcheck@latest +go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest +go install github.com/securego/gosec/v2/cmd/gosec@latest +``` + +## Full Static Analysis Toolchain + +| Tool | Purpose | +|---|---| +| **NilAway** | Advanced nil pointer safety analysis (Uber) | +| **Staticcheck** | Production-grade static analyzer | +| **golangci-lint** | Meta-linter with multiple analyzers | +| **gosec** | Security-focused static analysis | +| **go vet** | Built-in Go static analysis | + +```bash +make analyze # Run all static analysis tools +make staticcheck # Advanced static analysis +make security # Security vulnerability scanning +make deadcode # Find unreachable code +deadcode -test ./... # Include test files +``` + +## Best Practices + +1. Always run `make nil-safety` before committing +2. Use NilAway for the most comprehensive nil flow analysis +3. Include nil checks before pointer dereferences +4. Use defensive programming in overlay rendering (see `app/app.go:1225`) diff --git a/.claude/docs/opentelemetry.md b/.claude/docs/opentelemetry.md new file mode 100644 index 000000000..12125100f --- /dev/null +++ b/.claude/docs/opentelemetry.md @@ -0,0 +1,44 @@ +# OpenTelemetry Observability + +Stapler Squad supports OpenTelemetry instrumentation for APM integration (Datadog, etc.). Disabled by default. + +## Environment Variables + +```bash +OTEL_ENABLED=true ./stapler-squad +DD_TRACE_ENABLED=true ./stapler-squad + +# Configure OTLP endpoint (default: localhost:4317) +OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317 OTEL_ENABLED=true ./stapler-squad + +# Set environment and version for trace metadata +OTEL_SERVICE_ENVIRONMENT=production OTEL_SERVICE_VERSION=1.0.0 OTEL_ENABLED=true ./stapler-squad +``` + +## Datadog Agent Configuration (for OTLP ingestion) + +```yaml +# /etc/datadog-agent/datadog.yaml +otlp_config: + receiver: + protocols: + grpc: + endpoint: 0.0.0.0:4317 +``` + +## Instrumented Operations + +- All HTTP requests (via otelhttp middleware) +- All ConnectRPC endpoints (via otelconnect interceptor) +- History cache operations (cache hit/miss, load duration) +- Search engine operations (sync, search duration, result count) + +## Trace Attributes + +| Attribute | Description | +|---|---| +| `session.id`, `session.title`, `session.status` | Session context | +| `history.entry_count` | History loading metrics | +| `search.query`, `search.result_count`, `search.duration_ms` | Search metrics | +| `cache.hit`, `cache.refresh_duration_ms` | Cache performance | +| `sync.sessions_added`, `sync.sessions_updated` | Index sync metrics | diff --git a/.claude/docs/profiling.md b/.claude/docs/profiling.md new file mode 100644 index 000000000..1b6e2a667 --- /dev/null +++ b/.claude/docs/profiling.md @@ -0,0 +1,54 @@ +# Profiling and Debugging Lock-Ups + +## Quick Start + +```bash +./stapler-squad --profile --trace +``` + +## Capture Profiles (while lock-up is occurring) + +```bash +curl http://localhost:6060/debug/pprof/goroutine?debug=2 > goroutines.txt +curl http://localhost:6060/debug/pprof/block?debug=1 > block.txt +curl http://localhost:6060/debug/pprof/mutex?debug=1 > mutex.txt +``` + +## Analyze After Exit + +```bash +go tool trace /tmp/stapler-squad-trace-.out +``` + +## Profiling Flags + +```bash +./stapler-squad --profile # Enable profiling HTTP server (port 6060) +./stapler-squad --profile --profile-port 8080 # Custom port +./stapler-squad --trace # Execution tracing only +``` + +## CPU / Memory / Trace Profiling + +```bash +# CPU (30 seconds) +curl http://localhost:6060/debug/pprof/profile?seconds=30 > cpu.prof +go tool pprof -http=:8081 cpu.prof + +# Memory +curl http://localhost:6060/debug/pprof/heap > heap.prof +go tool pprof -http=:8081 heap.prof + +# Race detection +go build -race . && ./stapler-squad --profile +``` + +## Makefile Shortcuts + +```bash +make restart-web-profile # Restart with --profile --trace +make restart-web PROFILE_FLAGS="--profile --trace" +make restart-web-profile PROFILE_PORT=8080 +``` + +See `docs/PROFILING.md` for the comprehensive guide. diff --git a/.claude/docs/pty-multiplexing.md b/.claude/docs/pty-multiplexing.md new file mode 100644 index 000000000..1dfc2fe0f --- /dev/null +++ b/.claude/docs/pty-multiplexing.md @@ -0,0 +1,85 @@ +# External Session Monitoring (PTY Multiplexing) + +Monitor and interact with Claude sessions running in external terminals (IntelliJ, VS Code, etc.) through the `ssq-mux` PTY multiplexer. Enables bidirectional terminal streaming without requiring sessions to be started from stapler-squad. + +## Architecture + +``` +External Terminal (IntelliJ) → ssq-mux → PTY → Claude Process + ↓ + Unix Socket (/tmp/ssq-mux-.sock) + ↓ + stapler-squad (auto-discovers & connects) +``` + +## Installation + +```bash +cd /path/to/stapler-squad +./scripts/install-mux.sh +# Installs to ~/.local/bin/ssq-mux +``` + +## Setup Methods + +### Method 1: Shell Alias (Recommended) + +```bash +# Add to ~/.zshrc or ~/.bashrc +alias claude='ssq-mux claude' +source ~/.zshrc +``` + +### Method 2: PATH Override + +```bash +# Create wrapper script at ~/bin/claude +#!/bin/bash +exec ssq-mux /usr/local/bin/claude "$@" +chmod +x ~/bin/claude +export PATH="$HOME/bin:$PATH" +``` + +### Method 3: IDE Terminal Configuration + +**IntelliJ IDEA / PyCharm / WebStorm:** +1. Settings → Tools → Terminal +2. Shell path: `~/.local/bin/ssq-mux` +3. Shell arguments: `claude` + +**VS Code:** +```json +"terminal.integrated.profiles.osx": { + "ssq-mux": { + "path": "~/.local/bin/ssq-mux", + "args": ["claude"] + } +} +``` + +## Session Discovery + +Uses `fsnotify` to watch `/tmp/` for socket creation/deletion — immediate detection, no polling. Falls back to polling automatically. + +```bash +# Verify +ls /tmp/ssq-mux-*.sock +``` + +## Troubleshooting + +| Issue | Fix | +|---|---| +| `ssq-mux: command not found` | `export PATH="$HOME/.local/bin:$PATH"` | +| `stdin is not a terminal` | Must run from real terminal, not script/pipe | +| Sessions not discovered | Check socket exists, verify permissions (0600), check logs at `~/.stapler-squad/logs/stapler-squad.log` | +| Terminal output garbled | Resize the terminal window (SIGWINCH forwarded automatically) | +| Stale sockets | `rm /tmp/ssq-mux-*.sock` (when no sessions running) | + +## Protocol Details + +**Message Types:** Output, Input, Resize (SIGWINCH), Metadata (command/PID/cwd/env), Ping/Pong + +**Security:** Sockets created with 0600 permissions, local Unix domain sockets only (no network exposure). + +**Performance:** Zero overhead when no clients connected; direct PTY forwarding; automatic cleanup on process exit. diff --git a/.claude/docs/state-isolation.md b/.claude/docs/state-isolation.md new file mode 100644 index 000000000..ee9832b4b --- /dev/null +++ b/.claude/docs/state-isolation.md @@ -0,0 +1,53 @@ +# State File Isolation and Multi-Instance Support + +Hierarchical state file isolation prevents conflicts between tests, benchmarks, and multiple production instances. + +## Isolation Hierarchy (highest to lowest priority) + +| Priority | Mechanism | State Location | Activation | +|---|---|---|---| +| 1 | Explicit Instance ID | `~/.stapler-squad/instances/{INSTANCE_ID}/` | `STAPLER_SQUAD_INSTANCE=name` | +| 2 | Test Mode Auto-Detection | `~/.stapler-squad/test/test-{PID}/` | Automatic when running `go test` | +| 3 | Workspace-Based (default) | `~/.stapler-squad/workspaces/{WORKSPACE_HASH}/` | Default (SHA256 of cwd) | +| 4 | Global Shared State | `~/.stapler-squad/` | `STAPLER_SQUAD_WORKSPACE_MODE=false` | + +## Common Usage Patterns + +```bash +# Default: per-directory workspace isolation +./stapler-squad + +# Named instance (useful for project-specific state) +STAPLER_SQUAD_INSTANCE=work ./stapler-squad +STAPLER_SQUAD_INSTANCE=personal ./stapler-squad + +# Shared global state (legacy behavior) +STAPLER_SQUAD_INSTANCE=shared ./stapler-squad +STAPLER_SQUAD_WORKSPACE_MODE=false ./stapler-squad + +# Tests: isolated automatically — no config needed +go test ./... +``` + +## Instance Identification in Logs + +``` +[work] INFO: Session created +[pid-12345-1704132000] INFO: Session started +[work][DAEMON] INFO: Polling sessions +``` + +## Migration Notes + +- Existing `~/.stapler-squad/` state is preserved; workspace isolation is now the default. +- To restore old shared behavior: `STAPLER_SQUAD_INSTANCE=shared` or `STAPLER_SQUAD_WORKSPACE_MODE=false`. +- Tests auto-detect isolation — no code changes needed. + +## Troubleshooting + +| Issue | Fix | +|---|---| +| "Can't find sessions after restart" | Different directory active; use `STAPLER_SQUAD_WORKSPACE_MODE=false` for directory-independent state | +| "Tests modifying production state" | Shouldn't happen; verify `go test` is used (binary names must contain `.test`) | +| "Multiple instances conflicting" | Each workspace isolated by default; use explicit `STAPLER_SQUAD_INSTANCE` | +| "Want shared state across directories" | `STAPLER_SQUAD_INSTANCE=shared` or `STAPLER_SQUAD_WORKSPACE_MODE=false` | diff --git a/.claude/docs/tag-organization.md b/.claude/docs/tag-organization.md new file mode 100644 index 000000000..22328159b --- /dev/null +++ b/.claude/docs/tag-organization.md @@ -0,0 +1,51 @@ +# Tag-Based Session Organization + +Sessions support multi-dimensional organization through tags and 8 grouping strategies. + +## Grouping Modes (Web UI "Group by" dropdown) + +| Mode | Groups by | +|---|---| +| **Category** (default) | Category field; supports nested `Work/Frontend` → separate tags | +| **Tag** | Multi-membership — session appears in all its tag groups | +| **Branch** | Git branch name | +| **Path** | Repository path (abbreviated) | +| **Program** | claude, aider, etc. | +| **Status** | Running, Paused, Ready, etc. | +| **Session Type** | directory, worktree, etc. | +| **None** | Flat list | + +## Tag Management (Web UI) + +- Tags appear as blue pills on session cards +- "Add Tags" / "Edit Tags" button opens Tag Editor Modal +- Enter to add, × to remove; duplicates and empty tags prevented +- Tag Filter Dropdown: filter to sessions with a specific tag +- Combine with status and category filters + +## Tag Naming Conventions + +- Use **PascalCase** or **kebab-case**; keep tags 1-2 words +- Common categories: `Urgent`/`Low-Priority` (priority), `Frontend`/`Backend` (type), `Client-A`/`Internal` (client), `React`/`Go` (tech), `Planning`/`Development`/`Review` (phase) + +## Backward Compatibility + +- Existing `Category` field preserved; auto-migrates to tags on first load +- Nested categories (e.g. `Work/Frontend`) split into individual tags `["Work", "Frontend"]` +- `GroupByCategory` remains the default grouping strategy +- Migration is idempotent; no data loss + +## Technical Details + +- Tags stored in `tagIndex` map for O(1) lookup; prefix matching for partial queries +- `Tags []string` field in `session.Instance` struct with thread-safe methods (`AddTag`, `RemoveTag`, `HasTag`, `SetTags`) +- Tags serialized in JSON persistence and Protobuf schema +- Strategy pattern for grouping engine; expansion state preserved across strategy changes + +## Example Use Cases + +**By Project Phase:** tag `Planning`/`Development`/`Review` → Group By Tag → filter to `Development` + +**Multi-Project:** tag `Client-A`/`Frontend` → Group By Tag for client view → Group By Program for tool view + +**Priority:** tag `Urgent`/`Backlog` → Group By Tag → Tag Filter → `Urgent` for daily standups diff --git a/.claude/product-marketing-context.md b/.claude/product-marketing-context.md new file mode 100644 index 000000000..e4e131403 --- /dev/null +++ b/.claude/product-marketing-context.md @@ -0,0 +1,191 @@ +# Product Marketing Context + +``` +type: open-source +name: Seneschal +tagline: Your agentic estate, managed. +positioning: tmux for the Agentic Age +``` + +--- + +## 1. Project Overview + +**Name**: Seneschal + +**One-liner**: The seneschal of your AI coding agents — session management, approval gates, and real-time oversight from a single browser tab. + +**What it does**: Seneschal is a locally-run web dashboard that lets developers run multiple AI coding agents simultaneously — Claude Code, Aider, Gemini CLI, Codex — in isolated tmux sessions with git worktrees. Every agent gets a live terminal stream and diff view. A structured review queue and 42+ auto-approval rules handle routine decisions; the rest surface as petitions for you to assent or deny. You are the lord. Seneschal manages your estate. + +**Origin story**: Forked from [claude-squad](https://github.com/smtg-ai/claude-squad), a TUI for managing multiple Claude Code sessions. A browser is a better interface for this job — it opens instantly in multiple windows, works over SSH without port-forwarding a terminal, and lets you monitor agents from any device on the network. + +**Category**: AI agent management / developer productivity tooling +**Project type**: CLI that starts a web server — `brew install seneschal` → `localhost:8543` +**Stack**: Go + ConnectRPC backend, React 19 + Next.js frontend, xterm.js terminals, SQLite + +--- + +## 2. The Estate Metaphor + +The medieval seneschal managed the lord's estate: overseeing workers, controlling what left the grounds, adjudicating disputes, and handling routine affairs without troubling the lord for every small decision. + +In the agentic age, your AI agents are your holdings. Seneschal manages them on your behalf. + +| Estate concept | Product concept | +|----------------|-----------------| +| Holdings | Agent sessions | +| Plots under cultivation | Git worktrees (isolated per agent) | +| Petitions to the court | Pending approvals — agent actions awaiting human assent | +| Standing orders | Auto-approval rules (42 built-in; what Seneschal handles without asking) | +| The court | Review queue — where petitions are heard and decided | +| Estate view | Dashboard — all holdings visible at a glance | +| Fallowing a plot | Session hibernation — checkpoint and suspend | +| Outstanding obligations | Unfinished work — uncommitted changes, commits ahead of main | +| The lord | You | + +Use this vocabulary selectively in marketing copy and onboarding. Don't force it into technical documentation. + +--- + +## 3. Audience + +**Primary users**: Software engineers who run AI coding assistants daily and want to parallelize their work — running 3–5 agents across separate branches simultaneously without losing track of any of them. + +**Secondary users**: Technical leads who want visibility into agent activity across a project — what's running, what's waiting for a decision, what's diverged. + +**Contributors**: Go/React/TypeScript developers interested in AI tooling, terminal emulation, real-time streaming, and developer productivity infrastructure. + +**Not for**: Developers who only run one agent at a time; teams with strict policies blocking localhost web servers; non-technical users expecting a hosted SaaS. + +--- + +## 4. Problem & Differentiation + +**The frustration**: Running AI agents in parallel means juggling multiple terminal windows, missing approval prompts, not knowing which agent needs attention, and git branches stepping on each other. + +**What people use instead**: claude-squad (TUI), raw terminal windows, ad-hoc tmux configs. They fall short because TUIs don't work well across SSH or multiple monitors, and raw terminals have no organization, approval tooling, or cross-session visibility. + +**Core design bet**: A browser is a better interface for agent oversight than a terminal. It opens instantly in multiple windows, works over SSH with just port forwarding, and is accessible from any device on the network. + +**Word-of-mouth pitch**: "It's tmux for the agentic age — but in a browser, with an approval queue and diff viewer built in." + +**The differentiator that matters most**: The approval gate. Seneschal is the only tool that gives you a structured, auditable, rule-driven review layer between your agents and your codebase. The court where petitions are heard. + +--- + +## 5. Brand Voice & Tone + +**Personality**: Pragmatic, technically precise, quietly intellectual, unpretentious. The kind of tool that doesn't explain itself more than once. + +**Technical depth**: Expert-first. Assumes you know what tmux, git worktrees, and AI coding agents are. No hand-holding. + +**Writing style**: Terse and declarative. Short sentences. Direct value statements. The estate metaphor enriches without replacing precision. + +**Use**: "sessions", "holdings", "petitions", "approval rules", "worktrees", "review queue", "standing orders", "assent", "real-time", "isolated" + +**Avoid**: "AI-powered", "revolutionize", "effortlessly", "seamlessly", "unlock", "supercharge", "cutting-edge", "empower" + +**Voice examples**: +- *"Your agentic estate, managed."* +- *"Five agents running. Three petitions pending. One browser tab."* +- *"The seneschal handles the routine. You handle the rest."* +- *"Standing orders cover the safe operations. Everything else comes to you."* + +--- + +## 6. Visual Direction + +**Color mood**: Dark + technical. Deep slate backgrounds, indigo primary, VS Code-style terminal surfaces. Status indicators in green/amber/red. The palette of a tool built for focus, not decoration. + +**Color palette**: +- Background: `#0f1117` +- Primary: `#6366f1` (hover: `#818cf8`, active: `#4f46e5`) +- Text: `#e2e8f0` / `#94a3b8` (muted) +- Success: `#22c55e` | Warning: `#f59e0b` | Error: `#ef4444` +- Terminal surface: `#1e1e1e` / `#252526` + +**Typography**: Monospace-forward for terminal and code content; geometric sans for UI chrome. Dense, information-rich layouts — every pixel earns its place. + +**Aesthetic references**: Linear's precision meets VS Code's dark theme meets a medieval estate ledger — structured, authoritative, nothing wasted. + +**Logo direction**: Combination mark. The word "Seneschal" in a geometric sans with a small icon — possibilities include a stylized key (the seneschal held the keys to the estate), a wax seal motif, or a simple grid of nodes representing parallel sessions under one authority. Avoid anything that reads as a robot, brain, or sparkle. + +--- + +## 7. Adoption Goals + +**Primary metric**: GitHub stars → installs → active daily users + +**Discovery path**: Word of mouth in AI developer communities (Twitter/X, Hacker News, r/LocalLLaMA, Claude Discord, Aider community), GitHub search for "claude-code manager", blog posts from power users running parallel agent workflows. + +**Trust signals**: Active commit history, forked lineage from claude-squad, MIT license, real demo GIF in README, single-binary install, 42 built-in approval rules that work on day one. + +**Adoption barrier**: "I already have terminals open. Do I really need a web UI?" — overcome by showing the approval queue and parallel visibility as genuinely new capability, not just a prettier terminal. + +**Aha moment**: The first time an agent petitions for approval and you catch it immediately from the dashboard — without switching windows, without losing context on the four other agents running in parallel. + +--- + +## 8. Key Messages + +**Headline**: Your agentic estate, managed. + +**Supporting points**: +1. Run five agents in parallel without losing track of any — every holding visible in one court. +2. Standing orders handle the routine. The review queue surfaces only the petitions that need you. +3. Isolated worktrees mean each agent works its own plot — no branch conflicts, no dirty state bleed. + +**CTA**: `brew install seneschal` → open `localhost:8543` → start your first parallel session. + +--- + +## 9. GitHub Presence + +**README purpose**: Quick start (install → run → first session) with demo GIF up top. Convince in 60 seconds, then provide depth. Not a reference manual. + +**Social proof**: Stars, install count, forked from claude-squad (credibility through lineage). + +**Contribution posture**: Welcoming — PRs and bug reports encouraged. Good first issues labeled. + +**Suggested GitHub topics**: `ai-agents`, `claude-code`, `aider`, `developer-tools`, `tmux`, `agent-management`, `git-worktrees`, `approval-workflows` + +--- + +## 10. Name Vetting Record + +Vetted via `pm-name-vetting` skill on 2026-05-21. + +| Name | Status | Notes | +|------|--------|-------| +| **Seneschal** | ✅ Clear | Four minor repos, none significant, none in our space | +| **Tribune** | ✅ Clear | Tribune Publishing has repos; no dev tool conflict | +| **Ganglion** | ✅ Clear | Only OpenBCI hardware; different domain | +| Epoch | ⚠️ Ambiguous | One small AI interface project | +| Arbiter | ⚠️ Ambiguous | Multi-agent simulation framework, different niche | +| Thalamus | ⚠️ Ambiguous | Neuroscience + AI serving, different domains | +| Limen | ⚠️ Ambiguous | VPN/proxy platform, different space | +| Vigil | ❌ Conflicted | Multiple active AI agent projects | +| Meridian | ❌ Conflicted | Two Claude Code-adjacent projects | +| Synapse | ❌ Conflicted | Matrix homeserver (tens of thousands of stars) | +| Membrane | ❌ Conflicted | Elixir multimedia framework by Software Mansion | +| Relay | ❌ Conflicted | Facebook's GraphQL framework | +| Seam | ❌ Conflicted | Active smart building API company | +| Plexus | ❌ Conflicted | AI API gateway + Maven ecosystem | +| Quorum | ❌ Conflicted | ConsenSys blockchain project | +| Mentat | ❌ Conflicted | AbanteAI's AI coding assistant | +| Conduit | ❌ Conflicted | **Literally competing product** — GitHub's workspace manager for coding agents | +| Giskard | ❌ Conflicted | Active AI testing platform | +| Weave | ❌ Conflicted | W&B toolkit + multi-agent orchestration | +| Argus | ❌ Conflicted | Multiple monitoring tools; one named "Claude Code Agent Monitoring" | +| Panoptes | ❌ Conflicted | Yahoo network telemetry + workflow monitoring tool | + +--- + +## 11. Skills That Read This File + +| Skill | What it uses | +|-------|-------------| +| `pm-brand-guidelines` | Voice, visual direction, color palette | +| `ui-frontend-design` | Aesthetic references, color mood, typography | +| `ui-logo-designer` | Logo direction, personality, visual references | +| `pm-name-vetting` | `name_candidates_vetted` section | diff --git a/.claude/rules/css-architecture.md b/.claude/rules/css-architecture.md new file mode 100644 index 000000000..0ac515b54 --- /dev/null +++ b/.claude/rules/css-architecture.md @@ -0,0 +1,106 @@ +--- +globs: + - "web-app/**/*.css" + - "web-app/**/*.module.css" + - "web-app/**/*.css.ts" + - "web-app/**/*.tsx" + - "web-app/**/*.ts" + - "web-app/**/*.html" +--- + +# CSS Architecture Rules (ADR-009) + +This project has adopted **vanilla-extract** for all new CSS. See `docs/adr/009-vanilla-extract-type-safe-css.md`. + +## New Components: Use vanilla-extract + +All new component styles go in a `.css.ts` file colocated with the component: + +``` +components/ + Button/ + Button.tsx + Button.css.ts ← styles here, NOT Button.module.css +``` + +Import tokens from the shared theme contract — never hardcode values or use `var()` strings directly: + +```ts +// Button.css.ts +import { style, recipe } from '@vanilla-extract/css'; +import { vars } from '../../styles/theme.css'; + +export const button = recipe({ + base: { + borderRadius: vars.radii.md, + fontWeight: 600, + }, + variants: { + intent: { + primary: { background: vars.color.actionPrimary, color: vars.color.textInverse }, + danger: { background: vars.color.statusDanger, color: vars.color.textInverse }, + }, + size: { + sm: { padding: `${vars.space[1]} ${vars.space[2]}`, fontSize: vars.fontSize.sm }, + md: { padding: `${vars.space[2]} ${vars.space[4]}`, fontSize: vars.fontSize.base }, + }, + }, + defaultVariants: { intent: 'primary', size: 'md' }, +}); +``` + +```tsx +// Button.tsx +import { button } from './Button.css'; +