Skip to content

Editor and AI batteries

An editor’s extension list grows with every language you use, and every AI agent wants the same skills and MCP servers in its own format. Here a language aspect gives what it needs once. Each editor and each agent reads from one pool. The example is one standalone home, ana, built with home-manager.

modules/fleet.nix
# Standalone homes. Language aspects add editor extensions and settings; AI tools add agent extensions.
{
yen,
lib,
inputs,
...
}:
let
b = yen.lib.batteries;
in
{
yen.kinds.fleet = {
spawns.home = _: yen.home;
sinks.out.reduce = yen.lib.reduce.merge;
};
yen.kinds.home = {
sinks = {
homeManager.out = b.home-manager.system {
# (1)
from = yen.kinds.home.sinks.homeManager;
homeManagerConfiguration = _: inputs.home-manager.lib.homeManagerConfiguration;
pkgs = _: inputs.nixpkgs.legacyPackages.x86_64-linux;
};
vscode.homeManager = b.editors.vscode { from = yen.kinds.home.sinks.vscode; }; # (2)
zed.homeManager = b.editors.zed { from = yen.kinds.home.sinks.zed; }; # (3)
# (4) one pool of agent extensions; each agent picks the types it reads
agent-ext.homeManager = b.ai.extensions {
from = yen.kinds.home.sinks.agent-ext;
types = [ "skill" ];
build = skills: {
home.file = lib.listToAttrs (
map (s: lib.nameValuePair ".claude/skills/${s.name}" { source = s.path; }) skills
);
};
};
};
};
yen.fleet.main = { };
yen.home.ana = { };
yen.flake = { out, ... }: out;
yen.aspects.home = {
at = yen.kinds.home;
homeManager =
{ scope, ... }:
{
home.username = scope.name;
home.homeDirectory = "/home/${scope.name}";
home.stateVersion = "25.05";
nixpkgs.config.allowUnfree = true; # vscode
};
};
}

The home kind has one sink for each editor and one for the agents. The batteries are the values of the sinks’ homeManager keys. (1) builds the home. (2) turns the vscode sink into programs.vscode. (3) does the same for Zed, with extensions by name. At (4), agent-ext is the pool of agent extensions. types says which records this agent reads: here, only skills.

Each language aspect gives its part to the editors, so no editor file lists the languages:

modules/languages.nix
# Each language aspect gives what both editors need; no editor file lists the languages.
{ yen, inputs, ... }:
let
pkgs = inputs.nixpkgs.legacyPackages.x86_64-linux;
in
{
yen.aspects.nix-lang = {
at = yen.kinds.home;
vscode = {
# (1)
extensions = [ pkgs.vscode-extensions.jnoortheen.nix-ide ];
settings."nix.enableLanguageServer" = true;
};
zed.extensions = [ "nix" ]; # (2)
};
yen.aspects.go-lang = {
at = yen.kinds.home;
vscode.extensions = [ pkgs.vscode-extensions.golang.go ];
zed.extensions = [ "go" ];
};
# skills from a directory, and an mcp server, in one pool
yen.aspects.agents = {
at = yen.kinds.home;
agent-ext = yen.lib.batteries.ai.skills ../skills ++ [
# (3)
{
type = "mcp";
name = "github";
}
];
};
}

(1) gives VS Code a list of extensions and its settings. (2) gives Zed the same language by name. At (3), ai.skills reads a directory and gives one record for each subdirectory that has a SKILL.md. The aspect adds an MCP server record to the same pool. This is the skill it finds:

skills/review/SKILL.md
---
name: review
description: review a diff
---
Read the diff. List the risks.

The directory notes has no SKILL.md, so it is not a skill. The github record has the type mcp, so the skills agent skips it.

Battery Value of Gives
editors.vscode { from; profile; } a home-manager key of a sink of { extensions; settings; } programs.vscode
editors.zed { from; } the same, extensions by name programs.zed-editor
ai.extensions { from; types; build; } any key build of the records of the given types (an element may be a list)
ai.skills dir an element one { type = "skill"; name; path; } per subdirectory with a SKILL.md
ContributeCommunitySponsor