From 4ba3544b8a83f345d4d1b541f82eea8a6ac6bade Mon Sep 17 00:00:00 2001 From: neiii <1mrtemeck1@gmail.com> Date: Fri, 9 Jan 2026 04:54:28 +0000 Subject: [PATCH 1/2] test: serialize BRIDLE_CONFIG_DIR env in config manager tests --- .beads/issues.jsonl | 10 +++++++ src/config/manager/mod.rs | 59 +++++++++++++++++++++++++++------------ 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 9fb271e..ed5dfba 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -15,12 +15,15 @@ {"id":"bridle-223","title":"AMP Code theme field not parsed","description":"## Problem\nAMP Code theme shows '(not supported)' even when configured.\n\n## Evidence\nConfig has `\"amp.theme\": \"dark\"` but parser shows not supported.\n\n## Preliminary Analysis\n`extract_theme()` explicitly returns `None` for amp-code harness - implementation is missing.\n\n## Research Required\n**Agent must verify**: Is this a bridle logic bug (missing implementation) or does AMP Code not actually support a theme setting?\n\nCheck:\n1. Does official AMP Code documentation confirm theme is a valid setting?\n2. What's the correct key name (`amp.theme` vs something else)?\n3. Is `harness-locate` involved in theme extraction?\n\n## Test Command\n```bash\ncargo run -- profile show amp-code full-test\n```","status":"closed","priority":3,"issue_type":"bug","created_at":"2025-12-29T18:22:56.813809Z","updated_at":"2025-12-29T20:53:55.944991Z","closed_at":"2025-12-29T20:53:55.944991Z","close_reason":"Already working - amp-code theme parsing correctly reads amp.theme from settings.json. Verified with test profile."} {"id":"bridle-2q5","title":"Fix: active profiles show resources from global dir, TUI preserves resources on switch","description":"Two bugs fixed: (1) show_profile looked in profile dir for resources even when profile was active - now looks in global harness config dir. (2) TUI used switch_profile which passed None for harness_for_resources, causing resource dirs to be deleted during switch - now uses switch_profile_with_resources.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-30T00:17:58.117593Z","updated_at":"2025-12-30T00:18:19.559948Z","closed_at":"2025-12-30T00:18:19.559948Z","close_reason":"Fixed both issues: show_profile now looks in global dir for active profiles, TUI preserves resources on switch"} {"id":"bridle-2t9","title":"AMP Code: amp.model.default not parsed","description":"## Summary\nThe AMP Code harness fails to extract the model from `amp.model.default` key in settings.json.\n\n## Expected Behavior\nWhen a profile has `amp.model.default` set, `profile show` should display the model value.\n\n## Actual Behavior\nModel shows as `null` even when `amp.model.default` is set in the config.\n\n## Reproduction\n```bash\ncargo run -- profile show amp-code full\n```\n\nOutput shows:\n```\nModel: (none)\n```\n\n## Artifacts\n\n### Test Config (`~/.config/bridle/profiles/amp-code/full/settings.json`)\n```json\n{\n \"amp.model.default\": \"claude-sonnet-4-20250514\",\n \"amp.theme\": \"dark\",\n ...\n}\n```\n\n### Relevant Code Location\n`src/config/manager/extraction/mod.rs` - AmpCode extraction logic\n\n### Current Extraction Logic\nThe AMP extraction looks for `amp.model` but the actual key used by AMP is `amp.model.default`.\n\n## Fix Approach\nUpdate the model extraction for AMP to check `amp.model.default` key instead of or in addition to `amp.model`.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-31T13:38:04.027274Z","updated_at":"2025-12-31T13:46:22.511391Z","closed_at":"2025-12-31T13:46:22.511391Z","close_reason":"Fixed - parsing now extracts all fields correctly"} +{"id":"bridle-2v0","title":"Add Crush MCP read/write + extraction (crush.json)","description":"Add Crush MCP read/write + extraction support using crush.json and the correct MCP key (mcp).","acceptance_criteria":"MCP install writes to crush.json under 'mcp' while preserving other fields; profile show lists MCP servers.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-09T03:32:02.491294Z","updated_at":"2026-01-09T04:20:22.500719Z","closed_at":"2026-01-09T04:20:22.500719Z","close_reason":"Crush MCP uses crush.json mcp key"} {"id":"bridle-2xr","title":"Installer doesn't check if harness supports component type before installing","description":"## Problem\nThe installer attempts to install agents/commands/plugins for all harnesses without checking if the harness actually supports that component type.\n\n## Current Behavior\n- Tries to install agents to Goose (which doesn't support agents)\n- Tries to install commands to Goose (which doesn't support commands)\n- Results in files being created in wrong locations or errors\n\n## Expected Behavior\nBefore installing a component, check if the harness supports it:\n```rust\nif harness.agents(\u0026Scope::Global).is_ok() {\n // Install agent\n} else {\n println!(\"Skipping agent {} - not supported by {}\", name, harness.name());\n}\n```\n\n## Fix\n1. In `install.rs`, before calling `install_agent()`, check `harness.agents().is_some()`\n2. Same for commands and plugins\n3. Print informative skip message when component type not supported","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-01-02T15:47:09.736374Z","updated_at":"2026-01-02T17:25:48.624275Z","closed_at":"2026-01-02T17:25:48.624275Z","close_reason":"Implemented harness capability checks - tests passing","dependencies":[{"issue_id":"bridle-2xr","depends_on_id":"bridle-wxn","type":"blocks","created_at":"2026-01-02T15:47:55.157429Z","created_by":"daemon"}]} {"id":"bridle-2z6","title":"Implement recursive directory copy for profile resources","description":"## Context\nDepends on bridle-b1h analysis. This task implements the fix.\n\n## Implementation Plan\n\n### Step 1: Add directory copy helper\nAdd a helper function to recursively copy directories:\n```rust\nfn copy_dir_recursive(src: \u0026Path, dst: \u0026Path) -\u003e Result\u003c()\u003e\n```\n\n### Step 2: Modify copy_config_files\nIn `copy_config_files` when `source_is_live=true`:\n1. Query harness for resource directory names: `harness.agents()`, `harness.commands()`, `harness.skills()`\n2. For each resource type, if directory exists in source, copy it recursively to profile\n\n### Step 3: Verify TUI/CLI parity\nBoth use same `ProfileInfo` from `show_profile()`, so fixing the data fixes both. Verify:\n- `cargo run -- profile show opencode maestro` shows resources\n- `cargo run -- tui` → select maestro → shows same resources\n\n### Step 4: Handle edge cases\n- Symlinks in resource directories\n- Empty directories\n- Profile update (`--from-current` on existing profile) should replace directories\n\n## Test Plan\n```bash\n# Delete and recreate test profile\ncargo run -- profile delete opencode test\ncargo run -- profile create opencode test --from-current\n\n# Verify CLI\ncargo run -- profile show opencode test\n\n# Verify TUI\ncargo run -- tui\n```\n\n## Files to modify\n- `src/config/manager.rs`: copy_config_files function (~line 240)","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T14:14:14.375557Z","updated_at":"2025-12-29T15:14:21.837231Z","closed_at":"2025-12-29T15:14:21.837231Z","close_reason":"Implemented recursive directory copy for profile resources - all tests passing","dependencies":[{"issue_id":"bridle-2z6","depends_on_id":"bridle-b1h","type":"blocks","created_at":"2025-12-29T14:14:19.902595Z","created_by":"daemon"}]} {"id":"bridle-372","title":"MCP server installation and transformation","description":"Extend install to handle MCP servers with cross-harness format transformation","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-02T01:19:45.668671Z","updated_at":"2026-01-02T02:44:41.871764Z","closed_at":"2026-01-02T02:44:41.871764Z","close_reason":"Implemented MCP discovery with standard mcpServers format support","dependencies":[{"issue_id":"bridle-372","depends_on_id":"bridle-sfy","type":"blocks","created_at":"2026-01-02T01:19:57.960561Z","created_by":"daemon"}]} {"id":"bridle-3do","title":"Create OutputFormat enum and output module","description":"## Parent Epic\nbridle-xgw: Nushell-friendly structured output for CLI display commands\n\n## Objective\nCreate the core output abstraction module with format enum, shell detection, and output helpers.\n\n## New File: src/cli/output.rs\n\n```rust\n//! Structured output support for CLI commands.\n//!\n//! Provides format selection (text/json/auto) with automatic Nushell detection.\n\nuse clap::ValueEnum;\nuse serde::Serialize;\n\n/// Output format for CLI commands.\n#[derive(Debug, Clone, Copy, Default, ValueEnum)]\npub enum OutputFormat {\n /// Human-readable text output\n Text,\n /// JSON output for scripting and piping\n Json,\n /// Auto-detect: JSON in Nushell, text otherwise\n #[default]\n Auto,\n}\n\n/// Resolved output format after auto-detection.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum ResolvedFormat {\n Text,\n Json,\n}\n\nimpl OutputFormat {\n /// Resolve auto format based on shell detection.\n pub fn resolve(self) -\u003e ResolvedFormat {\n match self {\n Self::Text =\u003e ResolvedFormat::Text,\n Self::Json =\u003e ResolvedFormat::Json,\n Self::Auto =\u003e {\n if is_nushell() {\n ResolvedFormat::Json\n } else {\n ResolvedFormat::Text\n }\n }\n }\n }\n}\n\n/// Detect if running inside Nushell.\n///\n/// Checks for the `NU_VERSION` environment variable which Nushell sets.\nfn is_nushell() -\u003e bool {\n std::env::var(\"NU_VERSION\").is_ok()\n}\n\n/// Output data in the specified format.\n///\n/// For JSON format, serializes to compact JSON.\n/// For text format, calls the provided text formatting function.\npub fn output\u003cT, F\u003e(data: \u0026T, format: ResolvedFormat, text_fn: F)\nwhere\n T: Serialize,\n F: FnOnce(\u0026T),\n{\n match format {\n ResolvedFormat::Json =\u003e {\n // Use compact JSON for easy piping\n println!(\"{}\", serde_json::to_string(data).expect(\"serialization should not fail\"));\n }\n ResolvedFormat::Text =\u003e {\n text_fn(data);\n }\n }\n}\n\n/// Output a list of items in the specified format.\n///\n/// For JSON, outputs as a JSON array.\n/// For text, calls the provided formatting function for each item.\npub fn output_list\u003cT, F\u003e(items: \u0026[T], format: ResolvedFormat, text_fn: F)\nwhere\n T: Serialize,\n F: FnOnce(\u0026[T]),\n{\n match format {\n ResolvedFormat::Json =\u003e {\n println!(\"{}\", serde_json::to_string(items).expect(\"serialization should not fail\"));\n }\n ResolvedFormat::Text =\u003e {\n text_fn(items);\n }\n }\n}\n```\n\n## Update src/cli/mod.rs\n\nAdd the new module:\n```rust\npub mod output;\n// ... existing modules\n```\n\n## Design Decisions\n\n1. **ValueEnum derive**: Integrates directly with clap for `--output` flag parsing\n2. **Default = Auto**: Users get smart behavior without configuration\n3. **Two-phase resolution**: `OutputFormat` → `ResolvedFormat` separates parsing from logic\n4. **Closure for text**: Avoids trait complexity; commands provide their existing println logic\n5. **Compact JSON**: No pretty-printing; easier for piping and parsing\n\n## Verification\n```bash\ncargo check\ncargo clippy -- -D warnings\n```\n\n## Dependencies\n- Depends on: bridle-dsh (Serialize derives)","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-28T22:33:58.575275Z","updated_at":"2025-12-29T21:46:14.309477Z","closed_at":"2025-12-29T21:46:14.309477Z","close_reason":"Created output.rs with OutputFormat enum, ResolvedFormat, is_nushell detection, and output/output_list functions","dependencies":[{"issue_id":"bridle-3do","depends_on_id":"bridle-dsh","type":"blocks","created_at":"2025-12-28T22:36:03.298851Z","created_by":"daemon"}]} {"id":"bridle-3jc","title":"Install manifest tracking for upgrades","description":"Track installed components with source repo and version for upgrade support","status":"closed","priority":3,"issue_type":"task","created_at":"2026-01-02T01:19:48.011951Z","updated_at":"2026-01-02T12:26:54.246094Z","closed_at":"2026-01-02T12:26:54.246094Z","close_reason":"Implemented manifest tracking for install/uninstall with JSON persistence, source tracking, and comprehensive tests. Quality gates pass.","dependencies":[{"issue_id":"bridle-3jc","depends_on_id":"bridle-1ol","type":"blocks","created_at":"2026-01-02T01:19:58.366739Z","created_by":"daemon"}]} +{"id":"bridle-3lo","title":"Wire Crush harness into CLI/TUI harness lists","description":"Add Crush harness plumbing across CLI/TUI: id mapping to 'crush', CLI arg parsing, TUI tab label, install instructions/status.","acceptance_criteria":"'bridle status' and TUI show Crush; 'bridle profile list crush' resolves; no runtime panics.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-09T03:32:02.195896Z","updated_at":"2026-01-09T04:20:22.407954Z","closed_at":"2026-01-09T04:20:22.407954Z","close_reason":"Added Crush harness to CLI/TUI mappings"} {"id":"bridle-4gk","title":"Skills/Commands/Agents extraction uses harness system paths not profile paths","description":"In manager.rs, extract_profile_info() uses harness.skills(Scope::Global), harness.commands(Scope::Global), etc. which query the harness's system-level directories rather than the profile-specific directories.\n\n## Verification Steps\n\n1. Create directories in a test profile:\n ```bash\n mkdir -p ~/.config/bridle/profiles/opencode/test/agent\n mkdir -p ~/.config/bridle/profiles/opencode/test/command\n mkdir -p ~/.config/bridle/profiles/opencode/test/skill\n echo '# Test Agent' \u003e ~/.config/bridle/profiles/opencode/test/agent/test-agent.md\n echo '# Test Command' \u003e ~/.config/bridle/profiles/opencode/test/command/test-cmd.md\n ```\n\n2. Run profile show:\n ```bash\n cargo run -- profile show opencode test\n ```\n\n3. **Before fix**: Skills, Commands, Agents all show 'directory not found'\n\n4. **After fix**: Should show:\n ```\n Agents: 1 (test-agent)\n Commands: 1 (test-cmd)\n Skills: directory not found # (if no skill dir, that's ok)\n ```\n\n5. Verify it looks at profile path, not harness system path:\n - Should read from: ~/.config/bridle/profiles/opencode/test/agent/\n - NOT from: ~/.config/opencode/agent/\n\n## Affected Harnesses\n- OpenCode: agent/, command/, skill/ directories\n- Claude Code: commands/ directory\n- Goose: May have extensions but uses different structure","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-28T14:37:31.917866Z","updated_at":"2025-12-29T15:59:38.85042Z","closed_at":"2025-12-29T15:59:38.85042Z","close_reason":"Fixed: fallback to *.md pattern when harness.agents() returns empty results"} +{"id":"bridle-4pl","title":"Implement Crush profile create/switch/show (from-current)","description":"Implement Crush profile create/switch/show including --from-current, using global-only config and safe switching semantics.","acceptance_criteria":"'bridle profile create crush \u003cname\u003e --from-current' works; switch works; show displays model/MCP/skills where present.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-09T03:32:02.293822Z","updated_at":"2026-01-09T04:20:22.779567Z","closed_at":"2026-01-09T04:20:22.779567Z","close_reason":"Crush profile create/switch/show supported"} {"id":"bridle-4s2","title":"Extract shared CLI/TUI display logic","description":"## Problem\n\nDisplay logic is duplicated across three locations, violating the architecture principle documented in AGENTS.md.\n\n| Component | Location | Lines |\n|-----------|----------|-------|\n| CLI profile display | cli/profile.rs → print_profile_text() | ~100 |\n| TUI profile display | tui/mod.rs → render_profile_expanded() | ~165 |\n| TUI detail pane | tui/widgets/detail_pane.rs → render_profile_details() | ~165 |\n\n**Critical**: render_profile_expanded() and render_profile_details() are near-identical copies with the same Section enum defined twice. This is a DRY violation within TUI itself.\n\n## Recommendation\n\nCreate a shared display module:\n\n```rust\n// src/display/mod.rs\npub struct ProfileSection {\n pub label: \u0026'static str,\n pub value: Option\u003cString\u003e,\n pub children: Vec\u003cProfileSection\u003e,\n}\n\npub fn profile_to_sections(info: \u0026ProfileInfo) -\u003e Vec\u003cProfileSection\u003e\n```\n\n## Caveat\n\nCLI uses flat println! output while TUI uses tree-branch formatting (├─/└─). Need either:\n- A DisplayFormat enum parameter, or\n- A Renderer trait with CLI/TUI implementations\n\n## Acceptance Criteria\n\n- [ ] Single source of truth for display data structure\n- [ ] CLI and TUI consume the same intermediate representation\n- [ ] No duplicate Section enums\n- [ ] Feature parity guaranteed between CLI and TUI","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-30T19:02:50.262114Z","updated_at":"2025-12-30T20:08:52.996791Z","closed_at":"2025-12-30T20:08:52.996791Z","close_reason":"All 5 phases complete: display module with semantic IR, CLI/TUI renderers integrated, ~300 lines duplicate code eliminated. Coach approved."} {"id":"bridle-4s2.1","title":"Create display module with semantic IR","description":"## Context\nFirst phase of display extraction. Creates the shared semantic IR that CLI and TUI will both use.\n\n## Key Changes\n- Create `src/display/mod.rs` with:\n - `SectionKind` enum (Header, Field, McpServer { enabled: bool }, ResourceGroup, ResourceItem, Error)\n - `ProfileNode` struct (kind, label, text, children)\n - `profile_to_nodes(\u0026ProfileInfo) -\u003e Vec\u003cProfileNode\u003e` function\n - `format_mcp_detail(\u0026McpServerInfo) -\u003e String` helper\n\n## Patterns to Follow\n- See `config/profile_name.rs:59` for Display trait pattern\n- See `harness/display.rs` for data struct pattern\n- Keep ratatui-free (no UI types in this module)\n\n## Success Criteria\n- [ ] `cargo check` passes\n- [ ] New module compiles with all ProfileInfo fields handled\n- [ ] No ratatui dependency in display module\n\n## References\n- Full plan: `.beads/artifacts/bridle-4s2/plan.md`\n- Research: `.beads/artifacts/bridle-4s2/research.md`","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-30T19:22:24.804995Z","updated_at":"2025-12-30T19:28:45.795029Z","closed_at":"2025-12-30T19:28:45.795029Z","close_reason":"Phase 1 complete: Created display module with semantic IR (SectionKind, ProfileNode, profile_to_nodes, format_mcp_detail). Coach approved.","dependencies":[{"issue_id":"bridle-4s2.1","depends_on_id":"bridle-4s2","type":"parent-child","created_at":"2025-12-30T19:22:24.806601Z","created_by":"daemon"}]} {"id":"bridle-4s2.2","title":"Add CLI renderer","description":"## Context\nSecond phase. Adds text renderer and integrates into CLI.\n\n## Key Changes\n- `src/display/mod.rs`: Add `nodes_to_text(\u0026[ProfileNode]) -\u003e String`\n- `src/cli/profile.rs`: Replace `print_profile_text()` body to use shared renderer\n- Can remove `print_resource_summary()` helper after integration\n\n## Patterns to Follow\n- Match exact current output format (spaces, newlines, markers)\n- See `cli/output.rs:38-54` for output abstraction pattern\n\n## Success Criteria\n- [ ] `cargo run -- profile show opencode test` output identical to before\n- [ ] `cargo run -- profile show claude test` output identical\n- [ ] `cargo run -- profile show goose test` output identical\n- [ ] Manual diff of output before/after shows no changes\n\n## References\n- Current CLI impl: `cli/profile.rs:101-200`\n- Full plan: `.beads/artifacts/bridle-4s2/plan.md`","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-30T19:22:29.012358Z","updated_at":"2025-12-30T19:40:36.024475Z","closed_at":"2025-12-30T19:40:36.024475Z","close_reason":"Phase 2 complete: Added nodes_to_text() CLI renderer, integrated with profile.rs, removed print_resource_summary(). Coach approved.","dependencies":[{"issue_id":"bridle-4s2.2","depends_on_id":"bridle-4s2","type":"parent-child","created_at":"2025-12-30T19:22:29.018789Z","created_by":"daemon"}]} @@ -30,11 +33,13 @@ {"id":"bridle-7kk","title":"Goose: MCP server command/args not extracted from extensions","description":"## Summary\nGoose harness extracts MCP/extension names and enabled status but fails to extract command, args, and other details from stdio-type extensions.\n\n## Expected Behavior\nWhen parsing config.yaml extensions with `type: stdio`, the command and args should be extracted into McpServerInfo.\n\n## Actual Behavior\nOnly extension names, enabled status, and type are extracted. Command (`cmd`) and args show as null.\n\n## Reproduction\n```bash\ncargo run -- profile show goose full\n```\n\nOutput shows:\n```\nMCP Servers:\n context7 (enabled)\n filesystem (disabled)\n github (enabled)\n memory (disabled)\n```\n\nBut missing the command details like `npx -y @anthropic/context7-mcp-server`.\n\n## Artifacts\n\n### Test Config (`~/.config/bridle/profiles/goose/full/config.yaml`)\n```yaml\nextensions:\n context7:\n enabled: true\n type: stdio\n cmd: npx\n args:\n - \"-y\"\n - \"@anthropic/context7-mcp-server\"\n github:\n enabled: true\n type: stdio\n cmd: npx\n args:\n - \"-y\"\n - \"@modelcontextprotocol/server-github\"\n env:\n GITHUB_PERSONAL_ACCESS_TOKEN: \"${GITHUB_TOKEN}\"\n```\n\n### Relevant Code Location\n`src/config/manager/extraction/mod.rs` - Goose MCP extraction logic\n\n### Current Behavior\nThe extraction captures extension names, enabled status, and type but doesn't populate `McpServerInfo.command` (from `cmd` key) or `McpServerInfo.args`.\n\n## Fix Approach\nUpdate Goose extension extraction to:\n1. Map `cmd` to `McpServerInfo.command`\n2. Map `args` array to `McpServerInfo.args`\n3. Optionally extract `env` for environment variables","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-31T13:38:20.711854Z","updated_at":"2025-12-31T13:46:22.521878Z","closed_at":"2025-12-31T13:46:22.521878Z","close_reason":"Fixed - parsing now extracts all fields correctly"} {"id":"bridle-7qw","title":"Auto-save active profile before switching","description":"When switching profiles, bridle should save the current live config back to the active profile BEFORE copying the new profile over.\n\n**Current behavior:** switch_profile() only copies profile → live config, losing any edits made to the live config.\n\n**Expected behavior:** \n1. Check if there's an active profile for this harness\n2. If yes, copy live config → active profile (save changes)\n3. Then copy new profile → live config\n\n**Reproduction:**\n1. Switch to Profile A\n2. Edit the harness config externally (add MCP server, change setting)\n3. Switch to Profile B\n4. Switch back to Profile A\n5. Edits from step 2 are gone\n\n**Fix location:** ProfileManager::switch_profile() in src/config/manager.rs","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-28T11:45:16.241673Z","updated_at":"2025-12-28T11:47:43.102919Z","closed_at":"2025-12-28T11:47:43.102919Z","close_reason":"Fixed: switch_profile now auto-saves current live config back to active profile before switching, preventing edit loss."} {"id":"bridle-87s","title":"Clean up dead theme code","description":"## Problem\n\nThe ENTIRE Theme struct in tui/theme.rs is unused. 18 methods defined, zero calls anywhere.\n\nCurrent state:\n- Theme struct has #[allow(dead_code)] on struct AND impl block\n- 18 methods: border_active, highlight, profile_active, mcp_enabled, etc.\n- TUI renders use inline Style::default().fg(Color::X) instead\n\nThis is misleading — looks like theming exists but doesn't work.\n\n## Options\n\n### Option A: Remove dead code\nDelete theme.rs entirely. Remove #[allow(dead_code)] markers elsewhere.\n\n### Option B: Implement theming\nWire up the Theme struct:\n1. Load theme from config\n2. Pass Theme to all render functions\n3. Replace inline Style calls with theme.method() calls\n\n## Acceptance Criteria\n\n- [ ] No #[allow(dead_code)] in tui/\n- [ ] Either Theme is used everywhere OR deleted\n- [ ] Consistent styling approach across TUI","status":"closed","priority":2,"issue_type":"chore","created_at":"2025-12-30T19:03:13.828954Z","updated_at":"2025-12-30T21:45:02.166495Z","closed_at":"2025-12-30T21:45:02.166495Z","close_reason":"Closed"} +{"id":"bridle-8m0","title":"Implement Crush install support (skills + MCP only)","description":"Support installing skills + MCP to Crush targets; block agents/commands/plugins as unsupported (Goose-style gating).","acceptance_criteria":"Install flow allows skills+MCP to Crush; attempting agents/commands/plugins for Crush is blocked with a clear message.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-09T03:32:02.390952Z","updated_at":"2026-01-09T04:20:22.871973Z","closed_at":"2026-01-09T04:20:22.871973Z","close_reason":"Crush install supports skills+MCP via harness-locate capabilities"} {"id":"bridle-94s","title":"AMP Code model field not parsed (flat key structure)","description":"## Problem\nAMP Code model shows '(not set)' even when configured.\n\n## Evidence\nConfig has `\"amp.model.default\": \"claude-sonnet-4-20250514\"` but parser shows not set.\n\n## Preliminary Analysis\n`extract_model_ampcode()` expects nested JSON `{\"amp\":{\"model\":...}}` but AMP uses FLAT keys like `\"amp.model.default\"`.\n\n## Research Required\n**Agent must verify**: Is this a bridle logic bug (wrong JSON structure assumption) or does `harness-locate` provide incorrect parsing guidance for AMP?\n\nCheck:\n1. Confirm AMP's official settings.json structure (flat vs nested)\n2. Does `harness-locate` provide model extraction for AMP?\n3. What's the correct JSON path to extract AMP's model?\n\n## Test Command\n```bash\ncargo run -- profile show amp-code full-test\n```","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-12-29T18:22:54.466809Z","updated_at":"2025-12-29T20:14:14.427222Z","closed_at":"2025-12-29T20:14:14.427222Z","close_reason":"Fixed AMP flat key parsing - model and theme now extracted correctly"} {"id":"bridle-957","title":"OpenCode agents treated as directories instead of config","description":"## Problem\nParser shows 'Agents: (directory not found)' for OpenCode, but agents are configured IN the config file, not as directories.\n\n## Evidence\nConfig has `agent.general` and `agent.build` with model/temperature settings, but parser looks for directories.\n\n## Preliminary Analysis\n`extract_agents()` calls `harness.agents(\u0026Scope::Global)` expecting directory paths. For OpenCode, agents are defined inline in `opencode.jsonc`.\n\n## Research Required\n**Agent must verify**: Is this a bridle logic bug or does `harness-locate` incorrectly report OpenCode agents as directory-based?\n\nCheck:\n1. What does `harness.agents(\u0026Scope::Global)` return for OpenCode?\n2. Does `harness-locate` distinguish between config-based and directory-based agents?\n3. Should bridle have harness-specific agent parsing logic?\n\n## Test Command\n```bash\ncargo run -- profile show opencode full-test\n```","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-29T18:22:50.491775Z","updated_at":"2025-12-29T20:50:52.295944Z","closed_at":"2025-12-29T20:50:52.295944Z","close_reason":"Documented as harness-locate issue - agents() only returns directory path, not config-based agents. See harness-locate-bugs.md Issue 3"} {"id":"bridle-959","title":"[Epic] bridle install command - Install skills/MCPs/agents from GitHub","status":"closed","priority":1,"issue_type":"epic","created_at":"2026-01-02T01:19:28.174299Z","updated_at":"2026-01-02T12:27:15.711782Z","closed_at":"2026-01-02T12:27:15.711782Z","close_reason":"Epic complete. All child beads closed: bridle-nr5 (skill discovery), bridle-hod (skill installation), bridle-sfy (install CLI), bridle-372 (MCP discovery), bridle-bo3 (agent/command discovery), bridle-1ol (uninstall), bridle-3jc (manifest tracking). Full install/uninstall functionality with manifest tracking implemented."} {"id":"bridle-979","title":"OpenCode MCP servers not parsed","description":"## Problem\nOpenCode MCP servers show '(none)' even when configured in `opencode.jsonc`.\n\n## Evidence\nConfig has 5 MCP servers under `mcp` key but parser shows none.\n\n## Preliminary Analysis\nBridle's `extract_mcp_servers()` looks for a SEPARATE MCP file via `harness.mcp_filename()`. OpenCode embeds MCP config directly in `opencode.jsonc` at the `mcp` key - there is no separate file.\n\n## Research Required\n**Agent must verify**: Is this a bridle logic bug or does `harness-locate` incorrectly report OpenCode's MCP location?\n\nCheck:\n1. What does `harness.mcp_filename()` return for OpenCode?\n2. What does `harness.mcp_config_path()` return?\n3. Does `harness-locate` provide a way to indicate embedded MCP configs?\n4. Should bridle handle embedded vs separate MCP configs differently?\n\n## Test Command\n```bash\ncargo run -- profile show opencode full-test\n```","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-29T18:22:46.458489Z","updated_at":"2025-12-29T20:38:36.069852Z","closed_at":"2025-12-29T20:38:36.069852Z","close_reason":"Fixed OpenCode MCP parsing - tests passing"} {"id":"bridle-98z","title":"Skills/Commands directories not found in profiles (all harnesses)","description":"## Problem\nParser shows '(directory not found)' or '(none)' for skills/commands even when directories exist in profile.\n\n## Evidence\n- Created directories at `~/.config/bridle/profiles/{harness}/full-test/skills/` and `.../commands/`\n- Files verified with `ls`\n- Parser still shows not found\n\n## Preliminary Analysis\n`init.rs` calls `create_from_current_if_missing` with `None` for `harness_for_resources`, so resource directories may not be copied. However `profile.rs` passes `Some(\u0026harness)`.\n\n## Research Required\n**Agent must verify**: Is this a bridle logic bug or does `harness-locate` return incorrect paths for skills/commands directories?\n\nCheck:\n1. What does `harness.skills(\u0026Scope::Global)` return for each harness?\n2. Is the directory name extraction logic correct?\n3. Does `create_from_current` properly copy these directories?\n\n## Affected\nAll 4 harnesses: Claude Code, OpenCode, Goose, AMP Code\n\n## Test Command\n```bash\ncargo run -- profile show claude-code full-test\n```","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-29T18:22:38.454569Z","updated_at":"2025-12-29T20:48:04.082024Z","closed_at":"2025-12-29T20:48:04.082024Z","close_reason":"Documented as harness-locate design issue - inconsistent directory structures between skills (nested) and commands (flat) in OpenCode. See harness-locate-bugs.md"} +{"id":"bridle-9es","title":"Crush harness support","description":"End-to-end support for the Crush harness in Bridle: profiles, CLI/TUI, install (skills + MCP), capability gating, and tests.","acceptance_criteria":"Crush appears in CLI/TUI; profiles work; skills+MCP install works; unsupported resources are blocked; cargo fmt/clippy/test pass.","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-09T03:32:01.905926Z","updated_at":"2026-01-09T04:20:23.056522Z","closed_at":"2026-01-09T04:20:23.056522Z","close_reason":"End-to-end Crush harness support implemented"} {"id":"bridle-a52","title":"Implement MCP config read/write helpers for all harnesses","description":"# Task: Implement MCP Config Read/Write Helpers\n\n## Context\n\nEach harness stores MCP configuration differently. We need helpers to read existing config, merge new MCPs, and write back without losing data.\n\n## Config Locations and Formats\n\n| Harness | File | Key | Format |\n|---------|------|-----|--------|\n| Claude Code | .mcp.json | mcpServers | JSON |\n| OpenCode | opencode.jsonc | mcp | JSONC (JSON with comments) |\n| Goose | config.yaml | extensions | YAML |\n| AMP Code | settings.json | \"amp.mcpServers\" | JSON (literal string key) |\n\n## File to Create: src/install/mcp_config.rs\n\n### Required Functions\n\n```rust\nuse harness_locate::{Harness, HarnessKind, McpServer};\nuse std::path::Path;\nuse std::collections::HashMap;\n\n/// Read existing MCP servers from a harness config file\n/// Returns empty HashMap if file doesn't exist or has no MCPs\npub fn read_mcp_config(\n kind: HarnessKind,\n config_path: \u0026Path,\n) -\u003e Result\u003cHashMap\u003cString, serde_json::Value\u003e, McpConfigError\u003e\n\n/// Write MCP servers to a harness config file (additive merge)\n/// Preserves existing MCPs and other config fields\n/// CRITICAL: Must preserve YAML comments for Goose\npub fn write_mcp_config(\n kind: HarnessKind,\n config_path: \u0026Path,\n servers: \u0026HashMap\u003cString, serde_json::Value\u003e,\n) -\u003e Result\u003c(), McpConfigError\u003e\n\n/// Check if an MCP with the given name exists in config\npub fn mcp_exists(\n kind: HarnessKind,\n config_path: \u0026Path,\n name: \u0026str,\n) -\u003e Result\u003cbool, McpConfigError\u003e\n\n/// Get the JSON key path for MCP servers in this harness\nfn get_mcp_key(kind: HarnessKind) -\u003e \u0026'static str {\n match kind {\n HarnessKind::ClaudeCode =\u003e \"mcpServers\",\n HarnessKind::OpenCode =\u003e \"mcp\",\n HarnessKind::Goose =\u003e \"extensions\",\n HarnessKind::AmpCode =\u003e \"amp.mcpServers\", // literal key, not nested\n }\n}\n```\n\n### Error Type\n\n```rust\n#[derive(Debug, thiserror::Error)]\npub enum McpConfigError {\n #[error(\"Failed to read config file: {0}\")]\n ReadError(#[from] std::io::Error),\n \n #[error(\"Failed to parse JSON: {0}\")]\n JsonParseError(#[from] serde_json::Error),\n \n #[error(\"Failed to parse YAML: {0}\")]\n YamlParseError(String),\n \n #[error(\"Failed to parse JSONC: {0}\")]\n JsoncParseError(String),\n \n #[error(\"Failed to write config: {0}\")]\n WriteError(String),\n}\n```\n\n## Implementation Details\n\n### JSON (Claude, AMP)\n- Use `serde_json` for read/write\n- For AMP, the key is literally `\"amp.mcpServers\"` as a string, NOT nested under `amp.mcpServers`\n- Example AMP structure: `{\"amp.mcpServers\": {\"mcp-name\": {...}}}`\n\n### JSONC (OpenCode)\n- Use existing `crate::config::jsonc` module\n- Check what's already implemented there\n- JSONC = JSON with // and /* */ comments\n- Comments should be preserved on write\n\n### YAML (Goose)\n- **CRITICAL**: Must preserve comments when rewriting\n- Use a YAML library that supports comment preservation\n- Recommended: Check if `serde_yaml` preserves comments, if not use `yaml-rust2` or similar\n- The `extensions` key contains MCPs mixed with other extension types\n- Filter by checking for MCP-specific fields (command/cmd, type: \"stdio\", etc.)\n\n### Goose Extensions Structure\n```yaml\nextensions:\n developer:\n enabled: true\n type: builtin\n my-mcp-server: # This is an MCP\n type: stdio\n cmd: npx\n args: [\"-y\", \"@modelcontextprotocol/server-fetch\"]\n envs:\n API_KEY: \"${API_KEY}\"\n```\n\n## Verification Steps\n\n1. Unit tests for each harness format\n2. Test reading non-existent file (should return empty HashMap)\n3. Test reading file with no MCPs (should return empty HashMap)\n4. Test additive merge (existing MCPs preserved)\n5. Test YAML comment preservation specifically\n\n## Dependencies\n\n- This task blocks: \"Implement core MCP installation function\"\n- Depends on: \"Update harness-locate to 0.3\"","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-04T19:14:12.555957Z","updated_at":"2026-01-04T19:37:39.085178Z","closed_at":"2026-01-04T19:37:39.085178Z","close_reason":"MCP config helpers implemented with read/write/exists functions for all 4 harnesses","dependencies":[{"issue_id":"bridle-a52","depends_on_id":"bridle-0wk","type":"blocks","created_at":"2026-01-04T19:15:55.026115Z","created_by":"daemon"}]} {"id":"bridle-aan","title":"Install types and data structures","description":"Define core types for the install module: SkillInfo, InstallTarget, InstallOptions, InstallReport, etc.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-02T01:19:41.02392Z","updated_at":"2026-01-02T02:00:11.970036Z","closed_at":"2026-01-02T02:00:11.970036Z","close_reason":"Install types module complete - 10 types implemented"} {"id":"bridle-alh","title":"Add integration test for profile switch file preservation","description":"Add test to tests/cli_integration.rs verifying unknown files are preserved during profile switch.\n\nTest should:\n1. Create temp harness config with unknown.txt\n2. Create profile with only settings.json\n3. Switch profiles\n4. Verify unknown.txt preserved AND settings.json applied\n\nSee fix.md for test implementation.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-03T13:55:12.567089Z","updated_at":"2026-01-03T14:41:16.479407Z","closed_at":"2026-01-03T14:41:16.479407Z","close_reason":"Integration test already implemented in bridle-foj.2","dependencies":[{"issue_id":"bridle-alh","depends_on_id":"bridle-foj","type":"blocks","created_at":"2026-01-03T13:55:23.140444Z","created_by":"daemon"}]} @@ -50,6 +55,7 @@ {"id":"bridle-bo3","title":"Agent and command installation","description":"Extend install to handle agents and commands with transformation","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-02T01:19:46.31014Z","updated_at":"2026-01-02T02:52:46.362175Z","closed_at":"2026-01-02T02:52:46.362175Z","close_reason":"Implemented agent and command discovery and installation","dependencies":[{"issue_id":"bridle-bo3","depends_on_id":"bridle-sfy","type":"blocks","created_at":"2026-01-02T01:19:58.064403Z","created_by":"daemon"}]} {"id":"bridle-bx6","title":"TUI: Harness ordering and favorites","description":"## Summary\nAllow users to set a default/favorite harness that appears first when opening the TUI.\n\n## Requirements\n1. **Config setting**: Add `default_harness` or `harness_order` to BridleConfig\n2. **TUI keybind**: Press 'f' to set current harness as favorite\n3. **Persistent**: Favorite persists across TUI restarts\n4. **TUI opens to favorite**: Selected harness on startup is the favorite\n\n## Design Decision\nUse `default_harness: Option\u003cString\u003e` in config rather than full ordering array. Simpler, covers the main use case.\n\n## Config Changes (bridle.rs)\n```rust\npub struct BridleConfig {\n // ... existing fields\n /// Default harness to show on TUI startup\n #[serde(default)]\n pub default_harness: Option\u003cString\u003e,\n}\n```\n\n## TUI Changes\n1. On startup, find index of default_harness in harnesses vec, select it\n2. Add 'f' keybind in handle_normal_key()\n3. 'f' sets current harness as default, saves config, shows status message\n4. Update help modal with new keybind\n\n## Acceptance Criteria\n- [ ] `default_harness` field in config.toml\n- [ ] TUI opens to configured default harness\n- [ ] 'f' key sets current harness as favorite\n- [ ] Status message confirms \"Set X as default harness\"\n- [ ] Help modal shows 'f' keybind\n- [ ] Falls back to first harness if default not found","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-30T22:08:58.408117Z","updated_at":"2025-12-30T22:28:59.29375Z","closed_at":"2025-12-30T22:28:59.29375Z","close_reason":"Implemented harness favorites: added default_harness to BridleConfig, 'f' key sets current harness as default, TUI opens to default harness, help modal updated"} {"id":"bridle-chg","title":"Extraction uses wrong directory names for profile resources","description":"## Problem\nThe extraction code in `extraction/mod.rs` uses hardcoded directory names that don't match what harness-locate returns.\n\n## Current Behavior\n- `extract_commands()` passes hardcoded `\"commands\"` to `extract_resource_summary()`\n- `extract_agents()` passes hardcoded `\"agents\"`\n- But OpenCode uses `command/` and `agent/` (singular)\n\n## Partial Fix Applied\nEarlier fix extracts subdirectory name from `dir.path` like `extract_plugins()` does.\n\n## Remaining Issue\nNeed to verify this fix works correctly for all harnesses and that the path extraction handles edge cases.\n\n## Files\n- `src/config/manager/extraction/mod.rs` - extract_commands(), extract_agents()\n\n## Related\n- bridle-98z (closed) - original fix attempted","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-01-02T15:47:26.2505Z","updated_at":"2026-01-02T16:15:12.339132Z","closed_at":"2026-01-02T16:15:12.339132Z","close_reason":"Closed"} +{"id":"bridle-cjf","title":"Add automated tests for Crush harness","description":"Add automated tests for Crush harness (unit + CLI integration).","acceptance_criteria":"cargo test passes; tests cover model extraction, MCP read/write, and basic profile operations for Crush.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-09T03:32:02.586994Z","updated_at":"2026-01-09T04:20:22.595827Z","closed_at":"2026-01-09T04:20:22.595827Z","close_reason":"Added Crush automated tests"} {"id":"bridle-d3p","title":"Profile switch loses unrecognized directories (.claude-plugin, hooks, plugins, etc.)","description":"## Problem\nWhen switching profiles, Bridle only copies recognized directories (agents, commands, skills, recipes). All other directories are silently lost.\n\n## Root Cause\n`lifecycle.rs:126`: Main loop only copies **files**, not directories\n`files.rs:134-136`: `copy_resource_directories` only handles: `agent, agents, command, commands, skill, skills, recipes`\n\n## Data Loss Matrix\n| Directory | Status |\n|-----------|--------|\n| agents/ | ✅ Copied |\n| commands/ | ✅ Copied |\n| skills/ | ✅ Copied |\n| .claude-plugin/ | ❌ LOST |\n| plugins/ | ❌ LOST |\n| hooks/ | ❌ LOST |\n| mcp/ | ❌ LOST |\n| .claude-flow/ | ❌ LOST |\n| rules/ | ❌ LOST |\n\n## Affected Test Profiles\n- `fcakyon-codex`: 17 plugins in `plugins/` directory\n- `claude-flow`: `.claude-flow/`, `hooks/` directories\n- `atxtechbro`: `mcp/` directory\n- `marcusrbrown`: `rules/` directory\n\n## Proposed Fix\nOption A: Copy ALL directories (preserve everything)\nOption B: Add known dirs: `.claude-plugin, plugins, hooks, mcp, rules, .claude-flow`\nOption C: Whitelist approach with explicit unknown-dir handling\n\n## Acceptance Criteria\n- [ ] All directories in profile are preserved on switch\n- [ ] Add test case for directory preservation\n- [ ] Warn user if unknown directories found (optional)","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-31T16:20:46.126414Z","updated_at":"2025-12-31T17:46:30.318885Z","closed_at":"2025-12-31T17:46:30.318885Z","close_reason":"Fixed - directories now preserved during profile switch"} {"id":"bridle-ded","title":"Config extractors for theme, model, and MCP status","description":"## Goal\nParse harness-specific config files to extract theme, model, and MCP server details with enabled status.\n\n## Dependencies\n- Depends on: bridle-jm0 (Enhanced ProfileInfo data structures)\n\n## Changes\n\nAdd config extraction methods:\n\n```rust\nimpl ProfileManager {\n /// Extract MCP servers with name and enabled status\n pub fn extract_mcp_servers(\u0026self, harness: \u0026Harness, scope: Scope) -\u003e Result\u003cVec\u003cMcpServerInfo\u003e, String\u003e {\n let servers = harness.parse_mcp_config(scope)?;\n Ok(servers.into_iter().map(|(name, server)| {\n McpServerInfo {\n name,\n enabled: match server {\n McpServer::Stdio(s) =\u003e s.enabled.unwrap_or(true),\n McpServer::Sse(s) =\u003e s.enabled.unwrap_or(true),\n McpServer::Http(s) =\u003e s.enabled.unwrap_or(true),\n },\n }\n }).collect())\n }\n \n /// Extract theme (OpenCode only, returns None for others)\n pub fn extract_theme(\u0026self, harness: \u0026Harness, profile_path: \u0026Path) -\u003e Option\u003cString\u003e {\n match harness.kind() {\n HarnessKind::OpenCode =\u003e {\n // Parse opencode.jsonc for \"theme\" field\n let config_path = profile_path.join(\"opencode.jsonc\");\n // ... parse and extract\n }\n _ =\u003e None, // Not supported for Claude Code or Goose\n }\n }\n \n /// Extract model from config\n pub fn extract_model(\u0026self, harness: \u0026Harness, profile_path: \u0026Path) -\u003e Option\u003cString\u003e {\n match harness.kind() {\n HarnessKind::ClaudeCode =\u003e {\n // Parse settings.json for \"model\" field\n }\n HarnessKind::OpenCode =\u003e {\n // Parse opencode.jsonc for \"model\" field\n }\n HarnessKind::Goose =\u003e {\n // Parse config.yaml for \"GOOSE_MODEL\" or \"default_model\"\n }\n }\n }\n}\n```\n\n## Config File Locations\n- Claude Code: settings.json (JSON)\n- OpenCode: opencode.jsonc (JSONC - need to handle comments)\n- Goose: config.yaml (YAML)\n\n## Error Handling\n- Capture parsing errors in extraction_errors field\n- Don't fail entire extraction if one field fails\n- Return descriptive error messages for display\n\n## Acceptance Criteria\n- [ ] extract_mcp_servers() returns Vec\u003cMcpServerInfo\u003e with enabled status\n- [ ] extract_theme() returns theme for OpenCode, None for others\n- [ ] extract_model() works for all three harnesses\n- [ ] JSONC parsing handles comments correctly\n- [ ] YAML parsing works for Goose config\n- [ ] Parsing errors captured, not thrown\n- [ ] cargo test passes\n- [ ] cargo clippy passes","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-28T12:00:04.831026Z","updated_at":"2025-12-28T12:53:46.184769Z","closed_at":"2025-12-28T12:53:46.184769Z","close_reason":"Implemented config extractors for theme, model, and MCP enabled status","dependencies":[{"issue_id":"bridle-ded","depends_on_id":"bridle-jm0","type":"blocks","created_at":"2025-12-28T12:00:46.861781Z","created_by":"daemon"}]} {"id":"bridle-dib","title":"Add integration tests for MCP installation","description":"# Task: Integration Tests for MCP Installation\n\n## Context\n\nAfter implementing MCP installation, we need integration tests to verify the full flow works correctly across all harnesses.\n\n## Test File: tests/mcp_installation.rs\n\n### Test 1: Install MCP to Claude Code Profile\n\n```rust\n#[test]\nfn test_install_mcp_to_claude_profile() {\n // Setup: Create temp profile directory\n let temp_dir = tempdir().unwrap();\n let profile_dir = temp_dir.path().join(\"claude-code\").join(\"test-profile\");\n fs::create_dir_all(\u0026profile_dir).unwrap();\n \n // Create empty .mcp.json\n fs::write(profile_dir.join(\".mcp.json\"), r#\"{\"mcpServers\":{}}\"#).unwrap();\n \n // Create test McpServer\n let server = McpServer::Stdio(StdioMcpServer {\n command: \"npx\".to_string(),\n args: vec![\"-y\".to_string(), \"@modelcontextprotocol/server-fetch\".to_string()],\n env: HashMap::new(),\n ..Default::default()\n });\n \n // Install\n let target = InstallTarget {\n harness: \"claude-code\".to_string(),\n profile: ProfileName::try_from(\"test-profile\").unwrap(),\n };\n let result = install_mcp(\"fetch\", \u0026server, \u0026target, \u0026InstallOptions::default());\n \n // Verify\n assert!(matches!(result, Ok(InstallOutcome::Installed(_))));\n \n // Check file content\n let content = fs::read_to_string(profile_dir.join(\".mcp.json\")).unwrap();\n let json: serde_json::Value = serde_json::from_str(\u0026content).unwrap();\n assert!(json[\"mcpServers\"][\"fetch\"].is_object());\n}\n```\n\n### Test 2: Install MCP to OpenCode Profile (JSONC)\n\n```rust\n#[test]\nfn test_install_mcp_to_opencode_profile() {\n // Setup with JSONC file containing comments\n let original = r#\"{\n // OpenCode configuration\n \"theme\": \"dark\",\n \"mcp\": {}\n}\"#;\n \n // ... similar to above, verify JSONC comments preserved\n}\n```\n\n### Test 3: Install MCP to Goose Profile (YAML)\n\n```rust\n#[test]\nfn test_install_mcp_to_goose_profile() {\n // Setup with YAML file containing comments\n let original = r#\"\n# Goose configuration\nGOOSE_PROVIDER: anthropic\nextensions: {}\n\"#;\n \n // ... verify YAML comments preserved and MCP added to extensions\n}\n```\n\n### Test 4: Conflict Detection\n\n```rust\n#[test]\nfn test_mcp_conflict_detection() {\n // Setup: Profile with existing MCP\n let existing = r#\"{\"mcpServers\":{\"fetch\":{\"command\":\"npx\"}}}\"#;\n \n // Try to install same MCP name\n let result = install_mcp(\"fetch\", \u0026server, \u0026target, \u0026InstallOptions { force: false });\n \n // Should skip\n assert!(matches!(result, Ok(InstallOutcome::Skipped(_))));\n}\n\n#[test]\nfn test_mcp_force_overwrite() {\n // Same setup as above but with force: true\n let result = install_mcp(\"fetch\", \u0026server, \u0026target, \u0026InstallOptions { force: true });\n \n // Should succeed\n assert!(matches!(result, Ok(InstallOutcome::Installed(_))));\n}\n```\n\n### Test 5: Additive Merge\n\n```rust\n#[test]\nfn test_mcp_additive_merge() {\n // Setup: Profile with existing MCP\n let existing = r#\"{\"mcpServers\":{\"existing-mcp\":{\"command\":\"existing\"}}}\"#;\n \n // Install new MCP\n install_mcp(\"new-mcp\", \u0026new_server, \u0026target, \u0026options).unwrap();\n \n // Verify BOTH exist\n let content = fs::read_to_string(config_path).unwrap();\n let json: serde_json::Value = serde_json::from_str(\u0026content).unwrap();\n assert!(json[\"mcpServers\"][\"existing-mcp\"].is_object());\n assert!(json[\"mcpServers\"][\"new-mcp\"].is_object());\n}\n```\n\n### Test 6: Active Profile Sync\n\n```rust\n#[test]\nfn test_mcp_syncs_to_active_harness() {\n // Setup: Create profile and mark it as active in BridleConfig\n // Install MCP\n // Verify MCP appears in BOTH profile dir AND live harness config\n}\n```\n\n### Test 7: Env Var Warning\n\n```rust\n#[test]\nfn test_mcp_env_var_warning() {\n // Create MCP with env var references\n let server = McpServer::Stdio(StdioMcpServer {\n env: hashmap! {\n \"API_KEY\".to_string() =\u003e EnvValue::EnvRef { env: \"MY_API_KEY\".to_string() }\n },\n ..Default::default()\n });\n \n // Install and capture stderr\n // Verify warning is printed about unresolved env vars\n}\n```\n\n## Real-World MCP Test (Manual)\n\nDocument manual testing with real GitHub MCPs:\n\n```bash\n# Test fetch MCP (simple, no env vars)\ncargo run -- install modelcontextprotocol/servers --component mcp\n\n# Test filesystem MCP (has args)\ncargo run -- install modelcontextprotocol/servers\n\n# Verify in each harness config:\n# - Claude: ~/.claude/.mcp.json\n# - OpenCode: ~/.config/opencode/opencode.jsonc\n# - Goose: ~/.config/goose/config.yaml\n# - AMP: Check amp config location\n```\n\n## Acceptance Criteria\n\n1. All unit tests pass\n2. Tests cover all 4 harness formats\n3. Tests verify comment preservation (YAML, JSONC)\n4. Tests verify additive merge behavior\n5. Tests verify conflict detection\n6. Manual testing documented and verified\n\n## Dependencies\n\n- Depends on: All implementation tasks complete\n- Depends on: \"Wire up MCP installation in CLI\"","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-04T19:15:46.532442Z","updated_at":"2026-01-04T19:54:04.832857Z","closed_at":"2026-01-04T19:54:04.832857Z","close_reason":"Integration tests for MCP installation added","dependencies":[{"issue_id":"bridle-dib","depends_on_id":"bridle-iy7","type":"blocks","created_at":"2026-01-04T19:15:55.354931Z","created_by":"daemon"}]} @@ -57,6 +63,7 @@ {"id":"bridle-dj9","title":"Add AmpCode harness support","description":"## Overview\nAdd support for AmpCode harness (Anthropic's VS Code extension).\n\n## Implementation Details\n\n### 1. src/harness/mod.rs\nAdd HarnessKind::AmpCode to harness ID mapping:\n```rust\nharness_locate::HarnessKind::AmpCode =\u003e \"amp-code\",\n```\n\n### 2. src/cli/profile.rs \nAdd CLI aliases for AmpCode:\n```rust\n\"amp-code\" | \"amp\" | \"ampcode\" =\u003e HarnessKind::AmpCode,\n```\nUpdate help text to include amp-code in valid options.\n\n### 3. src/config/manager.rs\nAdd AmpCode extraction functions:\n\n**Theme extraction**: Return None (AmpCode doesn't have theme config)\n```rust\n\"amp-code\" =\u003e None,\n```\n\n**Model extraction**: Add to match statement\n```rust\n\"amp-code\" =\u003e self.extract_model_ampcode(profile_path),\n```\n\n**extract_model_ampcode function**:\n```rust\nfn extract_model_ampcode(\u0026self, profile_path: \u0026std::path::Path) -\u003e Option\u003cString\u003e {\n let config_path = profile_path.join(\"settings.json\");\n let content = std::fs::read_to_string(\u0026config_path).ok()?;\n let parsed: serde_json::Value = serde_json::from_str(\u0026content).ok()?;\n \n let amp = parsed.get(\"amp\")?;\n \n // Check for direct model string\n if let Some(model) = amp.get(\"model\").and_then(|m| m.as_str()) {\n return Some(model.to_string());\n }\n \n // Check for model object with opus/sonnet/haiku booleans\n if let Some(model_obj) = amp.get(\"model\").and_then(|m| m.as_object()) {\n for model in [\"opus\", \"sonnet\", \"haiku\"] {\n if model_obj.get(model).and_then(|v| v.as_bool()).unwrap_or(false) {\n return Some(format!(\"claude-{}\", model));\n }\n }\n }\n \n None\n}\n```\n\n### 4. Make MCP parsing non-fatal\nChange MCP extraction to return empty Vec on error instead of propagating:\n```rust\nlet mcp_servers = match self.extract_mcp_servers(harness, \u0026path) {\n Ok(servers) =\u003e servers,\n Err(e) =\u003e {\n extraction_errors.push(format!(\"MCP config: {}\", e));\n Vec::new()\n }\n};\n```\n\n### 5. Dependencies\nBump harness-locate to 0.2.1+ for AmpCode MCP parsing support.\n\n## AmpCode Config Structure\nConfig file: `settings.json`\n```json\n{\n \"amp\": {\n \"model\": \"claude-sonnet-4\" // or object with opus/sonnet/haiku booleans\n }\n}\n```\n\n## Reference\nOriginal commit: d5e0a28 on nushell branch","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-31T12:10:06.546583Z","updated_at":"2025-12-31T13:43:29.557713Z","closed_at":"2025-12-31T13:43:29.557713Z","close_reason":"Fixed all extraction bugs"} {"id":"bridle-dqp","title":"Implement core MCP installation function","description":"# Task: Implement Core MCP Installation Function\n\n## Context\n\nCreate the main `install_mcp()` function following the same pattern as `install_skill()` in `src/install/installer.rs`.\n\n## File to Create: src/install/mcp_installer.rs\n\n### Main Function\n\n```rust\nuse crate::install::mcp_config::{read_mcp_config, write_mcp_config, mcp_exists, McpConfigError};\nuse crate::install::types::{InstallOptions, InstallOutcome, InstallTarget};\nuse crate::install::installer::{validate_component_name, parse_harness_kind};\nuse crate::config::BridleConfig;\nuse harness_locate::{Harness, HarnessKind, McpServer, Scope};\nuse std::path::PathBuf;\n\n#[derive(Debug, thiserror::Error)]\npub enum McpInstallError {\n #[error(\"Invalid MCP name: {0}\")]\n InvalidName(String),\n \n #[error(\"Harness not found: {0}\")]\n HarnessNotFound(String),\n \n #[error(\"Harness does not support MCP: {0}\")]\n McpNotSupported(String),\n \n #[error(\"Config error: {0}\")]\n ConfigError(#[from] McpConfigError),\n \n #[error(\"Failed to convert MCP to native format: {0}\")]\n ConversionError(String),\n \n #[error(\"IO error: {0}\")]\n IoError(#[from] std::io::Error),\n}\n\n/// Install an MCP server to a target profile\n/// \n/// Follows the same pattern as install_skill():\n/// 1. Validate name\n/// 2. Get profile directory path\n/// 3. Check for conflicts (skip if exists)\n/// 4. Convert to native format using harness-locate 0.3 API\n/// 5. Write to profile's config file\n/// 6. If profile is active, also write to live harness config\n/// \n/// # Arguments\n/// * `name` - MCP server name (used as key in config)\n/// * `server` - McpServer from harness-locate\n/// * `target` - Installation target (harness + profile)\n/// * `options` - Installation options (force, etc.)\n/// \n/// # Returns\n/// * `InstallOutcome::Installed` - Successfully installed\n/// * `InstallOutcome::Skipped` - Skipped (already exists or not supported)\npub fn install_mcp(\n name: \u0026str,\n server: \u0026McpServer,\n target: \u0026InstallTarget,\n options: \u0026InstallOptions,\n) -\u003e Result\u003cInstallOutcome, McpInstallError\u003e\n```\n\n### Implementation Steps\n\n```rust\npub fn install_mcp(...) -\u003e Result\u003cInstallOutcome, McpInstallError\u003e {\n // 1. Validate MCP name (reuse existing validator)\n validate_component_name(name)\n .map_err(|e| McpInstallError::InvalidName(e.to_string()))?;\n \n // 2. Parse harness kind and locate harness\n let kind = parse_harness_kind(\u0026target.harness)\n .ok_or_else(|| McpInstallError::HarnessNotFound(target.harness.clone()))?;\n let harness = Harness::locate(kind)\n .map_err(|e| McpInstallError::HarnessNotFound(e.to_string()))?;\n \n // 3. Check if harness supports MCP\n if harness.mcp(\u0026Scope::Global).ok().flatten().is_none() {\n return Err(McpInstallError::McpNotSupported(target.harness.clone()));\n }\n \n // 4. Get profile directory and config file path\n let profiles_dir = BridleConfig::profiles_dir();\n let profile_dir = profiles_dir.join(\u0026target.harness).join(target.profile.as_str());\n let profile_config_path = get_profile_mcp_config_path(\u0026profile_dir, kind);\n \n // 5. Check for conflicts (unless force)\n if !options.force \u0026\u0026 mcp_exists(kind, \u0026profile_config_path, name)? {\n eprintln!(\" ! MCP '{}' already exists in profile. Use --force to overwrite.\", name);\n return Ok(InstallOutcome::Skipped(format!(\"MCP '{}' already exists\", name)));\n }\n \n // 6. Convert to native format using harness-locate 0.3 API\n let native_value = server.to_native_value(kind, name)\n .map_err(|e| McpInstallError::ConversionError(e.to_string()))?;\n \n // 7. Warn about unresolved env vars\n let env_vars = server.env_var_names();\n if !env_vars.is_empty() {\n eprintln!(\" ~ Warning: MCP '{}' has unresolved env vars: {:?}\", name, env_vars);\n eprintln!(\" You may need to set these before using the MCP.\");\n }\n \n // 8. Read existing config, merge, write back\n let mut existing = read_mcp_config(kind, \u0026profile_config_path)?;\n existing.insert(name.to_string(), native_value);\n write_mcp_config(kind, \u0026profile_config_path, \u0026existing)?;\n \n // 9. If profile is active, also write to live harness config\n write_to_harness_if_active(name, server, \u0026target, kind, \u0026harness)?;\n \n Ok(InstallOutcome::Installed(format!(\"MCP '{}' installed\", name)))\n}\n```\n\n### Helper Functions\n\n```rust\n/// Get the MCP config file path within a profile directory\nfn get_profile_mcp_config_path(profile_dir: \u0026Path, kind: HarnessKind) -\u003e PathBuf {\n match kind {\n HarnessKind::ClaudeCode =\u003e profile_dir.join(\".mcp.json\"),\n HarnessKind::OpenCode =\u003e profile_dir.join(\"opencode.jsonc\"),\n HarnessKind::Goose =\u003e profile_dir.join(\"config.yaml\"),\n HarnessKind::AmpCode =\u003e profile_dir.join(\"settings.json\"),\n }\n}\n\n/// Write MCP to live harness config if the target profile is currently active\nfn write_to_harness_if_active(\n name: \u0026str,\n server: \u0026McpServer,\n target: \u0026InstallTarget,\n kind: HarnessKind,\n harness: \u0026Harness,\n) -\u003e Result\u003c(), McpInstallError\u003e {\n let config = BridleConfig::load()?;\n \n // Check if this profile is the active one for this harness\n if config.active_profile_for(\u0026target.harness).as_ref() != Some(\u0026target.profile) {\n return Ok(()); // Not active, nothing to do\n }\n \n // Get the live harness MCP config path\n let harness_config_path = harness.mcp(\u0026Scope::Global)\n .ok()\n .flatten()\n .map(|m| m.path)\n .ok_or_else(|| McpInstallError::McpNotSupported(target.harness.clone()))?;\n \n // Read, merge, write to live config\n let native_value = server.to_native_value(kind, name)\n .map_err(|e| McpInstallError::ConversionError(e.to_string()))?;\n let mut existing = read_mcp_config(kind, \u0026harness_config_path)?;\n existing.insert(name.to_string(), native_value);\n write_mcp_config(kind, \u0026harness_config_path, \u0026existing)?;\n \n Ok(())\n}\n\n/// Check if a harness supports MCP installation\npub fn harness_supports_mcp(harness_id: \u0026str) -\u003e bool {\n parse_harness_kind(harness_id)\n .and_then(|kind| Harness::locate(kind).ok())\n .and_then(|h| h.mcp(\u0026Scope::Global).ok().flatten())\n .is_some()\n}\n```\n\n## Update src/install/mod.rs\n\n```rust\npub mod mcp_config;\npub mod mcp_installer;\n\npub use mcp_installer::{install_mcp, harness_supports_mcp, McpInstallError};\n```\n\n## Verification Steps\n\n1. Unit test: install to inactive profile (only profile config modified)\n2. Unit test: install to active profile (both profile and harness config modified)\n3. Unit test: skip on conflict (no force flag)\n4. Unit test: overwrite on conflict (with force flag)\n5. Unit test: env var warning is printed\n6. Integration test: full flow with real harness\n\n## Dependencies\n\n- Depends on: \"Implement MCP config read/write helpers\"\n- Depends on: \"Update harness-locate to 0.3\"","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-04T19:14:40.276585Z","updated_at":"2026-01-04T19:44:24.562032Z","closed_at":"2026-01-04T19:44:24.562032Z","close_reason":"Core MCP installation function implemented with install_mcp, conflict handling, env var warnings","dependencies":[{"issue_id":"bridle-dqp","depends_on_id":"bridle-0wk","type":"blocks","created_at":"2026-01-04T19:15:55.112825Z","created_by":"daemon"},{"issue_id":"bridle-dqp","depends_on_id":"bridle-a52","type":"blocks","created_at":"2026-01-04T19:15:55.191732Z","created_by":"daemon"}]} {"id":"bridle-dsh","title":"Add Serialize derives to data structures","description":"## Parent Epic\nbridle-xgw: Nushell-friendly structured output for CLI display commands\n\n## Objective\nAdd `#[derive(Serialize)]` to all data structures that will be output as JSON.\n\n## Files to Modify\n\n### src/config/manager.rs\nAdd Serialize derive to:\n- `McpServerInfo` - MCP server name and enabled status\n- `ResourceSummary` - items list and directory_exists flag \n- `ProfileInfo` - full profile information\n\n### Current State\n```rust\n#[derive(Debug, Clone)]\npub struct McpServerInfo {\n pub name: String,\n pub enabled: bool,\n}\n\n#[derive(Debug, Clone)]\npub struct ResourceSummary {\n pub items: Vec\u003cString\u003e,\n pub directory_exists: bool,\n}\n\n#[derive(Debug, Clone)]\npub struct ProfileInfo {\n pub name: String,\n pub harness_id: String,\n pub is_active: bool,\n pub path: PathBuf,\n pub mcp_servers: Vec\u003cMcpServerInfo\u003e,\n pub skills: ResourceSummary,\n pub commands: ResourceSummary,\n pub plugins: ResourceSummary,\n pub agents: ResourceSummary,\n pub rules_file: Option\u003cString\u003e,\n pub theme: Option\u003cString\u003e,\n pub model: Option\u003cString\u003e,\n pub extraction_errors: Vec\u003cString\u003e,\n}\n```\n\n### Target State\n```rust\nuse serde::Serialize;\n\n#[derive(Debug, Clone, Serialize)]\npub struct McpServerInfo { ... }\n\n#[derive(Debug, Clone, Serialize)]\npub struct ResourceSummary { ... }\n\n#[derive(Debug, Clone, Serialize)]\npub struct ProfileInfo { ... }\n```\n\n## JSON Output Examples\n\n### ProfileInfo\n```json\n{\n \"name\": \"test\",\n \"harness_id\": \"opencode\",\n \"is_active\": true,\n \"path\": \"/Users/d0/.config/bridle/profiles/opencode/test\",\n \"mcp_servers\": [\n {\"name\": \"filesystem\", \"enabled\": true},\n {\"name\": \"github\", \"enabled\": true}\n ],\n \"skills\": {\"items\": [\"rust\", \"python\"], \"directory_exists\": true},\n \"commands\": {\"items\": [], \"directory_exists\": false},\n \"plugins\": {\"items\": [\"plugin1\"], \"directory_exists\": true},\n \"agents\": {\"items\": [], \"directory_exists\": false},\n \"rules_file\": \"RULES.md\",\n \"theme\": \"dark\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"extraction_errors\": []\n}\n```\n\n## Verification\n```bash\ncargo check # Ensure Serialize derives compile\ncargo clippy -- -D warnings # No new warnings\n```\n\n## Notes\n- serde is already in Cargo.toml with derive feature\n- PathBuf serializes as string automatically\n- Option\u003cT\u003e serializes as null or value\n- Vec\u003cT\u003e serializes as JSON array","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-28T22:33:32.857308Z","updated_at":"2025-12-29T20:49:24.858846Z","closed_at":"2025-12-29T20:49:24.858846Z","close_reason":"Added Serialize derives to McpServerInfo, ResourceSummary, and ProfileInfo in manager.rs"} +{"id":"bridle-dvl","title":"Update README for Crush harness","description":"Update README to include Crush as a supported harness and note unsupported components.","acceptance_criteria":"README lists Crush and accurately reflects component support.","status":"closed","priority":3,"issue_type":"chore","created_at":"2026-01-09T03:32:30.673884Z","updated_at":"2026-01-09T04:20:22.687353Z","closed_at":"2026-01-09T04:20:22.687353Z","close_reason":"README updated for Crush"} {"id":"bridle-dxb","title":"Add .claude-plugin/ format parsing for Claude Code profiles","description":"## Problem\nBridle doesn't parse the `.claude-plugin/` marketplace format used by many popular Claude Code configurations.\n\n## Evidence\n- `fcakyon-codex` profile has 160+ files across 17 plugins (azure-tools, github-dev, linear-tools, mongodb-tools, gcloud-tools, etc.) but Bridle only shows 'Model: opusplan'\n- `wshobson-agents` profile uses only .claude-plugin format, shows nothing in Bridle\n\n## .claude-plugin Structure\n```\n.claude-plugin/\n├── marketplace.json # Plugin metadata (name, version, dependencies)\n├── plugin.json # Alternative metadata format\n└── ...\n\nplugins/\u003cplugin-name\u003e/\n├── .claude-plugin/plugin.json # Per-plugin metadata\n├── .mcp.json # MCP server configs\n├── commands/*.md # Slash commands\n├── skills/**/SKILL.md # Skill definitions\n├── agents/*.md # Agent definitions\n├── hooks/hooks.json # Hook configurations\n└── hooks/scripts/*.py # Hook scripts\n```\n\n## Acceptance Criteria\n- [ ] Parse `.claude-plugin/marketplace.json` for plugin list\n- [ ] Scan `plugins/*/` subdirectories for nested resources\n- [ ] Aggregate commands, skills, agents, MCPs from all plugins\n- [ ] Show plugin count in profile summary\n- [ ] TUI/CLI parity for plugin display\n\n## Test Profiles\n- `~/.config/bridle/profiles/claude-code/fcakyon-codex/` (17 plugins)\n- `~/.config/bridle/profiles/claude-code/wshobson-agents/` (67 plugins)","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-31T16:16:38.768944Z","updated_at":"2025-12-31T18:40:36.479337Z","closed_at":"2025-12-31T18:40:36.479337Z","close_reason":"Implemented Claude Code plugin parsing - marketplace.json + directory fallback"} {"id":"bridle-e0z","title":"Add CLI integration tests","description":"## Problem\n\n- tests/ directory doesn't exist\n- All 16 tests are unit tests inside manager.rs\n- No end-to-end CLI tests\n\n## Recommendation\n\nAdd integration tests using assert_cmd:\n\n```rust\n// tests/cli_integration.rs\nuse assert_cmd::Command;\nuse predicates::prelude::*;\n\n#[test]\nfn test_profile_list_empty() {\n let temp = tempfile::tempdir().unwrap();\n Command::cargo_bin(\"bridle\")\n .env(\"HOME\", temp.path())\n .args([\"profile\", \"list\", \"opencode\"])\n .assert()\n .success()\n .stdout(predicate::str::contains(\"No profiles\"));\n}\n\n#[test]\nfn test_profile_create_and_show() {\n // Create profile, verify it appears in list, verify show works\n}\n\n#[test]\nfn test_invalid_harness_name() {\n // Verify error handling for unknown harness\n}\n```\n\n## Dependencies\n\nAdd to Cargo.toml:\n```toml\n[dev-dependencies]\nassert_cmd = \"2\"\npredicates = \"3\"\n```\n\n## Acceptance Criteria\n\n- [ ] tests/ directory exists\n- [ ] Integration tests for profile list/create/show/delete\n- [ ] Tests use isolated temp directories\n- [ ] CI runs integration tests","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-30T19:03:23.770358Z","updated_at":"2025-12-30T22:43:57.589092Z","closed_at":"2025-12-30T22:43:57.589092Z","close_reason":"Added 12 CLI integration tests using assert_cmd + predicates. Tests cover: profile list/create/show/delete, duplicate profile error, config set/get, help/version. Added BRIDLE_CONFIG_DIR env var support for test isolation."} {"id":"bridle-e2r","title":"Profile sync doesn't copy resource subdirectories to harness config","description":"## Problem\nWhen a profile is activated, `copy_config_files()` only copies top-level files, not subdirectories like `commands/`, `agents/`, `skills/`.\n\n## Current Behavior\n- Bridle profile at `~/.config/bridle/profiles/claude-code/plugin-test/commands/` has files\n- But `~/.claude/commands/` remains empty after profile activation\n- Only top-level files (settings.json, .mcp.json) are synced\n\n## Expected Behavior\nAll resource directories should be synced:\n- `commands/` → `~/.claude/commands/`\n- `agents/` → `~/.claude/agents/` \n- `skills/` → `~/.claude/skills/`\n- `plugins/` → `~/.claude/plugins/`\n\n## Fix\n1. In `files.rs` `copy_config_files()`, add logic to copy resource directories\n2. Use harness-locate to get the list of resource directories for the harness\n3. Recursively copy each directory that exists in the profile\n\n## Related\n- bridle-2z6 (closed) - implemented recursive copy but may not be used for all cases","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-01-02T15:47:09.734349Z","updated_at":"2026-01-02T16:07:46.401055Z","closed_at":"2026-01-02T16:07:46.401055Z","close_reason":"Fixed profile sync to use harness-aware paths via harness.commands/agents/skills/plugins(). Profiles now use canonical names (commands/, agents/, skills/, plugins/) for cross-harness portability."} @@ -65,6 +72,7 @@ {"id":"bridle-foj","title":"Fix profile switch data loss - merge-based approach","description":"CRITICAL: lifecycle.rs:155-158 deletes entire harness config directory during profile switch, destroying ALL runtime files bridle doesn't manage.\n\nReplace delete-and-replace with selective sync:\n- Refactor switch_profile_with_resources() to only touch files IN the profile\n- Add copy_dir_recursive() helper function\n- Files/dirs in target NOT in profile are PRESERVED\n\nSee fix.md for full implementation details.","status":"closed","priority":0,"issue_type":"bug","created_at":"2026-01-03T13:55:09.887364Z","updated_at":"2026-01-03T14:18:44.809811Z","closed_at":"2026-01-03T14:18:44.809817Z"} {"id":"bridle-foj.1","title":"Refactor switch_profile_with_resources to merge-based sync","description":"## Context\nReplace destructive delete-and-replace with merge-based sync to prevent data loss.\n\n## Key Changes\n- `src/config/manager/lifecycle.rs` - Remove temp-dir pattern (lines 126-158), replace with direct merge\n\n## Implementation\nRemove:\n- Lines 126-130 (temp dir creation)\n- Lines 155-158 (remove_dir_all + rename)\n\nAdd merge loop:\n- Ensure target_dir exists\n- Iterate profile entries\n- Files: std::fs::copy()\n- Dirs: remove_dir_all + copy_dir_filtered()\n- Skip MCP file (handled separately)\n\n## Patterns to Follow\n- Use existing `files::copy_dir_filtered()` at files.rs:108\n- Preserve MCP handling (lines 164-171)\n- Preserve marker handling (lines 177-180)\n\n## Success Criteria\n- [ ] cargo check passes\n- [ ] cargo clippy -- -D warnings passes\n- [ ] cargo test passes\n- [ ] Manual: unknown file in ~/.claude/ survives profile switch\n\n## References\n- Full plan: .beads/artifacts/bridle-foj/plan.md\n- Spec: fix.md","status":"closed","priority":0,"issue_type":"task","created_at":"2026-01-03T14:01:41.436506Z","updated_at":"2026-01-03T14:12:25.314211Z","closed_at":"2026-01-03T14:12:25.314221Z","dependencies":[{"issue_id":"bridle-foj.1","depends_on_id":"bridle-foj","type":"parent-child","created_at":"2026-01-03T14:01:41.438693Z","created_by":"daemon"}]} {"id":"bridle-foj.2","title":"Add integration test for file preservation","description":"## Context\nAdd test verifying unknown files are preserved during profile switch.\n\n## Key Changes\n- `tests/cli_integration.rs` OR unit test in `lifecycle.rs`\n\n## Implementation\n```rust\n#[test]\nfn test_switch_preserves_unknown_files() {\n // Setup: config dir with unknown.txt\n // Setup: profile with settings.json\n // Execute: merge-based sync\n // Assert: unknown.txt preserved AND settings.json applied\n}\n```\n\n## Success Criteria\n- [ ] Test passes with fix\n- [ ] Test would fail if fix reverted (validates correctness)\n\n## References\n- Full plan: .beads/artifacts/bridle-foj/plan.md\n- Test template: fix.md lines 126-152","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-03T14:01:45.933051Z","updated_at":"2026-01-03T14:18:37.795386Z","closed_at":"2026-01-03T14:18:37.795394Z","dependencies":[{"issue_id":"bridle-foj.2","depends_on_id":"bridle-foj","type":"parent-child","created_at":"2026-01-03T14:01:45.94277Z","created_by":"daemon"}]} +{"id":"bridle-fwq","title":"Drop Copilot harness references for Crush branch build","description":"Remove/disable CopilotCli harness references so Bridle compiles against the Crush PR dependency (which doesn't include Copilot).","acceptance_criteria":"No CopilotCli references remain; harness list/parsers updated; cargo check passes.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-09T03:32:02.098068Z","updated_at":"2026-01-09T04:20:22.963222Z","closed_at":"2026-01-09T04:20:22.963222Z","close_reason":"No Copilot refs on master; Crush branch compiles"} {"id":"bridle-g7a","title":"TUI Polish: Help Modal, Empty States, Edge Cases","description":"# TUI Polish \u0026 Edge Cases\n\n**Epic**: bridle-pm8 (TUI Redesign)\n**Phase**: 5 of 5\n**Depends on**: bridle-z15 (Dashboard), bridle-s70 (Card View)\n\n## Objective\nFinal polish pass for both TUI views: adaptive help modal, empty states, and edge case handling.\n\n## 1. Adaptive Help Modal\n\nHelp modal content adapts based on current view mode.\n\n### Implementation\n\n**File**: `src/tui/mod.rs` (update existing `render_help_modal`)\n\n```rust\nfn render_help_modal(frame: \u0026mut Frame, area: Rect, view_mode: ViewMode) {\n let mut help_text = vec![\n Line::from(Span::styled(\"Navigation\", Style::default().add_modifier(Modifier::BOLD))),\n Line::from(\" ← / h Previous harness\"),\n Line::from(\" → / l Next harness\"),\n ];\n \n // View-specific navigation\n match view_mode {\n ViewMode::Dashboard =\u003e {\n help_text.extend([\n Line::from(\" j / ↓ Next profile\"),\n Line::from(\" k / ↑ Previous profile\"),\n ]);\n }\n #[cfg(feature = \"tui-cards\")]\n ViewMode::Cards =\u003e {\n help_text.extend([\n Line::from(\" j / ↓ Next row of cards\"),\n Line::from(\" k / ↑ Previous row of cards\"),\n ]);\n }\n }\n \n help_text.extend([\n Line::from(\"\"),\n Line::from(Span::styled(\"Actions\", Style::default().add_modifier(Modifier::BOLD))),\n Line::from(\" Enter Switch to profile\"),\n Line::from(\" Space Show details\"),\n Line::from(\" n New profile\"),\n Line::from(\" d Delete profile\"),\n Line::from(\" e Edit profile\"),\n Line::from(\" r Refresh\"),\n Line::from(\"\"),\n Line::from(Span::styled(\"Harness Status\", Style::default().add_modifier(Modifier::BOLD))),\n Line::from(\" ● Managed (has active profile)\"),\n Line::from(\" + Installed with config\"),\n Line::from(\" - Binary only (no config)\"),\n Line::from(\" Not installed (grayed)\"),\n Line::from(\"\"),\n Line::from(Span::styled(\"General\", Style::default().add_modifier(Modifier::BOLD))),\n Line::from(\" ? Toggle help\"),\n Line::from(\" q / Esc Quit\"),\n ]);\n \n // Add view indicator\n help_text.push(Line::from(\"\"));\n let view_name = match view_mode {\n ViewMode::Dashboard =\u003e \"Dashboard\",\n #[cfg(feature = \"tui-cards\")]\n ViewMode::Cards =\u003e \"Cards\",\n };\n help_text.push(Line::styled(\n format!(\"Current view: {}\", view_name),\n Style::default().fg(Color::DarkGray)\n ));\n \n // Render modal (same positioning logic as before)\n let width = 45;\n let height = help_text.len() as u16 + 4;\n let x = area.width.saturating_sub(width) / 2;\n let y = area.height.saturating_sub(height) / 2;\n let modal_area = Rect::new(x, y, width.min(area.width), height.min(area.height));\n \n frame.render_widget(Clear, modal_area);\n \n let block = Block::default()\n .title(\" Help \")\n .title_alignment(Alignment::Center)\n .borders(Borders::ALL)\n .border_style(Style::default().fg(Color::Cyan))\n .style(Style::default().bg(Color::Black));\n \n Paragraph::new(help_text).block(block).render(modal_area, buf);\n}\n```\n\n## 2. Empty States\n\n### Dashboard: No Profiles\n```\n┌─ Profiles ───────────────────┬─ Details ────────────────────┐\n│ │ │\n│ │ │\n│ No profiles yet │ Select a profile to view │\n│ │ details │\n│ Press 'n' to create │ │\n│ │ │\n└──────────────────────────────┴──────────────────────────────┘\n```\n\n### Dashboard: Harness Not Installed\n```\n┌─ Profiles ───────────────────┬─ Details ────────────────────┐\n│ │ │\n│ │ │\n│ Goose is not installed │ │\n│ │ │\n│ Install it to manage │ │\n│ profiles │ │\n└──────────────────────────────┴──────────────────────────────┘\n```\n\n### Card View: No Profiles\nOnly the \"+ New Profile\" card shows.\n\n### Implementation\n\n```rust\n// In ProfileTable widget\nfn render_empty_state(area: Rect, buf: \u0026mut Buffer, message: \u0026str, hint: \u0026str) {\n let block = Block::default()\n .title(\" Profiles \")\n .borders(Borders::ALL)\n .border_style(Theme::BORDER_INACTIVE);\n \n let inner = block.inner(area);\n block.render(area, buf);\n \n let text = vec![\n Line::raw(\"\"),\n Line::styled(message, Style::default().fg(Color::DarkGray)),\n Line::raw(\"\"),\n Line::styled(hint, Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC)),\n ];\n \n Paragraph::new(text)\n .alignment(Alignment::Center)\n .render(inner, buf);\n}\n```\n\n## 3. Edge Cases\n\n### Long Profile Names\n- Truncate with ellipsis in table/card title\n- Full name shown in detail pane\n\n```rust\nfn truncate_name(name: \u0026str, max_len: usize) -\u003e String {\n if name.len() \u003c= max_len {\n name.to_string()\n } else {\n format!(\"{}…\", \u0026name[..max_len-1])\n }\n}\n```\n\n### Long Model Names \n- Already handled: split on '/' and take last segment\n- Further truncate to 18 chars in table\n\n### Many MCP Servers\n- Dashboard detail pane: Show all (scrollable if needed)\n- Card view: Truncate with \"+N more\"\n\n### Very Long MCP Server Names\n```rust\nfn truncate_server_name(name: \u0026str) -\u003e \u0026str {\n if name.len() \u003e 20 {\n \u0026name[..17].trim_end() // Leave room for \"...\"\n } else {\n name\n }\n}\n```\n\n### Unicode in Names\n- Ensure proper width calculation using `unicode-width` crate (optional)\n- Or just treat all chars as width 1 (simpler, may misalign CJK)\n\n### Terminal Resize\n- Layout recalculates on each frame (already handled by ratatui)\n- Card view columns adjust automatically\n\n### No Harness Selected (edge case)\n- Should never happen (always select first harness on init)\n- Defensive: show \"Select a harness\" message\n\n## 4. Detail Modal for Card View\n\nWhen user presses Space in card view, show a modal with full details.\n\n```rust\n#[cfg(feature = \"tui-cards\")]\nfn render_detail_modal(frame: \u0026mut Frame, profile: \u0026ProfileInfo) {\n let area = frame.area();\n \n // 80% width, 80% height, centered\n let width = (area.width * 80 / 100).min(60);\n let height = (area.height * 80 / 100).min(25);\n let x = (area.width - width) / 2;\n let y = (area.height - height) / 2;\n let modal_area = Rect::new(x, y, width, height);\n \n frame.render_widget(Clear, modal_area);\n \n // Reuse DetailPane content rendering\n let content = DetailPane::render_content(profile);\n \n let block = Block::default()\n .title(format!(\" {} \", profile.name))\n .title_alignment(Alignment::Center)\n .borders(Borders::ALL)\n .border_style(Style::default().fg(Color::Cyan))\n .style(Style::default().bg(Color::Black));\n \n Paragraph::new(content).block(block).render(modal_area, frame.buffer_mut());\n}\n```\n\n## 5. Keyboard Handling Updates\n\n```rust\n// Space key behavior\nKeyCode::Char(' ') =\u003e {\n match app.view_mode {\n ViewMode::Dashboard =\u003e {\n // Could toggle focus to detail pane for scrolling\n // Or just ignore (details always visible)\n app.status_message = Some(\"Details shown on right\".to_string());\n }\n #[cfg(feature = \"tui-cards\")]\n ViewMode::Cards =\u003e {\n app.show_detail_modal = true;\n }\n }\n}\n```\n\n## Acceptance Criteria\n\n- [ ] Help modal shows correct keybinds for current view\n- [ ] Help modal shows current view name at bottom\n- [ ] Empty state renders correctly (no profiles)\n- [ ] Empty state for uninstalled harness\n- [ ] Long names truncate with ellipsis\n- [ ] Many MCP servers handled gracefully\n- [ ] Card view detail modal works (Space key)\n- [ ] Terminal resize doesn't break layout\n\n## Testing\n\n```bash\n# Test empty states\ncargo run -- profile delete claude-code default\ncargo run -- tui\n\n# Test with many MCPs (edit config manually)\n# Test terminal resize (drag window while TUI running)\n# Test help modal (press ?)\n\n# Card view tests (if feature enabled)\ncargo run --features tui-cards -- tui\n# Press Space on a card to see detail modal\n```\n","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-28T17:10:49.630819Z","updated_at":"2025-12-28T19:25:44.256268Z","closed_at":"2025-12-28T19:25:44.256268Z","close_reason":"Added view-mode-aware help modal, empty state rendering for ProfileTable, truncation already existed. All Polish phase tasks complete.","dependencies":[{"issue_id":"bridle-g7a","depends_on_id":"bridle-z15","type":"blocks","created_at":"2025-12-28T17:10:58.81144Z","created_by":"daemon"},{"issue_id":"bridle-g7a","depends_on_id":"bridle-s70","type":"blocks","created_at":"2025-12-28T17:10:58.885729Z","created_by":"daemon"}]} {"id":"bridle-gz6","title":"TUI Improvement Plan - First-run UX fixes","description":"Fix first-run TUI experience: empty profile lists, broken help, CLI-only profile creation, missing guidance. 5 beads covering auto-bootstrap, help modal, inline creation, empty states, and status indicators.","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-28T10:15:54.931178Z","updated_at":"2025-12-28T10:51:48.624329Z","closed_at":"2025-12-28T10:51:48.624329Z","close_reason":"All child beads complete: auto-bootstrap (gz6.1), help modal (gz6.2), inline profile creation (gz6.3), empty state UX (gz6.6), status indicators (gz6.7). First-run TUI experience significantly improved."} {"id":"bridle-gz6.1","title":"Auto-bootstrap profiles from current harness config","description":"On TUI launch or bridle init, detect installed harnesses with existing configs and auto-create a 'default' profile from their current configuration. Add ProfileManager::create_from_current_if_missing(). Idempotent - running twice doesn't duplicate. Skips BinaryOnly/ConfigOnly/NotInstalled harnesses.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-28T10:16:03.76339Z","updated_at":"2025-12-28T10:37:47.436226Z","closed_at":"2025-12-28T10:37:47.436226Z","close_reason":"Implemented auto-bootstrap profiles: added ProfileManager::create_from_current_if_missing(), hooked into TUI App::new() and bridle init. Only bootstraps FullyInstalled harnesses, idempotent.","dependencies":[{"issue_id":"bridle-gz6.1","depends_on_id":"bridle-gz6","type":"parent-child","created_at":"2025-12-28T10:16:03.765172Z","created_by":"daemon"}]} @@ -91,6 +99,7 @@ {"id":"bridle-krm","title":"Verify Nushell integration end-to-end","description":"## Parent Epic\nbridle-xgw: Nushell-friendly structured output for CLI display commands\n\n## Objective\nFinal verification that all JSON output works correctly and auto-detection functions properly.\n\n## Verification Checklist\n\n### 1. JSON Validity\n```bash\n# All commands should produce valid JSON\ncargo run -- -o json status | jq . \u003e /dev/null \u0026\u0026 echo \"✓ status JSON valid\"\ncargo run -- -o json profile list opencode | jq . \u003e /dev/null \u0026\u0026 echo \"✓ profile list JSON valid\"\ncargo run -- -o json profile show opencode test | jq . \u003e /dev/null \u0026\u0026 echo \"✓ profile show JSON valid\"\n```\n\n### 2. Auto-Detection\n```bash\n# Simulate Nushell environment\nNU_VERSION=0.99.0 cargo run -- status | head -1 # Should start with { or [\nNU_VERSION=0.99.0 cargo run -- --output text status | head -1 # Should be text\n\n# Without NU_VERSION (normal shell)\nunset NU_VERSION\ncargo run -- status | head -1 # Should be text (e.g., \"Harness Status:\")\n```\n\n### 3. Flag Variations\n```bash\n# Global flag positions\ncargo run -- --output json status\ncargo run -- -o json status\ncargo run -- status --output json # Should also work (global)\n\n# All three formats\ncargo run -- -o text status\ncargo run -- -o json status \ncargo run -- -o auto status\n```\n\n### 4. Edge Cases\n```bash\n# Empty results\ncargo run -- -o json profile list nonexistent-harness # Should error gracefully\ncargo run -- -o json profile show opencode nonexistent # Should error gracefully\n\n# Profiles with extraction errors\n# (verify extraction_errors field is populated if parsing fails)\n```\n\n### 5. jq Integration Examples\n```bash\n# Status queries\ncargo run -- -o json status | jq '.harnesses[] | select(.installed) | .name'\ncargo run -- -o json status | jq '.active_profiles | map(.harness)'\n\n# Profile queries\ncargo run -- -o json profile list opencode | jq '.[].name'\ncargo run -- -o json profile list opencode | jq '.[] | select(.is_active)'\ncargo run -- -o json profile show opencode test | jq '.mcp_servers[].name'\n```\n\n### 6. Quality Gates\n```bash\ncargo fmt -- --check\ncargo clippy -- -D warnings\ncargo test\n```\n\n## Success Criteria\n- [ ] All JSON outputs are valid (parseable by jq)\n- [ ] NU_VERSION detection works correctly\n- [ ] --output flag overrides auto-detection\n- [ ] Error cases produce appropriate error messages (not invalid JSON)\n- [ ] All quality gates pass\n- [ ] Help text shows --output flag with description\n\n## Dependencies\n- Depends on: bridle-apb (status JSON)\n- Depends on: bridle-lfo (profile list JSON)\n- Depends on: bridle-x1d (profile show JSON)","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-28T22:35:41.54899Z","updated_at":"2025-12-29T21:45:38.478299Z","closed_at":"2025-12-29T21:45:38.478299Z","close_reason":"Removed per user request","dependencies":[{"issue_id":"bridle-krm","depends_on_id":"bridle-apb","type":"blocks","created_at":"2025-12-28T22:36:03.832417Z","created_by":"daemon"},{"issue_id":"bridle-krm","depends_on_id":"bridle-lfo","type":"blocks","created_at":"2025-12-28T22:36:03.907671Z","created_by":"daemon"},{"issue_id":"bridle-krm","depends_on_id":"bridle-x1d","type":"blocks","created_at":"2025-12-28T22:36:03.984387Z","created_by":"daemon"}]} {"id":"bridle-lfo","title":"Add JSON output to bridle profile list command","description":"## Parent Epic\nbridle-xgw: Nushell-friendly structured output for CLI display commands\n\n## Objective\nRefactor `bridle profile list \u003charness\u003e` to support JSON output.\n\n## File to Modify: src/cli/profile.rs\n\n### Current Flow\n`list_profiles()` calls `ProfileManager::list()` which returns `Vec\u003cProfileInfo\u003e`, then prints each profile with println!.\n\n### Refactored list_profiles()\n\n```rust\nuse crate::cli::output::{ResolvedFormat, output_list};\n\npub fn list_profiles(harness: \u0026str, format: ResolvedFormat) -\u003e Result\u003c()\u003e {\n let harness_kind = HarnessKind::from_id(harness)?;\n let manager = ProfileManager::new(harness_kind);\n let profiles = manager.list()?;\n\n if profiles.is_empty() {\n // Empty case - still valid JSON\n output_list(\u0026profiles, format, |_| {\n println!(\"No profiles found for {}\", harness_kind.display_name());\n });\n return Ok(());\n }\n\n output_list(\u0026profiles, format, |profiles| {\n println!(\"\\nProfiles for {}:\", harness_kind.display_name());\n println!(\"{:-\u003c50}\", \"\");\n for profile in profiles {\n let active = if profile.is_active { \" (active)\" } else { \"\" };\n println!(\" • {}{}\", profile.name, active);\n }\n });\n\n Ok(())\n}\n```\n\n### JSON Output Example\n\n```json\n[\n {\n \"name\": \"test\",\n \"harness_id\": \"opencode\",\n \"is_active\": true,\n \"path\": \"/Users/d0/.config/bridle/profiles/opencode/test\",\n \"mcp_servers\": [{\"name\": \"filesystem\", \"enabled\": true}],\n \"skills\": {\"items\": [\"rust\"], \"directory_exists\": true},\n \"commands\": {\"items\": [], \"directory_exists\": false},\n \"plugins\": {\"items\": [], \"directory_exists\": false},\n \"agents\": {\"items\": [], \"directory_exists\": false},\n \"rules_file\": null,\n \"theme\": \"dark\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"extraction_errors\": []\n },\n {\n \"name\": \"minimal\",\n \"harness_id\": \"opencode\",\n \"is_active\": false,\n ...\n }\n]\n```\n\n### Nushell Usage Examples\n\n```bash\n# List all profiles with active status\nbridle profile list opencode | from json | select name is_active\n\n# Find active profile\nbridle profile list claude | from json | where is_active == true | get name\n\n# Count MCP servers across profiles\nbridle profile list opencode | from json | each { |p| $p.mcp_servers | length } | math sum\n\n# Filter profiles with specific plugin\nbridle profile list opencode | from json | where (\"some-plugin\" in $it.plugins.items)\n```\n\n## Verification\n```bash\ncargo run -- profile list opencode # Text output\ncargo run -- -o json profile list opencode # JSON array\ncargo run -- -o json profile list opencode | jq '.[0].name'\n```\n\n## Dependencies\n- Depends on: bridle-dsh (Serialize on ProfileInfo)\n- Depends on: bridle-nfd (global --output flag)","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-28T22:35:06.140913Z","updated_at":"2025-12-29T21:56:38.925631Z","closed_at":"2025-12-29T21:56:38.925631Z","close_reason":"Added JSON output support to profile list using output_list with ProfileListEntry struct","dependencies":[{"issue_id":"bridle-lfo","depends_on_id":"bridle-nfd","type":"blocks","created_at":"2025-12-28T22:36:03.532922Z","created_by":"daemon"},{"issue_id":"bridle-lfo","depends_on_id":"bridle-dsh","type":"blocks","created_at":"2025-12-28T22:36:03.608823Z","created_by":"daemon"}]} {"id":"bridle-mon","title":"Document harness config schemas for profile parsing","description":"Create documentation capturing the actual config format for each harness to guide profile parsing implementation. See research findings from profile parsing bug investigation.","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-28T14:37:58.257442Z","updated_at":"2025-12-28T15:56:05.826509Z","closed_at":"2025-12-28T15:56:05.826509Z","close_reason":"Documentation already exists in AGENTS.md (lines 327-400): test profile locations, verification commands, config structures for all three harnesses, and known issues table. Code implementation matches documented schemas."} +{"id":"bridle-mth","title":"Open draft PR for feature/crush-support","description":"Create feature/crush-support branch push and open a draft PR in Bridle.","acceptance_criteria":"Draft PR exists; branch pushed; PR description references harness-barn PR #7 dependency.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-09T03:32:02.684825Z","updated_at":"2026-01-09T04:23:03.672254Z","closed_at":"2026-01-09T04:23:03.672254Z","close_reason":"Draft PR opened: https://github.com/neiii/bridle/pull/26"} {"id":"bridle-nfd","title":"Add global --output flag to CLI","description":"## Parent Epic\nbridle-xgw: Nushell-friendly structured output for CLI display commands\n\n## Objective\nAdd global `--output` / `-o` flag to the main CLI struct so it's available to all subcommands.\n\n## File to Modify: src/main.rs\n\n### Current State\n```rust\n#[derive(Parser)]\n#[command(author, version, about, long_about = None)]\nstruct Cli {\n #[command(subcommand)]\n command: Commands,\n}\n```\n\n### Target State\n```rust\nuse crate::cli::output::OutputFormat;\n\n#[derive(Parser)]\n#[command(author, version, about, long_about = None)]\nstruct Cli {\n /// Output format: text, json, or auto (default: auto)\n /// \n /// Auto-detects Nushell and uses JSON, otherwise text.\n #[arg(long, short = 'o', default_value = \"auto\", global = true)]\n output: OutputFormat,\n\n #[command(subcommand)]\n command: Commands,\n}\n```\n\n## Passing Format to Commands\n\nUpdate command dispatch to pass the resolved format:\n\n```rust\nfn main() -\u003e Result\u003c()\u003e {\n let cli = Cli::parse();\n let format = cli.output.resolve();\n\n match cli.command {\n Commands::Status =\u003e cli::status::display_status(format)?,\n Commands::Profile(cmd) =\u003e cli::profile::handle_command(cmd, format)?,\n // ... other commands (may ignore format for now)\n }\n Ok(())\n}\n```\n\n## Command Signature Updates\n\nCommands that support output format will need updated signatures:\n\n```rust\n// status.rs\npub fn display_status(format: ResolvedFormat) -\u003e Result\u003c()\u003e\n\n// profile.rs \npub fn handle_command(cmd: ProfileCommands, format: ResolvedFormat) -\u003e Result\u003c()\u003e\npub fn list_profiles(harness: \u0026str, format: ResolvedFormat) -\u003e Result\u003c()\u003e\npub fn show_profile(harness: \u0026str, name: \u0026str, format: ResolvedFormat) -\u003e Result\u003c()\u003e\n```\n\n## Usage Examples\n\n```bash\n# Global flag works with any subcommand\nbridle --output json status\nbridle -o json profile list claude\nbridle profile show opencode test --output json\n\n# Short form\nbridle -o json status\n```\n\n## Verification\n```bash\ncargo run -- --help # Should show --output flag\ncargo run -- -o json status # Should output JSON\ncargo run -- status --output text # Should output text\n```\n\n## Dependencies\n- Depends on: bridle-3do (OutputFormat enum)","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-28T22:34:20.60064Z","updated_at":"2025-12-29T21:47:53.638887Z","closed_at":"2025-12-29T21:47:53.638887Z","close_reason":"Added global --output flag to Cli struct, resolves format in main(), passes to status/profile commands","dependencies":[{"issue_id":"bridle-nfd","depends_on_id":"bridle-3do","type":"blocks","created_at":"2025-12-28T22:36:03.382004Z","created_by":"daemon"}]} {"id":"bridle-nr5","title":"Skill discovery via skills-locate","description":"Wrap skills-locate library to discover skills from GitHub repos and fetch SKILL.md content","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-02T01:19:42.429792Z","updated_at":"2026-01-02T02:18:52.694458Z","closed_at":"2026-01-02T02:18:52.694458Z","close_reason":"Implemented skill discovery module with skills-locate integration","dependencies":[{"issue_id":"bridle-nr5","depends_on_id":"bridle-aan","type":"blocks","created_at":"2026-01-02T01:19:57.571112Z","created_by":"daemon"}]} {"id":"bridle-osa","title":"Fix TUI not redrawing after external editor exits","description":"When editing a profile/config through the TUI and exiting the external editor ($EDITOR), the TUI fails to restore terminal state and redraw. Screen shows whatever was behind the terminal with only 'Edited X' message at bottom. Reproducible with :q in vim.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-28T16:35:27.743552Z","updated_at":"2025-12-28T16:41:15.720211Z","closed_at":"2025-12-28T16:41:15.720211Z","close_reason":"Fixed by adding needs_full_redraw flag that triggers terminal.clear() after editor exits"} @@ -111,6 +120,7 @@ {"id":"bridle-srf.4","title":"Manual verification of fix across all harnesses","description":"## Context\nVerify the fix works end-to-end with real harnesses.\n\n## Test Procedure\n```bash\n# Test with OpenCode\ncargo run -- profile create opencode test-a --from-current\ncargo run -- install anthropics/skills # Install skill to test-a\ncargo run -- profile create opencode test-b --from-current\ncargo run -- profile switch opencode test-b\ncargo run -- profile show opencode test-b # Should NOT have skills\n\n# Verify isolation persists\ncargo run -- profile switch opencode test-a\ncargo run -- profile show opencode test-a # Should have skills\ncargo run -- profile switch opencode test-b\ncargo run -- profile show opencode test-b # Should still NOT have skills\n\n# Verify backup created\nls ~/.config/bridle/backups/opencode/\n\n# Cleanup\ncargo run -- profile delete opencode test-a\ncargo run -- profile delete opencode test-b\n```\n\n## Harnesses to Test\n- [ ] OpenCode\n- [ ] Claude Code\n- [ ] Goose\n\n## Success Criteria\n- [ ] Skills installed to test-a stay in test-a only\n- [ ] test-b remains empty throughout all switches\n- [ ] Backups visible in ~/.config/bridle/backups/\n\n## References\n- Full plan: `.beads/artifacts/bridle-srf/plan.md` (Phase 4)","acceptance_criteria":"- All 4 harnesses tested manually\n- No skill leakage observed\n- profile diff shows expected differences\n- Screenshots or terminal output captured","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-07T20:42:17.670586Z","updated_at":"2026-01-07T22:38:12.172442Z","closed_at":"2026-01-07T22:38:12.172442Z","close_reason":"Closed","dependencies":[{"issue_id":"bridle-srf.4","depends_on_id":"bridle-srf","type":"parent-child","created_at":"2026-01-07T20:42:17.679483Z","created_by":"daemon"},{"issue_id":"bridle-srf.4","depends_on_id":"bridle-srf.2","type":"blocks","created_at":"2026-01-07T20:42:23.685579Z","created_by":"daemon"}]} {"id":"bridle-tel","title":"Add harness trait abstraction for testability","description":"Create a trait abstraction for harness config operations to enable unit testing of ProfileManager.\n\n- Abstract config path access\n- Abstract installation_status checks \n- Create MockHarness for tests\n- Add test for switch_profile preserving edits","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-28T11:49:24.952265Z","updated_at":"2025-12-28T11:55:27.911433Z","closed_at":"2025-12-28T11:55:27.911433Z","close_reason":"Added HarnessConfig trait abstraction for testability. MockHarness enables unit testing. Added test proving switch_profile preserves edits."} {"id":"bridle-un7","title":"OpenCode model extraction misses nested agent.general.model path","description":"extract_model_opencode() only checks top-level 'model' field but OpenCode configs commonly use nested 'agent.general.model' path.\n\n## Verification Steps\n\n1. Check the test profile config:\n ```bash\n cat ~/.config/bridle/profiles/opencode/test/opencode.jsonc | grep -A2 'agent'\n # Should show agent.general.model path\n ```\n\n2. Run profile show:\n ```bash\n cargo run -- profile show opencode test\n ```\n\n3. **Before fix**: Model shows '(not set)' despite agent.general.model being configured\n\n4. **After fix**: Model should display the nested value, e.g.:\n ```\n Model: anthropic/claude-sonnet-4-5\n ```\n\n5. Test both config styles work:\n - Top-level: `{ \"model\": \"openai/gpt-4o\" }`\n - Nested: `{ \"agent\": { \"general\": { \"model\": \"anthropic/claude-sonnet-4-5\" } } }`\n \n Priority: top-level should override nested (or document which takes precedence)\n\n## Code Location\n- File: src/config/manager.rs\n- Function: extract_model_opencode() (around line 389)","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-28T14:24:20.123931Z","updated_at":"2025-12-28T14:56:35.248422Z","closed_at":"2025-12-28T14:56:35.248422Z","close_reason":"Fixed: extract_model_opencode now checks nested agent.general.model path with top-level taking priority"} +{"id":"bridle-v74","title":"Switch harness-barn deps to Crush PR branch","description":"Point harness-locate + skills-locate deps at harness-barn PR #7 branch (edlsh/feat/add-crush-harness) to consume HarnessKind::Crush.","acceptance_criteria":"Cargo builds with the new dependency source; Cargo.lock updated; cargo check passes.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-09T03:32:02.002482Z","updated_at":"2026-01-09T04:20:22.317932Z","closed_at":"2026-01-09T04:20:22.317932Z","close_reason":"Pinned harness-barn to Crush PR branch"} {"id":"bridle-wo4","title":"Standardize error reporting","description":"## Problem\n\nMixed approaches to error reporting:\n- 54 eprintln!() calls in CLI commands (profile.rs: 41, config_cmd.rs: 8, init.rs: 5)\n- color_eyre chain at top level\n- Direct string returns in some places\n\n## Recommendation\n\nReturn Result\u003cT\u003e from all functions, let color_eyre handle presentation:\n\n```rust\n// Instead of:\neprintln!(\"Error: {}\", e);\nreturn;\n\n// Do:\nreturn Err(e.into());\n// Let main.rs handle presentation\n```\n\n## Benefits\n\n- Consistent error formatting\n- Errors can be tested\n- color_eyre provides better context\n- Cleaner function signatures\n\n## Acceptance Criteria\n\n- [ ] Zero eprintln! calls for error reporting in CLI\n- [ ] All CLI commands return Result\u003c()\u003e\n- [ ] main.rs handles all error presentation\n- [ ] Error context preserved via color_eyre","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-30T19:03:18.372209Z","updated_at":"2025-12-30T22:39:06.389419Z","closed_at":"2025-12-30T22:39:06.389419Z","close_reason":"Refactored 54 eprintln! calls to use Result-based error handling. Added UnknownHarness, CommandFailed, UnknownSetting, InvalidValue error variants. All functions now return Result\u003c()\u003e and propagate errors with ?."} {"id":"bridle-wxn","title":"Update harness-locate to v0.1.2+ with Claude agents/plugins support","description":"## Dependency Update Required\n\nOnce harness-locate is updated to support Claude Code agents and plugins, update bridle's Cargo.toml to use the new version.\n\n## harness-locate Changes Expected\n- `agents()` returns `Some(DirectoryResource)` for Claude Code (path: ~/.claude/agents/)\n- `plugins()` returns `Some(DirectoryResource)` for Claude Code (path: ~/.claude/plugins/)\n\n## Bridle Changes After Update\n1. Update `Cargo.toml` to use harness-locate \u003e= 0.1.2\n2. Remove any workarounds for missing Claude agents/plugins support\n3. Test installation to all harnesses\n\n## Blocks\n- bridle-b7d (use harness-locate paths)\n- bridle-2xr (check harness support)\n- bridle-z77 (Claude plugin structure)","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-02T15:47:36.302333Z","updated_at":"2026-01-02T16:10:17.357957Z","closed_at":"2026-01-02T16:10:17.357957Z","close_reason":"harness-locate updated with Claude agents/plugins support via local path dependency"} {"id":"bridle-x0p","title":"Research: Ratatui TUI testing best practices","description":"## Summary\nResearch and document best practices for testing ratatui TUI applications. Currently the TUI has no automated tests.\n\n## Research Questions\n1. What testing approaches exist for ratatui TUIs?\n2. How to use TestBackend for rendering tests?\n3. How to do snapshot testing with insta crate?\n4. How to test keyboard/mouse event handling?\n5. What's the recommended test structure?\n\n## Preliminary Findings\n- **TestBackend**: Built-in mock backend for rendering tests\n- **Snapshot testing**: insta crate is officially supported by ratatui\n- **Event testing**: Extract logic into testable methods, test state transitions\n- **Best practices**: \n - Widget unit tests with direct Buffer\n - Integration tests with TestBackend\n - Use rstest for parameterized tests\n - Separate rendering from event handling logic\n\n## Deliverables\n- [ ] Document in improvements.md or separate TUI_TESTING.md\n- [ ] Include code examples for each approach\n- [ ] Recommend which tests to add first (highest value)\n- [ ] Create follow-up implementation beads for actual tests","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-30T22:09:04.731088Z","updated_at":"2025-12-30T22:50:10.409649Z","closed_at":"2025-12-30T22:50:10.409649Z","close_reason":"Documented TUI testing best practices: TestBackend usage, insta snapshots, event handling tests, recommended test structure. Created TUI_TESTING.md with examples and priority tests for bridle."} diff --git a/src/config/manager/mod.rs b/src/config/manager/mod.rs index 4a568ef..40381e1 100644 --- a/src/config/manager/mod.rs +++ b/src/config/manager/mod.rs @@ -291,9 +291,28 @@ mod tests { DirectoryStructure, extract_resource_summary, list_files_matching, list_subdirs_with_file, }; use super::*; + use std::ffi::OsString; use std::fs; + use std::sync::{Mutex, OnceLock}; use tempfile::TempDir; + static TEST_ENV_LOCK: OnceLock> = OnceLock::new(); + + struct TestEnvGuard { + _lock: std::sync::MutexGuard<'static, ()>, + prev: Option, + } + + impl Drop for TestEnvGuard { + fn drop(&mut self) { + if let Some(prev) = &self.prev { + unsafe { std::env::set_var("BRIDLE_CONFIG_DIR", prev) }; + } else { + unsafe { std::env::remove_var("BRIDLE_CONFIG_DIR") }; + } + } + } + struct MockHarness { id: String, config_dir: PathBuf, @@ -348,17 +367,21 @@ mod tests { } } - fn setup_test_env(temp: &TempDir) { + fn setup_test_env(temp: &TempDir) -> TestEnvGuard { + let lock = TEST_ENV_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap(); + + let prev = std::env::var_os("BRIDLE_CONFIG_DIR"); let bridle_config_dir = temp.path().join("bridle_config"); fs::create_dir_all(&bridle_config_dir).unwrap(); - // SAFETY: Tests run single-threaded (--test-threads=1), no concurrent env access unsafe { std::env::set_var("BRIDLE_CONFIG_DIR", &bridle_config_dir) }; + + TestEnvGuard { _lock: lock, prev } } #[test] fn switch_profile_preserves_edits() { let temp = TempDir::new().unwrap(); - setup_test_env(&temp); + let _env = setup_test_env(&temp); let profiles_dir = temp.path().join("profiles"); let live_config = temp.path().join("live_config"); fs::create_dir_all(&live_config).unwrap(); @@ -431,7 +454,7 @@ mod tests { #[test] fn switch_profile_restores_mcp_config() { let temp = TempDir::new().unwrap(); - setup_test_env(&temp); + let _env = setup_test_env(&temp); let profiles_dir = temp.path().join("profiles"); let live_config = temp.path().join("live_config"); let mcp_file = temp.path().join(".mcp.json"); @@ -468,7 +491,7 @@ mod tests { #[test] fn switch_preserves_unknown_files() { let temp = TempDir::new().unwrap(); - setup_test_env(&temp); + let _env = setup_test_env(&temp); let profiles_dir = temp.path().join("profiles"); let live_config = temp.path().join("live_config"); fs::create_dir_all(&live_config).unwrap(); @@ -592,7 +615,7 @@ mod tests { #[test] fn switch_saves_new_directories_to_old_profile() { let temp = TempDir::new().unwrap(); - setup_test_env(&temp); + let _env = setup_test_env(&temp); let profiles_dir = temp.path().join("profiles"); let live_config = temp.path().join("live_config"); fs::create_dir_all(&live_config).unwrap(); @@ -629,7 +652,7 @@ mod tests { #[test] fn deep_nesting_survives_multiple_round_trips() { let temp = TempDir::new().unwrap(); - setup_test_env(&temp); + let _env = setup_test_env(&temp); let profiles_dir = temp.path().join("profiles"); let live_config = temp.path().join("live_config"); fs::create_dir_all(&live_config).unwrap(); @@ -676,7 +699,7 @@ mod tests { #[test] fn wide_directory_structure_preserved() { let temp = TempDir::new().unwrap(); - setup_test_env(&temp); + let _env = setup_test_env(&temp); let profiles_dir = temp.path().join("profiles"); let live_config = temp.path().join("live_config"); fs::create_dir_all(&live_config).unwrap(); @@ -799,7 +822,7 @@ mod tests { #[test] fn switch_profile_does_not_leak_skills_to_other_profiles() { let temp = TempDir::new().unwrap(); - setup_test_env(&temp); + let _env = setup_test_env(&temp); let profiles_dir = temp.path().join("profiles"); let live_config = temp.path().join("live_config"); fs::create_dir_all(&live_config).unwrap(); @@ -868,7 +891,7 @@ mod tests { #[test] fn switch_to_empty_profile_clears_harness_resources() { let temp = TempDir::new().unwrap(); - setup_test_env(&temp); + let _env = setup_test_env(&temp); let profiles_dir = temp.path().join("profiles"); let live_config = temp.path().join("live_config"); fs::create_dir_all(&live_config).unwrap(); @@ -926,7 +949,7 @@ mod tests { #[test] fn switch_profile_does_not_leak_agents() { let temp = TempDir::new().unwrap(); - setup_test_env(&temp); + let _env = setup_test_env(&temp); let profiles_dir = temp.path().join("profiles"); let live_config = temp.path().join("live_config"); fs::create_dir_all(&live_config).unwrap(); @@ -984,7 +1007,7 @@ mod tests { #[test] fn switch_profile_does_not_leak_commands() { let temp = TempDir::new().unwrap(); - setup_test_env(&temp); + let _env = setup_test_env(&temp); let profiles_dir = temp.path().join("profiles"); let live_config = temp.path().join("live_config"); fs::create_dir_all(&live_config).unwrap(); @@ -1038,7 +1061,7 @@ mod tests { #[test] fn switch_profile_does_not_leak_multiple_resource_types() { let temp = TempDir::new().unwrap(); - setup_test_env(&temp); + let _env = setup_test_env(&temp); let profiles_dir = temp.path().join("profiles"); let live_config = temp.path().join("live_config"); fs::create_dir_all(&live_config).unwrap(); @@ -1123,7 +1146,7 @@ mod tests { #[test] fn switch_profile_isolation_opencode_style() { let temp = TempDir::new().unwrap(); - setup_test_env(&temp); + let _env = setup_test_env(&temp); let profiles_dir = temp.path().join("profiles"); let live_config = temp.path().join("live_config"); fs::create_dir_all(&live_config).unwrap(); @@ -1180,7 +1203,7 @@ mod tests { #[test] fn switch_profile_isolation_claude_style() { let temp = TempDir::new().unwrap(); - setup_test_env(&temp); + let _env = setup_test_env(&temp); let profiles_dir = temp.path().join("profiles"); let live_config = temp.path().join("live_config"); fs::create_dir_all(&live_config).unwrap(); @@ -1233,7 +1256,7 @@ mod tests { #[test] fn switch_profile_isolation_goose_style() { let temp = TempDir::new().unwrap(); - setup_test_env(&temp); + let _env = setup_test_env(&temp); let profiles_dir = temp.path().join("profiles"); let live_config = temp.path().join("live_config"); fs::create_dir_all(&live_config).unwrap(); @@ -1284,7 +1307,7 @@ mod tests { #[test] fn comprehensive_resource_leak_verification() { let temp = TempDir::new().unwrap(); - setup_test_env(&temp); + let _env = setup_test_env(&temp); let profiles_dir = temp.path().join("profiles"); let live_config = temp.path().join("live_config"); let mcp_config = temp.path().join("mcp.json"); @@ -1484,7 +1507,7 @@ mod tests { #[test] fn mcp_config_does_not_leak_between_profiles() { let temp = TempDir::new().unwrap(); - setup_test_env(&temp); + let _env = setup_test_env(&temp); let profiles_dir = temp.path().join("profiles"); let live_config = temp.path().join("live_config"); let mcp_config = temp.path().join("mcp.json"); From ece364b7dda165a1620e62d3c0569f3682c6fda2 Mon Sep 17 00:00:00 2001 From: neiii <1mrtemeck1@gmail.com> Date: Fri, 9 Jan 2026 04:55:07 +0000 Subject: [PATCH 2/2] chore(release): 0.2.6 --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc552df..7ef92ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.6] - 2026-01-09 + +### Fixed + +- Resolve TUI and profile switching performance issues (#25) +- Implement complete profile resource isolation (#24) + ## [0.2.5] - 2026-01-06 ### Added diff --git a/Cargo.lock b/Cargo.lock index bb0b1b6..7bea10d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -170,7 +170,7 @@ dependencies = [ [[package]] name = "bridle" -version = "0.2.5" +version = "0.2.6" dependencies = [ "assert_cmd", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 5d5a3bc..4551ac1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bridle" -version = "0.2.5" +version = "0.2.6" edition = "2024" description = "Unified configuration manager for AI coding assistants (Claude Code, OpenCode, Goose, AMP Code)" license = "MIT"