{ 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; }; }