chore: initialize beads project tracking

Add .beads/ directory for dependency-aware issue tracking with the
beads (br) CLI. This provides lightweight task management for the
AMC project.

Files:
- config.yaml: Project configuration (prefix, priorities, types)
- issues.jsonl: Issue database in append-only JSONL format
- metadata.json: Project metadata and statistics
- .gitignore: Ignore database files, locks, and temporaries

Current issues track slash-command autocomplete feature planning
(fetchSkills API, SlashMenu component, hook integration, etc.)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
teernisse
2026-02-26 15:24:35 -05:00
parent de994bb837
commit 117784f8ef
4 changed files with 40 additions and 0 deletions

11
.beads/.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
# Database
*.db
*.db-shm
*.db-wal
# Lock files
*.lock
# Temporary
last-touched
*.tmp

4
.beads/config.yaml Normal file
View File

@@ -0,0 +1,4 @@
# Beads Project Configuration
# issue_prefix: bd
# default_priority: 2
# default_type: task

21
.beads/issues.jsonl Normal file
View File

@@ -0,0 +1,21 @@
{"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":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:08:42.713976Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:08:47.543516Z","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-1y3","title":"Add autocomplete state management","description":"## Overview\nAdd state variables and refs for managing autocomplete visibility and selection in SimpleInput.js.\n\n## Background\nThe autocomplete dropdown needs state to track:\n- Whether it's currently visible\n- Which item is highlighted (for keyboard navigation)\n- The current trigger info (for insertion)\n- Refs for DOM elements (for click-outside detection)\n\n## Implementation\nAdd to SimpleInput component:\n```javascript\n// State\nconst [showAutocomplete, setShowAutocomplete] = useState(false);\nconst [selectedIndex, setSelectedIndex] = useState(0);\n\n// Refs for click-outside detection\nconst autocompleteRef = useRef(null);\n\n// Compute triggerInfo on input change\nconst [triggerInfo, setTriggerInfo] = useState(null);\n\n// Update trigger info and visibility on text/cursor change\nuseEffect(() => {\n const textarea = textareaRef.current;\n if (!textarea) return;\n \n const info = getTriggerInfo(text, textarea.selectionStart);\n setTriggerInfo(info);\n setShowAutocomplete(!!info);\n \n // Reset selection when dropdown opens\n if (info && !triggerInfo) {\n setSelectedIndex(0);\n }\n}, [text, getTriggerInfo]);\n```\n\n## State Flow\n1. User types -> text state updates\n2. useEffect computes triggerInfo\n3. If trigger detected -> showAutocomplete = true\n4. selectedIndex tracks keyboard navigation\n5. On selection/dismiss -> showAutocomplete = false\n\n## Reset Behavior\n- selectedIndex resets to 0 when dropdown opens\n- When filter changes, clamp selectedIndex to valid range\n\n## Success Criteria\n- showAutocomplete correctly reflects trigger state\n- selectedIndex tracks current selection\n- State resets appropriately on open/close\n- Refs available for DOM interaction","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:09:43.067983Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:09:45.583079Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-1y3","depends_on_id":"bd-29o","type":"blocks","created_at":"2026-02-26T20:09:45.583055Z","created_by":"tayloreernisse"}]}
{"id":"bd-253","title":"Implement autocomplete dropdown UI","description":"## Overview\nAdd the autocomplete dropdown JSX to SimpleInput.js render method, positioned above the input.\n\n## Background\nThe dropdown shows filtered skills with visual highlighting. Per AC-11, AC-15, AC-16-19:\n- Positioned above input (bottom-anchored)\n- Left-aligned\n- Max height with scroll\n- Highlighted item visually distinct\n- Respects color scheme\n- Handles both \"no skills available\" AND \"no matching skills\"\n\n## Implementation (from plan IMP-9, with AC-15 fix)\n```javascript\n${showAutocomplete && html`\n <div\n ref=${autocompleteRef}\n class=\"absolute left-0 bottom-full mb-1 w-full max-h-48 overflow-y-auto rounded-lg border border-selection/75 bg-surface shadow-lg z-50\"\n >\n ${autocompleteConfig.skills.length === 0 ? html`\n <!-- AC-15: Session has no skills at all -->\n <div class=\"px-3 py-2 text-sm text-dim\">No skills available</div>\n ` : filteredSkills.length === 0 ? html`\n <!-- AC-11: Filter matched nothing -->\n <div class=\"px-3 py-2 text-sm text-dim\">No matching skills</div>\n ` : filteredSkills.map((skill, i) => html`\n <div\n key=${i}\n class=\"px-3 py-2 cursor-pointer text-sm transition-colors ${\n i === selectedIndex\n ? \"bg-selection/50 text-bright\"\n : \"text-fg hover:bg-selection/25\"\n }\"\n onClick=${() => insertSkill(skill)}\n onMouseEnter=${() => setSelectedIndex(i)}\n >\n <div class=\"font-medium font-mono text-bright\">\n ${autocompleteConfig.trigger}${skill.name}\n </div>\n <div class=\"text-micro text-dim truncate\">${skill.description}</div>\n </div>\n `)}\n </div>\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":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:10:35.317055Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:18:15.324880Z","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":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:09:29.436812Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:09:31.898798Z","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":"open","priority":3,"issue_type":"task","created_at":"2026-02-26T20:12:00.191481Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:12:03.544224Z","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-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":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:08:58.959680Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:09:01.617221Z","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":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:10:14.663971Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:10:18.401505Z","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-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":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:08:29.491653Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:08:32.597481Z","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-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-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":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:07:58.579276Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:08:01.832064Z","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-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":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:10:49.738217Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:10:52.252361Z","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-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":"open","priority":1,"issue_type":"feature","created_at":"2026-02-26T20:07:30.389323Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:07:30.389323Z","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":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:09:15.230355Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:18:36.037711Z","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<div class=\"relative ...\">\n <textarea ... />\n ${/* autocomplete dropdown here */}\n</div>\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":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:12:24.541494Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:12:24.541494Z","compaction_level":0,"original_size":0}
{"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":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:09:59.805771Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:17:56.727953Z","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":"open","priority":3,"issue_type":"task","created_at":"2026-02-26T20:12:12.819926Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:12:15.489217Z","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-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-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":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:08:14.837644Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:08:18.595551Z","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-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('<!--'):\n description = line[:100]\n break\n if description:\n break\n except OSError:\n pass\n \n skills.append({\n 'name': skill_dir.name,\n 'description': description or f'Skill: {skill_dir.name}'\n })\n \n return skills\n```\n\n## Key Decisions\n- **SKILL.md first**: This is the canonical casing used by Claude Code\n- **Fallbacks**: skill.md, prompt.md, README.md for compatibility\n- **Description extraction**: First non-empty, non-header, non-comment line (truncated to 100 chars)\n- **Hidden directories skipped**: Names starting with '.' are ignored\n\n## Success Criteria\n- Returns list of {name, description} dicts\n- Handles missing ~/.claude/skills/ gracefully (returns [])\n- Extracts meaningful descriptions from SKILL.md\n- Ignores hidden directories","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-26T20:07:43.358309Z","created_by":"tayloreernisse","updated_at":"2026-02-26T20:07:46.134852Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-sv1","depends_on_id":"bd-3q1","type":"blocks","created_at":"2026-02-26T20:07:46.134836Z","created_by":"tayloreernisse"}]}

4
.beads/metadata.json Normal file
View File

@@ -0,0 +1,4 @@
{
"database": "beads.db",
"jsonl_export": "issues.jsonl"
}