Skip to content

feat: implement DevSync v2.0 AI-powered config distribution (#63) - #79

Merged
troylar merged 14 commits into
mainfrom
issue-63-v2-llm-foundation
Feb 21, 2026
Merged

feat: implement DevSync v2.0 AI-powered config distribution (#63)#79
troylar merged 14 commits into
mainfrom
issue-63-v2-llm-foundation

Conversation

@troylar

@troylar troylar commented Feb 21, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add AI-powered extraction and installation — LLM reads project configs to produce abstract practice declarations, and adapts incoming practices to recipients' existing setups with intelligent merging
  • Add LLM provider abstraction — HTTP-only (no SDK deps) support for Anthropic, OpenAI, and OpenRouter via httpx
  • Simplify CLI from 20+ to 6 commandssetup, tools, extract, install, list, uninstall
  • Remove deprecated subsystems — library, template, TUI, legacy install/download/update/delete (49 files deleted, ~18K lines removed)
  • Update ReadTheDocs documentation — complete rewrite of docs site for v2 architecture (13 obsolete files deleted, 30+ files rewritten, 2 new pages)

Changes

New: LLM Provider Layer (devsync/llm/)

  • provider.py — Abstract LLMProvider ABC, LLMResponse, resolve_provider() with env var auto-detection
  • anthropic.py, openai_provider.py, openrouter.py — HTTP-only implementations via httpx
  • config.py — API key resolution from env vars, ~/.devsync/config.yaml (never stores keys)
  • prompts.py — Extraction, adaptation, and merge prompt templates
  • response_models.pyExtractionResult, AdaptationPlan, structured response parsers

New: Core AI Engine (devsync/core/)

  • practice.pyPracticeDeclaration, MCPDeclaration, CredentialSpec dataclasses
  • extractor.pyPracticeExtractor with AI and no-AI fallback paths
  • adapter.pyPracticeAdapter with LLM-powered semantic merge
  • package_manifest_v2.py — v2 manifest parser with v1 backward compatibility
  • mcp_credential_prompter.py — Interactive credential prompting for MCP servers

New: CLI Commands (devsync/cli/)

  • setup.py — Interactive LLM provider configuration
  • extract.py — Practice extraction with --upgrade for v1→v2 conversion
  • install_v2.py — AI-powered installation with plan review
  • list_v2.py — Simplified package listing
  • main.py — Rewritten for v2 command surface

Removed (49 files)

  • Library system: download.py, update.py, delete.py, storage/library.py
  • Template system: 7 template_*.py CLI files, storage/template_*.py, core/template_manifest.py
  • TUI: tui/installer.py
  • Legacy commands: install.py, install_new.py, list.py, package.py, package_create.py, package_install.py
  • MCP standalone: mcp_configure.py, mcp_install.py, mcp_sync.py
  • 24 corresponding test files

Documentation — ReadTheDocs (docs/)

  • Deleted 13 obsolete pages: cli/download.md, cli/update.md, cli/delete.md, cli/package.md, entire mcp-server/ section (7 files), tutorials/ai-merge-workflow.md, tutorials/ci-cd-integration.md
  • Created 2 new CLI pages: cli/setup.md, cli/extract.md
  • Rewrote core pages: index.md, quickstart.md, concepts.md, cli/index.md, cli-reference.md
  • Rewrote CLI pages: install.md, list.md, uninstall.md
  • Rewrote packages section: index.md, creating.md, components.md, installing.md, examples.md
  • Rewrote 5 tutorials for v2 extract/install workflow
  • Updated 7 IDE integration pages, 4 advanced pages, 2 reference pages
  • Updated mkdocs.yml nav structure
  • mkdocs build --strict passes with no warnings

Other Documentation

  • VISION.md — Updated for AI-powered config distribution direction
  • CLAUDE.md — Updated architecture, module structure, and command reference
  • README.md — Rewritten for two-command flow (extract + install)

Issue References

Closes #63, Closes #64, Closes #65, Closes #66, Closes #67, Closes #68
Closes #69, Closes #70, Closes #71, Closes #72, Closes #73, Closes #74
Closes #75, Closes #76, Closes #77, Closes #78

Test Plan

  • Unit tests pass: 1037 passed (pytest tests/unit/)
  • Lint passes: ruff check — all checks passed
  • Format passes: black --check — all files formatted
  • Type check passes: mypy — no issues in 86 source files
  • mkdocs build --strict passes — no broken links or warnings
  • devsync tools detects AI tools without API key
  • devsync setup configures API key interactively
  • devsync extract produces devsync-package.yaml
  • devsync install ./package adapts and installs
  • devsync install ./v1-package works via file-copy (backward compat)
  • devsync extract --no-ai produces v1-style package
  • devsync extract --upgrade ./v1-pkg converts to v2

Security Considerations

  • Path traversal protection: Added resolve() + prefix checks for manifest ref.file paths; rejected instruction names containing .., /, or \
  • API key safety: Keys read from env vars at runtime only; ~/.devsync/config.yaml stores provider name and model preference, never key values
  • Credential handling: MCP credentials prompted interactively, written to .devsync/.env; never stored in manifests or installation records
  • HTTP-only LLM calls: Uses httpx with default TLS verification; no SDK dependencies

Known Remaining Issues

  • _install_v2_fallback does not use the conflict strategy parameter
  • _clone_source temp directory not cleaned up after installation
  • build_mcp_config result is discarded (MCP config not persisted to tool configs)
  • Source-file-to-practice index mapping in extractor is fragile for multi-practice files
  • 1 pre-existing test failure: test_main_module_execution (Typer/Click version incompatibility with --help)
  • Credential input not masked in MCP prompter (should use password=True)
  • Path traversal guard in install_v2.py uses str.startswith instead of is_relative_to

Vision Alignment

Supports: zero-friction distribution, IDE-agnostic, git-as-distribution, lean CLI, credential safety, standards-first. Only new dependency is httpx (HTTP-only LLM calls, no SDK deps). All AI features degrade gracefully to file-copy mode without API keys.


Generated with Claude Code

… templates (#63, #64, #65)

Phase 0 of v2.0: HTTP-only LLM providers (Anthropic, OpenAI, OpenRouter),
PracticeDeclaration/MCPDeclaration models, prompt templates, and response
models with full test coverage.
…rser (#66, #67, #68)

- Setup command for interactive LLM provider configuration
- PracticeExtractor with AI and no-AI fallback paths
- V2 package manifest parser with v1 backwards compatibility
…ed prompting (#69, #70, #71, #72)

- Extract command produces shareable v2 packages from project configs
- Install v2 with AI-powered adaptation and v1 backwards compat
- PracticeAdapter with LLM-powered semantic merge
- MCP credential prompting with .env file output
…73, #74)

6 commands: setup, tools, extract, install, list, uninstall, version.
Removed library/template/mcp subcommand groups from main.py.
Remove library system, template system, TUI, legacy install/list/update/delete,
standalone MCP commands, and package_create CLI. Also remove 24 test files that
reference deleted modules.
Update product vision to reflect AI-powered config distribution.
Update CLAUDE.md with v2 module structure, commands, and workflow.
Add --upgrade flag to extract command that converts v1 ai-config-kit
packages to v2 devsync-package format. Supports AI-powered conversion
when LLM is configured, falls back to file-copy mode otherwise.
Rewrite README to focus on two-command flow (extract + install),
AI-powered features, and v1 migration path.
Fix unused variables, incorrect method names (list_packages ->
get_installed_packages), incorrect import (clone_repository ->
GitOperations.clone_repository), and apply black formatting.
…#63)

Fix _package_has_tool returning True unconditionally (tool filter was
a no-op). Add path traversal checks for manifest ref.file paths and
instruction_name values to prevent reading/writing outside intended
directories.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

Copy link
Copy Markdown

📊 Code Quality Report

Linting Issues

�[1m�[91mF401 �[0m[�[1m�[96m*�[0m] �[1m`typer` imported but unused�[0m
 �[1m�[94m-->�[0m devsync/cli/setup.py:3:8
  �[1m�[94m|�[0m
�[1m�[94m1 |�[0m """Setup command for configuring LLM provider."""
�[1m�[94m2 |�[0m
�[1m�[94m3 |�[0m import typer
  �[1m�[94m|�[0m        �[1m�[91m^^^^^�[0m
�[1m�[94m4 |�[0m from rich.console import Console
�[1m�[94m5 |�[0m from rich.prompt import Confirm, Prompt
  �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove unused import: `typer`�[0m

�[1m�[91mF541 �[0m[�[1m�[96m*�[0m] �[1mf-string without any placeholders�[0m
  �[1m�[94m-->�[0m devsync/cli/setup.py:45:19
   �[1m�[94m|�[0m
�[1m�[94m43 |�[0m     default_model = _PROVIDER_DEFAULTS[provider_name]
�[1m�[94m44 |�[0m
�[1m�[94m45 |�[0m     console.print(f"\nSet your API key as an environment variable:")
   �[1m�[94m|�[0m                   �[1m�[91m^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^�[0m
�[1m�[94m46 |�[0m     console.print(f"  [cyan]export {env_var}=your-key-here[/cyan]")
�[1m�[94m47 |�[0m     console.print(f"\nAdd this to your shell profile (~/.zshrc, ~/.bashrc) for persistence.\n")
   �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove extraneous `f` prefix�[0m

�[1m�[91mF541 �[0m[�[1m�[96m*�[0m] �[1mf-string without any placeholders�[0m
  �[1m�[94m-->�[0m devsync/cli/setup.py:47:19
   �[1m�[94m|�[0m
�[1m�[94m45 |�[0m     console.print(f"\nSet your API key as an environment variable:")
�[1m�[94m46 |�[0m     console.print(f"  [cyan]export {env_var}=your-key-here[/cyan]")
�[1m�[94m47 |�[0m     console.print(f"\nAdd this to your shell profile (~/.zshrc, ~/.bashrc) for persistence.\n")
   �[1m�[94m|�[0m                   �[1m�[91m^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^�[0m
�[1m�[94m48 |�[0m
�[1m�[94m49 |�[0m     model = Prompt.ask("Model", default=default_model)
   �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove extraneous `f` prefix�[0m

�[1m�[91mF401 �[0m[�[1m�[96m*�[0m] �[1m`json` imported but unused�[0m
 �[1m�[94m-->�[0m devsync/llm/anthropic.py:3:8
  �[1m�[94m|�[0m
�[1m�[94m1 |�[0m """Anthropic Claude provider using HTTP-only calls."""
�[1m�[94m2 |�[0m
�[1m�[94m3 |�[0m import json
  �[1m�[94m|�[0m        �[1m�[91m^^^^�[0m
�[1m�[94m4 |�[0m from typing import Optional
  �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove unused import: `json`�[0m

�[1m�[91mI001 �[0m[�[1m�[96m*�[0m] �[1mImport block is un-sorted or un-formatted�[0m
  �[1m�[94m-->�[0m devsync/llm/config.py:7:1
   �[1m�[94m|�[0m
�[1m�[94m 5 |�[0m   """
�[1m�[94m 6 |�[0m
�[1m�[94m 7 |�[0m �[1m�[91m/�[0m from dataclasses import dataclass, field
�[1m�[94m 8 |�[0m �[1m�[91m|�[0m from pathlib import Path
�[1m�[94m 9 |�[0m �[1m�[91m|�[0m from typing import Optional
�[1m�[94m10 |�[0m �[1m�[91m|�[0m
�[1m�[94m11 |�[0m �[1m�[91m|�[0m import yaml
   �[1m�[94m|�[0m �[1m�[91m|___________^�[0m
   �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mOrganize imports�[0m

�[1m�[91mF401 �[0m[�[1m�[96m*�[0m] �[1m`dataclasses.field` imported but unused�[0m
 �[1m�[94m-->�[0m devsync/llm/config.py:7:36
  �[1m�[94m|�[0m
�[1m�[94m5 |�[0m """
�[1m�[94m6 |�[0m
�[1m�[94m7 |�[0m from dataclasses import dataclass, field
  �[1m�[94m|�[0m                                    �[1m�[91m^^^^^�[0m
�[1m�[94m8 |�[0m from pathlib import Path
�[1m�[94m9 |�[0m from typing import Optional
  �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove unused import: `dataclasses.field`�[0m

�[1m�[91mF401 �[0m[�[1m�[96m*�[0m] �[1m`typing.Optional` imported but unused�[0m
 �[1m�[94m-->�[0m devsync/llm/response_models.py:5:20
  �[1m�[94m|�[0m
�[1m�[94m3 |�[0m import json
�[1m�[94m4 |�[0m from dataclasses import dataclass, field
�[1m�[94m5 |�[0m from typing import Optional
  �[1m�[94m|�[0m                    �[1m�[91m^^^^^^^^�[0m
�[1m�[94m6 |�[0m
�[1m�[94m7 |�[0m from devsync.core.practice import MCPDeclaration, PracticeDeclaration
  �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove unused import: `typing.Optional`�[0m

�[1m�[91mF401 �[0m[�[1m�[96m*�[0m] �[1m`pytest` imported but unused�[0m
 �[1m�[94m-->�[0m tests/unit/cli/test_extract.py:6:8
  �[1m�[94m|�[0m
�[1m�[94m4 |�[0m from unittest.mock import MagicMock, patch
�[1m�[94m5 |�[0m
�[1m�[94m6 |�[0m import pytest
  �[1m�[94m|�[0m        �[1m�[91m^^^^^^�[0m
�[1m�[94m7 |�[0m import yaml
  �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove unused import: `pytest`�[0m

�[1m�[91mF401 �[0m[�[1m�[96m*�[0m] �[1m`pathlib.Path` imported but unused�[0m
 �[1m�[94m-->�[0m tests/unit/cli/test_setup.py:3:21
  �[1m�[94m|�[0m
�[1m�[94m1 |�[0m """Tests for the setup command."""
�[1m�[94m2 |�[0m
�[1m�[94m3 |�[0m from pathlib import Path
  �[1m�[94m|�[0m                     �[1m�[91m^^^^�[0m
�[1m�[94m4 |�[0m from unittest.mock import MagicMock, patch
  �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove unused import: `pathlib.Path`�[0m

�[1m�[91mF401 �[0m[�[1m�[96m*�[0m] �[1m`pytest` imported but unused�[0m
 �[1m�[94m-->�[0m tests/unit/cli/test_setup.py:6:8
  �[1m�[94m|�[0m
�[1m�[94m4 |�[0m from unittest.mock import MagicMock, patch
�[1m�[94m5 |�[0m
�[1m�[94m6 |�[0m import pytest
  �[1m�[94m|�[0m        �[1m�[91m^^^^^^�[0m
�[1m�[94m7 |�[0m
�[1m�[94m8 |�[0m from devsync.cli.setup import setup_command
  �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove unused import: `pytest`�[0m

�[1m�[91mF401 �[0m[�[1m�[96m*�[0m] �[1m`devsync.llm.config.load_config` imported but unused�[0m
 �[1m�[94m-->�[0m tests/unit/cli/test_setup.py:9:43
  �[1m�[94m|�[0m
�[1m�[94m8 |�[0m from devsync.cli.setup import setup_command
�[1m�[94m9 |�[0m from devsync.llm.config import LLMConfig, load_config
  �[1m�[94m|�[0m                                           �[1m�[91m^^^^^^^^^^^�[0m
  �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove unused import: `devsync.llm.config.load_config`�[0m

�[1m�[91mF401 �[0m[�[1m�[96m*�[0m] �[1m`pytest` imported but unused�[0m
 �[1m�[94m-->�[0m tests/unit/core/test_adapter.py:7:8
  �[1m�[94m|�[0m
�[1m�[94m5 |�[0m from unittest.mock import MagicMock
�[1m�[94m6 |�[0m
�[1m�[94m7 |�[0m import pytest
  �[1m�[94m|�[0m        �[1m�[91m^^^^^^�[0m
�[1m�[94m8 |�[0m
�[1m�[94m9 |�[0m from devsync.core.adapter import PracticeAdapter
  �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove unused import: `pytest`�[0m

�[1m�[91mF401 �[0m[�[1m�[96m*�[0m] �[1m`pytest` imported but unused�[0m
 �[1m�[94m-->�[0m tests/unit/core/test_extractor.py:7:8
  �[1m�[94m|�[0m
�[1m�[94m5 |�[0m from unittest.mock import MagicMock, patch
�[1m�[94m6 |�[0m
�[1m�[94m7 |�[0m import pytest
  �[1m�[94m|�[0m        �[1m�[91m^^^^^^�[0m
�[1m�[94m8 |�[0m
�[1m�[94m9 |�[0m from devsync.core.extractor import PracticeExtractor
  �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove unused import: `pytest`�[0m

�[1m�[91mF401 �[0m[�[1m�[96m*�[0m] �[1m`pathlib.Path` imported but unused�[0m
 �[1m�[94m-->�[0m tests/unit/core/test_mcp_credential_prompter.py:3:21
  �[1m�[94m|�[0m
�[1m�[94m1 |�[0m """Tests for MCP credential prompting."""
�[1m�[94m2 |�[0m
�[1m�[94m3 |�[0m from pathlib import Path
  �[1m�[94m|�[0m                     �[1m�[91m^^^^�[0m
�[1m�[94m4 |�[0m from unittest.mock import MagicMock, patch
  �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove unused import: `pathlib.Path`�[0m

�[1m�[91mF401 �[0m[�[1m�[96m*�[0m] �[1m`pytest` imported but unused�[0m
 �[1m�[94m-->�[0m tests/unit/core/test_mcp_credential_prompter.py:6:8
  �[1m�[94m|�[0m
�[1m�[94m4 |�[0m from unittest.mock import MagicMock, patch
�[1m�[94m5 |�[0m
�[1m�[94m6 |�[0m import pytest
  �[1m�[94m|�[0m        �[1m�[91m^^^^^^�[0m
�[1m�[94m7 |�[0m
�[1m�[94m8 |�[0m from devsync.core.mcp_credential_prompter import build_mcp_config, prompt_mcp_credentials
  �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove unused import: `pytest`�[0m

�[1m�[91mF401 �[0m[�[1m�[96m*�[0m] �[1m`pytest` imported but unused�[0m
 �[1m�[94m-->�[0m tests/unit/llm/test_config.py:5:8
  �[1m�[94m|�[0m
�[1m�[94m3 |�[0m from pathlib import Path
�[1m�[94m4 |�[0m
�[1m�[94m5 |�[0m import pytest
  �[1m�[94m|�[0m        �[1m�[91m^^^^^^�[0m
�[1m�[94m6 |�[0m
�[1m�[94m7 |�[0m from devsync.llm.config import LLMConfig, load_config, save_config
  �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove unused import: `pytest`�[0m

�[1m�[91mF401 �[0m[�[1m�[96m*�[0m] �[1m`pytest` imported but unused�[0m
 �[1m�[94m-->�[0m tests/unit/llm/test_openrouter.py:5:8
  �[1m�[94m|�[0m
�[1m�[94m3 |�[0m from unittest.mock import MagicMock, patch
�[1m�[94m4 |�[0m
�[1m�[94m5 |�[0m import pytest
  �[1m�[94m|�[0m        �[1m�[91m^^^^^^�[0m
�[1m�[94m6 |�[0m
�[1m�[94m7 |�[0m from devsync.llm.openrouter import OpenRouterProvider
  �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove unused import: `pytest`�[0m

�[1m�[91mF401 �[0m[�[1m�[96m*�[0m] �[1m`devsync.llm.provider.LLMProviderError` imported but unused�[0m
 �[1m�[94m-->�[0m tests/unit/llm/test_openrouter.py:8:34
  �[1m�[94m|�[0m
�[1m�[94m7 |�[0m from devsync.llm.openrouter import OpenRouterProvider
�[1m�[94m8 |�[0m from devsync.llm.provider import LLMProviderError
  �[1m�[94m|�[0m                                  �[1m�[91m^^^^^^^^^^^^^^^^�[0m
  �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove unused import: `devsync.llm.provider.LLMProviderError`�[0m

�[1m�[91mF401 �[0m[�[1m�[96m*�[0m] �[1m`pytest` imported but unused�[0m
 �[1m�[94m-->�[0m tests/unit/llm/test_provider.py:6:8
  �[1m�[94m|�[0m
�[1m�[94m4 |�[0m from unittest.mock import patch
�[1m�[94m5 |�[0m
�[1m�[94m6 |�[0m import pytest
  �[1m�[94m|�[0m        �[1m�[91m^^^^^^�[0m
�[1m�[94m7 |�[0m
�[1m�[94m8 |�[0m from devsync.llm.provider import LLMProvider, LLMProviderError, LLMResponse, resolve_provider
  �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove unused import: `pytest`�[0m

�[1m�[91mF401 �[0m[�[1m�[96m*�[0m] �[1m`devsync.llm.provider.LLMProvider` imported but unused�[0m
 �[1m�[94m-->�[0m tests/unit/llm/test_provider.py:8:34
  �[1m�[94m|�[0m
�[1m�[94m6 |�[0m import pytest
�[1m�[94m7 |�[0m
�[1m�[94m8 |�[0m from devsync.llm.provider import LLMProvider, LLMProviderError, LLMResponse, resolve_provider
  �[1m�[94m|�[0m                                  �[1m�[91m^^^^^^^^^^^�[0m
  �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove unused import: `devsync.llm.provider.LLMProvider`�[0m

�[1m�[91mI001 �[0m[�[1m�[96m*�[0m] �[1mImport block is un-sorted or un-formatted�[0m
  �[1m�[94m-->�[0m tests/unit/llm/test_response_models.py:3:1
   �[1m�[94m|�[0m
�[1m�[94m 1 |�[0m   """Tests for LLM response models and parsers."""
�[1m�[94m 2 |�[0m
�[1m�[94m 3 |�[0m �[1m�[91m/�[0m import json
�[1m�[94m 4 |�[0m �[1m�[91m|�[0m
�[1m�[94m 5 |�[0m �[1m�[91m|�[0m import pytest
�[1m�[94m 6 |�[0m �[1m�[91m|�[0m
�[1m�[94m 7 |�[0m �[1m�[91m|�[0m from devsync.llm.response_models import (
�[1m�[94m 8 |�[0m �[1m�[91m|�[0m     AdaptationAction,
�[1m�[94m 9 |�[0m �[1m�[91m|�[0m     AdaptationPlan,
�[1m�[94m10 |�[0m �[1m�[91m|�[0m     ExtractionResult,
�[1m�[94m11 |�[0m �[1m�[91m|�[0m     MergeDecision,
�[1m�[94m12 |�[0m �[1m�[91m|�[0m     parse_adaptation_response,
�[1m�[94m13 |�[0m �[1m�[91m|�[0m     parse_extraction_response,
�[1m�[94m14 |�[0m �[1m�[91m|�[0m     parse_merge_response,
�[1m�[94m15 |�[0m �[1m�[91m|�[0m )
�[1m�[94m16 |�[0m �[1m�[91m|�[0m from devsync.core.practice import MCPDeclaration, PracticeDeclaration
   �[1m�[94m|�[0m �[1m�[91m|_____________________________________________________________________^�[0m
   �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mOrganize imports�[0m

�[1m�[91mF401 �[0m[�[1m�[96m*�[0m] �[1m`devsync.llm.response_models.MergeDecision` imported but unused�[0m
  �[1m�[94m-->�[0m tests/unit/llm/test_response_models.py:11:5
   �[1m�[94m|�[0m
�[1m�[94m 9 |�[0m     AdaptationPlan,
�[1m�[94m10 |�[0m     ExtractionResult,
�[1m�[94m11 |�[0m     MergeDecision,
   �[1m�[94m|�[0m     �[1m�[91m^^^^^^^^^^^^^�[0m
�[1m�[94m12 |�[0m     parse_adaptation_response,
�[1m�[94m13 |�[0m     parse_extraction_response,
   �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove unused import: `devsync.llm.response_models.MergeDecision`�[0m

�[1m�[91mF401 �[0m[�[1m�[96m*�[0m] �[1m`devsync.core.practice.MCPDeclaration` imported but unused�[0m
  �[1m�[94m-->�[0m tests/unit/llm/test_response_models.py:16:35
   �[1m�[94m|�[0m
�[1m�[94m14 |�[0m     parse_merge_response,
�[1m�[94m15 |�[0m )
�[1m�[94m16 |�[0m from devsync.core.practice import MCPDeclaration, PracticeDeclaration
   �[1m�[94m|�[0m                                   �[1m�[91m^^^^^^^^^^^^^^�[0m
   �[1m�[94m|�[0m
�[1m�[96mhelp�[0m: �[1mRemove unused import: `devsync.core.practice.MCPDeclaration`�[0m

Found 23 errors.
[�[36m*�[0m] 23 fixable with the `--fix` option.

@github-actions

github-actions Bot commented Feb 21, 2026

Copy link
Copy Markdown

Coverage report

Warning

The diff for this PR is too large to be retrieved from GitHub's API (maximum 300 files). Diff coverage is not available for this PR.

This PR does not seem to contain any modification to coverable code.

@codecov

codecov Bot commented Feb 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.18985% with 215 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.44%. Comparing base (dda2f4b) to head (aecfac3).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
devsync/cli/install_v2.py 47.36% 90 Missing ⚠️
devsync/cli/main.py 0.00% 24 Missing ⚠️
devsync/core/extractor.py 83.15% 16 Missing ⚠️
devsync/cli/list_v2.py 74.54% 14 Missing ⚠️
devsync/cli/extract.py 89.25% 13 Missing ⚠️
devsync/core/adapter.py 84.33% 13 Missing ⚠️
devsync/core/mcp_credential_prompter.py 77.96% 13 Missing ⚠️
devsync/llm/openrouter.py 77.77% 10 Missing ⚠️
devsync/core/package_manifest_v2.py 92.85% 8 Missing ⚠️
devsync/llm/openai_provider.py 84.44% 7 Missing ⚠️
... and 4 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #79      +/-   ##
==========================================
+ Coverage   76.19%   84.44%   +8.24%     
==========================================
  Files          84       75       -9     
  Lines        8254     6202    -2052     
==========================================
- Hits         6289     5237    -1052     
+ Misses       1965      965    -1000     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

Copy link
Copy Markdown

📊 Code Quality Report

Linting Issues

All checks passed!

@troylar

troylar commented Feb 21, 2026

Copy link
Copy Markdown
Owner Author

Code Review

Bugs Found (5)

Bug 1: Temp directory leaked on Git clone installs
install_v2.py#L1920-L1930_clone_source() creates tempfile.mkdtemp() but never cleans it up. Every Git-sourced install leaks a temp directory.

Bug 2: LLM JSON parsing doesn't strip markdown fences
extractor.py#L116-L117 — MCP extraction calls json.loads(response.content) directly. If the LLM wraps output in ```json fences, parsing silently fails and MCP servers are dropped.

Bug 3: parse_adaptation_response always returns empty practice_name
response_models.py#L157-L163 — Hardcoded practice_name="". The caller in adapter.py reassigns it, making this work by coincidence. Any other caller gets a broken action.

Bug 4: --conflict flag is ignored in fallback install path
install_v2.py#L1996-L2002_install_v2_fallback accepts conflict parameter but never reads it. Existing files are always silently skipped regardless of --conflict overwrite.

Bug 5: _upgrade_v1_package returns success (0) when no files found
extract.py#L573-L575 — Returns 0 when a valid v1 manifest has zero readable instruction files. CI/scripts see success for a no-op.

Security Warnings (2)

  • Credential input not masked: mcp_credential_prompter.py uses Prompt.ask() without password=True — credentials visible in terminal
  • Weak path traversal guard: install_v2.py uses str.startswith() instead of Path.is_relative_to() — bypassable with crafted paths

Test Gaps

  • openrouter.py: Missing API error, validate_api_key tests
  • install_v2.py: AI install path, Git clone, tool filter, conflict strategies untested
  • list_v2.py: --tool filter and JSON output with data untested
  • mcp_credential_prompter.py: _write_env_file path untested
  • setup.py: Reconfigure flow untested

Notes

  • 1 pre-existing test failure (test_main_module_execution — Typer/Click version compat)
  • Duplicate "fix lint" commits (f896f12, 8250fc1) — consider squashing before merge
  • Merge conflicts in CLAUDE.md will need resolution

Automated code review by Claude Code

- Fix temp directory leak in Git clone installs (install_v2.py)
- Strip markdown fences from LLM JSON responses (extractor.py)
- Read practice_name from JSON in parse_adaptation_response (response_models.py)
- Apply --conflict flag in fallback install path (install_v2.py)
- Return exit code 1 when v1 upgrade finds no files (extract.py)
- Mask credential input with password=True + validate non-empty (mcp_credential_prompter.py)
- Use Path.relative_to() instead of str.startswith() for path traversal guard (install_v2.py, extract.py)
@github-actions

Copy link
Copy Markdown

📊 Code Quality Report

Linting Issues

All checks passed!

@github-actions

Copy link
Copy Markdown

📊 Code Quality Report

Linting Issues

All checks passed!

@troylar
troylar merged commit cda34ab into main Feb 21, 2026
21 of 23 checks passed
@troylar
troylar deleted the issue-63-v2-llm-foundation branch February 22, 2026 00:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment