diff --git a/.pi-lens/cache/jscpd.json b/.pi-lens/cache/jscpd.json new file mode 100644 index 0000000..ee25c61 --- /dev/null +++ b/.pi-lens/cache/jscpd.json @@ -0,0 +1,7 @@ +{ + "success": true, + "clones": [], + "duplicatedLines": 0, + "totalLines": 0, + "percentage": 0 +} \ No newline at end of file diff --git a/.pi-lens/cache/jscpd.meta.json b/.pi-lens/cache/jscpd.meta.json new file mode 100644 index 0000000..555945b --- /dev/null +++ b/.pi-lens/cache/jscpd.meta.json @@ -0,0 +1,3 @@ +{ + "timestamp": "2026-04-11T04:17:20.531Z" +} \ No newline at end of file diff --git a/.pi-lens/cache/knip.json b/.pi-lens/cache/knip.json new file mode 100644 index 0000000..a4147c6 --- /dev/null +++ b/.pi-lens/cache/knip.json @@ -0,0 +1,9 @@ +{ + "success": false, + "issues": [], + "unusedExports": [], + "unusedFiles": [], + "unusedDeps": [], + "unlistedDeps": [], + "summary": "Failed to parse output" +} \ No newline at end of file diff --git a/.pi-lens/cache/knip.meta.json b/.pi-lens/cache/knip.meta.json new file mode 100644 index 0000000..2eda7c6 --- /dev/null +++ b/.pi-lens/cache/knip.meta.json @@ -0,0 +1,3 @@ +{ + "timestamp": "2026-04-11T04:17:21.374Z" +} \ No newline at end of file diff --git a/.pi-lens/cache/session-start-guidance.json b/.pi-lens/cache/session-start-guidance.json new file mode 100644 index 0000000..f2c1cb4 --- /dev/null +++ b/.pi-lens/cache/session-start-guidance.json @@ -0,0 +1,3 @@ +{ + "content": "📌 pi-lens active — as you work on this project, fix any errors you encounter (including pre-existing). Prefer: lsp_navigation for definitions/references, ast_grep_search for code patterns, grep for text/TODO search." +} \ No newline at end of file diff --git a/.pi-lens/cache/session-start-guidance.meta.json b/.pi-lens/cache/session-start-guidance.meta.json new file mode 100644 index 0000000..e321daf --- /dev/null +++ b/.pi-lens/cache/session-start-guidance.meta.json @@ -0,0 +1,3 @@ +{ + "timestamp": "2026-04-11T04:21:36.939Z" +} \ No newline at end of file diff --git a/.pi-lens/cache/todo-baseline.json b/.pi-lens/cache/todo-baseline.json new file mode 100644 index 0000000..fc69ce2 --- /dev/null +++ b/.pi-lens/cache/todo-baseline.json @@ -0,0 +1,3 @@ +{ + "items": [] +} \ No newline at end of file diff --git a/.pi-lens/cache/todo-baseline.meta.json b/.pi-lens/cache/todo-baseline.meta.json new file mode 100644 index 0000000..449e16d --- /dev/null +++ b/.pi-lens/cache/todo-baseline.meta.json @@ -0,0 +1,3 @@ +{ + "timestamp": "2026-04-11T04:21:36.940Z" +} \ No newline at end of file diff --git a/.pi-lens/turn-state.json b/.pi-lens/turn-state.json new file mode 100644 index 0000000..dd32709 --- /dev/null +++ b/.pi-lens/turn-state.json @@ -0,0 +1,6 @@ +{ + "files": {}, + "turnCycles": 0, + "maxCycles": 3, + "lastUpdated": "2026-04-11T04:17:22.397Z" +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 5d465b9..1ed3682 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -106,6 +106,37 @@ meta = with lib; { **Shared lib**: `portsLib = import ../../lib/ports.nix { inherit lib; }; portHelpers = portsLib.mkPortHelpers { ... };` +## LIBRARY FUNCTIONS + +### `lib.ports` + +Port management utilities. See [Port Management](#port-management). + +### `lib.agents` + +Harness-agnostic agent management. Reads canonical `agent.toml` from the AGENTS +flake input and renders tool-specific configs. + +**Functions:** + +| Function | Purpose | +|----------|--------| +| `loadCanonical { agentsInput }` | Load canonical agents from AGENTS flake | +| `renderForOpencode { pkgs, canonical, modelOverrides }` | Render to OpenCode file-based agents | +| `renderForClaudeCode { pkgs, canonical, modelOverrides }` | Render to Claude Code agents + settings.json | +| `renderForPi { pkgs, canonical }` | Render to Pi AGENTS.md + SYSTEM.md | +| `renderForTool { pkgs, agentsInput, tool, modelOverrides }` | Dispatch to correct renderer | +| `shellHookForTool { pkgs, agentsInput, tool, modelOverrides }` | Generate devShell shellHook | + +### `lib.coding-rules` + +Coding rules injection (renamed from `lib.opencode-rules`). The old name still works. + +| Function | Purpose | +|----------|--------| +| `mkCodingRules { agents, languages, concerns, frameworks }` | Generate rules config + shellHook | +| `mkOpencodeRules` | Backward-compat alias for `mkCodingRules` | + ## PORT MANAGEMENT Central port management: `config.m3ta.ports.get "service"` with host-specific via `hostOverrides` @@ -168,3 +199,27 @@ Use `td usage -q` for subsequent reads. - `td version` - Check version For full workflow details, see the [td documentation](./docs/packages/td.md). + +## MIGRATION: Agent System (OpenCode → Canonical TOML) + +The agent system was migrated from embedded `agents.json` to harness-agnostic +canonical `agent.toml` + `system-prompt.md` in the AGENTS repo. Renderers in +`lib/agents.nix` generate tool-specific configs. + +### What changed in this repo + +- **`lib/agents.nix`**: New — 3 renderers (OpenCode, Claude Code, Pi) + dispatcher + shellHook +- **`lib/coding-rules.nix`**: Renamed from `opencode-rules.nix`, `mkCodingRules` replaces `mkOpencodeRules` +- **`modules/home-manager/coding/agents/`**: New — per-tool HM sub-modules +- **`modules/home-manager/coding/opencode.nix`**: Slimmed — no longer handles agents/skills/context +- **`flake.nix`**: Exports new `agents` HM module + +### What the user must do + +See `modules/home-manager/AGENTS.md` for the full migration guide. Summary: + +1. Move `agentsInput`/`externalSkills` from `coding.opencode` to `coding.agents.opencode` +2. Add `modelOverrides` with previously hardcoded model strings +3. Run `home-manager switch` +4. Remove legacy `agents.json` + `prompts/*.txt` from AGENTS repo +5. Remove `lib.agentsJson` backward-compat bridge from AGENTS `flake.nix` diff --git a/docs/guides/using-modules.md b/docs/guides/using-modules.md index 3c0ba9a..e04208c 100644 --- a/docs/guides/using-modules.md +++ b/docs/guides/using-modules.md @@ -32,7 +32,13 @@ modules/home-manager/ │ └── zellij-ps.nix └── coding/ # Development tools ├── default.nix # Aggregates coding modules - └── editors.nix + ├── editors.nix + ├── opencode.nix # OpenCode non-agent config + └── agents/ # Per-tool agent deployment + ├── default.nix + ├── opencode.nix + ├── claude-code.nix + └── pi.nix ``` ## Importing Modules @@ -197,6 +203,61 @@ m3ta.coding.editors = { **Documentation**: [Editors Module](../modules/home-manager/coding/editors.md) +### `coding.opencode` + +OpenCode AI coding assistant (non-agent config: theme, formatter, plugins). + +```nix +coding.opencode = { + enable = true; + ohMyOpencodeSettings = { + agents.sisyphus.model = "anthropic/claude-opus-4-5"; + }; + extraSettings = { + provider.anthropic.name = "Anthropic"; + }; +}; +``` + +### `coding.agents.opencode` + +OpenCode agent deployment from canonical TOML definitions. + +```nix +coding.agents.opencode = { + enable = true; + agentsInput = inputs.agents; + modelOverrides = { + chiron = "anthropic/claude-sonnet-4"; + }; + externalSkills = [ + { src = inputs.skills-anthropic; } + ]; +}; +``` + +### `coding.agents.claude-code` + +Claude Code agent deployment from canonical TOML definitions. + +```nix +coding.agents.claude-code = { + enable = true; + agentsInput = inputs.agents; +}; +``` + +### `coding.agents.pi` + +Pi agent deployment from canonical TOML definitions. + +```nix +coding.agents.pi = { + enable = true; + agentsInput = inputs.agents; +}; +``` + ## Common Patterns ### Module Configuration diff --git a/flake.nix b/flake.nix index af33a49..2930806 100644 --- a/flake.nix +++ b/flake.nix @@ -79,6 +79,7 @@ default = import ./modules/home-manager; ports = import ./modules/home-manager/ports.nix; opencode = import ./modules/home-manager/coding/opencode.nix; + agents = import ./modules/home-manager/coding/agents; zellij-ps = import ./modules/home-manager/zellij-ps.nix; }; diff --git a/lib/agents.nix b/lib/agents.nix index fe1af6f..147e9af 100644 --- a/lib/agents.nix +++ b/lib/agents.nix @@ -5,50 +5,53 @@ # # 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"; +# rendered = m3taLib.agents.renderForOpencode { +# inherit pkgs canonical; +# modelOverrides = { chiron = "anthropic/claude-sonnet-4"; }; # }; # in { ... } -# -# Renderers are stubs for now (Tasks 9-11 will provide real implementations). {lib}: let + # ── Shared helpers ───────────────────────────────────────────── + # 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; + action = lib.last parts; + pattern = lib.concatStringsSep ":" (lib.init parts); + in {inherit pattern action;}; + agentsLib = { + # ── loadCanonical ───────────────────────────────────────────── + # # 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. + # ── OpenCode renderer ───────────────────────────────────────── + # + # Produces a directory of agent *.md files suitable for + # ~/.config/opencode/agents/ (system-level) + # .opencode/agents/ (project-level) + # # Each file has YAML frontmatter (description, mode, optional model, # optional permission) followed by the agent's systemPrompt content. + # The filename (without .md) becomes the agent name in OpenCode. + 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). + # Render one permission section to YAML lines. # intent-only → single line: " : " - # intent+rules → nested block starting with "*": , then specific rules + # intent+rules → nested block renderPermSection = tool: section: if !(section ? rules) || section.rules == [] then [" ${tool}: ${section.intent}"] @@ -59,8 +62,6 @@ 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 [] @@ -70,7 +71,6 @@ lib.mapAttrsToList renderPermSection permissions ); - # Build the YAML frontmatter string for one agent. mkFrontmatter = name: agent: let descLine = "description: \"${agent.description}.\""; modeLine = "mode: ${agent.mode}"; @@ -85,18 +85,14 @@ 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 ); @@ -106,25 +102,245 @@ ${copyCommands} ''; - # Stub: Claude Code renderer — produces .claude/agents/*.md files. - # To be implemented in Task 10. + # ── Claude Code renderer ────────────────────────────────────── + # + # Produces a directory containing: + # .claude/agents/.md — one per agent with YAML frontmatter + # .claude/settings.json — permission rules in Claude Code DSL + # + # Claude Code requires: + # - name field: [a-z0-9-]+ (kebab-case) + # - description field: required + # - All agents are subagents (no primary/subagent distinction) + renderForClaudeCode = { pkgs, canonical, modelOverrides ? {}, - }: - pkgs.runCommand "claude-code-agents" {} "echo stub > $out"; + }: let + # Claude Code permission DSL format: "Tool(pattern)" or just "Tool" + # Canonical bash rules → "Bash(pattern)" entries + # Canonical edit rules → "Edit(pattern)" entries + renderPermAllow = permissions: let + bashRules = + if !(permissions ? bash) + then [] + else if permissions.bash.intent == "allow" + then ["Bash"] + else + map + (r: let parsed = parseRule r; in "Bash(${parsed.pattern})") + (lib.filter (r: (parseRule r).action == "allow") (permissions.bash.rules or [])); + editRules = + if !(permissions ? edit) + then [] + else if permissions.edit.intent == "allow" + then ["Edit"] + else + map + (r: let parsed = parseRule r; in "Edit(${parsed.pattern})") + (lib.filter (r: (parseRule r).action == "allow") (permissions.edit.rules or [])); + webRules = + lib.optional (permissions.webfetch.intent or "" == "allow") "WebFetch"; + in + bashRules ++ editRules ++ webRules; + + renderPermDeny = permissions: let + bashRules = + if !(permissions ? bash) + then [] + else + map + (r: let parsed = parseRule r; in "Bash(${parsed.pattern})") + (lib.filter (r: (parseRule r).action == "deny") (permissions.bash.rules or [])); + editRules = + if !(permissions ? edit) + then [] + else + map + (r: let parsed = parseRule r; in "Edit(${parsed.pattern})") + (lib.filter (r: (parseRule r).action == "deny") (permissions.edit.rules or [])); + in + bashRules ++ editRules; + + # Build YAML frontmatter for one Claude Code agent .md file. + mkClaudeFrontmatter = name: agent: let + descLine = "description: \"${agent.description}\""; + modelLine = + lib.optionalString + (modelOverrides ? ${name}) + "model: ${modelOverrides.${name}}\n"; + skillsLine = + if (agent ? skills) && agent.skills != [] + then "skills:\n" + lib.concatStringsSep "\n" (map (s: " - ${s}") agent.skills) + "\n" + else ""; + in "---\n${descLine}\n${modelLine}${skillsLine}---\n"; + + mkClaudeAgentContent = name: agent: + (mkClaudeFrontmatter name agent) + agent.systemPrompt; + + mkClaudeAgentFile = name: agent: + pkgs.writeText "${name}.md" (mkClaudeAgentContent name agent); + + agentFiles = lib.mapAttrs mkClaudeAgentFile canonical; + + # Build settings.json with permission rules aggregated from all agents. + allAllows = lib.flatten (lib.mapAttrsToList (_: agent: renderPermAllow (agent.permissions or {})) canonical); + allDenies = lib.flatten (lib.mapAttrsToList (_: agent: renderPermDeny (agent.permissions or {})) canonical); + + settingsJson = builtins.toJSON { + permissions = { + allow = lib.unique (lib.sort (a: b: a < b) allAllows); + deny = lib.unique (lib.sort (a: b: a < b) allDenies); + }; + }; + + settingsFile = pkgs.writeText "claude-settings.json" settingsJson; + + copyAgentCommands = lib.concatStringsSep "\n" ( + lib.mapAttrsToList (name: file: "cp ${file} $out/.claude/agents/${name}.md") agentFiles + ); + in + pkgs.runCommand "claude-code-agents" {} '' + mkdir -p $out/.claude/agents + ${copyAgentCommands} + cp ${settingsFile} $out/.claude/settings.json + ''; + + # ── Pi renderer ─────────────────────────────────────────────── + # + # This renderer produces: + # AGENTS.md — concatenated agent descriptions + specialist listing + # SYSTEM.md — primary agent's system prompt (replaces Pi default) + # agents/{name}.md — one per agent for pi-subagents (YAML frontmatter + prompt) + # + # The agents/ files use pi-subagents frontmatter format: + # name, description, tools, extensions, model, thinking, skill, + # output, defaultReads, defaultProgress, interactive, maxSubagentDepth - # Stub: Pi renderer — produces AGENTS.md + SYSTEM.md. - # To be implemented in Task 11. renderForPi = { pkgs, canonical, - }: - pkgs.runCommand "pi-agents" {} "echo stub > $out"; + modelOverrides ? {}, + primaryAgent ? null, + }: let + # Find the primary agent (there should be exactly one). + primaryAgents = lib.filterAttrs (_: a: a.mode == "primary") canonical; + primaryNames = lib.attrNames primaryAgents; + primaryName = + if primaryAgent != null + then primaryAgent + else if primaryNames == [] + then throw "lib.agents.renderForPi: no primary agent found" + else builtins.head primaryNames; + primary = builtins.getAttr primaryName primaryAgents; - # Dispatch renderer by tool name. + # Subagents for the specialist listing. + subagents = lib.filterAttrs (_: a: a.mode != "primary") canonical; + + # ── Permission → Pi tool mapping ────────────────────────────── + # + # Pi built-in tools: read, bash, edit, write, grep, find, ls, + # mcp, subagent, web_search, fetch_content, etc. + # Canonical tools: bash, edit, webfetch, websearch, question, external_directory + # + # We map canonical permissions to Pi's tool list. + # intent=allow → include tool; intent=deny → exclude; intent=ask → include (Pi has no ask granularity) + # When specific allow rules exist, the tool is always included (Pi can't restrict by pattern). + + piToolsForAgent = agent: let + perms = agent.permissions or {}; + tools = []; + # Always available: read (no permission concept in Pi) + addIf = tool: section: + if section.intent == "allow" || section.intent == "ask" + then [tool] + else []; + # bash → bash + withBash = tools ++ (addIf "bash" (perms.bash or {intent = "ask";})); + # edit → edit + withEdit = withBash ++ (addIf "edit" (perms.edit or {intent = "deny";})); + # webfetch → fetch_content + withFetch = withEdit ++ (addIf "fetch_content" (perms.webfetch or {intent = "deny";})); + # websearch → web_search + withSearch = withFetch ++ (addIf "web_search" (perms.websearch or {intent = "deny";})); + in + lib.unique (withSearch ++ ["read" "grep" "find" "ls"]); + + # ── Build YAML frontmatter for pi-subagents .md files ────────── + mkPiFrontmatter = name: agent: let + tools = piToolsForAgent agent; + descLine = "description: \"${agent.description}\""; + toolsLine = "tools: ${lib.concatStringsSep ", " tools}"; + model = + if modelOverrides ? ${name} + then "model: ${modelOverrides.${name}}" + else ""; + skillsLine = + if (agent ? skills) && agent.skills != [] + then "skill: ${lib.concatStringsSep ", " agent.skills}" + else ""; + in + "---\n" + + "name: ${name}\n" + + "${descLine}\n" + + "${toolsLine}\n" + + (lib.optionalString (model != "") "${model}\n") + + (lib.optionalString (skillsLine != "") "${skillsLine}\n") + + "---\n"; + + mkPiAgentContent = name: agent: + (mkPiFrontmatter name agent) + agent.systemPrompt; + + mkPiAgentFile = name: agent: + pkgs.writeText "${name}.md" (mkPiAgentContent name agent); + + piAgentFiles = lib.mapAttrs mkPiAgentFile canonical; + + # ── Build AGENTS.md content ─────────────────────────────────── + primaryDn = primary.display_name or primaryName; + specialistEntries = let + mkEntry = name: agent: let + dn = agent.display_name or name; + in + "- **" + dn + "**: " + agent.description; + in + lib.mapAttrsToList mkEntry subagents; + agentsMd = + "# Agent Instructions\n" + + "\n" + + "## " + + primaryDn + + "\n" + + "\n" + + primary.description + + "\n" + + "\n" + + ( + if subagents == {} + then "" + else "## Available Specialists\n\n" + lib.concatStringsSep "\n" specialistEntries + "\n" + ); + + agentsMdFile = pkgs.writeText "AGENTS.md" agentsMd; + systemMdFile = pkgs.writeText "SYSTEM.md" primary.systemPrompt; + + copyAgentCommands = lib.concatStringsSep "\n" ( + lib.mapAttrsToList (name: file: "cp ${file} $out/agents/${name}.md") piAgentFiles + ); + in + pkgs.runCommand "pi-agents" {} '' + mkdir -p $out/agents + cp ${agentsMdFile} $out/AGENTS.md + cp ${systemMdFile} $out/SYSTEM.md + ${copyAgentCommands} + ''; + + # ── renderForTool dispatcher ────────────────────────────────── + # + # Dispatches to the correct renderer by tool name. # tool: "opencode" | "claude-code" | "pi" + renderForTool = { pkgs, agentsInput, @@ -144,8 +360,60 @@ inherit pkgs canonical modelOverrides; } else if tool == "pi" - then agentsLib.renderForPi {inherit pkgs canonical;} + then + agentsLib.renderForPi { + inherit pkgs canonical modelOverrides; + } else throw "lib.agents.renderForTool: unknown tool '${tool}'. Must be opencode, claude-code, or pi."; + + # ── shellHookForTool ────────────────────────────────────────── + # + # Generates a shellHook string for use in devShells that symlinks + # rendered agent files into the project directory. + # + # Usage: + # devShells.default = pkgs.mkShell { + # shellHook = m3taLib.agents.shellHookForTool { + # inherit pkgs; + # agentsInput = inputs.agents; + # tool = "opencode"; + # modelOverrides = { chiron = "anthropic/claude-sonnet-4"; }; + # }; + # }; + + shellHookForTool = { + pkgs, + agentsInput, + tool, + modelOverrides ? {}, + }: let + rendered = agentsLib.renderForTool { + inherit pkgs agentsInput tool modelOverrides; + }; + in + if tool == "opencode" + then '' + # Agent files for OpenCode + mkdir -p .opencode/agents + ln -sfn ${rendered}/* .opencode/agents/ + '' + else if tool == "claude-code" + then '' + # Agent files for Claude Code + mkdir -p .claude/agents + ln -sfn ${rendered}/.claude/agents/* .claude/agents/ + ln -sfn ${rendered}/.claude/settings.json .claude/settings.json + '' + else if tool == "pi" + then '' + # Agent files for Pi + ln -sfn ${rendered}/AGENTS.md AGENTS.md + mkdir -p .pi + ln -sfn ${rendered}/SYSTEM.md .pi/SYSTEM.md + mkdir -p .pi/agents + ln -sfn ${rendered}/agents/* .pi/agents/ + '' + else throw "lib.agents.shellHookForTool: unknown tool '${tool}'"; }; in agentsLib diff --git a/lib/coding-rules.nix b/lib/coding-rules.nix new file mode 100644 index 0000000..365b9ee --- /dev/null +++ b/lib/coding-rules.nix @@ -0,0 +1,121 @@ +# Opencode rules management utilities +# +# This module provides functions to configure Opencode agent rules across +# multiple projects. Rules are defined in the AGENTS repository and can be +# selectively included based on language, framework, and concerns. +# +# Usage in your configuration: +# +# # In your flake or configuration: +# let +# m3taLib = inputs.m3ta-nixpkgs.lib.${system}; +# +# rules = m3taLib.coding-rules.mkCodingRules { +# agents = inputs.agents; +# languages = [ "python" "typescript" ]; +# concerns = [ "coding-style" "naming" "documentation" ]; +# frameworks = [ "react" "fastapi" ]; +# }; +# in { +# # Use in your devShell: +# devShells.default = pkgs.mkShell { +# shellHook = rules.shellHook; +# inherit (rules) instructions; +# }; +# } +# +# The shellHook creates: +# - A `.opencode-rules/` symlink pointing to the AGENTS repository rules directory +# - An `opencode.json` file with a $schema reference and instructions list +# +# The instructions list contains paths relative to the project root, all prefixed +# with `.opencode-rules/`, making them portable across different project locations. +{lib}: let + # Create Opencode rules configuration from AGENTS repository + # + # Args: + # agents: Path to the AGENTS repository (non-flake input) + # languages: Optional list of language-specific rules to include + # (e.g., [ "python" "typescript" "rust" ]) + # concerns: Optional list of concern rules to include + # Default: [ "coding-style" "naming" "documentation" "testing" "git-workflow" "project-structure" ] + # frameworks: Optional list of framework-specific rules to include + # (e.g., [ "react" "fastapi" "django" ]) + # extraInstructions: Optional list of additional instruction paths + # (for custom rules outside standard locations) + # + # Returns: + # An attribute set containing: + # - shellHook: Bash code to create symlink and opencode.json + # - instructions: List of rule file paths (relative to project root) + # + # Example: + # mkCodingRules { + # agents = inputs.agents; + # languages = [ "python" ]; + # frameworks = [ "fastapi" ]; + # } + # # Returns: + # # { + # # shellHook = "..."; + # # instructions = [ + # # ".opencode-rules/concerns/coding-style.md" + # # ".opencode-rules/concerns/naming.md" + # # ".opencode-rules/concerns/documentation.md" + # # ".opencode-rules/concerns/testing.md" + # # ".opencode-rules/concerns/git-workflow.md" + # # ".opencode-rules/concerns/project-structure.md" + # # ".opencode-rules/languages/python.md" + # # ".opencode-rules/frameworks/fastapi.md" + # # ]; + # # } + mkCodingRules = { + agents, + languages ? [], + concerns ? [ + "coding-style" + "naming" + "documentation" + "testing" + "git-workflow" + "project-structure" + ], + frameworks ? [], + extraInstructions ? [], + }: let + rulesDir = ".opencode-rules"; + + # Build instructions list by mapping concerns, languages, frameworks to their file paths + # All paths are relative to project root via the rulesDir symlink + instructions = + (map (c: "${rulesDir}/concerns/${c}.md") concerns) + ++ (map (l: "${rulesDir}/languages/${l}.md") languages) + ++ (map (f: "${rulesDir}/frameworks/${f}.md") frameworks) + ++ extraInstructions; + + # Generate JSON configuration for Opencode + opencodeConfig = { + "$schema" = "https://opencode.ai/config.json"; + inherit instructions; + }; + in { + inherit instructions; + + # Shell hook to set up rules in the project + # Creates a symlink to the AGENTS rules directory and generates opencode.json + shellHook = '' + # Create/update symlink to AGENTS rules directory + ln -sfn ${agents}/rules ${rulesDir} + + # Generate opencode.json configuration file + cat > opencode.json <<'OPENCODE_EOF' + ${builtins.toJSON opencodeConfig} + OPENCODE_EOF + ''; + }; + + # Backward-compat alias + mkOpencodeRules = mkCodingRules; +in { + inherit mkCodingRules mkOpencodeRules; +} diff --git a/lib/default.nix b/lib/default.nix index b4dd130..a6bdfa3 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -7,8 +7,11 @@ # Port management utilities ports = import ./ports.nix {inherit lib;}; - # OpenCode rules injection utilities - opencode-rules = import ./opencode-rules.nix {inherit lib;}; + # Coding rules injection utilities (renamed from opencode-rules) + coding-rules = import ./coding-rules.nix {inherit lib;}; + + # Backward-compat alias: opencode-rules → coding-rules + opencode-rules = import ./coding-rules.nix {inherit lib;}; # Agent configuration management utilities agents = import ./agents.nix {inherit lib;}; diff --git a/modules/home-manager/AGENTS.md b/modules/home-manager/AGENTS.md index c08b63c..1629ba4 100644 --- a/modules/home-manager/AGENTS.md +++ b/modules/home-manager/AGENTS.md @@ -13,7 +13,13 @@ home-manager/ │ └── zellij-ps.nix └── coding/ # Development tools ├── default.nix # Category aggregator - └── editors.nix # Neovim + Zed configs + ├── editors.nix # Neovim + Zed configs + ├── opencode.nix # OpenCode non-agent config (theme, plugins, formatter) + └── agents/ # Per-tool agent deployment (canonical TOML → rendered) + ├── default.nix + ├── opencode.nix # File-based agents + skills + context + ├── claude-code.nix # Claude Code agents + settings.json + └── pi.nix # Pi AGENTS.md + SYSTEM.md ``` ## Where to Look @@ -24,11 +30,16 @@ home-manager/ | Add coding module | `coding/.nix`, import in `coding/default.nix` | | Add new category | Create `/default.nix`, import in root `default.nix` | | Module with host ports | Import `../../lib/ports.nix`, use `mkPortHelpers` | +| Add agent renderer | `coding/agents/.nix`, import in `coding/agents/default.nix` | ## Option Namespaces - `cli.*` - CLI tools (e.g., `cli.zellij-ps.enable`) - `coding.editors.*` - Editor configs (e.g., `coding.editors.neovim.enable`) +- `coding.opencode.*` - OpenCode non-agent config (theme, plugins, formatter) +- `coding.agents.opencode.*` - OpenCode agent deployment (file-based agents) +- `coding.agents.claude-code.*` - Claude Code agent deployment +- `coding.agents.pi.*` - Pi agent deployment - `m3ta.ports.*` - Port management (shared with NixOS) ## Patterns @@ -72,3 +83,153 @@ config = mkMerge [ | `generateEnvVars` | Available | Not available | | Output file | `~/.config/m3ta/ports.json` | `/etc/m3ta/ports.json` | | Package access | `pkgs.*` via overlay | `pkgs.*` via overlay | + +## Agent Modules + +Agent definitions are stored as canonical `agent.toml` + `system-prompt.md` in the +[AGENTS repo](https://code.m3ta.dev/m3tam3re/AGENTS). Renderers in `lib/agents.nix` +transform these into tool-specific configs. Each tool has its own HM sub-module +under `coding/agents/`. + +### OpenCode (`coding.agents.opencode`) + +Renders file-based agents to `~/.config/opencode/agents/*.md`: + +```nix +coding.agents.opencode = { + enable = true; + agentsInput = inputs.agents; + modelOverrides = { + chiron = "anthropic/claude-sonnet-4"; + }; + externalSkills = [ + { src = inputs.skills-anthropic; } + ]; +}; +``` + +**Options:** `enable`, `agentsInput`, `modelOverrides`, `externalSkills` + +### Claude Code (`coding.agents.claude-code`) + +Renders agents to `~/.claude/agents/*.md` + `~/.claude/settings.json`: + +```nix +coding.agents.claude-code = { + enable = true; + agentsInput = inputs.agents; + modelOverrides = {}; +}; +``` + +**Options:** `enable`, `agentsInput`, `modelOverrides` + +### Pi (`coding.agents.pi`) + +Renders `AGENTS.md` + `SYSTEM.md` to `~/.pi/agent/`: + +```nix +coding.agents.pi = { + enable = true; + agentsInput = inputs.agents; +}; +``` + +**Options:** `enable`, `agentsInput` + +### Project-level usage + +For per-project agent setup via `flake.nix` + `direnv`: + +```nix +m3taLib.agents.shellHookForTool { + inherit pkgs; + agentsInput = inputs.agents; + tool = "opencode"; + modelOverrides = { chiron = "anthropic/claude-sonnet-4"; }; +}; +``` + +## Migration Guide (OpenCode agents) + +The agent system was migrated from embedded `agents.json` to file-based canonical +`agent.toml` definitions. Here is how to migrate your home-manager config. + +### What changed + +| Before | After | +|--------|-------| +| `coding.opencode.agentsInput` | `coding.agents.opencode.agentsInput` | +| `coding.opencode.externalSkills` | `coding.agents.opencode.externalSkills` | +| Agents embedded in `config.json` | File-based `~/.config/opencode/agents/*.md` | +| Model hardcoded in `agents.json` | Per-machine `modelOverrides` | +| `mkOpencodeRules` | `mkCodingRules` (old name still works) | + +### Migration steps + +**1. Update home-manager config:** + +Move `agentsInput` and `externalSkills` from `coding.opencode` to `coding.agents.opencode`. +Add `modelOverrides` with the models previously hardcoded in agents.json: + +```nix +# BEFORE (legacy): +coding.opencode = { + enable = true; + agentsInput = inputs.agents; + externalSkills = [{ src = inputs.skills-anthropic; }]; + ohMyOpencodeSettings = { ... }; +}; + +# AFTER (new): +coding.opencode = { + enable = true; + ohMyOpencodeSettings = { ... }; +}; + +coding.agents.opencode = { + enable = true; + agentsInput = inputs.agents; + externalSkills = [{ src = inputs.skills-anthropic; }]; + modelOverrides = { + chiron = "zai-coding-plan/glm-5"; + "chiron-forge" = "zai-coding-plan/glm-5"; + }; +}; +``` + +**2. Run `home-manager switch`:** + +```bash +home-manager switch --flake . +``` + +**3. Verify agents are deployed:** + +```bash +ls ~/.config/opencode/agents/ +# Should show: chiron.md chiron-forge.md hermes.md athena.md apollo.md calliope.md +``` + +**4. Remove legacy files from AGENTS repo** (after confirming everything works): + +```bash +cd /home/m3tam3re/p/AI/AGENTS +rm agents/agents.json +rm prompts/chiron.txt prompts/chiron-forge.txt prompts/hermes.txt \ + prompts/athena.txt prompts/apollo.txt prompts/calliope.txt +rmdir prompts/ # if empty +# Also remove lib.agentsJson from flake.nix +``` + +**5. Final cleanup:** After legacy files are removed from AGENTS repo, +remove `lib.agentsJson` from the AGENTS `flake.nix` (it's only needed for +backward compatibility during the transition). + +### Key advantage of the new system + +Prompt changes no longer require `home-manager switch`. Since agents are +deployed as file-based `~/.config/opencode/agents/*.md` (symlinks to Nix store), +you only need to edit the `system-prompt.md` in the AGENTS repo, commit, update +the flake lock, and run `home-manager switch`. Or for local development, edit +the file directly and restart the tool. diff --git a/modules/home-manager/coding/agents/claude-code.nix b/modules/home-manager/coding/agents/claude-code.nix new file mode 100644 index 0000000..2375d88 --- /dev/null +++ b/modules/home-manager/coding/agents/claude-code.nix @@ -0,0 +1,90 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; let + cfg = config.coding.agents.claude-code; + mcpCfg = config.programs.mcp or null; +in { + options.coding.agents.claude-code = { + enable = mkEnableOption "Claude Code agent management via canonical agent.toml definitions"; + + agentsInput = mkOption { + type = types.nullOr types.anything; + default = null; + description = '' + The `agents` flake input (your personal AGENTS repo). + When set, agents are rendered from canonical agent.toml files + and symlinked to ~/.claude/agents/. + ''; + }; + + modelOverrides = mkOption { + type = types.attrsOf types.str; + default = {}; + description = '' + Per-agent model overrides. Maps agent slug to model alias or ID. + Example: { chiron = "claude-sonnet-4-20250514"; } + ''; + example = literalExpression '' + { + chiron = "claude-sonnet-4-20250514"; + "chiron-forge" = "claude-sonnet-4-20250514"; + } + ''; + }; + + mcpServers = mkOption { + type = types.attrsOf types.anything; + default = if mcpCfg != null then mcpCfg.servers else {}; + defaultText = literalExpression "config.programs.mcp.servers"; + description = '' + MCP server configurations for Claude Code. + Merged into ~/.claude/settings.json alongside permissions. + Automatically inherits from config.programs.mcp.servers. + ''; + }; + }; + + config = mkIf cfg.enable (let + agentsLib = (import ../../../../lib {inherit lib;}).agents; + + # Rendered agents + permissions (only if agentsInput is set) + rendered = mkIf (cfg.agentsInput != null) ( + agentsLib.renderForClaudeCode { + inherit pkgs; + canonical = cfg.agentsInput.lib.loadAgents; + modelOverrides = cfg.modelOverrides; + } + ); + + # Merge MCP servers into the rendered settings.json. + # The renderer produces { permissions: { allow, deny } }. + # We add mcpServers on top. + settingsJson = + if cfg.agentsInput != null + then let + renderedSettings = builtins.fromJSON (builtins.readFile "${rendered}/.claude/settings.json"); + withMcp = + if cfg.mcpServers != {} + then renderedSettings // {mcpServers = cfg.mcpServers;} + else renderedSettings; + in + pkgs.writeText "claude-settings.json" (builtins.toJSON withMcp) + else if cfg.mcpServers != {} + then pkgs.writeText "claude-settings.json" (builtins.toJSON {mcpServers = cfg.mcpServers;}) + else null; + in { + # Rendered agent files symlinked to ~/.claude/agents/ + home.file.".claude/agents" = mkIf (cfg.agentsInput != null) { + source = "${rendered}/.claude/agents"; + }; + + # Rendered settings.json with permissions + MCP servers + home.file.".claude/settings.json" = mkIf (settingsJson != null) { + source = "${settingsJson}"; + }; + }); +} diff --git a/modules/home-manager/coding/agents/default.nix b/modules/home-manager/coding/agents/default.nix new file mode 100644 index 0000000..681a47a --- /dev/null +++ b/modules/home-manager/coding/agents/default.nix @@ -0,0 +1,10 @@ +# Per-tool agent sub-modules +# Each module handles rendering canonical agent.toml definitions +# for a specific AI coding tool. +{ + imports = [ + ./opencode.nix + ./claude-code.nix + ./pi.nix + ]; +} diff --git a/modules/home-manager/coding/agents/opencode.nix b/modules/home-manager/coding/agents/opencode.nix new file mode 100644 index 0000000..babde2e --- /dev/null +++ b/modules/home-manager/coding/agents/opencode.nix @@ -0,0 +1,113 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; let + cfg = config.coding.agents.opencode; +in { + options.coding.agents.opencode = { + enable = mkEnableOption "OpenCode agent management via canonical agent.toml definitions"; + + agentsInput = mkOption { + type = types.nullOr types.anything; + default = null; + description = '' + The `agents` flake input (your personal AGENTS repo). + When set, agents are rendered from canonical agent.toml files + and symlinked to ~/.config/opencode/agents/. + ''; + }; + + modelOverrides = mkOption { + type = types.attrsOf types.str; + default = {}; + description = '' + Per-agent model overrides. Maps agent slug to model string. + Example: { chiron = "anthropic/claude-sonnet-4"; } + ''; + example = literalExpression '' + { + chiron = "anthropic/claude-sonnet-4"; + "chiron-forge" = "anthropic/claude-sonnet-4"; + } + ''; + }; + + externalSkills = mkOption { + type = types.listOf (types.submodule { + options = { + src = mkOption { + type = types.anything; + description = "Flake input pointing to a skills repository root."; + }; + skillsDir = mkOption { + type = types.str; + default = "skills"; + description = '' + Subdirectory inside src that contains skill folders. + ''; + }; + selectSkills = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + description = '' + List of skill names to cherry-pick from this source. + null means include every skill found in skillsDir. + ''; + }; + }; + }); + default = []; + description = '' + External skill sources passed to mkOpencodeSkills. + Each entry maps directly to an element of the externalSkills + list accepted by the AGENTS flake's lib.mkOpencodeSkills. + ''; + example = literalExpression '' + [ + { src = inputs.skills-anthropic; selectSkills = [ "claude-api" ]; } + { src = inputs.skills-vercel; } + ] + ''; + }; + }; + + config = mkIf cfg.enable { + # Rendered agent files symlinked to ~/.config/opencode/agents/ + xdg.configFile."opencode/agents" = mkIf (cfg.agentsInput != null) { + source = (import ../../../../lib {inherit lib;}).agents.renderForOpencode { + inherit pkgs; + canonical = cfg.agentsInput.lib.loadAgents; + modelOverrides = cfg.modelOverrides; + }; + }; + + # Skills (merged from personal AGENTS repo + optional external skills) + xdg.configFile."opencode/skills" = mkIf (cfg.agentsInput != null) { + source = cfg.agentsInput.lib.mkOpencodeSkills { + inherit pkgs; + customSkills = "${cfg.agentsInput}/skills"; + externalSkills = + map ( + entry: + {inherit (entry) src skillsDir;} + // optionalAttrs (entry.selectSkills != null) {inherit (entry) selectSkills;} + ) + cfg.externalSkills; + }; + }; + + # Static config dirs from AGENTS repo + xdg.configFile."opencode/context" = mkIf (cfg.agentsInput != null) { + source = "${cfg.agentsInput}/context"; + }; + xdg.configFile."opencode/commands" = mkIf (cfg.agentsInput != null) { + source = "${cfg.agentsInput}/commands"; + }; + xdg.configFile."opencode/prompts" = mkIf (cfg.agentsInput != null) { + source = "${cfg.agentsInput}/prompts"; + }; + }; +} diff --git a/modules/home-manager/coding/agents/pi.nix b/modules/home-manager/coding/agents/pi.nix new file mode 100644 index 0000000..9548435 --- /dev/null +++ b/modules/home-manager/coding/agents/pi.nix @@ -0,0 +1,234 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; let + cfg = config.coding.agents.pi; + mcpCfg = config.programs.mcp or null; +in { + options.coding.agents.pi = { + enable = mkEnableOption "Pi agent management via canonical agent.toml definitions"; + + mcpServers = mkOption { + type = types.attrsOf types.anything; + default = if mcpCfg != null then mcpCfg.servers else {}; + defaultText = literalExpression "config.programs.mcp.servers"; + description = '' + MCP server configurations for Pi (pi-mcp-adapter). + Written to ~/.pi/agent/mcp.json. + Automatically inherits from config.programs.mcp.servers. + ''; + }; + + agentsInput = mkOption { + type = types.nullOr types.anything; + default = null; + description = '' + The `agents` flake input (your personal AGENTS repo). + When set, the primary agent's system prompt is rendered as SYSTEM.md, + all agents are listed in AGENTS.md, and subagent .md files are deployed. + ''; + }; + + modelOverrides = mkOption { + type = types.attrsOf types.str; + default = {}; + description = '' + Per-agent model overrides for Pi subagents. + Maps agent slug to model string, e.g.: + { chiron = "anthropic/claude-sonnet-4"; chiron-forge = "anthropic/claude-sonnet-4"; } + ''; + }; + + primaryAgent = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Override which canonical agent is used as primary for SYSTEM.md. + When null, the first agent with mode="primary" is used. + ''; + }; + + settings = mkOption { + type = types.submodule { + freeformType = types.attrsOf types.anything; + options = { + packages = mkOption { + type = types.listOf types.str; + default = []; + description = '' + Pi packages to install (npm:, git:, or local paths). + These are written to ~/.pi/agent/settings.json. + ''; + }; + + defaultProvider = mkOption { + type = types.nullOr types.str; + default = null; + description = "Default LLM provider (e.g. 'anthropic', 'openai', 'zai')."; + }; + + defaultModel = mkOption { + type = types.nullOr types.str; + default = null; + description = "Default model ID."; + }; + + defaultThinkingLevel = mkOption { + type = types.nullOr (types.enum ["off" "minimal" "low" "medium" "high" "xhigh"]); + default = null; + description = "Default extended thinking level."; + }; + + theme = mkOption { + type = types.nullOr types.str; + default = null; + description = "Pi theme name."; + }; + + hideThinkingBlock = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Hide thinking blocks in output."; + }; + + quietStartup = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Hide startup header."; + }; + + compaction = mkOption { + type = types.nullOr (types.submodule { + options = { + enabled = mkOption { + type = types.nullOr types.bool; + default = null; + }; + reserveTokens = mkOption { + type = types.nullOr types.int; + default = null; + }; + keepRecentTokens = mkOption { + type = types.nullOr types.int; + default = null; + }; + }; + }); + default = null; + description = "Auto-compaction settings."; + }; + + enabledModels = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + description = "Model patterns for Ctrl+P cycling."; + }; + + sessionDir = mkOption { + type = types.nullOr types.str; + default = null; + description = "Directory where session files are stored."; + }; + + extensions = mkOption { + type = types.listOf types.str; + default = []; + description = "Local extension file paths or directories."; + }; + + skills = mkOption { + type = types.listOf types.str; + default = []; + description = "Local skill file paths or directories."; + }; + }; + }; + default = {}; + description = '' + Pi settings written to ~/.pi/agent/settings.json. + Only non-null values are included in the generated JSON. + See pi docs/settings.md for all options. + ''; + }; + }; + + config = mkIf cfg.enable (let + # Build settings.json by filtering out null values recursively + filterNulls = attrs: + lib.filterAttrs (_: v: v != null) ( + builtins.mapAttrs (_: v: + if builtins.isAttrs v + then let + filtered = filterNulls v; + in + if filtered == {} then null else filtered + else v) attrs + ); + + piSettings = filterNulls cfg.settings; + + # Rendered agents (only computed when agentsInput is set) + rendered = + if cfg.agentsInput != null + then + (import ../../../../lib {inherit lib;}).agents.renderForPi { + inherit pkgs; + canonical = cfg.agentsInput.lib.loadAgents; + modelOverrides = cfg.modelOverrides; + primaryAgent = cfg.primaryAgent; + } + else null; + + # Dynamic home.file entries for agent .md files + agentFiles = + if cfg.agentsInput != null + then + let + agentNames = builtins.attrNames cfg.agentsInput.lib.loadAgents; + in + builtins.listToAttrs ( + map (name: { + name = ".pi/agent/agents/${name}.md"; + value = {source = "${rendered}/agents/${name}.md";}; + }) + agentNames + ) + else {}; + in { + home.file = mkMerge [ + # ── MCP servers from programs.mcp → ~/.pi/agent/mcp.json ─────── + (mkIf (cfg.mcpServers != {}) { + ".pi/agent/mcp.json".text = builtins.toJSON {mcpServers = cfg.mcpServers;}; + }) + + # ── ~/.pi/agent/settings.json ────────────────────────────────── + { + ".pi/agent/settings.json".text = builtins.toJSON piSettings; + } + + # ── AGENTS.md — agent descriptions and specialist listing ────── + (mkIf (cfg.agentsInput != null) { + ".pi/agent/AGENTS.md".source = "${rendered}/AGENTS.md"; + }) + + # ── SYSTEM.md — primary agent's system prompt ────────────────── + (mkIf (cfg.agentsInput != null) { + ".pi/agent/SYSTEM.md".source = "${rendered}/SYSTEM.md"; + }) + + # ── Agents — pi-subagents .md files ──────────────────────────── + agentFiles + + # ── Skills symlinked from AGENTS repo ────────────────────────── + (mkIf (cfg.agentsInput != null) { + ".pi/agent/skills".source = cfg.agentsInput.lib.mkOpencodeSkills { + inherit pkgs; + customSkills = "${cfg.agentsInput}/skills"; + }; + }) + ]; + }); +} diff --git a/modules/home-manager/coding/default.nix b/modules/home-manager/coding/default.nix index a20a685..eb22f5d 100644 --- a/modules/home-manager/coding/default.nix +++ b/modules/home-manager/coding/default.nix @@ -3,5 +3,6 @@ imports = [ ./editors.nix ./opencode.nix + ./agents ]; } diff --git a/modules/home-manager/coding/opencode.nix b/modules/home-manager/coding/opencode.nix index a7ef8fa..c9bf28b 100644 --- a/modules/home-manager/coding/opencode.nix +++ b/modules/home-manager/coding/opencode.nix @@ -10,54 +10,6 @@ in { options.coding.opencode = { enable = mkEnableOption "opencode AI coding assistant"; - agentsInput = mkOption { - type = types.nullOr types.anything; - default = null; - description = '' - The `agents` flake input (your personal AGENTS repo). - When set, skills, context, commands, prompts and the agents.json - are all symlinked from this input. - ''; - }; - - externalSkills = mkOption { - type = types.listOf (types.submodule { - options = { - src = mkOption { - type = types.anything; - description = "Flake input pointing to a skills repository root."; - }; - skillsDir = mkOption { - type = types.str; - default = "skills"; - description = '' - Subdirectory inside src that contains skill folders. - ''; - }; - selectSkills = mkOption { - type = types.nullOr (types.listOf types.str); - default = null; - description = '' - List of skill names to cherry-pick from this source. - null means include every skill found in skillsDir. - ''; - }; - }; - }); - default = []; - description = '' - External skill sources passed to mkOpencodeSkills. - Each entry maps directly to an element of the externalSkills - list accepted by the AGENTS flake's lib.mkOpencodeSkills. - ''; - example = literalExpression '' - [ - { src = inputs.skills-anthropic; selectSkills = [ "claude-api" ]; } - { src = inputs.skills-vercel; } - ] - ''; - }; - ohMyOpencodeSettings = mkOption { type = types.attrs; default = {}; @@ -103,33 +55,6 @@ in { }; config = mkIf cfg.enable { - # --- Skills (merged from personal AGENTS repo + optional external skills) --- - xdg.configFile."opencode/skills" = mkIf (cfg.agentsInput != null) { - source = cfg.agentsInput.lib.mkOpencodeSkills { - inherit pkgs; - customSkills = "${cfg.agentsInput}/skills"; - externalSkills = - map ( - entry: - {inherit (entry) src skillsDir;} - // optionalAttrs (entry.selectSkills != null) {inherit (entry) selectSkills;} - ) - cfg.externalSkills; - }; - }; - - # --- Static config dirs from AGENTS repo --- - xdg.configFile."opencode/context" = mkIf (cfg.agentsInput != null) { - source = "${cfg.agentsInput}/context"; - }; - xdg.configFile."opencode/commands" = mkIf (cfg.agentsInput != null) { - source = "${cfg.agentsInput}/commands"; - }; - xdg.configFile."opencode/prompts" = mkIf (cfg.agentsInput != null) { - source = "${cfg.agentsInput}/prompts"; - }; - - # --- Core opencode program settings --- programs.opencode = { enable = true; enableMcpIntegration = true; @@ -144,17 +69,10 @@ in { }; }; } - # Load agents.json from AGENTS repo when available - (mkIf (cfg.agentsInput != null) { - agent = builtins.fromJSON (builtins.readFile "${cfg.agentsInput}/agents/agents.json"); - }) - # Machine/org-specific provider config cfg.extraSettings ]; }; - # --- oh-my-opencode plugin config --- - # Base defaults (no models — those must be set per machine via ohMyOpencodeSettings) home.file.".config/opencode/oh-my-opencode.json".text = builtins.toJSON ( recursiveUpdate {