\n`}\n```\n\n## Two Empty States (IMPORTANT)\n1. **\"No skills available\"** (AC-15): `autocompleteConfig.skills.length === 0`\n - Agent has zero skills installed\n - Dropdown still appears to inform user\n \n2. **\"No matching skills\"** (AC-11): `filteredSkills.length === 0`\n - Skills exist, but filter text matches none\n - User typed something that does not match any skill\n\n## Styling Decisions\n- **bottom-full mb-1**: Positions above input with 4px gap\n- **max-h-48**: ~192px max height (AC-17)\n- **overflow-y-auto**: Vertical scroll for long lists\n- **z-50**: Ensures dropdown above other content\n- **bg-selection/50**: Highlighted item background\n- **text-dim/text-bright**: Color hierarchy\n\n## Key Details\n- Uses index as key (plan note: handles duplicate skill names from curated + user)\n- onMouseEnter updates selectedIndex (mouse hover selection)\n- onClick calls insertSkill directly\n- Nested ternary: skills.length → filteredSkills.length → map\n\n## Container Requirements\nSimpleInput outer container needs position: relative for absolute positioning.\n\n## Success Criteria\n- Dropdown appears above input when showAutocomplete true\n- \"No skills available\" when agent has zero skills\n- \"No matching skills\" when filter matches nothing\n- Selected item visually highlighted\n- Scrollable when many skills\n- Mouse hover updates selection\n- Click inserts skill","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T20:10:35.317055Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:52:25.639932Z","closed_at":"2026-02-26T21:52:25.639671Z","close_reason":"Added autocomplete dropdown UI with empty states, mouse hover, click selection","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-253","depends_on_id":"bd-2uj","type":"blocks","created_at":"2026-02-26T20:10:38.204758Z","created_by":"tayloreernisse"},{"issue_id":"bd-253","depends_on_id":"bd-3us","type":"blocks","created_at":"2026-02-26T20:12:27.289036Z","created_by":"tayloreernisse"}]}
{"id":"bd-29o","title":"Implement filtered skills computation","description":"## Overview\nAdd useMemo for filteredSkills in SimpleInput.js that filters the skills list based on user input.\n\n## Background\nAs the user types after the trigger character, the list should filter to show only matching skills. This provides instant feedback and makes finding skills faster.\n\n## Implementation (from plan IMP-6)\n```javascript\nconst filteredSkills = useMemo(() => {\n if (!autocompleteConfig || !triggerInfo) return [];\n \n const { skills } = autocompleteConfig;\n const { filterText } = triggerInfo;\n \n let filtered = skills;\n if (filterText) {\n filtered = skills.filter(s =>\n s.name.toLowerCase().includes(filterText)\n );\n }\n \n // Already sorted by server, but ensure alphabetical\n return filtered.sort((a, b) => a.name.localeCompare(b.name));\n}, [autocompleteConfig, triggerInfo]);\n```\n\n## Filtering Behavior (from AC-6)\n- Case-insensitive matching\n- Matches anywhere in skill name (not just prefix)\n- Empty filterText shows all skills\n- No matches returns empty array (handled by UI)\n\n## Why useMemo\n- Skills list could be large (50+)\n- Filter runs on every keystroke\n- Sorting on each render would be wasteful\n- Memoization prevents unnecessary recalculation\n\n## Key Details\n- triggerInfo.filterText is already lowercase\n- Server pre-sorts, but we re-sort after filtering (stability)\n- localeCompare for proper alphabetical ordering\n\n## Success Criteria\n- Returns empty array when no autocompleteConfig\n- Filters based on filterText (case-insensitive)\n- Results sorted alphabetically\n- Empty input shows all skills\n- Memoized to prevent unnecessary recalculation","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T20:09:29.436812Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:49:16.259177Z","closed_at":"2026-02-26T21:49:16.258997Z","close_reason":"Added triggerInfo state and filteredSkills useMemo with case-insensitive filtering","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-29o","depends_on_id":"bd-3s3","type":"blocks","created_at":"2026-02-26T20:09:31.898778Z","created_by":"tayloreernisse"}]}
{"id":"bd-2a1","title":"Implement backspace dismissal of autocomplete","description":"## Overview\nEnsure that backspacing over the trigger character dismisses the autocomplete dropdown (AC-9).\n\n## Background\nWhen the user types '/com' and then backspaces to '/', they might continue backspacing to remove the '/'. At that point, the autocomplete should dismiss.\n\n## Implementation\nThis is already handled by the existing trigger detection:\n- When text changes, triggerInfo is recomputed\n- If cursor is before the trigger char, getTriggerInfo returns null\n- showAutocomplete becomes false\n\n## Verification Needed\n- Type '/com'\n- Backspace to '/'\n- Backspace to ''\n- Dropdown should dismiss when '/' is deleted\n\n## Success Criteria\n- Backspacing past trigger dismisses dropdown\n- No special code needed if detection is correct\n- Add test case if not already covered","status":"closed","priority":3,"issue_type":"task","created_at":"2026-02-26T20:12:00.191481Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:55:09.492178Z","closed_at":"2026-02-26T21:55:09.492131Z","close_reason":"Already works via trigger detection - backspacing past trigger naturally returns null from getTriggerInfo","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-2a1","depends_on_id":"bd-3s3","type":"blocks","created_at":"2026-02-26T20:12:03.544203Z","created_by":"tayloreernisse"}]}
-{"id":"bd-2al","title":"Add HTTP routes for spawn API endpoints","description":"## Overview\nAdd routes for /api/spawn, /api/projects, /api/projects/refresh, and /api/health to amc_server/mixins/http.py.\n\n## Background\nThe spawn feature requires four new API endpoints:\n- POST /api/spawn - Spawn a new agent\n- GET /api/projects - List available projects\n- POST /api/projects/refresh - Refresh projects cache\n- GET /api/health - Server health check with Zellij status\n\n## Implementation (IMP-2)\n\n### Add to do_GET:\n```python\nelif self.path == '/api/projects':\n self._handle_projects()\nelif self.path == '/api/health':\n self._handle_health()\n```\n\n### Add to do_POST:\n```python\nelif self.path == '/api/spawn':\n self._handle_spawn()\nelif self.path == '/api/projects/refresh':\n self._handle_projects_refresh()\n```\n\n### Update do_OPTIONS for CORS:\n```python\ndef do_OPTIONS(self):\n # CORS preflight for API endpoints\n # AC-39: Keep wildcard CORS consistent with existing endpoints;\n # localhost-only binding (AC-24) is the real security boundary\n self.send_response(204)\n self.send_header('Access-Control-Allow-Origin', '*')\n self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')\n self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization')\n self.end_headers()\n```\n\n## CORS Design Decision\nAC-39 specifies consistent wildcard CORS across all endpoints. The security boundary is localhost-only binding (AC-24), not CORS headers. This is appropriate for a dev-machine tool where:\n- Server binds to 127.0.0.1 only\n- External network access not possible\n- Auth token provides additional protection for spawn endpoint\n\n## Route Ordering\nPlace new routes after existing API routes but before catch-all/404 handling to maintain consistency with existing patterns.\n\n## Acceptance Criteria\n- AC-1, AC-3, AC-4: Routes accessible\n- AC-34: projects/refresh endpoint\n- AC-39: Consistent CORS headers\n\n## Verification\n```bash\n# Test routes\ncurl http://localhost:7400/api/projects\ncurl http://localhost:7400/api/health\ncurl -X POST http://localhost:7400/api/projects/refresh\ncurl -X OPTIONS http://localhost:7400/api/spawn -v\n```\n\n## Success Criteria\n- All four endpoints respond correctly\n- CORS preflight returns correct headers\n- No 404s for new routes","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T21:40:18.578864Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:40:21.977440Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-2al","depends_on_id":"bd-5m4","type":"blocks","created_at":"2026-02-26T21:40:21.977425Z","created_by":"tayloreernisse"}]}
+{"id":"bd-2al","title":"Add HTTP routes for spawn API endpoints","description":"## Overview\nAdd routes for /api/spawn, /api/projects, /api/projects/refresh, and /api/health to amc_server/mixins/http.py.\n\n## Background\nThe spawn feature requires four new API endpoints:\n- POST /api/spawn - Spawn a new agent\n- GET /api/projects - List available projects\n- POST /api/projects/refresh - Refresh projects cache\n- GET /api/health - Server health check with Zellij status\n\n## Implementation (IMP-2)\n\n### Add to do_GET:\n```python\nelif self.path == '/api/projects':\n self._handle_projects()\nelif self.path == '/api/health':\n self._handle_health()\n```\n\n### Add to do_POST:\n```python\nelif self.path == '/api/spawn':\n self._handle_spawn()\nelif self.path == '/api/projects/refresh':\n self._handle_projects_refresh()\n```\n\n### Update do_OPTIONS for CORS:\n```python\ndef do_OPTIONS(self):\n # CORS preflight for API endpoints\n # AC-39: Keep wildcard CORS consistent with existing endpoints;\n # localhost-only binding (AC-24) is the real security boundary\n self.send_response(204)\n self.send_header('Access-Control-Allow-Origin', '*')\n self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')\n self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization')\n self.end_headers()\n```\n\n## CORS Design Decision\nAC-39 specifies consistent wildcard CORS across all endpoints. The security boundary is localhost-only binding (AC-24), not CORS headers. This is appropriate for a dev-machine tool where:\n- Server binds to 127.0.0.1 only\n- External network access not possible\n- Auth token provides additional protection for spawn endpoint\n\n## Route Ordering\nPlace new routes after existing API routes but before catch-all/404 handling to maintain consistency with existing patterns.\n\n## Acceptance Criteria\n- AC-1, AC-3, AC-4: Routes accessible\n- AC-34: projects/refresh endpoint\n- AC-39: Consistent CORS headers\n\n## Verification\n```bash\n# Test routes\ncurl http://localhost:7400/api/projects\ncurl http://localhost:7400/api/health\ncurl -X POST http://localhost:7400/api/projects/refresh\ncurl -X OPTIONS http://localhost:7400/api/spawn -v\n```\n\n## Success Criteria\n- All four endpoints respond correctly\n- CORS preflight returns correct headers\n- No 404s for new routes","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T21:40:18.578864Z","created_by":"tayloreernisse","updated_at":"2026-02-26T22:02:54.270477Z","closed_at":"2026-02-26T22:02:54.270416Z","close_reason":"Added GET /api/projects, GET /api/health, POST /api/spawn, POST /api/projects/refresh routes to http.py. Updated CORS preflight to include GET method and Authorization header. All 366 tests pass.","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-2al","depends_on_id":"bd-5m4","type":"blocks","created_at":"2026-02-26T21:40:21.977425Z","created_by":"tayloreernisse"}]}
{"id":"bd-2cw","title":"Add visual feedback when spawned agent appears in dashboard","description":"## Overview\nAdd visual feedback (highlight animation) when a newly spawned agent's session card appears in the dashboard.\n\n## Background\nAfter spawning, users need confirmation that the agent appeared. Currently:\n- Toast shows 'agent spawned for project'\n- Session card appears on next poll\n\nThe gap between toast and card appearance could be confusing. Visual feedback bridges this.\n\n## Implementation Options\n\n### Option A: Highlight Animation\nAdd CSS animation to new session cards:\n```css\n@keyframes spawn-highlight {\n 0% { box-shadow: 0 0 0 2px var(--color-active); }\n 100% { box-shadow: 0 0 0 0 transparent; }\n}\n\n.session-card-new {\n animation: spawn-highlight 2s ease-out;\n}\n```\n\nTrack which sessions are 'new' in App.js state:\n```javascript\nconst [newSessionIds, setNewSessionIds] = useState(new Set());\n\n// After successful spawn\nonSpawn={(result) => {\n if (result.success) {\n // Mark spawn_id as 'new'\n // When session with matching spawn_id appears, add highlight class\n }\n}}\n```\n\n### Option B: 'Just spawned' Badge\nShow temporary badge on new session card:\n```javascript\n{isNew && html`\n \n Just spawned\n \n`}\n```\n\n### Chosen Approach: Option A\nHighlight animation is less intrusive and automatically fades.\n\n## Integration\n1. Spawn returns spawn_id in response\n2. Store spawn_id in App.js state\n3. On session data update, check for session with matching spawn_id\n4. Apply animation class to that card\n5. Remove from tracked set after animation\n\n## Acceptance Criteria\n- AC-15: Spawned agent appears in dashboard within 10 seconds\n- (implied) User can see which session was just spawned\n\n## Success Criteria\n- Session card has visible highlight on appearance\n- Animation fades naturally (2s)\n- Works for both Claude and Codex agents","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-26T21:43:30.558124Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:43:33.390376Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-2cw","depends_on_id":"bd-3v5","type":"blocks","created_at":"2026-02-26T21:43:33.390359Z","created_by":"tayloreernisse"}]}
{"id":"bd-2n7","title":"Load autocomplete config in Modal","description":"## Overview\nAdd useEffect in Modal.js to load skills when a session is opened, and pass autocompleteConfig to SimpleInput.\n\n## Background\nSkills are agent-global, not session-specific. We fetch once when session.agent changes and cache client-side. This follows the plan's data flow: Modal opens -> GET /api/skills -> SimpleInput gets config.\n\n## Implementation (from plan IMP-4)\n```javascript\nconst [autocompleteConfig, setAutocompleteConfig] = useState(null);\n\n// Load skills when agent type changes\nuseEffect(() => {\n if (!session) {\n setAutocompleteConfig(null);\n return;\n }\n \n const agent = session.agent || 'claude';\n fetchSkills(agent)\n .then(config => setAutocompleteConfig(config))\n .catch(() => setAutocompleteConfig(null));\n}, [session?.agent]);\n\n// In render, pass to SimpleInput:\n<\\${SimpleInput}\n ...\n autocompleteConfig=\\${autocompleteConfig}\n/>\n```\n\n## Dependency Array\n- Uses session?.agent to re-fetch when agent type changes\n- NOT session.id (would refetch on every session switch unnecessarily)\n- Skills are agent-global, so same agent = same skills\n\n## Error Handling\n- fetch failure: autocompleteConfig stays null\n- null config: SimpleInput disables autocomplete silently\n\n## Props to Add\nSimpleInput gets new optional prop:\n- autocompleteConfig: { trigger, skills } | null\n\n## Success Criteria\n- useEffect fetches on session.agent change\n- autocompleteConfig state managed correctly\n- Passed to SimpleInput as prop\n- Null on error (graceful degradation)","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T20:08:58.959680Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:46:27.279511Z","closed_at":"2026-02-26T21:46:27.279344Z","close_reason":"Added useEffect to load skills, state management, and prop passing through SessionCard to SimpleInput","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-2n7","depends_on_id":"bd-1ba","type":"blocks","created_at":"2026-02-26T20:09:01.617198Z","created_by":"tayloreernisse"}]}
{"id":"bd-2uj","title":"Implement skill insertion","description":"## Overview\nAdd insertSkill() function in SimpleInput.js that replaces the trigger+filter text with the full skill name.\n\n## Background\nWhen user selects a skill (Enter, Tab, or click), we need to:\n1. Replace the trigger+partial text with trigger+full skill name\n2. Add a trailing space\n3. Position cursor after the inserted text\n\n## Implementation (from plan IMP-8)\n```javascript\nconst insertSkill = useCallback((skill) => {\n if (\\!triggerInfo || \\!autocompleteConfig) return;\n \n const { trigger } = autocompleteConfig;\n const { replaceStart, replaceEnd } = triggerInfo;\n \n const before = text.slice(0, replaceStart);\n const after = text.slice(replaceEnd);\n const inserted = \\`\\${trigger}\\${skill.name} \\`;\n \n setText(before + inserted + after);\n setShowAutocomplete(false);\n \n // Move cursor after inserted text\n const newCursorPos = replaceStart + inserted.length;\n setTimeout(() => {\n if (textareaRef.current) {\n textareaRef.current.selectionStart = newCursorPos;\n textareaRef.current.selectionEnd = newCursorPos;\n textareaRef.current.focus();\n }\n }, 0);\n}, [text, triggerInfo, autocompleteConfig]);\n```\n\n## Example\nUser types: 'please run /com|' (cursor at |)\nUser selects 'commit'\nResult: 'please run /commit |'\n\n## Why setTimeout for Cursor\n- React may not have updated the textarea value yet\n- setTimeout(0) defers until after React render\n- Ensures cursor positioning happens on updated DOM\n\n## AC-20 Compliance\n'After skill insertion, cursor is positioned after the trailing space, ready to continue typing.'\n\n## Success Criteria\n- Replaces trigger+filter with trigger+skill+space\n- Preserves text before and after\n- Cursor positioned at end of insertion\n- Dropdown closes after insertion\n- Focus remains on textarea","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T20:10:14.663971Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:51:14.464158Z","closed_at":"2026-02-26T21:51:14.463898Z","close_reason":"Already implemented insertSkill callback in bd-3vd","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-2uj","depends_on_id":"bd-3vd","type":"blocks","created_at":"2026-02-26T20:10:18.401490Z","created_by":"tayloreernisse"}]}
{"id":"bd-30o","title":"Verify spawned agents have correct Zellij metadata","description":"## Overview\nEnsure spawned agent session files contain correct zellij_session and zellij_pane values.\n\n## Background\nPer AC-16 and AC-17:\n- Session data must include correct zellij_session\n- Session data must include correct zellij_pane\n- Dashboard must be able to send responses to spawned agents\n\nThis enables the dashboard to target the correct Zellij pane when sending responses.\n\n## Current Implementation\nThe amc-hook writes Zellij metadata by detecting:\n- ZELLIJ env var (set by Zellij for all panes)\n- ZELLIJ_SESSION_NAME (session name)\n- ZELLIJ_PANE_ID (pane ID within session)\n\n## Verification Steps\n1. Spawn an agent via API\n2. Find the session file (by spawn_id)\n3. Verify JSON contains:\n - zellij_session == 'infra'\n - zellij_pane is present and valid\n\n```bash\n# After spawn\ncat ~/.local/share/amc/sessions/*.json | jq 'select(.spawn_id \\!= null) | {session: .zellij_session, pane: .zellij_pane}'\n```\n\n## Hook Requirements\namc-hook must read and write these env vars:\n```python\nsession_data['zellij_session'] = os.environ.get('ZELLIJ_SESSION_NAME')\nsession_data['zellij_pane'] = os.environ.get('ZELLIJ_PANE_ID')\n```\n\n## Response Test\nAfter verifying metadata, test that dashboard can send responses:\n1. Open spawned agent's session in dashboard\n2. Wait for AskUserQuestion prompt\n3. Submit response\n4. Verify response appears in Zellij pane\n\n## Edge Cases\n1. Agent started outside Zellij -> no metadata (handled gracefully)\n2. Zellij env vars missing -> fields null or missing\n3. Multiple agents in same pane (rare) -> same pane ID\n\n## Success Criteria\n- Spawned agents have correct zellij_session in session file\n- Spawned agents have correct zellij_pane in session file\n- Dashboard can successfully send responses to spawned agents\n- Non-Zellij agents handled gracefully (no crash)","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-26T21:44:05.261736Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:44:08.335110Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-30o","depends_on_id":"bd-1zy","type":"blocks","created_at":"2026-02-26T21:44:08.335083Z","created_by":"tayloreernisse"}]}
{"id":"bd-30p","title":"Add /api/skills route to HttpMixin","description":"## Overview\nAdd the GET /api/skills endpoint to HttpMixin.do_GET that routes to SkillsMixin._serve_skills().\n\n## Background\nThe skills endpoint lives in HttpMixin because that's where all GET routing is handled. The handler just composes mixins. This follows the existing architecture pattern.\n\n## Implementation (from plan IMP-2)\nIn amc_server/mixins/http.py, add to do_GET method:\n```python\nelif path == '/api/skills':\n agent = query_params.get('agent', ['claude'])[0]\n self._serve_skills(agent)\n```\n\n## API Design\n- **Endpoint**: GET /api/skills?agent={claude|codex}\n- **Default agent**: 'claude' (if param missing)\n- **Response**: JSON from _serve_skills()\n\n## Why This Location\n- HttpMixin.do_GET handles ALL GET routing\n- Keeps routing logic centralized\n- SkillsMixin provides the implementation\n- Handler composes both mixins\n\n## Required Import\nAdd SkillsMixin to handler.py mixin list:\n```python\nfrom amc_server.mixins.skills import SkillsMixin\n# Add to AMCHandler class inheritance\n```\n\n## Success Criteria\n- Route /api/skills accessible via GET\n- Query param 'agent' passed to _serve_skills\n- Missing agent param defaults to 'claude'\n- Follows existing routing patterns in do_GET","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T20:08:29.491653Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:44:28.599198Z","closed_at":"2026-02-26T21:44:28.599149Z","close_reason":"Added /api/skills route to HttpMixin.do_GET with agent query param parsing","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-30p","depends_on_id":"bd-g9t","type":"blocks","created_at":"2026-02-26T20:08:32.597460Z","created_by":"tayloreernisse"}]}
-{"id":"bd-35a","title":"Integration: Wire up complete autocomplete feature","description":"## Overview\nFinal integration step: ensure all pieces work together in Modal.js and SimpleInput.js.\n\n## Background\nThis is the capstone bead that verifies the complete feature is wired up correctly. All previous beads implement individual pieces; this ensures they connect.\n\n## Checklist\n1. **SkillsMixin added to handler**\n - Import SkillsMixin in handler.py\n - Add to AMCHandler class inheritance\n\n2. **Modal loads config**\n - fetchSkills called on session.agent change\n - autocompleteConfig passed to SimpleInput\n\n3. **SimpleInput receives config**\n - New prop: autocompleteConfig\n - State management wired up\n - Keyboard handlers connected\n - Dropdown renders correctly\n\n## Verification Steps\n1. Start dev server\n2. Open a session modal\n3. Type '/' -> dropdown should appear\n4. Arrow down, Enter -> skill inserted\n5. Check Codex session with '$'\n\n## Common Issues to Check\n- Import paths correct\n- Mixin in correct position in inheritance\n- Props threaded through correctly\n- No console errors\n\n## Success Criteria\n- Feature works end-to-end\n- No console errors\n- Both Claude and Codex agents work\n- Manual testing checklist passes","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:12:39.799374Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:12:43.180050Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-35a","depends_on_id":"bd-2a1","type":"blocks","created_at":"2026-02-26T20:12:43.160167Z","created_by":"tayloreernisse"},{"issue_id":"bd-35a","depends_on_id":"bd-362","type":"blocks","created_at":"2026-02-26T20:12:43.135755Z","created_by":"tayloreernisse"},{"issue_id":"bd-35a","depends_on_id":"bd-3ny","type":"blocks","created_at":"2026-02-26T20:12:43.111964Z","created_by":"tayloreernisse"},{"issue_id":"bd-35a","depends_on_id":"bd-4lc","type":"blocks","created_at":"2026-02-26T20:12:43.180034Z","created_by":"tayloreernisse"}]}
-{"id":"bd-362","title":"Unit tests for SkillsMixin","description":"## Overview\nAdd comprehensive unit tests for amc_server/mixins/skills.py in tests/test_skills.py.\n\n## Background\nThe skill enumeration is the foundation of autocomplete. We need to test:\n- Both enumeration methods\n- Edge cases (missing files, bad JSON, empty directories)\n- Sorting behavior\n- Response format\n\n## Test Cases\n\n### Claude Enumeration\n```python\ndef test_enumerate_claude_skills_empty_directory(tmp_home):\n '''Returns empty list when ~/.claude/skills doesn't exist'''\n \ndef test_enumerate_claude_skills_reads_skill_md(tmp_home, claude_skill):\n '''Reads description from SKILL.md'''\n \ndef test_enumerate_claude_skills_fallback_to_readme(tmp_home):\n '''Falls back to README.md when SKILL.md missing'''\n \ndef test_enumerate_claude_skills_skips_hidden_dirs(tmp_home):\n '''Ignores directories starting with .'''\n \ndef test_enumerate_claude_skills_truncates_description(tmp_home):\n '''Description truncated to 100 chars'''\n \ndef test_enumerate_claude_skills_skips_headers(tmp_home):\n '''First non-header line used as description'''\n```\n\n### Codex Enumeration\n```python\ndef test_enumerate_codex_skills_reads_cache(tmp_home):\n '''Reads from skills-curated-cache.json'''\n \ndef test_enumerate_codex_skills_invalid_json(tmp_home):\n '''Continues without cache if JSON invalid'''\n \ndef test_enumerate_codex_skills_combines_cache_and_user(tmp_home):\n '''Returns both curated and user skills'''\n \ndef test_enumerate_codex_skills_no_deduplication(tmp_home):\n '''Duplicate names from cache and user both appear'''\n```\n\n### Serve Skills\n```python\ndef test_serve_skills_claude_trigger(mock_handler):\n '''Returns / trigger for claude agent'''\n \ndef test_serve_skills_codex_trigger(mock_handler):\n '''Returns $ trigger for codex agent'''\n \ndef test_serve_skills_default_to_claude(mock_handler):\n '''Unknown agent defaults to claude'''\n \ndef test_serve_skills_alphabetical_sort(mock_handler, skills_fixture):\n '''Skills sorted alphabetically (case-insensitive)'''\n```\n\n## Fixtures\n- tmp_home: pytest fixture that sets HOME to temp directory\n- claude_skill: creates a skill directory with SKILL.md\n- mock_handler: SkillsMixin instance with mocked _send_json\n\n## Success Criteria\n- All enumeration paths tested\n- Error handling verified (bad JSON, missing files)\n- Sorting correctness verified\n- Response format validated","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-26T20:11:15.502753Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:11:18.589476Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-362","depends_on_id":"bd-30p","type":"blocks","created_at":"2026-02-26T20:11:18.589458Z","created_by":"tayloreernisse"}]}
-{"id":"bd-3c7","title":"Handle edge case: empty or missing ~/projects/ directory","description":"## Overview\nHandle the case where ~/projects/ doesn't exist or is empty gracefully.\n\n## Background\nNot all users will have ~/projects/ configured. The spawn feature should degrade gracefully:\n- Return empty projects list (not error)\n- Dashboard shows helpful message\n- No crashes or confusing errors\n\n## Server Side\nload_projects_cache() already handles this:\n```python\nexcept OSError:\n _projects_cache = []\n```\n\n## Dashboard Side\nSpawnModal should handle empty projects list:\n1. Projects dropdown is empty\n2. Show message: 'No projects found in ~/projects/'\n3. Spawn button remains disabled (no selection possible)\n\n## Implementation\nIn SpawnModal.js, add condition:\n```javascript\n${projects.length === 0 && !loadingProjects && html`\n
\n No projects found in ~/projects/\n
\n`}\n```\n\n## Edge Cases\n1. ~/projects/ doesn't exist -> empty list\n2. ~/projects/ exists but empty -> empty list\n3. ~/projects/ exists, only hidden dirs -> empty list (they're excluded)\n4. ~/projects/ exists with permission error -> empty list (OSError caught)\n\n## Testing\n1. Temporarily rename ~/projects/\n2. Restart server\n3. Open spawn modal on All Projects\n4. Verify helpful message shown\n5. Verify no crash\n6. Restore ~/projects/\n\n## Success Criteria\n- Server doesn't crash without ~/projects/\n- Dashboard shows informative message\n- No confusing error toasts","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-26T21:43:13.655184Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:43:16.434409Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3c7","depends_on_id":"bd-15z","type":"blocks","created_at":"2026-02-26T21:43:16.434386Z","created_by":"tayloreernisse"}]}
-{"id":"bd-3cc","title":"E2E tests for autocomplete workflow","description":"## Overview\nCreate end-to-end test script that validates the complete autocomplete workflow from typing to insertion.\n\n## Background\nThe plan includes a manual testing checklist (lines 462-480). We should automate key workflows to ensure the feature works end-to-end.\n\n## Test Scenarios (from plan's testing checklist)\n\n### Core Flow\n```\n1. Claude session: Type \"/\" -> dropdown appears with Claude skills\n2. Codex session: Type \"$\" -> dropdown appears with Codex skills\n3. Claude session: Type \"$\" -> nothing happens (wrong trigger)\n4. Type \"/com\" -> list filters to skills containing \"com\"\n5. Mid-message: Type \"please run /commit\" -> autocomplete triggers on \"/\"\n6. Arrow keys navigate, Enter selects\n7. Escape dismisses without selection\n8. Click outside dismisses\n9. Selected skill shows as \"{trigger}skill-name \" in input\n10. Verify alphabetical ordering of skills\n11. Verify vertical scroll with many skills\n```\n\n### Edge Cases (from plan section)\n```\n- Session without skills (dropdown shows \"No skills available\")\n- Single skill (still shows dropdown)\n- Very long skill descriptions (CSS truncates with ellipsis - visual check)\n- Multiple triggers in one message (each \"/\" can trigger independently)\n- Backspace over trigger (dismisses autocomplete)\n```\n\n### Multiple Triggers Test (important edge case)\nUser types: \"first /commit then /review-pr finally\"\n- First \"/\" at position 6 can trigger\n- After inserting \"/commit \", cursor at position 14\n- Second \"/\" at position after text can trigger again\n- Verify each trigger works independently\n\n## Implementation Approach\nUse the Playwright MCP tools to:\n1. Navigate to dashboard\n2. Open a session modal\n3. Type trigger character\n4. Verify dropdown appears\n5. Navigate with arrows\n6. Select with Enter\n7. Verify insertion\n\n## Logging Requirements\n- Log each step being performed\n- Log expected vs actual behavior\n- Log timing for performance visibility\n- Log any errors with context\n\n## Test Script Location\ntests/e2e/test_autocomplete.py or similar\n\n## Success Criteria\n- All core scenarios pass\n- Edge cases handled\n- Detailed logging for debugging\n- Can run in CI environment","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-26T20:11:47.556903Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:18:56.881498Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3cc","depends_on_id":"bd-3ny","type":"blocks","created_at":"2026-02-26T20:11:50.670698Z","created_by":"tayloreernisse"}]}
+{"id":"bd-35a","title":"Integration: Wire up complete autocomplete feature","description":"## Overview\nFinal integration step: ensure all pieces work together in Modal.js and SimpleInput.js.\n\n## Background\nThis is the capstone bead that verifies the complete feature is wired up correctly. All previous beads implement individual pieces; this ensures they connect.\n\n## Checklist\n1. **SkillsMixin added to handler**\n - Import SkillsMixin in handler.py\n - Add to AMCHandler class inheritance\n\n2. **Modal loads config**\n - fetchSkills called on session.agent change\n - autocompleteConfig passed to SimpleInput\n\n3. **SimpleInput receives config**\n - New prop: autocompleteConfig\n - State management wired up\n - Keyboard handlers connected\n - Dropdown renders correctly\n\n## Verification Steps\n1. Start dev server\n2. Open a session modal\n3. Type '/' -> dropdown should appear\n4. Arrow down, Enter -> skill inserted\n5. Check Codex session with '$'\n\n## Common Issues to Check\n- Import paths correct\n- Mixin in correct position in inheritance\n- Props threaded through correctly\n- No console errors\n\n## Success Criteria\n- Feature works end-to-end\n- No console errors\n- Both Claude and Codex agents work\n- Manual testing checklist passes","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T20:12:39.799374Z","created_by":"tayloreernisse","updated_at":"2026-02-26T22:00:06.041741Z","closed_at":"2026-02-26T22:00:06.041691Z","close_reason":"Full integration complete with all blockers closed: skills enumeration, UI, click-outside, scroll-into-view, unit tests","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-35a","depends_on_id":"bd-2a1","type":"blocks","created_at":"2026-02-26T20:12:43.160167Z","created_by":"tayloreernisse"},{"issue_id":"bd-35a","depends_on_id":"bd-362","type":"blocks","created_at":"2026-02-26T20:12:43.135755Z","created_by":"tayloreernisse"},{"issue_id":"bd-35a","depends_on_id":"bd-3ny","type":"blocks","created_at":"2026-02-26T20:12:43.111964Z","created_by":"tayloreernisse"},{"issue_id":"bd-35a","depends_on_id":"bd-4lc","type":"blocks","created_at":"2026-02-26T20:12:43.180034Z","created_by":"tayloreernisse"}]}
+{"id":"bd-362","title":"Unit tests for SkillsMixin","description":"## Overview\nAdd comprehensive unit tests for amc_server/mixins/skills.py in tests/test_skills.py.\n\n## Background\nThe skill enumeration is the foundation of autocomplete. We need to test:\n- Both enumeration methods\n- Edge cases (missing files, bad JSON, empty directories)\n- Sorting behavior\n- Response format\n\n## Test Cases\n\n### Claude Enumeration\n```python\ndef test_enumerate_claude_skills_empty_directory(tmp_home):\n '''Returns empty list when ~/.claude/skills doesn't exist'''\n \ndef test_enumerate_claude_skills_reads_skill_md(tmp_home, claude_skill):\n '''Reads description from SKILL.md'''\n \ndef test_enumerate_claude_skills_fallback_to_readme(tmp_home):\n '''Falls back to README.md when SKILL.md missing'''\n \ndef test_enumerate_claude_skills_skips_hidden_dirs(tmp_home):\n '''Ignores directories starting with .'''\n \ndef test_enumerate_claude_skills_truncates_description(tmp_home):\n '''Description truncated to 100 chars'''\n \ndef test_enumerate_claude_skills_skips_headers(tmp_home):\n '''First non-header line used as description'''\n```\n\n### Codex Enumeration\n```python\ndef test_enumerate_codex_skills_reads_cache(tmp_home):\n '''Reads from skills-curated-cache.json'''\n \ndef test_enumerate_codex_skills_invalid_json(tmp_home):\n '''Continues without cache if JSON invalid'''\n \ndef test_enumerate_codex_skills_combines_cache_and_user(tmp_home):\n '''Returns both curated and user skills'''\n \ndef test_enumerate_codex_skills_no_deduplication(tmp_home):\n '''Duplicate names from cache and user both appear'''\n```\n\n### Serve Skills\n```python\ndef test_serve_skills_claude_trigger(mock_handler):\n '''Returns / trigger for claude agent'''\n \ndef test_serve_skills_codex_trigger(mock_handler):\n '''Returns $ trigger for codex agent'''\n \ndef test_serve_skills_default_to_claude(mock_handler):\n '''Unknown agent defaults to claude'''\n \ndef test_serve_skills_alphabetical_sort(mock_handler, skills_fixture):\n '''Skills sorted alphabetically (case-insensitive)'''\n```\n\n## Fixtures\n- tmp_home: pytest fixture that sets HOME to temp directory\n- claude_skill: creates a skill directory with SKILL.md\n- mock_handler: SkillsMixin instance with mocked _send_json\n\n## Success Criteria\n- All enumeration paths tested\n- Error handling verified (bad JSON, missing files)\n- Sorting correctness verified\n- Response format validated","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-26T20:11:15.502753Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:59:33.202383Z","closed_at":"2026-02-26T21:59:33.202340Z","close_reason":"Implemented 46 comprehensive unit tests covering all SkillsMixin methods","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-362","depends_on_id":"bd-30p","type":"blocks","created_at":"2026-02-26T20:11:18.589458Z","created_by":"tayloreernisse"}]}
+{"id":"bd-3c7","title":"Handle edge case: empty or missing ~/projects/ directory","description":"## Overview\nHandle the case where ~/projects/ doesn't exist or is empty gracefully.\n\n## Background\nNot all users will have ~/projects/ configured. The spawn feature should degrade gracefully:\n- Return empty projects list (not error)\n- Dashboard shows helpful message\n- No crashes or confusing errors\n\n## Server Side\nload_projects_cache() already handles this:\n```python\nexcept OSError:\n _projects_cache = []\n```\n\n## Dashboard Side\nSpawnModal should handle empty projects list:\n1. Projects dropdown is empty\n2. Show message: 'No projects found in ~/projects/'\n3. Spawn button remains disabled (no selection possible)\n\n## Implementation\nIn SpawnModal.js, add condition:\n```javascript\n${projects.length === 0 && !loadingProjects && html`\n
\n No projects found in ~/projects/\n
\n`}\n```\n\n## Edge Cases\n1. ~/projects/ doesn't exist -> empty list\n2. ~/projects/ exists but empty -> empty list\n3. ~/projects/ exists, only hidden dirs -> empty list (they're excluded)\n4. ~/projects/ exists with permission error -> empty list (OSError caught)\n\n## Testing\n1. Temporarily rename ~/projects/\n2. Restart server\n3. Open spawn modal on All Projects\n4. Verify helpful message shown\n5. Verify no crash\n6. Restore ~/projects/\n\n## Success Criteria\n- Server doesn't crash without ~/projects/\n- Dashboard shows informative message\n- No confusing error toasts","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-26T21:43:13.655184Z","created_by":"tayloreernisse","updated_at":"2026-02-26T22:08:26.546217Z","closed_at":"2026-02-26T22:08:26.546165Z","close_reason":"Implemented empty projects edge case: server already handled OSError/missing dir gracefully, added empty-state message to SpawnModal, added tests for missing dir + only-hidden-dirs + empty API responses","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3c7","depends_on_id":"bd-15z","type":"blocks","created_at":"2026-02-26T21:43:16.434386Z","created_by":"tayloreernisse"}]}
+{"id":"bd-3cc","title":"E2E tests for autocomplete workflow","description":"## Overview\nCreate end-to-end test script that validates the complete autocomplete workflow from typing to insertion.\n\n## Background\nThe plan includes a manual testing checklist (lines 462-480). We should automate key workflows to ensure the feature works end-to-end.\n\n## Test Scenarios (from plan's testing checklist)\n\n### Core Flow\n```\n1. Claude session: Type \"/\" -> dropdown appears with Claude skills\n2. Codex session: Type \"$\" -> dropdown appears with Codex skills\n3. Claude session: Type \"$\" -> nothing happens (wrong trigger)\n4. Type \"/com\" -> list filters to skills containing \"com\"\n5. Mid-message: Type \"please run /commit\" -> autocomplete triggers on \"/\"\n6. Arrow keys navigate, Enter selects\n7. Escape dismisses without selection\n8. Click outside dismisses\n9. Selected skill shows as \"{trigger}skill-name \" in input\n10. Verify alphabetical ordering of skills\n11. Verify vertical scroll with many skills\n```\n\n### Edge Cases (from plan section)\n```\n- Session without skills (dropdown shows \"No skills available\")\n- Single skill (still shows dropdown)\n- Very long skill descriptions (CSS truncates with ellipsis - visual check)\n- Multiple triggers in one message (each \"/\" can trigger independently)\n- Backspace over trigger (dismisses autocomplete)\n```\n\n### Multiple Triggers Test (important edge case)\nUser types: \"first /commit then /review-pr finally\"\n- First \"/\" at position 6 can trigger\n- After inserting \"/commit \", cursor at position 14\n- Second \"/\" at position after text can trigger again\n- Verify each trigger works independently\n\n## Implementation Approach\nUse the Playwright MCP tools to:\n1. Navigate to dashboard\n2. Open a session modal\n3. Type trigger character\n4. Verify dropdown appears\n5. Navigate with arrows\n6. Select with Enter\n7. Verify insertion\n\n## Logging Requirements\n- Log each step being performed\n- Log expected vs actual behavior\n- Log timing for performance visibility\n- Log any errors with context\n\n## Test Script Location\ntests/e2e/test_autocomplete.py or similar\n\n## Success Criteria\n- All core scenarios pass\n- Edge cases handled\n- Detailed logging for debugging\n- Can run in CI environment","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-26T20:11:47.556903Z","created_by":"tayloreernisse","updated_at":"2026-02-26T22:04:06.299931Z","closed_at":"2026-02-26T22:04:06.299744Z","close_reason":"Implemented 50 E2E tests (14 server-side Python, 36 client-side JS) covering: /api/skills endpoint, trigger detection, keyboard navigation, skill insertion, alphabetical ordering, cross-agent isolation, edge cases, and full workflow simulation","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3cc","depends_on_id":"bd-3ny","type":"blocks","created_at":"2026-02-26T20:11:50.670698Z","created_by":"tayloreernisse"}]}
{"id":"bd-3eu","title":"Implement Codex skill enumeration","description":"## Overview\nImplement _enumerate_codex_skills() method in SkillsMixin to discover skills from both the curated cache and user directory.\n\n## Background\nCodex stores skills in two locations:\n1. **Curated cache**: ~/.codex/vendor_imports/skills-curated-cache.json (pre-installed skills)\n2. **User skills**: ~/.codex/skills/*/ (user-created or installed)\n\nBoth need to be combined for the full skills list.\n\n## Implementation (from plan IMP-1)\n```python\ndef _enumerate_codex_skills(self):\n skills = []\n \n # 1. Curated skills from cache\n cache_file = Path.home() / '.codex/vendor_imports/skills-curated-cache.json'\n if cache_file.exists():\n try:\n data = json.loads(cache_file.read_text())\n for skill in data.get('skills', []):\n skills.append({\n 'name': skill.get('id', skill.get('name', '')),\n 'description': skill.get('shortDescription', skill.get('description', ''))[:100]\n })\n except (json.JSONDecodeError, OSError):\n pass # Continue without curated skills\n \n # 2. User-installed skills\n user_skills_dir = Path.home() / '.codex/skills'\n if user_skills_dir.exists():\n for skill_dir in user_skills_dir.iterdir():\n if skill_dir.is_dir() and not skill_dir.name.startswith('.'):\n skill_md = skill_dir / 'SKILL.md'\n description = ''\n if skill_md.exists():\n try:\n for line in skill_md.read_text().splitlines():\n line = line.strip()\n if line and not line.startswith('#'):\n description = line[:100]\n break\n except OSError:\n pass\n skills.append({\n 'name': skill_dir.name,\n 'description': description or f'User skill: {skill_dir.name}'\n })\n \n return skills\n```\n\n## Key Decisions\n- **Cache file structure**: Expected format {skills: [{id, shortDescription}, ...]}\n- **Fallback for missing fields**: Use 'name' if 'id' missing, 'description' if 'shortDescription' missing\n- **No deduplication**: If curated and user skills share a name, both appear (per Known Limitations)\n- **Error resilience**: JSON parse errors don't prevent user skills from loading\n\n## Out of Scope (per plan)\n- Duplicate skill names are NOT deduplicated\n- Server-side caching of enumeration results\n\n## Success Criteria\n- Returns combined list from cache + user directory\n- Handles missing files/directories gracefully\n- Truncates descriptions to 100 chars\n- JSON parse errors don't crash enumeration","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T20:07:58.579276Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:43:21.057360Z","closed_at":"2026-02-26T21:43:21.057312Z","close_reason":"Implemented _enumerate_codex_skills: reads curated cache + user skills directory, handles JSON errors gracefully","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3eu","depends_on_id":"bd-3q1","type":"blocks","created_at":"2026-02-26T20:08:01.832043Z","created_by":"tayloreernisse"}]}
{"id":"bd-3g8","title":"Slice 3 Integration: Polish and edge cases complete","description":"## Overview\nFinal integration checkpoint verifying all polish and edge case handling.\n\n## Background\nSlice 3 ensures the feature handles edge cases gracefully and provides a polished UX. This bead verifies:\n- Empty projects handling\n- Visual feedback for new agents\n- Special character support\n- Zellij metadata correctness\n- Warning banners\n\n## Verification Checklist\n\n### 1. Empty Projects Directory\n- [ ] Test with empty ~/projects/\n- [ ] Modal shows 'No projects found' message\n- [ ] No crash or confusing error\n\n### 2. Visual Feedback\n- [ ] Spawn an agent\n- [ ] Watch session cards area\n- [ ] New card has highlight animation\n- [ ] Animation fades after ~2s\n\n### 3. Special Characters\n- [ ] Create test project with hyphen: my-test\n- [ ] Spawn agent for it -> success\n- [ ] Create test project with underscore: my_test\n- [ ] Spawn agent for it -> success\n- [ ] Test with path traversal ../etc -> rejected\n\n### 4. Zellij Metadata\n- [ ] Spawn agent\n- [ ] Check session file: jq '.zellij_session' \n- [ ] Should be 'infra'\n- [ ] Check session file: jq '.zellij_pane'\n- [ ] Should be valid pane ID\n\n### 5. Dashboard Response\n- [ ] Spawn agent\n- [ ] Wait for AskUserQuestion\n- [ ] Submit response via dashboard\n- [ ] Response appears in Zellij pane\n\n### 6. Zellij Unavailable Warning\n- [ ] Stop Zellij session (zellij kill-session infra)\n- [ ] Reload dashboard\n- [ ] Warning banner should appear\n- [ ] Banner text clear and actionable\n- [ ] Start Zellij session\n- [ ] Warning banner disappears\n\n### 7. Projects Cache Refresh\n- [ ] Add new project to ~/projects/\n- [ ] POST /api/projects/refresh\n- [ ] New project appears in dropdown\n\n### 8. Background Refresh\n- [ ] Add project without manual refresh\n- [ ] Wait 5 minutes\n- [ ] Project appears in dropdown\n\n## Full Feature Walkthrough\n1. Start Zellij session 'infra'\n2. Start AMC server\n3. Open dashboard\n4. Select project in sidebar\n5. Click '+ New Agent'\n6. Select 'Claude'\n7. Click 'Spawn'\n8. See success toast\n9. See agent appear in Zellij\n10. See agent card in dashboard (possibly with highlight)\n11. Wait for AskUserQuestion\n12. Submit response\n13. Response appears in agent\n\n## Acceptance Criteria Covered\n- AC-15: Agent appears within 10 seconds\n- AC-16, AC-17: Correct Zellij metadata\n- AC-42: Warning banner\n\n## Success Criteria\nComplete feature works end-to-end with all edge cases handled gracefully.","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T21:45:39.460751Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:45:45.059903Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3g8","depends_on_id":"bd-14p","type":"blocks","created_at":"2026-02-26T21:45:44.875703Z","created_by":"tayloreernisse"},{"issue_id":"bd-3g8","depends_on_id":"bd-2cw","type":"blocks","created_at":"2026-02-26T21:45:44.790681Z","created_by":"tayloreernisse"},{"issue_id":"bd-3g8","depends_on_id":"bd-30o","type":"blocks","created_at":"2026-02-26T21:45:44.962392Z","created_by":"tayloreernisse"},{"issue_id":"bd-3g8","depends_on_id":"bd-3c7","type":"blocks","created_at":"2026-02-26T21:45:44.730490Z","created_by":"tayloreernisse"},{"issue_id":"bd-3g8","depends_on_id":"bd-3ke","type":"blocks","created_at":"2026-02-26T21:45:45.059877Z","created_by":"tayloreernisse"},{"issue_id":"bd-3g8","depends_on_id":"bd-zgt","type":"blocks","created_at":"2026-02-26T21:45:44.592034Z","created_by":"tayloreernisse"}]}
{"id":"bd-3ke","title":"Dashboard warning banner when Zellij session unavailable","description":"## Overview\nShow warning banner in dashboard when Zellij session 'infra' is unavailable.\n\n## Background\nPer AC-42, the dashboard should warn users when spawning won't work because the Zellij session doesn't exist. This prevents confusion when spawn attempts fail.\n\n## Implementation\n\n### Health Polling\nAdd periodic health check in App.js:\n```javascript\nconst [zelijAvailable, setZelijAvailable] = useState(true);\n\nuseEffect(() => {\n const checkHealth = async () => {\n try {\n const response = await fetch('/api/health');\n const data = await response.json();\n setZelijAvailable(data.zellij_available);\n } catch {\n // Server unreachable - handled elsewhere\n }\n };\n \n checkHealth();\n const interval = setInterval(checkHealth, 30000); // Check every 30s\n return () => clearInterval(interval);\n}, []);\n```\n\n### Warning Banner\nAdd before main content:\n```javascript\n${!zelijAvailable && html`\n
\n Zellij session 'infra' not found.\n Agent spawning is unavailable. Start Zellij with: zellij attach infra\n
\n`}\n```\n\n### Spawn Button State\nOptionally disable spawn button when Zellij unavailable:\n```javascript\n\n```\n\n## Banner Styling\n- bg-attention/20: Light warning background\n- border-attention/50: Warning border\n- text-attention: Warning text color\n- Positioned above main content (not fixed)\n\n## UX Considerations\n1. Banner doesn't block dashboard usage\n2. Clear actionable message with command to fix\n3. Automatically disappears when Zellij available\n4. Doesn't spam with repeated warnings\n\n## Success Criteria\n- Banner appears when Zellij unavailable\n- Banner disappears when Zellij becomes available\n- Spawn button disabled (optional)\n- Clear instructions to fix","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-26T21:44:21.824784Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:44:26.510277Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3ke","depends_on_id":"bd-3v5","type":"blocks","created_at":"2026-02-26T21:44:26.508776Z","created_by":"tayloreernisse"}]}
{"id":"bd-3ny","title":"Implement click-outside dismissal","description":"## Overview\nAdd useEffect in SimpleInput.js that dismisses the autocomplete dropdown when clicking outside.\n\n## Background\nAC-9 requires clicking outside to dismiss the dropdown. This is a common UX pattern that requires:\n- Event listener on document\n- Check if click target is inside dropdown or textarea\n- Cleanup on unmount\n\n## Implementation (from plan IMP-10)\n```javascript\nuseEffect(() => {\n if (!showAutocomplete) return;\n \n const handleClickOutside = (e) => {\n if (autocompleteRef.current && !autocompleteRef.current.contains(e.target) &&\n textareaRef.current && !textareaRef.current.contains(e.target)) {\n setShowAutocomplete(false);\n }\n };\n \n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n}, [showAutocomplete]);\n```\n\n## Why mousedown (not click)\n- mousedown fires immediately on press\n- click fires after release (feels sluggish)\n- Standard pattern for dropdown dismissal\n\n## What Counts as 'Inside'\n- Inside autocompleteRef (the dropdown)\n- Inside textareaRef (the input)\n- Both should keep dropdown open\n\n## Cleanup\n- Effect only adds listener when dropdown is visible\n- Cleanup removes listener when:\n - Dropdown closes\n - Component unmounts\n - Dependencies change\n\n## Success Criteria\n- Clicking outside dropdown+textarea dismisses\n- Clicking inside dropdown doesn't dismiss (onClick handles selection)\n- Clicking in textarea doesn't dismiss (keeps typing)\n- Listener cleaned up properly","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T20:10:49.738217Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:54:17.702299Z","closed_at":"2026-02-26T21:54:17.702245Z","close_reason":"Added click-outside dismissal useEffect with mousedown listener","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3ny","depends_on_id":"bd-253","type":"blocks","created_at":"2026-02-26T20:10:52.252341Z","created_by":"tayloreernisse"}]}
-{"id":"bd-3oo","title":"Add spawn constants to context.py","description":"## Overview\nAdd constants and utilities needed by SpawnMixin to amc_server/context.py.\n\n## Background\nThe spawn feature requires several shared constants and utilities that belong in context.py alongside existing path constants like SESSIONS_DIR. These enable:\n- Locating the projects directory for agent spawning\n- Targeting the correct Zellij session\n- Serializing concurrent spawn requests to prevent race conditions\n- Per-project rate limiting to prevent spam\n- Auth token generation for spawn endpoint security\n\n## Implementation (IMP-0)\nAdd after existing path constants:\n\n```python\nimport secrets\nimport threading\n\n# Projects directory for spawning agents\nPROJECTS_DIR = Path.home() / 'projects'\n\n# Default Zellij session for spawning\nZELLIJ_SESSION = 'infra'\n\n# Lock for serializing spawn operations (prevents Zellij race conditions)\n_spawn_lock = threading.Lock()\n\n# Rate limiting: track last spawn time per project (prevents spam)\n_spawn_timestamps: dict[str, float] = {}\nSPAWN_COOLDOWN_SEC = 10.0\n\n# Auth token for spawn endpoint (AC-37, AC-38)\n# Generated on server start, injected into dashboard HTML\n_auth_token: str = ''\n\n\ndef generate_auth_token():\n \"\"\"Generate a one-time auth token for this server instance.\"\"\"\n global _auth_token\n _auth_token = secrets.token_urlsafe(32)\n return _auth_token\n\n\ndef validate_auth_token(request_token: str) -> bool:\n \"\"\"Validate the Authorization header token.\"\"\"\n return request_token == f'Bearer {_auth_token}'\n\n\ndef start_projects_watcher():\n \"\"\"Start background thread to refresh projects cache every 5 minutes (AC-40).\"\"\"\n import logging\n from amc_server.mixins.spawn import load_projects_cache\n\n def _watch_loop():\n import time\n while True:\n try:\n time.sleep(300) # 5 minutes\n load_projects_cache()\n except Exception:\n logging.exception('Projects cache refresh failed')\n\n thread = threading.Thread(target=_watch_loop, daemon=True)\n thread.start()\n```\n\n## Design Decisions\n- **_spawn_lock**: Process-local threading.Lock is sufficient because AMC is single-process by design. Multi-worker would need file-based locking.\n- **_spawn_timestamps dict**: In-memory dict resets on restart, which is acceptable for rate limiting (no persistence needed).\n- **SPAWN_COOLDOWN_SEC = 10.0**: Prevents accidental spam while allowing reasonable iteration speed.\n- **Auth token**: Single-use per server lifetime prevents CSRF on localhost (defense in depth with localhost binding).\n- **Daemon thread**: Projects watcher exits automatically when main thread exits.\n\n## Acceptance Criteria\n- AC-33: Projects list loaded on server start\n- AC-35, AC-36: Rate limiting infrastructure\n- AC-37: Auth token generation\n- AC-40: Background projects refresh\n\n## Verification\n```python\nfrom amc_server.context import PROJECTS_DIR, ZELLIJ_SESSION, generate_auth_token\nassert PROJECTS_DIR.exists()\nassert ZELLIJ_SESSION == 'infra'\ntoken = generate_auth_token()\nassert len(token) > 20\n```\n\n## Success Criteria\n- All constants importable from context.py\n- generate_auth_token() returns unique token each call\n- validate_auth_token() correctly validates Bearer token format\n- start_projects_watcher() starts daemon thread without error","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T21:39:03.658826Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:39:03.658826Z","compaction_level":0,"original_size":0}
+{"id":"bd-3oo","title":"Add spawn constants to context.py","description":"## Overview\nAdd constants and utilities needed by SpawnMixin to amc_server/context.py.\n\n## Background\nThe spawn feature requires several shared constants and utilities that belong in context.py alongside existing path constants like SESSIONS_DIR. These enable:\n- Locating the projects directory for agent spawning\n- Targeting the correct Zellij session\n- Serializing concurrent spawn requests to prevent race conditions\n- Per-project rate limiting to prevent spam\n- Auth token generation for spawn endpoint security\n\n## Implementation (IMP-0)\nAdd after existing path constants:\n\n```python\nimport secrets\nimport threading\n\n# Projects directory for spawning agents\nPROJECTS_DIR = Path.home() / 'projects'\n\n# Default Zellij session for spawning\nZELLIJ_SESSION = 'infra'\n\n# Lock for serializing spawn operations (prevents Zellij race conditions)\n_spawn_lock = threading.Lock()\n\n# Rate limiting: track last spawn time per project (prevents spam)\n_spawn_timestamps: dict[str, float] = {}\nSPAWN_COOLDOWN_SEC = 10.0\n\n# Auth token for spawn endpoint (AC-37, AC-38)\n# Generated on server start, injected into dashboard HTML\n_auth_token: str = ''\n\n\ndef generate_auth_token():\n \"\"\"Generate a one-time auth token for this server instance.\"\"\"\n global _auth_token\n _auth_token = secrets.token_urlsafe(32)\n return _auth_token\n\n\ndef validate_auth_token(request_token: str) -> bool:\n \"\"\"Validate the Authorization header token.\"\"\"\n return request_token == f'Bearer {_auth_token}'\n\n\ndef start_projects_watcher():\n \"\"\"Start background thread to refresh projects cache every 5 minutes (AC-40).\"\"\"\n import logging\n from amc_server.mixins.spawn import load_projects_cache\n\n def _watch_loop():\n import time\n while True:\n try:\n time.sleep(300) # 5 minutes\n load_projects_cache()\n except Exception:\n logging.exception('Projects cache refresh failed')\n\n thread = threading.Thread(target=_watch_loop, daemon=True)\n thread.start()\n```\n\n## Design Decisions\n- **_spawn_lock**: Process-local threading.Lock is sufficient because AMC is single-process by design. Multi-worker would need file-based locking.\n- **_spawn_timestamps dict**: In-memory dict resets on restart, which is acceptable for rate limiting (no persistence needed).\n- **SPAWN_COOLDOWN_SEC = 10.0**: Prevents accidental spam while allowing reasonable iteration speed.\n- **Auth token**: Single-use per server lifetime prevents CSRF on localhost (defense in depth with localhost binding).\n- **Daemon thread**: Projects watcher exits automatically when main thread exits.\n\n## Acceptance Criteria\n- AC-33: Projects list loaded on server start\n- AC-35, AC-36: Rate limiting infrastructure\n- AC-37: Auth token generation\n- AC-40: Background projects refresh\n\n## Verification\n```python\nfrom amc_server.context import PROJECTS_DIR, ZELLIJ_SESSION, generate_auth_token\nassert PROJECTS_DIR.exists()\nassert ZELLIJ_SESSION == 'infra'\ntoken = generate_auth_token()\nassert len(token) > 20\n```\n\n## Success Criteria\n- All constants importable from context.py\n- generate_auth_token() returns unique token each call\n- validate_auth_token() correctly validates Bearer token format\n- start_projects_watcher() starts daemon thread without error","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T21:39:03.658826Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:58:00.710511Z","closed_at":"2026-02-26T21:58:00.710456Z","close_reason":"Added spawn constants, auth token functions, rate limiting, and projects watcher to context.py. All 302 tests pass.","compaction_level":0,"original_size":0}
{"id":"bd-3q1","title":"Create SkillsMixin class for skill enumeration","description":"## Overview\nCreate a new SkillsMixin class in amc_server/mixins/skills.py that handles enumerating available skills for both Claude and Codex agents.\n\n## Background & Rationale\nThe autocomplete feature needs to know what skills are available for each agent type. Skills are agent-global (not session-specific), so we enumerate them from well-known filesystem locations. This follows the existing mixin pattern used throughout the server (HttpMixin, StateMixin, etc.).\n\n## Implementation Details\n- Create new file: amc_server/mixins/skills.py\n- Class: SkillsMixin with methods:\n - _serve_skills(agent: str) -> serves JSON response\n - _enumerate_claude_skills() -> list of {name, description}\n - _enumerate_codex_skills() -> list of {name, description}\n- Sort skills alphabetically by name (case-insensitive)\n- Return JSON: {trigger: '/' or '$', skills: [...]}\n\n## File Locations\n- Claude skills: ~/.claude/skills/*/\n- Codex curated: ~/.codex/vendor_imports/skills-curated-cache.json\n- Codex user: ~/.codex/skills/*/\n\n## Acceptance Criteria (from plan)\n- AC-12: On session open, agent-specific config loaded with trigger + skills\n- AC-13: Claude skills from ~/.claude/skills/\n- AC-14: Codex skills from cache + user directory\n- AC-15: Empty skills list handled gracefully\n\n## Error Handling\n- Directory doesn't exist: return empty list\n- JSON parse error (Codex cache): skip cache, continue with user skills\n- File read error: skip that skill, continue enumeration\n\n## Success Criteria\n- SkillsMixin class exists with all three methods\n- Claude enumeration reads SKILL.md (canonical), falls back to skill.md, prompt.md, README.md\n- Codex enumeration reads curated cache + user skills directory\n- All skills sorted alphabetically\n- Empty directories return empty list (no error)","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-02-26T20:07:30.389323Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:41:09.021625Z","closed_at":"2026-02-26T21:41:09.021577Z","close_reason":"Created SkillsMixin class with _serve_skills orchestrator and stub enumeration methods","compaction_level":0,"original_size":0}
{"id":"bd-3s3","title":"Implement trigger detection logic","description":"## Overview\nAdd getTriggerInfo() function in SimpleInput.js that detects when autocomplete should activate.\n\n## Background\nAutocomplete triggers when the agent-specific trigger character (/ for Claude, $ for Codex) appears at:\n1. Position 0 (start of input)\n2. After whitespace (space, newline, tab)\n\nThis enables mid-message skill invocation like \"please run /commit\".\n\n## Acceptance Criteria Covered\n- **AC-1**: Triggers at position 0 or after whitespace\n- **AC-2**: Claude uses /, Codex uses $ (from autocompleteConfig.trigger)\n- **AC-3**: Wrong trigger for agent type is ignored (returns null)\n\n## Implementation (from plan IMP-5)\n```javascript\nconst getTriggerInfo = useCallback((value, cursorPos) => {\n // AC-3: If no config, ignore all triggers\n if (\\!autocompleteConfig) return null;\n \n const { trigger } = autocompleteConfig;\n \n // Find the start of the current \"word\" (after last whitespace before cursor)\n let wordStart = cursorPos;\n while (wordStart > 0 && \\!/\\s/.test(value[wordStart - 1])) {\n wordStart--;\n }\n \n // AC-3: Only match THIS agent's trigger, ignore others\n // Claude session typing \"$\" returns null (no autocomplete)\n // Codex session typing \"/\" returns null (no autocomplete)\n if (value[wordStart] === trigger) {\n return {\n trigger,\n filterText: value.slice(wordStart + 1, cursorPos).toLowerCase(),\n replaceStart: wordStart,\n replaceEnd: cursorPos\n };\n }\n \n return null;\n}, [autocompleteConfig]);\n```\n\n## Return Value\n```typescript\ntype TriggerInfo = {\n trigger: string; // The trigger char (/ or $)\n filterText: string; // Text after trigger for filtering (lowercase)\n replaceStart: number; // Start position for replacement\n replaceEnd: number; // End position for replacement (cursor pos)\n} | null;\n```\n\n## AC-3 Behavior (Wrong Trigger Ignored)\nThe function only checks for `autocompleteConfig.trigger`. If user types a different character:\n- Claude session: \"$\" is just a dollar sign, no autocomplete\n- Codex session: \"/\" is just a slash, no autocomplete\n\nThis is implicit in the implementation - we compare against ONE trigger character only.\n\n## Edge Cases\n- Empty input: wordStart = 0, checks value[0]\n- Mid-word: wordStart finds last whitespace, checks that position\n- Multiple triggers in message: each \"/\" or \"$\" can independently trigger\n- Backspace over trigger: cursorPos moves, wordStart recalculates\n\n## When This Returns null\n1. No autocompleteConfig (not loaded yet)\n2. Cursor not preceded by trigger character\n3. Wrong trigger for this agent type (AC-3)\n\n## Success Criteria\n- Returns TriggerInfo when trigger at valid position\n- Returns null for wrong trigger (AC-3)\n- Returns null when no config\n- filterText is lowercase for case-insensitive matching\n- replaceStart/End correct for skill insertion","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T20:09:15.230355Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:48:05.753400Z","closed_at":"2026-02-26T21:48:05.753210Z","close_reason":"Added getTriggerInfo callback that detects trigger at position 0 or after whitespace, returns filter info","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3s3","depends_on_id":"bd-2n7","type":"blocks","created_at":"2026-02-26T20:09:18.843732Z","created_by":"tayloreernisse"}]}
{"id":"bd-3us","title":"Ensure SimpleInput container has relative positioning","description":"## Overview\nVerify/add position: relative to SimpleInput's container for proper dropdown positioning.\n\n## Background\nThe autocomplete dropdown uses absolute positioning with 'bottom-full'. This requires the parent container to have position: relative.\n\n## Implementation\nCheck SimpleInput.js render:\n```javascript\n// The outer wrapper needs position: relative\n
\n \n ${/* autocomplete dropdown here */}\n
\n```\n\n## Why This Matters\nWithout position: relative on parent:\n- Dropdown positions relative to nearest positioned ancestor\n- Could appear in wrong location or off-screen\n- Layout becomes unpredictable\n\n## Verification\n1. Check existing SimpleInput wrapper class\n2. Add 'relative' if not present\n3. Test dropdown appears correctly above input\n\n## Success Criteria\n- Dropdown appears directly above textarea\n- No layout shifts or misalignment\n- Works in both modal and any other contexts","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T20:12:24.541494Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:51:58.571106Z","closed_at":"2026-02-26T21:51:58.570584Z","close_reason":"Wrapped textarea in relative flex-1 container for dropdown positioning","compaction_level":0,"original_size":0}
-{"id":"bd-3v5","title":"Integrate SpawnModal into App.js with header button","description":"## Overview\nAdd 'New Agent' button to App.js header and wire up SpawnModal component.\n\n## Background\nThe spawn button is located in the page header (AC-1), not on session cards. This placement:\n- Provides consistent access from any view\n- Clearly communicates 'spawn' as a top-level action\n- Follows dashboard visual hierarchy\n\n## Implementation (IMP-5)\n\n### Add Import\n```javascript\nimport { SpawnModal } from './SpawnModal.js';\n```\n\n### Add State (around line 14)\n```javascript\nconst [spawnModalOpen, setSpawnModalOpen] = useState(false);\n```\n\n### Add Button to Header (around line 341)\nPlace after status summary chips div:\n```javascript\n\n```\n\n### Add Spawn Result Handler (IMP-6)\n```javascript\nimport { showToast } from './Toast.js'; // Already imported\n\nconst handleSpawnResult = useCallback((result) => {\n if (result.success) {\n showToast(`${result.agentType} agent spawned for ${result.project}`, 'success');\n } else if (result.error) {\n showToast(result.error, 'error');\n }\n}, []);\n```\n\n### Add Modal (after ToastContainer, around line 426)\n```javascript\n<${SpawnModal}\n isOpen=${spawnModalOpen}\n onClose=${() => setSpawnModalOpen(false)}\n onSpawn=${handleSpawnResult}\n currentProject=${selectedProject}\n/>\n```\n\n## selectedProject Context\nPer plan note: selectedProject in App.js is already the short project name (e.g., 'amc'), not the full path. This comes from groupSessionsByProject() in status.js which uses projectName as the key.\n\nWhen selectedProject is:\n- A project name (e.g., 'amc'): Modal shows agent type only\n- null (All Projects tab): Modal shows project dropdown\n\n## Button Styling\n- border-active/40: Subtle active color border\n- bg-active/12: Light active background\n- text-active: Active color text\n- hover:bg-active/20: Brighter on hover\n\nThis matches the dashboard's existing accent button style.\n\n## Toast Integration\nUses existing showToast(message, type, duration) from Toast.js:\n- 'success' type: Green checkmark\n- 'error' type: Red exclamation\n\n## Acceptance Criteria\n- AC-1: Button in page header\n- AC-20: Toast for spawn errors\n- AC-21: Toast for spawn success\n\n## Verification\n1. Load dashboard\n2. Verify button visible in header\n3. Click button -> modal opens\n4. Spawn agent -> success toast\n5. Trigger error -> error toast\n\n## Success Criteria\n- Button styled consistently with dashboard\n- Modal opens on click\n- Toast shows on spawn result\n- selectedProject passed correctly to modal","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T21:41:53.399210Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:41:57.064260Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3v5","depends_on_id":"bd-15z","type":"blocks","created_at":"2026-02-26T21:41:57.064242Z","created_by":"tayloreernisse"}]}
+{"id":"bd-3v5","title":"Integrate SpawnModal into App.js with header button","description":"## Overview\nAdd 'New Agent' button to App.js header and wire up SpawnModal component.\n\n## Background\nThe spawn button is located in the page header (AC-1), not on session cards. This placement:\n- Provides consistent access from any view\n- Clearly communicates 'spawn' as a top-level action\n- Follows dashboard visual hierarchy\n\n## Implementation (IMP-5)\n\n### Add Import\n```javascript\nimport { SpawnModal } from './SpawnModal.js';\n```\n\n### Add State (around line 14)\n```javascript\nconst [spawnModalOpen, setSpawnModalOpen] = useState(false);\n```\n\n### Add Button to Header (around line 341)\nPlace after status summary chips div:\n```javascript\n\n```\n\n### Add Spawn Result Handler (IMP-6)\n```javascript\nimport { showToast } from './Toast.js'; // Already imported\n\nconst handleSpawnResult = useCallback((result) => {\n if (result.success) {\n showToast(`${result.agentType} agent spawned for ${result.project}`, 'success');\n } else if (result.error) {\n showToast(result.error, 'error');\n }\n}, []);\n```\n\n### Add Modal (after ToastContainer, around line 426)\n```javascript\n<${SpawnModal}\n isOpen=${spawnModalOpen}\n onClose=${() => setSpawnModalOpen(false)}\n onSpawn=${handleSpawnResult}\n currentProject=${selectedProject}\n/>\n```\n\n## selectedProject Context\nPer plan note: selectedProject in App.js is already the short project name (e.g., 'amc'), not the full path. This comes from groupSessionsByProject() in status.js which uses projectName as the key.\n\nWhen selectedProject is:\n- A project name (e.g., 'amc'): Modal shows agent type only\n- null (All Projects tab): Modal shows project dropdown\n\n## Button Styling\n- border-active/40: Subtle active color border\n- bg-active/12: Light active background\n- text-active: Active color text\n- hover:bg-active/20: Brighter on hover\n\nThis matches the dashboard's existing accent button style.\n\n## Toast Integration\nUses existing showToast(message, type, duration) from Toast.js:\n- 'success' type: Green checkmark\n- 'error' type: Red exclamation\n\n## Acceptance Criteria\n- AC-1: Button in page header\n- AC-20: Toast for spawn errors\n- AC-21: Toast for spawn success\n\n## Verification\n1. Load dashboard\n2. Verify button visible in header\n3. Click button -> modal opens\n4. Spawn agent -> success toast\n5. Trigger error -> error toast\n\n## Success Criteria\n- Button styled consistently with dashboard\n- Modal opens on click\n- Toast shows on spawn result\n- selectedProject passed correctly to modal","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T21:41:53.399210Z","created_by":"tayloreernisse","updated_at":"2026-02-26T22:03:08.708332Z","closed_at":"2026-02-26T22:03:08.708145Z","close_reason":"Integrated SpawnModal into App.js: added import, state, header button, spawn result handler with toast notifications, and modal rendering with currentProject passthrough","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3v5","depends_on_id":"bd-15z","type":"blocks","created_at":"2026-02-26T21:41:57.064242Z","created_by":"tayloreernisse"}]}
{"id":"bd-3vd","title":"Implement keyboard navigation for autocomplete","description":"## Overview\nAdd onKeyDown handler in SimpleInput.js for arrow key navigation, Enter/Tab selection, and Escape dismissal.\n\n## Background\nAC-7, AC-8, AC-9, AC-10 require keyboard control of the autocomplete dropdown. This must work WITHOUT interfering with normal textarea behavior when dropdown is closed.\n\n## Implementation (from plan IMP-7)\n```javascript\nonKeyDown=${(e) => {\n if (showAutocomplete) {\n // Always handle Escape when dropdown is open\n if (e.key === \"Escape\") {\n e.preventDefault();\n setShowAutocomplete(false);\n return;\n }\n \n // Handle Enter/Tab: select if matches exist, otherwise dismiss\n if (e.key === \"Enter\" || e.key === \"Tab\") {\n e.preventDefault();\n if (filteredSkills.length > 0 && filteredSkills[selectedIndex]) {\n insertSkill(filteredSkills[selectedIndex]);\n } else {\n setShowAutocomplete(false);\n }\n return;\n }\n \n // Arrow navigation only when there are matches\n if (filteredSkills.length > 0) {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setSelectedIndex(i => Math.min(i + 1, filteredSkills.length - 1));\n return;\n }\n if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setSelectedIndex(i => Math.max(i - 1, 0));\n return;\n }\n }\n \n // AC-10: ArrowLeft/ArrowRight are NOT handled here.\n // They fall through to default behavior but since we only call\n // getTriggerInfo on input events, cursor movement does not\n // reposition the dropdown. The dropdown stays anchored to the\n // original trigger position.\n }\n \n // Existing Enter-to-submit logic (only when dropdown is closed)\n if (e.key === \"Enter\" && \\!e.shiftKey) {\n e.preventDefault();\n handleSubmit(e);\n }\n}}\n```\n\n## Key Behaviors\n- **AC-7**: ArrowUp/Down navigates highlighted option\n- **AC-8**: Enter or Tab inserts selected skill\n- **AC-9**: Escape dismisses without insertion\n- **AC-10**: ArrowLeft/Right fall through - dropdown position locked to trigger\n- **Enter with no matches**: Dismisses dropdown (does NOT submit message\\!)\n\n## Why ArrowLeft/Right Are Ignored (AC-10)\nThe plan specifies that cursor movement should not reposition the dropdown. Since we only recalculate trigger position on `onInput` events (not cursor movement), ArrowLeft/Right naturally do not affect the dropdown. No special handling needed - just document this behavior.\n\n## Success Criteria\n- ArrowUp/Down changes selectedIndex\n- Enter/Tab inserts selected skill or dismisses if no matches\n- Escape always dismisses\n- ArrowLeft/Right do not interfere with dropdown\n- Normal Enter-to-submit works when dropdown closed","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T20:09:59.805771Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:50:41.883400Z","closed_at":"2026-02-26T21:50:41.883213Z","close_reason":"Added insertSkill callback and full keyboard navigation (ArrowUp/Down, Enter/Tab, Escape)","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3vd","depends_on_id":"bd-1y3","type":"blocks","created_at":"2026-02-26T20:10:03.108299Z","created_by":"tayloreernisse"}]}
{"id":"bd-4lc","title":"Implement scroll-into-view for selected item","description":"## Overview\nEnsure the highlighted item in the autocomplete dropdown scrolls into view when navigating with arrow keys.\n\n## Background\nFrom plan Slice 5 (Polish): 'Scroll into view for long lists'. When the user arrows down through a list longer than max-height, the selected item should remain visible.\n\n## Implementation\n```javascript\n// In the keyboard navigation effect or handler\nuseEffect(() => {\n if (showAutocomplete && autocompleteRef.current) {\n const container = autocompleteRef.current;\n const selectedEl = container.children[selectedIndex];\n if (selectedEl) {\n selectedEl.scrollIntoView({ block: 'nearest' });\n }\n }\n}, [selectedIndex, showAutocomplete]);\n```\n\n## Key Details\n- block: 'nearest' prevents jarring scroll when item already visible\n- Only scroll when autocomplete is open\n- children[selectedIndex] assumes direct children are the items\n\n## Alternative: scrollIntoViewIfNeeded\nSome browsers support scrollIntoViewIfNeeded, but scrollIntoView with block: 'nearest' is more widely supported and achieves the same effect.\n\n## Success Criteria\n- Arrowing to item below viewport scrolls it into view\n- Arrowing to item above viewport scrolls it into view\n- Items in viewport don't cause scroll","status":"closed","priority":3,"issue_type":"task","created_at":"2026-02-26T20:12:12.819926Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:55:32.378377Z","closed_at":"2026-02-26T21:55:32.378207Z","close_reason":"Added useEffect with scrollIntoView block:nearest for arrow key navigation","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-4lc","depends_on_id":"bd-253","type":"blocks","created_at":"2026-02-26T20:12:15.489198Z","created_by":"tayloreernisse"}]}
-{"id":"bd-5m4","title":"Implement SpawnMixin with spawn validation and execution","description":"## Overview\nCreate amc_server/mixins/spawn.py containing SpawnMixin class with all spawn-related methods.\n\n## Background\nSpawnMixin handles the core spawning logic: validating requests, executing Zellij commands, and waiting for session confirmation. This is the heart of the spawn feature, responsible for:\n- Security validation (path traversal, symlink escape)\n- Zellij session/tab/pane management\n- Spawn correlation via spawn_id\n- Concurrency control and rate limiting\n\n## File Structure\nCreate new file: amc_server/mixins/spawn.py\n\n## Implementation (IMP-1)\n\n### Module-Level\n```python\nimport json\nimport os\nimport subprocess\nimport time\nimport uuid\n\nfrom amc_server.context import (\n PROJECTS_DIR, SESSIONS_DIR, ZELLIJ_BIN, ZELLIJ_SESSION,\n _spawn_lock, _spawn_timestamps, SPAWN_COOLDOWN_SEC,\n validate_auth_token,\n)\n\n# Agent commands (AC-8, AC-9: full autonomous permissions)\nAGENT_COMMANDS = {\n 'claude': ['claude', '--dangerously-skip-permissions'],\n 'codex': ['codex', '--dangerously-bypass-approvals-and-sandbox'],\n}\n\n# Module-level cache for projects list (AC-33)\n_projects_cache: list[str] = []\n\n\ndef load_projects_cache():\n \"\"\"Scan ~/projects/ and cache the list. Called on server start.\"\"\"\n global _projects_cache\n try:\n projects = []\n for entry in PROJECTS_DIR.iterdir():\n if entry.is_dir() and not entry.name.startswith('.'):\n projects.append(entry.name)\n projects.sort()\n _projects_cache = projects\n except OSError:\n _projects_cache = []\n```\n\n### SpawnMixin Class\nImplement these methods:\n\n1. **_handle_spawn()**: POST /api/spawn handler\n - Verify auth token (AC-38)\n - Parse JSON body\n - Validate params (returns resolved_path to avoid TOCTOU)\n - Generate spawn_id (UUID)\n - Acquire _spawn_lock with 15s timeout\n - Check rate limit inside lock\n - Call _spawn_agent_in_project_tab\n - Update timestamp only on success\n - Return JSON response\n\n2. **_validate_spawn_params(project, agent_type)** -> dict\n - Check project not empty\n - Reject path traversal (/, \\, ..)\n - Resolve symlinks with Path.resolve()\n - Verify resolved path is under PROJECTS_DIR.resolve() (symlink escape check)\n - Verify resolved is directory\n - Verify agent_type in AGENT_COMMANDS\n - Return {'resolved_path': resolved} or {'error': msg, 'code': code}\n\n3. **_check_zellij_session_exists()** -> bool\n - Run zellij list-sessions\n - Parse line-by-line (avoid substring false positives)\n - Return True if ZELLIJ_SESSION found\n\n4. **_wait_for_session_file(spawn_id, timeout=10.0)** -> bool\n - Poll SESSIONS_DIR for .json files\n - Check each file for matching spawn_id\n - 0.25s poll interval\n - Return True if found within timeout\n\n5. **_spawn_agent_in_project_tab(project, project_path, agent_type, spawn_id)** -> dict\n - Check session exists (return SESSION_NOT_FOUND if not)\n - Create/switch to project tab: zellij --session infra action go-to-tab-name --create \n - Spawn pane: zellij --session infra action new-pane --name - --cwd -- \n - Pass AMC_SPAWN_ID via env dict\n - Wait for session file\n - Return {'ok': True} or {'ok': False, 'error': ..., 'code': ...}\n\n6. **_handle_projects()**: GET /api/projects\n - Return cached projects list\n\n7. **_handle_projects_refresh()**: POST /api/projects/refresh\n - Call load_projects_cache()\n - Return refreshed list\n\n8. **_handle_health()**: GET /api/health\n - Check Zellij session availability\n - Return status JSON\n\n## Security Considerations\n- **Path traversal**: Reject /, \\, .. in project name before any path operations\n- **Symlink escape**: resolve() + relative_to() check ensures final path is under PROJECTS_DIR\n- **TOCTOU**: Validation returns resolved path; spawn uses it directly without re-resolving\n- **Auth**: Bearer token required for /api/spawn only\n\n## Concurrency Design\n- **_spawn_lock**: Serializes ALL Zellij operations (tab creation + pane spawn are not atomic)\n- **15s timeout**: Prevents indefinite blocking if lock held by hung thread\n- **Rate limit inside lock**: Prevents race where two requests pass cooldown before either updates timestamp\n\n## Error Codes\n- MISSING_PROJECT, INVALID_PROJECT, PROJECT_NOT_FOUND\n- INVALID_AGENT_TYPE\n- SESSION_NOT_FOUND, TAB_ERROR, SPAWN_ERROR\n- ZELLIJ_NOT_FOUND, TIMEOUT\n- SESSION_FILE_TIMEOUT\n- RATE_LIMITED, SERVER_BUSY, UNAUTHORIZED\n\n## Acceptance Criteria\n- AC-8, AC-9: Full autonomous permissions for agents\n- AC-10, AC-11, AC-12: Zellij tab targeting\n- AC-13, AC-14: Pane cwd and naming\n- AC-18, AC-19: Graceful failure for missing zellij/project\n- AC-22, AC-23: Path security validation\n- AC-26, AC-27, AC-28, AC-29, AC-30: Spawn lifecycle\n\n## Success Criteria\n- All methods callable without error\n- Spawn returns success for valid project\n- Spawn rejects invalid paths\n- Spawn rejects missing Zellij session\n- Rate limiting works per-project","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T21:39:39.941019Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:39:43.382093Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-5m4","depends_on_id":"bd-3oo","type":"blocks","created_at":"2026-02-26T21:39:43.382069Z","created_by":"tayloreernisse"}]}
-{"id":"bd-93q","title":"Unit tests for trigger detection and filtering","description":"## Overview\nAdd unit tests for the client-side autocomplete logic (trigger detection, filtering) in a test file.\n\n## Background\nThe client-side logic is pure functions that can be tested in isolation:\n- getTriggerInfo: detects trigger and extracts filter text\n- filteredSkills: filters and sorts skills list\n\n## Test Cases\n\n### Trigger Detection\n```javascript\ndescribe('getTriggerInfo', () => {\n it('returns null when no autocompleteConfig')\n it('detects trigger at position 0')\n it('detects trigger after space')\n it('detects trigger after newline')\n it('returns null for non-trigger character')\n it('returns null for wrong trigger (/ in codex session)')\n it('extracts filterText correctly')\n it('filterText is lowercase')\n it('replaceStart/replaceEnd are correct')\n})\n```\n\n### Filtered Skills\n```javascript\ndescribe('filteredSkills', () => {\n it('returns empty array without config')\n it('returns all skills with empty filter')\n it('filters case-insensitively')\n it('matches anywhere in name')\n it('sorts alphabetically')\n it('returns empty array when no matches')\n})\n```\n\n## Test Data\n```javascript\nconst mockConfig = {\n trigger: '/',\n skills: [\n { name: 'commit', description: 'Create a git commit' },\n { name: 'review-pr', description: 'Review a pull request' },\n { name: 'comment', description: 'Add a comment' }\n ]\n};\n```\n\n## Implementation Notes\n- Extract getTriggerInfo to a testable module if needed\n- filteredSkills computation can be tested by extracting the logic\n\n## Success Criteria\n- All trigger detection scenarios covered\n- All filtering edge cases tested\n- Tests run in CI","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-26T20:11:31.262928Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:11:34.080388Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-93q","depends_on_id":"bd-29o","type":"blocks","created_at":"2026-02-26T20:11:34.080369Z","created_by":"tayloreernisse"}]}
+{"id":"bd-5m4","title":"Implement SpawnMixin with spawn validation and execution","description":"## Overview\nCreate amc_server/mixins/spawn.py containing SpawnMixin class with all spawn-related methods.\n\n## Background\nSpawnMixin handles the core spawning logic: validating requests, executing Zellij commands, and waiting for session confirmation. This is the heart of the spawn feature, responsible for:\n- Security validation (path traversal, symlink escape)\n- Zellij session/tab/pane management\n- Spawn correlation via spawn_id\n- Concurrency control and rate limiting\n\n## File Structure\nCreate new file: amc_server/mixins/spawn.py\n\n## Implementation (IMP-1)\n\n### Module-Level\n```python\nimport json\nimport os\nimport subprocess\nimport time\nimport uuid\n\nfrom amc_server.context import (\n PROJECTS_DIR, SESSIONS_DIR, ZELLIJ_BIN, ZELLIJ_SESSION,\n _spawn_lock, _spawn_timestamps, SPAWN_COOLDOWN_SEC,\n validate_auth_token,\n)\n\n# Agent commands (AC-8, AC-9: full autonomous permissions)\nAGENT_COMMANDS = {\n 'claude': ['claude', '--dangerously-skip-permissions'],\n 'codex': ['codex', '--dangerously-bypass-approvals-and-sandbox'],\n}\n\n# Module-level cache for projects list (AC-33)\n_projects_cache: list[str] = []\n\n\ndef load_projects_cache():\n \"\"\"Scan ~/projects/ and cache the list. Called on server start.\"\"\"\n global _projects_cache\n try:\n projects = []\n for entry in PROJECTS_DIR.iterdir():\n if entry.is_dir() and not entry.name.startswith('.'):\n projects.append(entry.name)\n projects.sort()\n _projects_cache = projects\n except OSError:\n _projects_cache = []\n```\n\n### SpawnMixin Class\nImplement these methods:\n\n1. **_handle_spawn()**: POST /api/spawn handler\n - Verify auth token (AC-38)\n - Parse JSON body\n - Validate params (returns resolved_path to avoid TOCTOU)\n - Generate spawn_id (UUID)\n - Acquire _spawn_lock with 15s timeout\n - Check rate limit inside lock\n - Call _spawn_agent_in_project_tab\n - Update timestamp only on success\n - Return JSON response\n\n2. **_validate_spawn_params(project, agent_type)** -> dict\n - Check project not empty\n - Reject path traversal (/, \\, ..)\n - Resolve symlinks with Path.resolve()\n - Verify resolved path is under PROJECTS_DIR.resolve() (symlink escape check)\n - Verify resolved is directory\n - Verify agent_type in AGENT_COMMANDS\n - Return {'resolved_path': resolved} or {'error': msg, 'code': code}\n\n3. **_check_zellij_session_exists()** -> bool\n - Run zellij list-sessions\n - Parse line-by-line (avoid substring false positives)\n - Return True if ZELLIJ_SESSION found\n\n4. **_wait_for_session_file(spawn_id, timeout=10.0)** -> bool\n - Poll SESSIONS_DIR for .json files\n - Check each file for matching spawn_id\n - 0.25s poll interval\n - Return True if found within timeout\n\n5. **_spawn_agent_in_project_tab(project, project_path, agent_type, spawn_id)** -> dict\n - Check session exists (return SESSION_NOT_FOUND if not)\n - Create/switch to project tab: zellij --session infra action go-to-tab-name --create \n - Spawn pane: zellij --session infra action new-pane --name - --cwd -- \n - Pass AMC_SPAWN_ID via env dict\n - Wait for session file\n - Return {'ok': True} or {'ok': False, 'error': ..., 'code': ...}\n\n6. **_handle_projects()**: GET /api/projects\n - Return cached projects list\n\n7. **_handle_projects_refresh()**: POST /api/projects/refresh\n - Call load_projects_cache()\n - Return refreshed list\n\n8. **_handle_health()**: GET /api/health\n - Check Zellij session availability\n - Return status JSON\n\n## Security Considerations\n- **Path traversal**: Reject /, \\, .. in project name before any path operations\n- **Symlink escape**: resolve() + relative_to() check ensures final path is under PROJECTS_DIR\n- **TOCTOU**: Validation returns resolved path; spawn uses it directly without re-resolving\n- **Auth**: Bearer token required for /api/spawn only\n\n## Concurrency Design\n- **_spawn_lock**: Serializes ALL Zellij operations (tab creation + pane spawn are not atomic)\n- **15s timeout**: Prevents indefinite blocking if lock held by hung thread\n- **Rate limit inside lock**: Prevents race where two requests pass cooldown before either updates timestamp\n\n## Error Codes\n- MISSING_PROJECT, INVALID_PROJECT, PROJECT_NOT_FOUND\n- INVALID_AGENT_TYPE\n- SESSION_NOT_FOUND, TAB_ERROR, SPAWN_ERROR\n- ZELLIJ_NOT_FOUND, TIMEOUT\n- SESSION_FILE_TIMEOUT\n- RATE_LIMITED, SERVER_BUSY, UNAUTHORIZED\n\n## Acceptance Criteria\n- AC-8, AC-9: Full autonomous permissions for agents\n- AC-10, AC-11, AC-12: Zellij tab targeting\n- AC-13, AC-14: Pane cwd and naming\n- AC-18, AC-19: Graceful failure for missing zellij/project\n- AC-22, AC-23: Path security validation\n- AC-26, AC-27, AC-28, AC-29, AC-30: Spawn lifecycle\n\n## Success Criteria\n- All methods callable without error\n- Spawn returns success for valid project\n- Spawn rejects invalid paths\n- Spawn rejects missing Zellij session\n- Rate limiting works per-project","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T21:39:39.941019Z","created_by":"tayloreernisse","updated_at":"2026-02-26T22:00:42.778998Z","closed_at":"2026-02-26T22:00:42.778950Z","close_reason":"Implemented SpawnMixin with all required methods: _handle_spawn, _validate_spawn_params, _check_zellij_session_exists, _wait_for_session_file, _spawn_agent_in_project_tab, _handle_projects, _handle_projects_refresh, _handle_health, and load_projects_cache","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-5m4","depends_on_id":"bd-3oo","type":"blocks","created_at":"2026-02-26T21:39:43.382069Z","created_by":"tayloreernisse"}]}
+{"id":"bd-93q","title":"Unit tests for trigger detection and filtering","description":"## Overview\nAdd unit tests for the client-side autocomplete logic (trigger detection, filtering) in a test file.\n\n## Background\nThe client-side logic is pure functions that can be tested in isolation:\n- getTriggerInfo: detects trigger and extracts filter text\n- filteredSkills: filters and sorts skills list\n\n## Test Cases\n\n### Trigger Detection\n```javascript\ndescribe('getTriggerInfo', () => {\n it('returns null when no autocompleteConfig')\n it('detects trigger at position 0')\n it('detects trigger after space')\n it('detects trigger after newline')\n it('returns null for non-trigger character')\n it('returns null for wrong trigger (/ in codex session)')\n it('extracts filterText correctly')\n it('filterText is lowercase')\n it('replaceStart/replaceEnd are correct')\n})\n```\n\n### Filtered Skills\n```javascript\ndescribe('filteredSkills', () => {\n it('returns empty array without config')\n it('returns all skills with empty filter')\n it('filters case-insensitively')\n it('matches anywhere in name')\n it('sorts alphabetically')\n it('returns empty array when no matches')\n})\n```\n\n## Test Data\n```javascript\nconst mockConfig = {\n trigger: '/',\n skills: [\n { name: 'commit', description: 'Create a git commit' },\n { name: 'review-pr', description: 'Review a pull request' },\n { name: 'comment', description: 'Add a comment' }\n ]\n};\n```\n\n## Implementation Notes\n- Extract getTriggerInfo to a testable module if needed\n- filteredSkills computation can be tested by extracting the logic\n\n## Success Criteria\n- All trigger detection scenarios covered\n- All filtering edge cases tested\n- Tests run in CI","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-26T20:11:31.262928Z","created_by":"tayloreernisse","updated_at":"2026-02-26T22:00:14.254354Z","closed_at":"2026-02-26T22:00:14.254309Z","close_reason":"Implemented: extracted getTriggerInfo and filteredSkills to dashboard/utils/autocomplete.js, added 21 tests covering all trigger detection and filtering scenarios","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-93q","depends_on_id":"bd-29o","type":"blocks","created_at":"2026-02-26T20:11:34.080369Z","created_by":"tayloreernisse"}]}
{"id":"bd-g9t","title":"Implement _serve_skills endpoint handler","description":"## Overview\nImplement _serve_skills(agent) method in SkillsMixin that serves the skills API response.\n\n## Background\nThis is the main entry point called by the HTTP route. It determines the agent type, calls the appropriate enumeration method, and returns a properly formatted JSON response.\n\n## Implementation (from plan IMP-1)\n```python\ndef _serve_skills(self, agent):\n \"\"\"Return autocomplete config for an agent.\"\"\"\n if agent == 'codex':\n trigger = '$'\n skills = self._enumerate_codex_skills()\n else: # claude (default)\n trigger = '/'\n skills = self._enumerate_claude_skills()\n \n # Sort alphabetically\n skills.sort(key=lambda s: s['name'].lower())\n \n self._send_json(200, {'trigger': trigger, 'skills': skills})\n```\n\n## Response Format\n```json\n{\n \"trigger\": \"/\",\n \"skills\": [\n { \"name\": \"commit\", \"description\": \"Create a git commit with a message\" },\n { \"name\": \"review-pr\", \"description\": \"Review a pull request\" }\n ]\n}\n```\n\n## Key Decisions\n- **Claude is default**: Unknown agents get Claude behavior (fail-safe)\n- **Sorting is server-side**: Client receives pre-sorted list (AC-5)\n- **Uses _send_json**: Follows existing pattern from HttpMixin\n\n## Success Criteria\n- Returns correct trigger character per agent\n- Skills are alphabetically sorted (case-insensitive)\n- Unknown agents default to Claude\n- Valid JSON response with 200 status","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T20:08:14.837644Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:43:40.277309Z","closed_at":"2026-02-26T21:43:40.277265Z","close_reason":"Already implemented in bd-3q1 — _serve_skills correctly handles agent type, sorting, and JSON response","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-g9t","depends_on_id":"bd-3eu","type":"blocks","created_at":"2026-02-26T20:08:18.595535Z","created_by":"tayloreernisse"},{"issue_id":"bd-g9t","depends_on_id":"bd-sv1","type":"blocks","created_at":"2026-02-26T20:08:18.574532Z","created_by":"tayloreernisse"}]}
-{"id":"bd-mjo","title":"Add API constants for spawn endpoints in dashboard","description":"## Overview\nAdd spawn-related API constants to dashboard/utils/api.js following existing patterns.\n\n## Background\nThe dashboard follows a pattern of defining API endpoints as constants in api.js. This enables:\n- Centralized endpoint management\n- Easy updates if paths change\n- Consistent usage across components\n\n## Implementation (IMP-2c)\nAdd to existing exports in dashboard/utils/api.js:\n\n```javascript\n// Spawn API endpoints\nexport const API_SPAWN = '/api/spawn';\nexport const API_PROJECTS = '/api/projects';\nexport const API_PROJECTS_REFRESH = '/api/projects/refresh';\nexport const API_HEALTH = '/api/health';\n```\n\n## Existing Pattern\nThe file already exports similar constants:\n```javascript\nexport const API_SESSIONS = '/api/sessions';\nexport const API_RESPOND = '/api/respond';\n// etc.\n```\n\n## Why Constants (Not Inline Strings)\n1. Single source of truth for endpoint paths\n2. IDE autocomplete and find-references\n3. Easy to grep for all API usage\n4. Refactoring-friendly\n\n## Success Criteria\n- Constants exported from api.js\n- No typos in paths\n- Consistent with existing constants","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T21:41:03.515881Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:41:03.515881Z","compaction_level":0,"original_size":0}
+{"id":"bd-mjo","title":"Add API constants for spawn endpoints in dashboard","description":"## Overview\nAdd spawn-related API constants to dashboard/utils/api.js following existing patterns.\n\n## Background\nThe dashboard follows a pattern of defining API endpoints as constants in api.js. This enables:\n- Centralized endpoint management\n- Easy updates if paths change\n- Consistent usage across components\n\n## Implementation (IMP-2c)\nAdd to existing exports in dashboard/utils/api.js:\n\n```javascript\n// Spawn API endpoints\nexport const API_SPAWN = '/api/spawn';\nexport const API_PROJECTS = '/api/projects';\nexport const API_PROJECTS_REFRESH = '/api/projects/refresh';\nexport const API_HEALTH = '/api/health';\n```\n\n## Existing Pattern\nThe file already exports similar constants:\n```javascript\nexport const API_SESSIONS = '/api/sessions';\nexport const API_RESPOND = '/api/respond';\n// etc.\n```\n\n## Why Constants (Not Inline Strings)\n1. Single source of truth for endpoint paths\n2. IDE autocomplete and find-references\n3. Easy to grep for all API usage\n4. Refactoring-friendly\n\n## Success Criteria\n- Constants exported from api.js\n- No typos in paths\n- Consistent with existing constants","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T21:41:03.515881Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:57:53.036404Z","closed_at":"2026-02-26T21:57:53.036359Z","close_reason":"Added API_SPAWN, API_PROJECTS, API_PROJECTS_REFRESH, API_HEALTH constants to dashboard/utils/api.js","compaction_level":0,"original_size":0}
{"id":"bd-sv1","title":"Implement Claude skill enumeration","description":"## Overview\nImplement _enumerate_claude_skills() method in SkillsMixin to discover skills from ~/.claude/skills/.\n\n## Background\nClaude Code stores user skills as directories under ~/.claude/skills/. Each skill directory contains a SKILL.md (canonical casing) with the skill definition. We need to enumerate these and extract descriptions.\n\n## Implementation (from plan IMP-1)\n```python\ndef _enumerate_claude_skills(self):\n skills = []\n skills_dir = Path.home() / '.claude/skills'\n \n if skills_dir.exists():\n for skill_dir in skills_dir.iterdir():\n if skill_dir.is_dir() and not skill_dir.name.startswith('.'):\n description = ''\n # Check files in priority order\n for md_name in ['SKILL.md', 'skill.md', 'prompt.md', 'README.md']:\n md_file = skill_dir / md_name\n if md_file.exists():\n try:\n content = md_file.read_text()\n for line in content.splitlines():\n line = line.strip()\n if line and not line.startswith('#') and not line.startswith('