# Harness-agnostic agent management utilities # # This module provides functions to load canonical agent definitions and # render them for different AI coding tools (OpenCode, Claude Code, Pi). # # Usage in your configuration: # # # In your flake or configuration: # let # m3taLib = inputs.m3ta-nixpkgs.lib.${system}; # # # Load canonical agents from the AGENTS flake input # canonical = m3taLib.agents.loadCanonical { agentsInput = inputs.agents; }; # # # Render for a specific tool # agentFiles = m3taLib.agents.renderForTool { # inherit pkgs; # agentsInput = inputs.agents; # tool = "opencode"; # }; # in { ... } # # Renderers are stubs for now (Tasks 9-11 will provide real implementations). {lib}: let agentsLib = { # Load canonical agent definitions from the AGENTS flake input. # agentsInput: the AGENTS flake (e.g., inputs.agents in a consuming flake) # Returns the canonical attrset from lib.loadAgents (keyed by slug). loadCanonical = {agentsInput}: agentsInput.lib.loadAgents; # OpenCode renderer — produces a directory of .opencode/agents/*.md files. # Each file has YAML frontmatter (description, mode, optional model, # optional permission) followed by the agent's systemPrompt content. renderForOpencode = { pkgs, canonical, modelOverrides ? {}, }: let # Split a rule string on the LAST colon to get { pattern, action }. # e.g. "rm -rf *:ask" → pattern="rm -rf *", action="ask" # e.g. "/run/agenix/**:deny" → pattern="/run/agenix/**", action="deny" parseRule = ruleStr: let parts = lib.strings.splitString ":" ruleStr; # last element is the action, everything before joined with ":" is pattern action = lib.last parts; pattern = lib.concatStringsSep ":" (lib.init parts); in {inherit pattern action;}; # Render one permission section to YAML lines (list of strings). # intent-only → single line: " : " # intent+rules → nested block starting with "*": , then specific rules renderPermSection = tool: section: if !(section ? rules) || section.rules == [] then [" ${tool}: ${section.intent}"] else let parsedRules = map parseRule section.rules; wildcardLine = " \"*\": ${section.intent}"; ruleLines = map (r: " \"${r.pattern}\": ${r.action}") parsedRules; in [" ${tool}:"] ++ [wildcardLine] ++ ruleLines; # Render the full permission block for an agent. # Returns empty list if no permissions. renderPermBlock = permissions: if permissions == {} || permissions == null then [] else ["permission:"] ++ lib.concatLists ( lib.mapAttrsToList renderPermSection permissions ); # Build the YAML frontmatter string for one agent. mkFrontmatter = name: agent: let descLine = "description: \"${agent.description}.\""; modeLine = "mode: ${agent.mode}"; modelLine = lib.optionalString (modelOverrides ? ${name}) "model: ${modelOverrides.${name}}\n"; permBlock = renderPermBlock (agent.permissions or {}); permLines = if permBlock == [] then "" else lib.concatStringsSep "\n" permBlock + "\n"; in "---\n${descLine}\n${modeLine}\n${modelLine}${permLines}---\n"; # Build the full markdown file content for one agent. mkAgentContent = name: agent: (mkFrontmatter name agent) + agent.systemPrompt; # Write each agent as a derivation text file. mkAgentFile = name: agent: pkgs.writeText "${name}.md" (mkAgentContent name agent); # Attrset of name → derivation for each agent. agentFiles = lib.mapAttrs mkAgentFile canonical; # Shell commands to copy each file into $out. copyCommands = lib.concatStringsSep "\n" ( lib.mapAttrsToList (name: file: "cp ${file} $out/${name}.md") agentFiles ); in pkgs.runCommand "opencode-agents" {} '' mkdir -p $out ${copyCommands} ''; # Stub: Claude Code renderer — produces .claude/agents/*.md files. # To be implemented in Task 10. renderForClaudeCode = { pkgs, canonical, modelOverrides ? {}, }: pkgs.runCommand "claude-code-agents" {} "echo stub > $out"; # Stub: Pi renderer — produces AGENTS.md + SYSTEM.md. # To be implemented in Task 11. renderForPi = { pkgs, canonical, }: pkgs.runCommand "pi-agents" {} "echo stub > $out"; # Dispatch renderer by tool name. # tool: "opencode" | "claude-code" | "pi" renderForTool = { pkgs, agentsInput, tool, modelOverrides ? {}, }: let canonical = agentsInput.lib.loadAgents; in if tool == "opencode" then agentsLib.renderForOpencode { inherit pkgs canonical modelOverrides; } else if tool == "claude-code" then agentsLib.renderForClaudeCode { inherit pkgs canonical modelOverrides; } else if tool == "pi" then agentsLib.renderForPi {inherit pkgs canonical;} else throw "lib.agents.renderForTool: unknown tool '${tool}'. Must be opencode, claude-code, or pi."; }; in agentsLib