Skip to content
25 changes: 11 additions & 14 deletions CODEBASE_MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Codebase Memory remains internal to the Compose network at `http://codebase-memo

## Workspace lifecycle

`open_workspace` now coordinates the Codebase Memory index automatically when the sidecar is available:
`open_workspace` coordinates the Codebase Memory index automatically when the sidecar is available:

- If the workspace already has a healthy index, LocalCodingMcp reuses it.
- If the workspace is not indexed yet, LocalCodingMcp calls `index_repository` once for that workspace.
Expand All @@ -35,6 +35,14 @@ refresh_codebase_memory_workspace(workspace_id)

This keeps normal workspace opening fast while making first-use indexing automatic and refreshes explicit.

## Skill activation

When `CodebaseMemory:Enabled=true`, LocalCodingMcp automatically enables the built-in `codebase-memory` skill so `route_skills` can recommend structural discovery without a separate setup step. When the proxy is disabled, the untouched built-in remains disabled.

An explicit user choice made with `set_skill_enabled` takes precedence and persists across restarts. This means a user can disable Codebase Memory routing even while the proxy remains available, or keep the skill enabled intentionally.

The skill covers architecture, codebase exploration, semantic discovery, dependency/caller tracing, ADRs, call paths, and impact analysis. It uses the LocalCodingMcp proxy tools for structural discovery, then normal LocalCodingMcp file/git/shell tools for exact source inspection, edits, tests, and verification.

## Proxy tools

LocalCodingMcp exposes three tools for the sidecar:
Expand All @@ -45,28 +53,17 @@ codebase_memory_list_tools
codebase_memory_call
```

Typical flow:
Typical flow after `open_workspace`:

```text
codebase_memory_status()
codebase_memory_list_tools()
codebase_memory_call(
tool: "get_architecture",
arguments_json: "{}"
)
```

`codebase_memory_call` only forwards names currently advertised by the sidecar's own `tools/list` response. This prevents the generic proxy from becoming an arbitrary HTTP/RPC escape hatch.

## Enable the skill

The built-in `codebase-memory` skill is disabled by default:

```text
set_skill_enabled(name: "codebase-memory", enabled: true)
```

When enabled, `route_skills` can recommend it for architecture, codebase exploration, semantic discovery, indexing, dependency/caller tracing, ADRs, call paths, and impact analysis. The skill uses the LocalCodingMcp proxy tools for structural discovery, then normal LocalCodingMcp file/git/shell tools for exact source inspection, edits, tests, and verification.
Use `codebase_memory_status` for diagnostics when needed. `codebase_memory_call` only forwards names currently advertised by the sidecar's own `tools/list` response. This prevents the generic proxy from becoming an arbitrary HTTP/RPC escape hatch.

If the sidecar is stopped, `codebase_memory_status` returns `available: false` instead of making the whole LocalCodingMcp server unavailable.

Expand Down
81 changes: 81 additions & 0 deletions LocalCodingMcp.Tests/BuiltInSkillDefaultTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using LocalCodingMcp.Services;

namespace LocalCodingMcp.Tests;

public sealed class BuiltInSkillDefaultTests : IDisposable
{
private readonly string _root = Path.Combine(Path.GetTempPath(), "mcp-skill-default-" + Guid.NewGuid().ToString("N"));

public void Dispose()
{
try { Directory.Delete(_root, true); } catch { }
}

[Fact]
public void ApplyBuiltInDefault_EnablesUntouchedBuiltIn()
{
var store = new SkillStore(_root);

store.ApplyBuiltInDefault("codebase-memory", enabled: true);

Assert.True(store.Get("codebase-memory").Enabled);
}

[Fact]
public void ApplyBuiltInDefault_FollowsSystemDefaultAcrossRestart()
{
var store = new SkillStore(_root);
store.ApplyBuiltInDefault("codebase-memory", enabled: true);

var reopened = new SkillStore(_root);
reopened.ApplyBuiltInDefault("codebase-memory", enabled: false);

Assert.False(reopened.Get("codebase-memory").Enabled);
}

[Fact]
public void ApplyBuiltInDefault_DoesNotOverrideExplicitUserDisable()
{
var store = new SkillStore(_root);
store.SetEnabledFromUser("codebase-memory", false);

store.ApplyBuiltInDefault("codebase-memory", enabled: true);

Assert.False(store.Get("codebase-memory").Enabled);
}

[Fact]
public void ApplyBuiltInDefault_DoesNotOverrideExplicitUserEnable()
{
var store = new SkillStore(_root);
store.SetEnabledFromUser("codebase-memory", true);

store.ApplyBuiltInDefault("codebase-memory", enabled: false);

Assert.True(store.Get("codebase-memory").Enabled);
}

[Fact]
public void ExplicitChoice_PersistsAcrossStoreInstances()
{
var store = new SkillStore(_root);
store.SetEnabledFromUser("codebase-memory", false);

var reopened = new SkillStore(_root);
reopened.ApplyBuiltInDefault("codebase-memory", enabled: true);

Assert.False(reopened.Get("codebase-memory").Enabled);
}

[Fact]
public void LegacyEnabledBuiltIn_IsTreatedAsExistingUserChoice()
{
var store = new SkillStore(_root);
store.SetEnabled("codebase-memory", true);

var reopened = new SkillStore(_root);
reopened.ApplyBuiltInDefault("codebase-memory", enabled: false);

Assert.True(reopened.Get("codebase-memory").Enabled);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using LocalCodingMcp.Hosting;
using LocalCodingMcp.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace LocalCodingMcp.Tests.Hosting;

public sealed class CodebaseMemorySkillRegistrationTests
{
[Fact]
public void Registration_EnablesCodebaseMemorySkill_WhenProxyEnabled()
{
WithRegistration(proxyEnabled: true, (services, _) =>
{
using var provider = services.BuildServiceProvider();
Assert.True(provider.GetRequiredService<SkillStore>().Get("codebase-memory").Enabled);
});
}

[Fact]
public void Registration_LeavesCodebaseMemorySkillDisabled_WhenProxyDisabled()
{
WithRegistration(proxyEnabled: false, (services, _) =>
{
using var provider = services.BuildServiceProvider();
Assert.False(provider.GetRequiredService<SkillStore>().Get("codebase-memory").Enabled);
});
}

[Fact]
public void Registration_PreservesExplicitDisable_AfterProxyWasEnabled()
{
WithRegistration(proxyEnabled: true, (services, root) =>
{
using (var provider = services.BuildServiceProvider())
provider.GetRequiredService<SkillStore>().SetEnabledFromUser("codebase-memory", false);

var config = BuildConfig(root, proxyEnabled: true);
var reopenedServices = new ServiceCollection();
reopenedServices.AddLocalCodingMcp(config, root, LocalCodingMcpTransport.Http);
using var reopened = reopenedServices.BuildServiceProvider();

Assert.False(reopened.GetRequiredService<SkillStore>().Get("codebase-memory").Enabled);
});
}

private static void WithRegistration(bool proxyEnabled, Action<ServiceCollection, string> assertion)
{
var root = Path.Combine(Path.GetTempPath(), $"local-coding-cbm-skill-{Guid.NewGuid():N}");
Directory.CreateDirectory(root);
try
{
var services = new ServiceCollection();
services.AddLocalCodingMcp(BuildConfig(root, proxyEnabled), root, LocalCodingMcpTransport.Http);
assertion(services, root);
}
finally
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
}

private static IConfiguration BuildConfig(string root, bool proxyEnabled)
=> new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
{
["Skills:Directory"] = Path.Combine(root, "skills"),
["ExecutionHistory:FilePath"] = Path.Combine(root, "history.jsonl"),
["CodebaseMemory:Enabled"] = proxyEnabled.ToString(),
["CodebaseMemory:Endpoint"] = "http://codebase-memory:9750/mcp"
}).Build();
}
3 changes: 3 additions & 0 deletions LocalCodingMcp/Hosting/McpServerRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ codebaseMemoryEndpoint.Scheme is not ("http" or "https")))
{
throw new InvalidOperationException("CodebaseMemory:Endpoint must be an absolute HTTP or HTTPS URI.");
}

skillStore.ApplyBuiltInDefault("codebase-memory", codebaseMemoryEnabled);

var codebaseMemoryClient = new CodebaseMemoryClient(
codebaseMemoryEnabled,
codebaseMemoryEndpoint,
Expand Down
45 changes: 45 additions & 0 deletions LocalCodingMcp/Services/SkillStore.BuiltInDefaults.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace LocalCodingMcp.Services;

public sealed partial class SkillStore
{
private const string UserEnabledChoiceFileName = ".enabled-by-user";
private const string SystemEnabledChoiceFileName = ".enabled-by-system";

public SkillDocument ApplyBuiltInDefault(string name, bool enabled)
{
var skill = Get(name);
if (!skill.BuiltIn)
throw new InvalidOperationException($"Skill '{name}' is not built in.");

var directory = GetSkillDirectory(name);
var userChoicePath = Path.Combine(directory, UserEnabledChoiceFileName);
if (File.Exists(userChoicePath)) return skill;

var systemChoicePath = Path.Combine(directory, SystemEnabledChoiceFileName);
if (File.Exists(systemChoicePath))
{
var updated = skill.Enabled == enabled ? skill : SetEnabled(name, enabled);
File.WriteAllText(systemChoicePath, enabled ? "true" : "false");
return updated;
}

// Built-ins were historically seeded disabled. An already-enabled built-in
// therefore represents a legacy user choice made before preference markers
// existed; preserve it rather than applying a new system default over it.
if (skill.Enabled) return skill;

var result = skill.Enabled == enabled ? skill : SetEnabled(name, enabled);
File.WriteAllText(systemChoicePath, enabled ? "true" : "false");
return result;
}

public SkillDocument SetEnabledFromUser(string name, bool enabled)
{
var skill = SetEnabled(name, enabled);
var directory = GetSkillDirectory(name);
File.WriteAllText(Path.Combine(directory, UserEnabledChoiceFileName), enabled ? "true" : "false");
var systemChoicePath = Path.Combine(directory, SystemEnabledChoiceFileName);
if (File.Exists(systemChoicePath)) File.Delete(systemChoicePath);
return skill;
}
}
4 changes: 2 additions & 2 deletions LocalCodingMcp/Tools/SkillTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,12 @@ public string UpdateSkill(
});
}

[McpServerTool, Description("Enable or disable a skill without deleting it. State persists across server restarts.")]
[McpServerTool, Description("Enable or disable a skill without deleting it. State persists across server restarts and is treated as an explicit user preference.")]
public string SetSkillEnabled(
[Description("Existing skill name")] string name,
[Description("true to enable, false to disable")] bool enabled)
{
var skill = _skills.SetEnabled(name, enabled);
var skill = _skills.SetEnabledFromUser(name, enabled);
return JsonSerializer.Serialize(new { name = skill.Name, enabled = skill.Enabled, built_in = skill.BuiltIn });
}

Expand Down
Loading