2026-04-10 16:47:55 +02:00
|
|
|
# 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;
|
|
|
|
|
|
2026-04-10 17:10:46 +02:00
|
|
|
# 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.
|
2026-04-10 16:47:55 +02:00
|
|
|
renderForOpencode = {
|
|
|
|
|
pkgs,
|
|
|
|
|
canonical,
|
|
|
|
|
modelOverrides ? {},
|
2026-04-10 17:10:46 +02:00
|
|
|
}: 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: " <tool>: <intent>"
|
|
|
|
|
# intent+rules → nested block starting with "*": <intent>, 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}
|
|
|
|
|
'';
|
2026-04-10 16:47:55 +02:00
|
|
|
|
|
|
|
|
# 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
|