{"id":"bd-11y","title":"Unit tests for SpawnMixin validation","description":"## Overview\nCreate tests/test_spawn.py with comprehensive tests for SpawnMixin validation logic.\n\n## Background\nThe spawn validation is security-critical. Tests must cover:\n- Path traversal attacks\n- Symlink escape attempts\n- Invalid project names\n- Invalid agent types\n- Rate limiting behavior\n- Auth token validation\n\n## Test Structure\n```python\nimport pytest\nfrom pathlib import Path\nfrom unittest.mock import patch, MagicMock\n\nfrom amc_server.mixins.spawn import SpawnMixin, load_projects_cache, _projects_cache\n\n\nclass TestValidateSpawnParams:\n \"\"\"Tests for _validate_spawn_params security validation.\"\"\"\n\n def test_missing_project_returns_error(self):\n mixin = SpawnMixin()\n result = mixin._validate_spawn_params('', 'claude')\n assert result == {'error': 'project is required', 'code': 'MISSING_PROJECT'}\n\n def test_path_traversal_slash_rejected(self):\n mixin = SpawnMixin()\n result = mixin._validate_spawn_params('../etc', 'claude')\n assert result['code'] == 'INVALID_PROJECT'\n\n def test_path_traversal_dotdot_rejected(self):\n mixin = SpawnMixin()\n result = mixin._validate_spawn_params('foo..bar', 'claude')\n # Should be rejected because '..' in name\n assert result['code'] == 'INVALID_PROJECT'\n\n def test_backslash_rejected(self):\n mixin = SpawnMixin()\n result = mixin._validate_spawn_params('foo\\\\bar', 'claude')\n assert result['code'] == 'INVALID_PROJECT'\n\n @patch('amc_server.mixins.spawn.PROJECTS_DIR')\n def test_symlink_escape_rejected(self, mock_projects_dir):\n \"\"\"Symlink pointing outside projects dir should be rejected.\"\"\"\n # Setup: create mock that resolves to /etc/passwd\n mock_projects_dir.resolve.return_value = Path('/home/user/projects')\n # ... test symlink that escapes\n\n @patch('amc_server.mixins.spawn.PROJECTS_DIR')\n def test_nonexistent_project_rejected(self, mock_projects_dir):\n mock_projects_dir.__truediv__.return_value.resolve.side_effect = OSError\n mixin = SpawnMixin()\n result = mixin._validate_spawn_params('nonexistent', 'claude')\n assert result['code'] == 'PROJECT_NOT_FOUND'\n\n def test_invalid_agent_type_rejected(self):\n mixin = SpawnMixin()\n # Assume valid project exists\n with patch.object(mixin, '_validate_spawn_params') as mock:\n # Test that invalid agent type is caught\n pass\n\n @patch('amc_server.mixins.spawn.PROJECTS_DIR')\n def test_valid_project_returns_resolved_path(self, mock_projects_dir):\n \"\"\"Valid project returns resolved path for TOCTOU prevention.\"\"\"\n resolved = Path('/home/user/projects/amc')\n mock_projects_dir.resolve.return_value = Path('/home/user/projects')\n mock_projects_dir.__truediv__.return_value.resolve.return_value = resolved\n mock_projects_dir.__truediv__.return_value.resolve.return_value.is_dir.return_value = True\n # ... verify resolved_path in result\n\n\nclass TestProjectsCache:\n \"\"\"Tests for projects cache loading.\"\"\"\n\n @patch('amc_server.mixins.spawn.PROJECTS_DIR')\n def test_load_excludes_hidden_directories(self, mock_projects_dir):\n \"\"\"Hidden directories (.git, etc) should be excluded.\"\"\"\n pass\n\n @patch('amc_server.mixins.spawn.PROJECTS_DIR')\n def test_load_only_includes_directories(self, mock_projects_dir):\n \"\"\"Files should be excluded, only directories listed.\"\"\"\n pass\n\n @patch('amc_server.mixins.spawn.PROJECTS_DIR')\n def test_load_handles_oserror_gracefully(self, mock_projects_dir):\n \"\"\"OSError should result in empty cache, not crash.\"\"\"\n mock_projects_dir.iterdir.side_effect = OSError\n load_projects_cache()\n assert _projects_cache == []\n\n\nclass TestRateLimiting:\n \"\"\"Tests for per-project rate limiting.\"\"\"\n\n def test_first_spawn_allowed(self):\n pass\n\n def test_rapid_spawn_same_project_rejected(self):\n pass\n\n def test_spawn_different_project_allowed(self):\n pass\n\n def test_spawn_after_cooldown_allowed(self):\n pass\n\n\nclass TestAuthToken:\n \"\"\"Tests for auth token validation.\"\"\"\n\n def test_valid_bearer_token_accepted(self):\n pass\n\n def test_missing_auth_header_rejected(self):\n pass\n\n def test_wrong_token_rejected(self):\n pass\n\n def test_malformed_bearer_rejected(self):\n pass\n```\n\n## Security Test Coverage\nMust test these attack vectors:\n1. Path traversal: ../, ..\\, /../\n2. Symlink escape: symlink -> /etc/passwd\n3. Unicode attacks: project names with special chars\n4. Empty/whitespace project names\n5. Very long project names\n\n## Mocking Strategy\n- PROJECTS_DIR: Mock to test path validation without real filesystem\n- subprocess.run: Mock Zellij commands\n- time.time: Mock for rate limiting tests\n- _spawn_lock: Verify lock behavior\n\n## Success Criteria\n- All validation paths tested\n- All error codes verified\n- Security vectors covered\n- No test pollution (cleanup mocks)","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T21:42:21.597651Z","created_by":"tayloreernisse","updated_at":"2026-02-26T22:04:07.723410Z","closed_at":"2026-02-26T22:04:07.723357Z","close_reason":"Implemented 36 tests across 8 test classes covering: validation (path traversal, symlink escape, agent types), projects cache, rate limiting, auth tokens, JSON parsing, lock contention, projects/health endpoints","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-11y","depends_on_id":"bd-5m4","type":"blocks","created_at":"2026-02-26T21:42:24.992165Z","created_by":"tayloreernisse"}]} {"id":"bd-14p","title":"Test and handle special characters in project names","description":"## Overview\nVerify spawn works correctly with projects that have special characters in their names.\n\n## Background\nProject names may contain:\n- Hyphens: my-project\n- Underscores: my_project\n- Numbers: project2\n- Spaces: my project (though uncommon)\n- Unicode: 日本語project\n\n## Current Validation\n_validate_spawn_params() rejects:\n- / (path separator)\n- \\ (Windows path separator)\n- .. (path traversal)\n\nOther characters are allowed as long as the resolved path exists and is under PROJECTS_DIR.\n\n## Testing Required\n\n### Safe Characters (should work)\n```bash\n# Hyphen\ncurl -X POST http://localhost:7400/api/spawn \\\n -d '{\"project\":\"my-project\",\"agent_type\":\"claude\"}'\n\n# Underscore\ncurl -X POST http://localhost:7400/api/spawn \\\n -d '{\"project\":\"my_project\",\"agent_type\":\"claude\"}'\n\n# Numbers\ncurl -X POST http://localhost:7400/api/spawn \\\n -d '{\"project\":\"project2\",\"agent_type\":\"claude\"}'\n```\n\n### Potentially Problematic (test carefully)\n```bash\n# Space (if project exists)\ncurl -X POST http://localhost:7400/api/spawn \\\n -d '{\"project\":\"my project\",\"agent_type\":\"claude\"}'\n\n# Unicode\ncurl -X POST http://localhost:7400/api/spawn \\\n -d '{\"project\":\"日本語\",\"agent_type\":\"claude\"}'\n```\n\n### Should Fail\n```bash\n# Path traversal\ncurl -X POST http://localhost:7400/api/spawn \\\n -d '{\"project\":\"../etc\",\"agent_type\":\"claude\"}'\n\n# Slash in name\ncurl -X POST http://localhost:7400/api/spawn \\\n -d '{\"project\":\"foo/bar\",\"agent_type\":\"claude\"}'\n```\n\n## Potential Issues\n1. **Zellij tab names**: May have restrictions on tab naming\n2. **Shell escaping**: Pane commands may need escaping\n3. **Filesystem encoding**: Unicode paths on different filesystems\n\n## Implementation\nIf issues found:\n1. Add character filtering to validation\n2. Sanitize tab/pane names for Zellij\n3. Document supported characters\n\n## Test Plan\nCreate test projects with various names:\n```bash\nmkdir -p ~/projects/{test-hyphen,test_underscore,test123,'test space','test日本語'}\n# Run spawn tests for each\n```\n\n## Success Criteria\n- Hyphen, underscore, number projects work\n- Space projects work if they exist\n- Unsupported characters fail gracefully with clear error\n- No shell injection possible","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-26T21:43:48.133328Z","created_by":"tayloreernisse","updated_at":"2026-02-26T22:09:42.715424Z","closed_at":"2026-02-26T22:09:42.715379Z","close_reason":"Implemented and tested: reject control chars/null bytes in validation, sanitize Zellij pane names (strip quotes/backticks/control chars, truncate to 64), added 44 new tests covering safe chars, dangerous chars, shell metacharacters, pane name sanitization, and spawn command construction","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-14p","depends_on_id":"bd-5m4","type":"blocks","created_at":"2026-02-26T21:43:51.288324Z","created_by":"tayloreernisse"}]} {"id":"bd-15z","title":"Implement SpawnModal component","description":"## Overview\nCreate dashboard/components/SpawnModal.js - a modal for spawning new agents.\n\n## Background\nThe SpawnModal is context-aware:\n- When on a specific project tab: shows only agent type selector\n- When on 'All Projects' tab: shows project dropdown + agent type selector\n\nThis enables two workflows from the plan:\n1. Quick spawn from project tab (no project selection needed)\n2. Spawn for any project from All Projects view\n\n## Component Structure\n```javascript\nimport { html, useState, useEffect, useCallback } from '../lib/preact.js';\nimport { API_PROJECTS, API_SPAWN, fetchWithTimeout } from '../utils/api.js';\n\nexport function SpawnModal({ isOpen, onClose, onSpawn, currentProject }) {\n // State\n const [projects, setProjects] = useState([]);\n const [selectedProject, setSelectedProject] = useState('');\n const [agentType, setAgentType] = useState('claude');\n const [loading, setLoading] = useState(false);\n const [loadingProjects, setLoadingProjects] = useState(false);\n const [closing, setClosing] = useState(false);\n const [error, setError] = useState(null);\n\n const needsProjectPicker = \\!currentProject;\n // ...\n}\n```\n\n## Implementation (IMP-4)\n\n### Props\n- **isOpen**: boolean - whether modal is visible\n- **onClose**: () => void - callback when modal closes\n- **onSpawn**: (result) => void - callback with spawn result\n- **currentProject**: string | null - current project if on project tab, null for All Projects\n\n### State Management\n- projects: array of project names from /api/projects\n- selectedProject: selected project (only used when needsProjectPicker)\n- agentType: 'claude' | 'codex'\n- loading: spawn request in progress\n- loadingProjects: fetching projects list\n- closing: animation state for exit\n- error: error message to display\n\n### Effects\n\n1. **Body scroll lock** (matches Modal.js):\n```javascript\nuseEffect(() => {\n if (\\!isOpen) return;\n document.body.style.overflow = 'hidden';\n return () => { document.body.style.overflow = ''; };\n}, [isOpen]);\n```\n\n2. **Escape key handler**:\n```javascript\nuseEffect(() => {\n if (\\!isOpen) return;\n const handleKeyDown = (e) => {\n if (e.key === 'Escape') handleClose();\n };\n document.addEventListener('keydown', handleKeyDown);\n return () => document.removeEventListener('keydown', handleKeyDown);\n}, [isOpen, handleClose]);\n```\n\n3. **Fetch projects when needed**:\n```javascript\nuseEffect(() => {\n if (isOpen && needsProjectPicker) {\n setLoadingProjects(true);\n fetchWithTimeout(API_PROJECTS)\n .then(r => r.json())\n .then(data => {\n setProjects(data.projects || []);\n setSelectedProject('');\n })\n .catch(err => setError(err.message))\n .finally(() => setLoadingProjects(false));\n }\n}, [isOpen, needsProjectPicker]);\n```\n\n4. **Reset state on open**:\n```javascript\nuseEffect(() => {\n if (isOpen) {\n setAgentType('claude');\n setError(null);\n setLoading(false);\n setClosing(false);\n }\n}, [isOpen]);\n```\n\n### Animated Close\n```javascript\nconst handleClose = useCallback(() => {\n if (loading) return;\n setClosing(true);\n setTimeout(() => {\n setClosing(false);\n onClose();\n }, 200);\n}, [loading, onClose]);\n```\n\n### Spawn Handler\n```javascript\nconst handleSpawn = async () => {\n const project = currentProject || selectedProject;\n if (\\!project) {\n setError('Please select a project');\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const response = await fetchWithTimeout(API_SPAWN, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${window.AMC_AUTH_TOKEN}`,\n },\n body: JSON.stringify({ project, agent_type: agentType })\n });\n const data = await response.json();\n\n if (data.ok) {\n onSpawn({ success: true, project, agentType });\n handleClose();\n } else {\n setError(data.error || 'Spawn failed');\n onSpawn({ error: data.error });\n }\n } catch (err) {\n const msg = err.name === 'AbortError' ? 'Request timed out' : err.message;\n setError(msg);\n onSpawn({ error: msg });\n } finally {\n setLoading(false);\n }\n};\n```\n\n### Render\nSee plan IMP-4 for full JSX structure. Key elements:\n- Backdrop with click-outside dismiss\n- Glass panel with animations\n- Project dropdown (conditional)\n- Agent type toggle buttons\n- Error display\n- Cancel/Spawn buttons\n\n## Styling Classes\nUses existing Tailwind classes from dashboard:\n- glass-panel: background blur effect\n- modal-backdrop-in/out: fade animations\n- modal-panel-in/out: slide animations\n- text-bright, text-dim: color hierarchy\n- border-active, bg-active: selection states\n\n## Auth Token (AC-38)\nUses window.AMC_AUTH_TOKEN injected by server:\n```javascript\n'Authorization': `Bearer ${window.AMC_AUTH_TOKEN}`\n```\n\n## Acceptance Criteria\n- AC-2: Project picker hidden when on project tab\n- AC-3: Project picker shown when on All Projects\n- AC-6: No default selection for project dropdown\n- AC-7: Agent type selector (Claude/Codex)\n- AC-20: Error display in modal\n- AC-25: Spawn button disabled while loading\n- AC-31: Dismiss via Escape, click-outside, Cancel\n- AC-32: Loading state for projects dropdown\n\n## Success Criteria\n- Modal opens/closes with animations\n- Project dropdown loads projects\n- Agent type toggle works\n- Spawn request sends correct payload\n- Auth token included in request\n- Errors display clearly\n- All dismiss methods work","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T21:41:30.654681Z","created_by":"tayloreernisse","updated_at":"2026-02-26T22:00:47.721958Z","closed_at":"2026-02-26T22:00:47.721907Z","close_reason":"Implemented SpawnModal component with project picker, agent type selector, spawn handler, animated close, escape/click-outside dismiss, loading states, and error feedback","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-15z","depends_on_id":"bd-mjo","type":"blocks","created_at":"2026-02-26T21:41:34.006897Z","created_by":"tayloreernisse"}]} {"id":"bd-196","title":"Update server.py startup for spawn feature","description":"## Overview\nModify amc_server/server.py to initialize spawn-related functionality on startup.\n\n## Background\nThe spawn feature requires several initialization steps at server start:\n1. Load projects cache so /api/projects responds immediately\n2. Generate auth token for spawn endpoint security\n3. Start background thread for periodic projects cache refresh\n\n## Implementation (IMP-2b)\nAdd to server initialization, before starting HTTP server:\n\n```python\nfrom amc_server.mixins.spawn import load_projects_cache\nfrom amc_server.context import generate_auth_token, start_projects_watcher\n\n# In server startup, before starting HTTP server:\nload_projects_cache()\nauth_token = generate_auth_token() # AC-37: Generate one-time token\nstart_projects_watcher() # AC-40: Auto-refresh every 5 minutes\n# Token is injected into dashboard HTML via template variable\n```\n\n## Initialization Order\n1. load_projects_cache() - Populate cache before any requests\n2. generate_auth_token() - Create token before dashboard serves\n3. start_projects_watcher() - Start background refresh\n\n## Auth Token Injection (IMP-2d)\nThe auth token must be injected into dashboard HTML for JavaScript access:\n\nIn HTTP mixin's dashboard serving code:\n```python\n# When serving index.html:\nfrom amc_server.context import _auth_token\nhtml_content = html_content.replace(\n '',\n f''\n)\n```\n\nAdd placeholder in dashboard/index.html
:\n```html\n\n```\n\n## Why Inline Script\nThe auth token changes each server restart, so it must be injected at runtime. An inline script sets window.AMC_AUTH_TOKEN for JavaScript access. This is acceptable for localhost dev tool; secure cookie alternative is more complex.\n\n## Acceptance Criteria\n- AC-33: Projects cache loaded on start\n- AC-37: Auth token generated on start\n- AC-40: Background watcher started\n\n## Verification\n1. Start server\n2. Check logs for cache load message\n3. Check /api/projects returns immediately\n4. Check /api/health returns status\n5. View page source to verify AMC_AUTH_TOKEN script tag\n\n## Success Criteria\n- Server starts without error\n- Projects cache populated before first request\n- Auth token available in dashboard\n- Background watcher running","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T21:40:49.453041Z","created_by":"tayloreernisse","updated_at":"2026-02-26T22:03:11.090299Z","closed_at":"2026-02-26T22:03:11.090252Z","close_reason":"Implemented server.py startup: load_projects_cache, generate_auth_token, start_projects_watcher; auth token injected into dashboard HTML via placeholder replacement","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-196","depends_on_id":"bd-3oo","type":"blocks","created_at":"2026-02-26T21:40:54.281501Z","created_by":"tayloreernisse"},{"issue_id":"bd-196","depends_on_id":"bd-5m4","type":"blocks","created_at":"2026-02-26T21:40:54.305954Z","created_by":"tayloreernisse"}]} {"id":"bd-1ba","title":"Add fetchSkills API helper","description":"## Overview\nAdd fetchSkills() function to dashboard/utils/api.js for calling the skills endpoint.\n\n## Background\nThis is the client-side helper that fetches the autocomplete config. It's a simple wrapper around fetch() following the existing API pattern in the file.\n\n## Implementation (from plan IMP-3)\n```javascript\nexport const API_SKILLS = '/api/skills';\n\nexport async function fetchSkills(agent) {\n const url = \\`\\${API_SKILLS}?agent=\\${encodeURIComponent(agent)}\\`;\n const response = await fetch(url);\n if (\\!response.ok) return null;\n return response.json();\n}\n```\n\n## Return Type\n```typescript\ntype AutocompleteConfig = {\n trigger: '/' | '$';\n skills: Array<{ name: string; description: string }>;\n}\n```\n\n## Error Handling\n- HTTP error: return null (graceful degradation, no autocomplete)\n- Network failure: returns null via failed fetch\n\n## Why Return null on Error\n- Autocomplete is a convenience feature, not critical\n- Modal should still work without autocomplete\n- Avoids error toasts for non-critical failure\n\n## Success Criteria\n- Function exported from api.js\n- Agent param properly encoded in URL\n- Returns parsed JSON on success\n- Returns null on any failure","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T20:08:42.713976Z","created_by":"tayloreernisse","updated_at":"2026-02-26T21:45:08.245545Z","closed_at":"2026-02-26T21:45:08.245502Z","close_reason":"Added API_SKILLS constant and fetchSkills() helper with error handling","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-1ba","depends_on_id":"bd-30p","type":"blocks","created_at":"2026-02-26T20:08:47.543493Z","created_by":"tayloreernisse"}]} {"id":"bd-1jo","title":"E2E test script for spawn workflow","description":"## Overview\nCreate tests/e2e_spawn.sh - end-to-end test script for the complete spawn workflow.\n\n## Background\nE2E tests verify the full spawn flow from API call to agent appearance in dashboard. This is critical because:\n- Unit tests can't catch integration issues\n- Zellij subprocess behavior is hard to mock\n- Session file correlation must work in practice\n\n## Test Script Structure\n```bash\n#\\!/usr/bin/env bash\nset -euo pipefail\n\n# Configuration\nSERVER_URL=\"http://localhost:7400\"\nTEST_PROJECT=\"amc\" # Must exist in ~/projects/\nAUTH_TOKEN=\"\" # Will be extracted from dashboard HTML\n\n# Colors for output\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nNC='\\033[0m'\n\nlog_info() { echo -e \"${GREEN}[INFO]${NC} $1\"; }\nlog_warn() { echo -e \"${YELLOW}[WARN]${NC} $1\"; }\nlog_error() { echo -e \"${RED}[ERROR]${NC} $1\"; }\nlog_test() { echo -e \"\\n${GREEN}[TEST]${NC} $1\"; }\n\n# Cleanup function\ncleanup() {\n log_info \"Cleaning up...\"\n # Kill any test-spawned agents\n # Remove test session files\n}\ntrap cleanup EXIT\n\n# Pre-flight checks\npreflight() {\n log_test \"Pre-flight checks\"\n \n # Server running?\n if \\! curl -s \"${SERVER_URL}/api/health\" > /dev/null; then\n log_error \"Server not running at ${SERVER_URL}\"\n exit 1\n fi\n \n # Zellij session exists?\n if \\! zellij list-sessions | grep -q \"^infra\"; then\n log_error \"Zellij session 'infra' not found\"\n exit 1\n fi\n \n # Test project exists?\n if [[ \\! -d \"$HOME/projects/${TEST_PROJECT}\" ]]; then\n log_error \"Test project '${TEST_PROJECT}' not found\"\n exit 1\n fi\n \n log_info \"Pre-flight checks passed\"\n}\n\n# Extract auth token from dashboard\nextract_auth_token() {\n log_test \"Extracting auth token\"\n AUTH_TOKEN=$(curl -s \"${SERVER_URL}/\" | grep -o 'AMC_AUTH_TOKEN = \"[^\"]*\"' | cut -d'\"' -f2)\n if [[ -z \"$AUTH_TOKEN\" ]]; then\n log_error \"Could not extract auth token\"\n exit 1\n fi\n log_info \"Auth token extracted: ${AUTH_TOKEN:0:8}...\"\n}\n\n# Test: Projects endpoint\ntest_projects_endpoint() {\n log_test \"GET /api/projects\"\n \n response=$(curl -s \"${SERVER_URL}/api/projects\")\n if \\! echo \"$response\" | jq -e '.projects' > /dev/null; then\n log_error \"Invalid response: $response\"\n return 1\n fi\n \n project_count=$(echo \"$response\" | jq '.projects | length')\n log_info \"Found $project_count projects\"\n}\n\n# Test: Health endpoint\ntest_health_endpoint() {\n log_test \"GET /api/health\"\n \n response=$(curl -s \"${SERVER_URL}/api/health\")\n if \\! echo \"$response\" | jq -e '.ok' > /dev/null; then\n log_error \"Invalid response: $response\"\n return 1\n fi\n \n zellij_available=$(echo \"$response\" | jq -r '.zellij_available')\n log_info \"Zellij available: $zellij_available\"\n}\n\n# Test: Spawn without auth (should fail)\ntest_spawn_no_auth() {\n log_test \"Spawn without auth (should fail)\"\n \n response=$(curl -s -X POST \"${SERVER_URL}/api/spawn\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"project\":\"amc\",\"agent_type\":\"claude\"}')\n \n code=$(echo \"$response\" | jq -r '.code')\n if [[ \"$code\" \\!= \"UNAUTHORIZED\" ]]; then\n log_error \"Expected UNAUTHORIZED, got: $code\"\n return 1\n fi\n log_info \"Correctly rejected unauthorized request\"\n}\n\n# Test: Spawn with invalid project\ntest_spawn_invalid_project() {\n log_test \"Spawn with invalid project\"\n \n response=$(curl -s -X POST \"${SERVER_URL}/api/spawn\" \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer ${AUTH_TOKEN}\" \\\n -d '{\"project\":\"../etc/passwd\",\"agent_type\":\"claude\"}')\n \n code=$(echo \"$response\" | jq -r '.code')\n if [[ \"$code\" \\!= \"INVALID_PROJECT\" ]]; then\n log_error \"Expected INVALID_PROJECT, got: $code\"\n return 1\n fi\n log_info \"Correctly rejected path traversal\"\n}\n\n# Test: Spawn with valid project\ntest_spawn_valid() {\n log_test \"Spawn with valid project\"\n \n # Record session count before\n session_count_before=$(ls -1 ~/.local/share/amc/sessions/*.json 2>/dev/null | wc -l)\n \n response=$(curl -s -X POST \"${SERVER_URL}/api/spawn\" \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer ${AUTH_TOKEN}\" \\\n -d \"{\\\"project\\\":\\\"${TEST_PROJECT}\\\",\\\"agent_type\\\":\\\"claude\\\"}\")\n \n ok=$(echo \"$response\" | jq -r '.ok')\n if [[ \"$ok\" \\!= \"true\" ]]; then\n log_error \"Spawn failed: $(echo \"$response\" | jq -r '.error')\"\n return 1\n fi\n \n # Verify session file created\n session_count_after=$(ls -1 ~/.local/share/amc/sessions/*.json 2>/dev/null | wc -l)\n if [[ $session_count_after -le $session_count_before ]]; then\n log_warn \"No new session file detected\"\n fi\n \n log_info \"Spawn successful\"\n}\n\n# Test: Rate limiting\ntest_rate_limiting() {\n log_test \"Rate limiting\"\n \n # First spawn should succeed (or fail due to recent test)\n # Second immediate spawn should be rate limited\n response=$(curl -s -X POST \"${SERVER_URL}/api/spawn\" \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer ${AUTH_TOKEN}\" \\\n -d \"{\\\"project\\\":\\\"${TEST_PROJECT}\\\",\\\"agent_type\\\":\\\"claude\\\"}\")\n \n code=$(echo \"$response\" | jq -r '.code')\n # Either RATE_LIMITED or success (if cooldown passed)\n log_info \"Second spawn result: $code\"\n}\n\n# Main\nmain() {\n preflight\n extract_auth_token\n \n test_projects_endpoint\n test_health_endpoint\n test_spawn_no_auth\n test_spawn_invalid_project\n # test_spawn_valid # Uncomment to actually spawn\n # test_rate_limiting\n \n log_info \"\\n=== All tests passed ===\"\n}\n\nmain \"$@\"\n```\n\n## Test Coverage\n1. **Pre-flight**: Server running, Zellij session exists, test project exists\n2. **Auth extraction**: Token injectable into requests\n3. **Projects endpoint**: Returns valid JSON with projects array\n4. **Health endpoint**: Returns status with Zellij availability\n5. **Auth rejection**: Spawn without token returns UNAUTHORIZED\n6. **Path traversal rejection**: Invalid project returns INVALID_PROJECT\n7. **Valid spawn**: Creates session file (optional - actually spawns)\n8. **Rate limiting**: Second spawn returns RATE_LIMITED\n\n## Running\n```bash\n# Dry run (no actual spawning)\n./tests/e2e_spawn.sh\n\n# Full test with spawning\n./tests/e2e_spawn.sh --spawn\n```\n\n## Logging\nDetailed output with colored prefixes:\n- [INFO] Normal operations\n- [WARN] Non-fatal issues\n- [ERROR] Failures\n- [TEST] Test case headers\n\n## Cleanup\ntrap ensures cleanup on exit:\n- Kills any test-spawned agents\n- Removes test session files\n- Restores state\n\n## Success Criteria\n- All pre-flight checks pass\n- All endpoint tests pass\n- Error cases handled correctly\n- Clean exit with summary","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T21:42:57.413609Z","created_by":"tayloreernisse","updated_at":"2026-02-26T22:09:41.968565Z","closed_at":"2026-02-26T22:09:41.968516Z","close_reason":"Implemented E2E spawn workflow tests in tests/e2e_spawn.sh covering: preflight checks, auth token extraction, health/projects endpoints, auth rejection (no token, wrong token, malformed auth), input validation (invalid JSON, path traversal, nonexistent project, invalid agent type, missing project, backslash), CORS preflight, live spawn with session verification, rate limiting, and dashboard visibility. Safe by default with --spawn flag for live tests.","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-1jo","depends_on_id":"bd-196","type":"blocks","created_at":"2026-02-26T21:43:01.408003Z","created_by":"tayloreernisse"},{"issue_id":"bd-1jo","depends_on_id":"bd-1tu","type":"blocks","created_at":"2026-02-26T21:43:01.378693Z","created_by":"tayloreernisse"}]} {"id":"bd-1jt","title":"Slice 1 Integration: Server-side spawning verification","description":"## Overview\nIntegration checkpoint verifying all Slice 1 (backend) components work together.\n\n## Background\nSlice 1 delivers the complete server-side spawn capability, testable via curl without the UI. This bead verifies the integration of:\n- context.py constants\n- SpawnMixin\n- amc-hook spawn_id support\n- HTTP routing\n- Handler integration\n- Server startup initialization\n\n## Verification Checklist\n\n### 1. Server Starts Clean\n```bash\n# Start server\n./bin/amc-server\n\n# Verify no errors in startup log\n# Verify 'Projects cache loaded' message\n```\n\n### 2. Health Endpoint\n```bash\ncurl http://localhost:7400/api/health | jq\n# Expected: {\"ok\": true, \"zellij_session\": \"infra\", \"zellij_available\": true, \"projects_count\": N}\n```\n\n### 3. Projects Endpoint\n```bash\ncurl http://localhost:7400/api/projects | jq\n# Expected: {\"projects\": [\"amc\", \"gitlore\", ...]}\n```\n\n### 4. Auth Token Injection\n```bash\ncurl http://localhost:7400/ | grep AMC_AUTH_TOKEN\n# Expected: \n```\n\n### 5. Spawn Without Auth (Should Fail)\n```bash\ncurl -X POST http://localhost:7400/api/spawn \\\n -H 'Content-Type: application/json' \\\n -d '{\"project\":\"amc\",\"agent_type\":\"claude\"}'\n# Expected: {\"ok\": false, \"code\": \"UNAUTHORIZED\"}\n```\n\n### 6. Spawn With Auth (Path Traversal - Should Fail)\n```bash\nTOKEN=$(curl -s http://localhost:7400/ | grep -o 'AMC_AUTH_TOKEN = \"[^\"]*\"' | cut -d'\"' -f2)\ncurl -X POST http://localhost:7400/api/spawn \\\n -H 'Content-Type: application/json' \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -d '{\"project\":\"../etc\",\"agent_type\":\"claude\"}'\n# Expected: {\"ok\": false, \"code\": \"INVALID_PROJECT\"}\n```\n\n### 7. Spawn With Auth (Valid - Should Succeed)\n```bash\ncurl -X POST http://localhost:7400/api/spawn \\\n -H 'Content-Type: application/json' \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -d '{\"project\":\"amc\",\"agent_type\":\"claude\"}'\n# Expected: {\"ok\": true, \"project\": \"amc\", \"agent_type\": \"claude\", \"spawn_id\": \"...\"}\n# Agent should appear in Zellij 'infra' session, 'amc' tab\n```\n\n### 8. Verify Session File\n```bash\n# Find newest session file\nls -lt ~/.local/share/amc/sessions/*.json | head -1\n# Check for spawn_id\ncat\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":"closed","priority":1,"issue_type":"task","created_at":"2026-02-26T21:45:39.460751Z","created_by":"tayloreernisse","updated_at":"2026-02-26T22:18:20.391913Z","closed_at":"2026-02-26T22:18:20.391866Z","close_reason":"All polish and edge cases verified: Empty projects shows friendly message, newlySpawned animation works, path traversal/special chars validated in spawn.py, zellij metadata fully supported, warning banner when Zellij unavailable. Feature complete.","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`\nzellij attach infra\n