Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/LLM_CLI_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,6 @@ sia-code search "AuthService"
- Prefer MCP when the client supports it; it removes the need to distribute prompt-side workflow files.
- `engineering_bootstrap` is the intended portable first-call surface for engineering workflows when users only add `uvx sia-code-mcp` to their MCP config.
- Keep the skill file short and practical for fallback environments.
- `memory git-context` is currently CLI-first for file history + blast radius workflows.
- Use `memory git-context` in CLI workflows or `git_context` through MCP.
- Update this file when CLI behavior changes.
- Keep both PyPI and local-checkout workflows documented during active development.
2 changes: 1 addition & 1 deletion docs/MCP_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Exposed in v1:
- `memory_export`, `memory_import`
- `config_show`, `config_path`, `config_get`, `config_set`
- `embed_start`, `embed_status`, `embed_stop`
- `git_context`

## Recommended First Call

Expand All @@ -61,7 +62,6 @@ For engineering work in Claude Code, OpenCode, Codex, or any MCP-aware client, p

Not exposed in v1:

- `memory git-context` (CLI-only today for file history + blast radius)
- interactive CLI mode
- watch mode indexing
- config editor launching
Expand Down
18 changes: 11 additions & 7 deletions sia_code/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@
from .config import Config
from .indexer.coordinator import IndexingCoordinator

console = Console(force_terminal=True)
console = Console(
force_terminal=True, color_system="auto" if sys.stdout.isatty() else None
)
err_console = Console(
stderr=True, force_terminal=True, color_system="auto" if sys.stderr.isatty() else None
)


def _display_skip_summary(
Expand Down Expand Up @@ -139,11 +144,11 @@ def create_backend(
)
if is_implicit_sqlite_default_on_legacy_usearch:
if not suppress_stdout_notices:
console.print(
err_console.print(
"[yellow]Detected legacy usearch index with implicit storage backend.[/yellow] "
"Using legacy backend for compatibility."
)
console.print(
err_console.print(
"[dim]Set 'storage.backend=sqlite-vec' and run 'sia-code index --clean .' "
"to migrate when ready.[/dim]"
)
Expand All @@ -167,10 +172,10 @@ def create_backend(

if effective_backend == "auto" and detected_backend == "usearch":
if not suppress_stdout_notices:
console.print(
err_console.print(
"[yellow]Detected legacy usearch index.[/yellow] Using it for compatibility."
)
console.print(
err_console.print(
"[dim]Set 'storage.backend=usearch' to pin legacy mode, "
"or run 'sia-code index --clean .' to migrate to sqlite-vec.[/dim]"
)
Expand Down Expand Up @@ -2969,15 +2974,14 @@ def memory_git_context(file_paths, no_blast_radius, no_narrative, output_format)
sia-code memory git-context src/api.py src/crud.py --format json
sia-code memory git-context src/api.py --no-narrative
"""
from .config import Config
from .memory.blast_radius import BlastRadiusAnalyzer
from .memory.diff_analyzer import DiffSemanticAnalyzer
from .memory.git_dynamic import GitDynamicMemory
from .memory.intent_classifier import IntentClassifier
from .memory.recency import RecencyConfig

project_dir = Path.cwd()
config = Config.load(project_dir / ".sia-code" / "config.json")
_, config = require_initialized()
gc = config.git_dynamic

recency_cfg = RecencyConfig(
Expand Down
24 changes: 13 additions & 11 deletions sia_code/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -834,7 +834,7 @@ def engineering_bootstrap(
hit_files = list({m["file_path"] for m in search_hits["matches"] if "file_path" in m})[:3]
if hit_files:
git_context_payload = _compute_git_context(
context.workspace_root, hit_files, limit=3
context.workspace_root, config, hit_files, limit=3
)
except Exception:
pass # Graceful — git context is supplementary
Expand Down Expand Up @@ -1109,8 +1109,11 @@ def memory_trace(
try:
if result.related_files:
dynamic_git = _compute_git_context(
context.workspace_root, result.related_files[:3],
limit=3, include_blast_radius=False,
context.workspace_root,
config,
result.related_files[:3],
limit=3,
include_blast_radius=False,
)
except Exception:
pass
Expand Down Expand Up @@ -1284,23 +1287,19 @@ def embed_stop():

def _compute_git_context(
workspace_root: Path,
config: Config,
file_paths: list[str],
limit: int = 5,
include_blast_radius: bool = True,
include_narrative: bool = True,
) -> dict:
"""Shared helper — computes git context for files.

Used by git_context tool, research, engineering_bootstrap, memory_trace.
"""
from .config import Config
"""Shared helper — computes git context for files."""
from .memory.blast_radius import BlastRadiusAnalyzer
from .memory.diff_analyzer import DiffSemanticAnalyzer
from .memory.git_dynamic import GitDynamicMemory
from .memory.intent_classifier import IntentClassifier
from .memory.recency import RecencyConfig

config = Config.load(workspace_root / ".sia-code" / "config.json")
gc = config.git_dynamic

if not gc.enabled:
Expand Down Expand Up @@ -1401,6 +1400,7 @@ def _compute_git_context(
def git_context(
workspace_root: str,
file_paths: list[str],
index_dir: str | None = None,
include_blast_radius: bool = True,
include_narrative: bool = True,
) -> dict:
Expand All @@ -1410,13 +1410,15 @@ def git_context(
co-change blast radius, and model-generated evolution narrative.
Auto-uses local flan-t5 model for narrative when available.
"""
context, config = _require_initialized_context(workspace_root, index_dir)
result = _compute_git_context(
Path(workspace_root),
context.workspace_root,
config,
file_paths,
include_blast_radius=include_blast_radius,
include_narrative=include_narrative,
)
return _ok(scope="project", result=result)
return _ok(context=context, result=result)

return mcp

Expand Down
9 changes: 5 additions & 4 deletions sia_code/runtime_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .config import Config

console = Console()
err_console = Console(stderr=True)


@dataclass(frozen=True)
Expand Down Expand Up @@ -46,11 +47,11 @@ def create_backend(
)
if is_implicit_sqlite_default_on_legacy_usearch:
if not suppress_stdout_notices:
console.print(
err_console.print(
"[yellow]Detected legacy usearch index with implicit storage backend.[/yellow] "
"Using legacy backend for compatibility."
)
console.print(
err_console.print(
"[dim]Set 'storage.backend=sqlite-vec' and run 'sia-code index --clean .' "
"to migrate when ready.[/dim]"
)
Expand All @@ -74,10 +75,10 @@ def create_backend(

if effective_backend == "auto" and detected_backend == "usearch":
if not suppress_stdout_notices:
console.print(
err_console.print(
"[yellow]Detected legacy usearch index.[/yellow] Using it for compatibility."
)
console.print(
err_console.print(
"[dim]Set 'storage.backend=usearch' to pin legacy mode, "
"or run 'sia-code index --clean .' to migrate to sqlite-vec.[/dim]"
)
Expand Down
2 changes: 1 addition & 1 deletion skills/sia-code/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ uvx sia-code memory working-set "auth flow" \
```

- `memory working-set` emits stable JSON for agent handoff.
- `memory git-context` is CLI-only and useful before risky refactors.
- `memory git-context` is useful before risky refactors; MCP clients use `git_context`.

## Multi-Repo and Worktrees

Expand Down
2 changes: 0 additions & 2 deletions tests/integration/test_git_dynamic_real_repos.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from sia_code.memory.git_dynamic import GitDynamicMemory
from sia_code.memory.blast_radius import BlastRadiusAnalyzer
from sia_code.memory.recency import RecencyConfig, RecencyScorer
from sia_code.memory.revert_detector import RevertDetector, CommitInfo
from sia_code.memory.intent_classifier import IntentClassifier


Expand Down Expand Up @@ -145,7 +144,6 @@ def test_within_window_full_weight(self):

def test_halflife_gives_half_weight(self):
from datetime import datetime, timedelta, timezone
import math

scorer = RecencyScorer(RecencyConfig(halflife_days=30, working_window_days=14))
now = datetime.now(timezone.utc)
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/test_cli_backend_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,31 @@ def fake_create_backend(

assert result.exit_code == 0
assert captured["suppress_stdout_notices"] is True


def test_git_context_command_uses_resolved_index(monkeypatch, tmp_path):
resolved = False

def fake_require_initialized():
nonlocal resolved
resolved = True
return tmp_path / "custom-index", Config()

monkeypatch.chdir(tmp_path)
monkeypatch.setattr("sia_code.cli.require_initialized", fake_require_initialized)

result = CliRunner().invoke(
main,
[
"memory",
"git-context",
"missing.py",
"--no-blast-radius",
"--no-narrative",
"--format",
"json",
],
)

assert result.exit_code == 0, result.output
assert resolved is True
2 changes: 0 additions & 2 deletions tests/unit/test_index_dir_resolution.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from pathlib import Path

import pytest

from sia_code.cli import resolve_index_dir
Expand Down
2 changes: 0 additions & 2 deletions tests/unit/test_mcp_lock.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from pathlib import Path

from sia_code.mcp_lock import index_lock, lock_path_for_index


Expand Down
60 changes: 56 additions & 4 deletions tests/unit/test_mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,62 @@ async def test_build_server_registers_expected_tool_names():
server = build_server()
tool_names = {tool.name for tool in await server.list_tools()}

assert {"init", "status", "search", "research"}.issubset(tool_names)
assert {"memory_working_set", "config_show", "embed_status"}.issubset(tool_names)
assert {"health_check", "engineering_bootstrap"}.issubset(tool_names)
assert "interactive" not in tool_names
assert tool_names == {
"compact",
"config_get",
"config_path",
"config_set",
"config_show",
"embed_start",
"embed_status",
"embed_stop",
"engineering_bootstrap",
"git_context",
"health_check",
"index",
"init",
"memory_add_decision",
"memory_approve",
"memory_changelog",
"memory_export",
"memory_import",
"memory_list",
"memory_reject",
"memory_search",
"memory_sync_git",
"memory_timeline",
"memory_trace",
"memory_working_set",
"research",
"search",
"status",
}


@pytest.mark.anyio
async def test_git_context_uses_explicit_index_dir(tmp_path):
workspace_root = tmp_path / "repo"
workspace_root.mkdir()
index_dir = tmp_path / "custom-index"
index_dir.mkdir()
Config().save(index_dir / "config.json")

server = build_server()
result = decode_tool_result(
await server.call_tool(
"git_context",
{
"workspace_root": str(workspace_root),
"index_dir": str(index_dir),
"file_paths": ["missing.py"],
"include_blast_radius": False,
"include_narrative": False,
},
)
)

assert result["ok"] is True
assert result["resolved_index_dir"] == str(index_dir.resolve())


@pytest.mark.anyio
Expand Down
3 changes: 1 addition & 2 deletions tests/unit/test_multi_hop.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,12 +506,11 @@ def test_uses_hybrid_variants_when_budgeted_embeddings_enabled(self, backend, sa
backend.embedding_enabled = True
backend.embedding_granularity = "budget"

original_search_hybrid = backend.search_hybrid
calls = []

def mock_search_hybrid(query, *args, **kwargs):
calls.append(query)
return original_search_hybrid(query, *args, **kwargs)
return []

backend.search_hybrid = mock_search_hybrid

Expand Down
11 changes: 10 additions & 1 deletion tests/unit/test_open_index_writable.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,16 @@ def __len__(self):
def ndim(self):
return 768

monkeypatch.setattr(usearch_backend, "Index", FakeIndex)
class FakeMetricKind:
Cos = "cos"
L2sq = "l2sq"

monkeypatch.setattr(
usearch_backend, "_lazy_usearch", lambda: (FakeIndex, FakeMetricKind)
)
monkeypatch.setattr(
usearch_backend.UsearchSqliteBackend, "_get_embedder", lambda self: None
)

backend = usearch_backend.UsearchSqliteBackend(path=tmp_path)
backend.open_index(writable=True)
Expand Down
Loading