{
config,
lib,
pkgs,
agentEnvLib,
...
}:
let
inherit (lib) mkOption types;
sortedInstructions = lib.sort (
a: b: if a.order == b.order then a.name < b.name else a.order < b.order
) (lib.mapAttrsToList (name: v: v // { inherit name; }) config.instructions);
# Transitive closure over skillDeps, keyed by store path.
skillClosure = map (e: e.skill) (
builtins.genericClosure {
startSet = map (s: {
key = s.outPath;
skill = s;
}) config.skills.packages;
operator =
e:
map (s: {
key = s.outPath;
skill = s;
}) e.skill.skillDeps;
}
);
skillsByName = lib.groupBy (s: s.skillName) skillClosure;
in
{
options = {
instructions = mkOption {
default = { };
description = "Fragments of the instruction file (CLAUDE.md / AGENTS.md), sorted by order, then name.";
type = types.attrsOf (
types.submodule {
options = {
text = mkOption { type = types.lines; };
order = mkOption {
type = types.int;
default = 1000;
};
};
}
);
};
skills.packages = mkOption {
type = types.listOf types.package;
default = [ ];
description = "Skills built with lib.mkSkill. Their skill dependencies are added automatically.";
};
packages = mkOption {
type = types.listOf types.package;
default = [ ];
description = "Packages put on the PATH of every launcher, e.g. tools that hooks in the agent settings call.";
};
# Read-only results used by the agent and launcher modules.
build = mkOption {
type = types.attrsOf types.anything;
default = { };
internal = true;
};
assertions = mkOption {
type = types.listOf types.attrs;
default = [ ];
internal = true;
};
};
config = {
build = {
instructionsText =
lib.concatMapStringsSep "\n\n" (i: lib.removeSuffix "\n" i.text) sortedInstructions + "\n";
skills = lib.mapAttrsToList (_: ss: lib.head ss) skillsByName;
runtimeInputs = lib.unique (config.packages ++ lib.concatMap (s: s.runtimeInputs) skillClosure);
};
assertions = lib.mapAttrsToList (name: ss: {
assertion = lib.length ss == 1;
message = "skill name '${name}' is provided by different packages: ${
lib.concatMapStringsSep ", " (s: s.outPath) ss
}";
}) skillsByName;
};
}