From 5ce10e8e25815ef82f388e06d4aa002273ac29c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Boh=C3=A1=C4=8Dek?= Date: Sat, 26 Sep 2026 21:52:08 +0200 Subject: [PATCH] Add core module: instruction fragments and skill closure --- modules/core.nix | 86 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 modules/core.nix diff --git a/modules/core.nix b/modules/core.nix new file mode 100644 index 0000000000000000000000000000000000000000..0100f39f94a3bf9c5dbf7d33d31b20a11e3ef7e8 --- /dev/null +++ b/modules/core.nix @@ -0,0 +1,86 @@ +{ + 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."; + }; + + # 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; + skillRuntimeInputs = lib.unique (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; + }; +}