Compare commits

..

10 Commits

Author SHA1 Message Date
Chiron
54fa93574b refactor: remove dead code, extract shared agent options, optimize flake
- Remove dead overlays/default.nix (flake defines overlays inline)
- Remove orphaned overlays/mods/{beads,n8n}.nix (never imported)
- Remove docs/packages/notesmd-cli.md (package doesn't exist)
- Extract externalSkills submodule to shared-options.nix (eliminates
  ~100 lines of duplication across opencode/claude-code/pi modules)
- Fix lib output: use nixpkgs.lib directly instead of instantiating
  a full nixpkgs just to get lib
- Add lib unit tests to flake checks
- Update stale comment in coding-rules.nix
2026-04-18 10:15:50 +00:00
Chiron
c9ecc0809f fix: add externalSkills option to pi agent module
Skills from flake inputs (e.g. Basecamp) were not being passed to
mkOpencodeSkills for the pi agent, so they never appeared in
~/.pi/agent/skills/. This adds the same externalSkills option that
the opencode agent module already has.
2026-04-18 10:07:44 +00:00
Chiron
44c7e0d19a chore: sync non-pi changes from remote (eigent update, formatting fixes, gitignore) 2026-04-18 10:05:59 +00:00
Chiron
a0f4d401df docs: update AGENTS.md to reflect current codebase state 2026-04-18 10:05:16 +00:00
Chiron
d04d405297 test: add basic lib function tests for agents and coding-rules 2026-04-18 10:05:16 +00:00
Chiron
2494da1054 docs: add CHANGELOG.md 2026-04-18 10:05:16 +00:00
Chiron
b2c8c935af refactor: remove redundant 'additions' overlay (identical to 'default') 2026-04-18 10:05:16 +00:00
Chiron
c6d8376dda refactor: tool-agnostic naming in coding-rules.nix internals 2026-04-18 10:05:16 +00:00
Chiron
0331316755 chore: remove dead overlay entries for non-existent flake inputs 2026-04-18 10:05:16 +00:00
Chiron
a4e540630d docs: clarify system binding in pkgs/default.nix 2026-04-18 10:05:16 +00:00
20 changed files with 642 additions and 671 deletions

2
.gitignore vendored
View File

@@ -43,3 +43,5 @@ flake.lock.bak
.sidecar-start.sh
.sidecar-base
.td-root
.pi-lens
.cache

View File

@@ -1,10 +1,5 @@
# m3ta-nixpkgs Knowledge Base
## MANDATORY: Use td for Task Management
You must run td usage --new-session at conversation start (or after /clear) to see current work.
Use td usage -q for subsequent reads.
**Generated:** 2026-02-14
**Commit:** dc2f3b6
**Branch:** master
@@ -114,8 +109,8 @@ 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.
Harness-agnostic agent management. Reads canonical `agent.toml` +
`system-prompt.md` from the AGENTS flake input and renders tool-specific configs.
**Functions:**
@@ -124,17 +119,18 @@ flake input and renders tool-specific configs.
| `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 |
| `renderForPi { pkgs, canonical, modelOverrides, primaryAgent }` | Render to Pi AGENTS.md + SYSTEM.md + agents/ |
| `renderForTool { pkgs, agentsInput, tool, modelOverrides }` | Dispatch to correct renderer by tool name |
| `shellHookForTool { pkgs, agentsInput, tool, modelOverrides }` | Generate devShell shellHook (symlinks rendered files) |
### `lib.coding-rules`
Coding rules injection (renamed from `lib.opencode-rules`). The old name still works.
Coding rules injection. Generates `coding-rules.json` + symlinks rules from
the AGENTS repository. The old `lib.opencode-rules` name still works.
| Function | Purpose |
|----------|--------|
| `mkCodingRules { agents, languages, concerns, frameworks }` | Generate rules config + shellHook |
| `mkCodingRules { agents, languages, concerns, frameworks, rulesDir }` | Generate rules config + shellHook. `rulesDir` defaults to `.opencode-rules` |
| `mkOpencodeRules` | Backward-compat alias for `mkCodingRules` |
## PORT MANAGEMENT
@@ -188,38 +184,34 @@ Types: `feat`, `fix`, `docs`, `style`, `refactor`, `chore`
## Task Management
This project uses **td** for tracking tasks across AI coding sessions.
Run `td usage --new-session` at conversation start to see current work.
Use `td usage -q` for subsequent reads.
**td** is an optional task-tracking package. See `docs/packages/td.md` for details.
**Quick reference:**
## Agent System Architecture
- `td usage --new-session` - Start new session and view tasks
- `td usage -q` - Quick view of current tasks (subsequent reads)
- `td version` - Check version
The agent system uses harness-agnostic canonical definitions stored as
`agent.toml` + `system-prompt.md` in the AGENTS repository. Renderers in
`lib/agents.nix` transform these into tool-specific configs at build time.
For full workflow details, see the [td documentation](./docs/packages/td.md).
### How it works
## MIGRATION: Agent System (OpenCode → Canonical TOML)
1. **Canonical definitions** live in the AGENTS repo as `agent.toml` files
(one per agent) with shared fields: name, description, mode, systemPrompt,
permissions, skills.
2. **`loadCanonical`** reads all agent definitions from the AGENTS flake input.
3. **Renderers** produce tool-specific output:
- `renderForOpencode``*.md` files with YAML frontmatter for `.opencode/agents/`
- `renderForClaudeCode``.claude/agents/*.md` + `.claude/settings.json` with permission rules
- `renderForPi``AGENTS.md`, `SYSTEM.md`, `agents/*.md` for Pi's subagent format
4. **`renderForTool`** dispatches to the correct renderer by tool name
(`"opencode"`, `"claude-code"`, or `"pi"`).
5. **`shellHookForTool`** generates a devShell shellHook that symlinks rendered
files into the project directory.
6. **HM modules** in `modules/home-manager/coding/agents/` handle per-tool
Home Manager integration.
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.
### Key files in this repo
### 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`
- `lib/agents.nix` — renderers, dispatcher, shellHook generator
- `lib/coding-rules.nix` — coding rules injection (`mkCodingRules`)
- `modules/home-manager/coding/agents/` — per-tool HM sub-modules (opencode, claude-code, pi)
- `modules/home-manager/coding/opencode.nix` — OpenCode HM module (slimmed, agents handled separately)

107
CHANGELOG.md Normal file
View File

@@ -0,0 +1,107 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/).
## [Unreleased]
### Changed
- Remove duplicate opencode-rules.nix (backward-compat alias preserved)
- Tool-agnostic naming in coding-rules lib internals
- Remove redundant overlay entries for non-existent flake inputs
- Remove redundant 'additions' overlay (identical to 'default')
### Removed
- Dead overlay entries for non-existent flake inputs
## [0.4.0] - 2026-04-15
### Added
- Pi-agent wrapper with systemd sandbox and per-host-user policy
- Containerized Pi agent
- `lib.agents.nix` with loadCanonical, renderers (OpenCode, Claude Code, Pi), and shellHook
- `lib.coding-rules` helper for per-project rule injection (renamed from opencode-rules)
- Home Manager modules for coding agents: `claude-code`, `opencode`, `pi`
- Agents rework with canonical TOML format and harness-agnostic renderers
- `vibetyper` and `eigent` packages
- `openspec` package
- `basecamp-cli` package
- `openshell` package (0.0.14 through 0.0.23)
- `openwork` package
- Opencode config moved into m3ta-nixpkgs
- Opencode dev shell with mkCodingRules demo
### Changed
- OpenCode flake input updated through v1.1.65 to v1.3.6
- Switched from local opencode package to upstream flake input
- Removed opencode-desktop (awaiting upstream fix), later re-enabled
- Nix eval warnings resolved
- Flake inputs updated throughout
### Fixed
- Pi settings sync
- Remove openwork sidecars in preFixup to prevent .opencode-wrapped conflict
- Remove sidecar binaries from openwork $out/bin to fix buildEnv conflict
- Vibetyper .desktop entry
- Opencode module formatting
- Formatting opencode module
## [0.3.0] - 2026-02-20
### Added
- `notesmd-cli` package with flake checks
- `sidecar` and `td` packages
- `opencode-desktop` package with Wayland support
- `mem0` package (1.0.2 through 1.0.9)
- `kestracli` / `kestractl` package (1.0.0 to 1.2.2)
### Changed
- Nix-update CI workflow optimized with caching and parallel processing
- Restructured n8n version handling for nix-update compatibility
- Switched formatter from nixpkgs-fmt to alejandra
- Replace local opencode with upstream flake input v1.1.27
### Fixed
- n8n build error
- n8n pnpm hash
- n8n update script
- Gitea runner opencode.url flake input
- nix-update workflow: YAML syntax, jobs indentation, PR body formatting
- Arithmetic increment failing with set -e in nix-update workflow
- Removed magic-nix-cache-action causing platform mapping error
- Opencode bun version requirement patched to match upstream lockfile
- Deprecated opencode update logic removed
- nix fmt without arg in workflow
- Extra Lua config renamed initLua
- Stt-ptt use pkill for better process management
## [0.2.0] - 2026-01-13
### Added
- Gitea Actions workflow for automated package updates with nix-update
- `n8n`, `beads`, and `opencode` packages
- `stt-ptt` package with auto-language detection
- `rofi-project-opener` for rofi-based project launching
- Hierarchical AGENTS.md knowledge base
- Dev shell structure with python and devops shells
- Port management modules (NixOS + Home Manager)
- Port helper library (`lib/ports.nix`)
### Changed
- Beads updated through v0.49.1
- N8n updated through v2.8.1
- Opencode updated through v1.1.18
- Documentation expanded with comprehensive patterns and HM module docs
### Fixed
- Python env version fix for marimo
## [0.1.0] - 2025-10-04
### Added
- Initial flake setup with packages, overlays, modules, and shells
- NixOS and Home Manager module infrastructure
- `lib/` shared utilities
- `overlays/mods/` for package modifications
- `templates/` for new packages/modules
- `examples/` for usage documentation

View File

@@ -1,117 +0,0 @@
# notesmd-cli
Obsidian CLI (Community) - Interact with Obsidian in the terminal.
## Description
notesmd-cli is a command-line interface for interacting with Obsidian, the popular knowledge management and note-taking application. It allows you to create, search, and manipulate notes directly from the terminal.
## Features
- 📝 **Note Creation**: Create new notes from the command line
- 🔍 **Search**: Search through your Obsidian vault
- 📂 **Vault Management**: Interact with your vault structure
- 🔗 **WikiLink Support**: Work with Obsidian's WikiLink format
- 🏷️ **Tag Support**: Manage and search by tags
-**Fast**: Lightweight Go binary with no external dependencies
## Installation
### Via Overlay
```nix
{pkgs, ...}: {
environment.systemPackages = with pkgs; [
notesmd-cli
];
}
```
### Direct Reference
```nix
{pkgs, ...}: {
environment.systemPackages = with pkgs; [
inputs.m3ta-nixpkgs.packages.${pkgs.system}.notesmd-cli
];
}
```
### Run Directly
```bash
nix run git+https://code.m3ta.dev/m3tam3re/nixpkgs#notesmd-cli
```
## Usage
### Basic Commands
```bash
# Show help
notesmd-cli --help
# Create a new note
notesmd-cli new "My Note Title"
# Search notes
notesmd-cli search "search term"
# List notes
notesmd-cli list
```
### Working with Vaults
```bash
# Specify vault path
notesmd-cli --vault /path/to/vault new "Note Title"
# Open a note in Obsidian
notesmd-cli open "Note Name"
```
### Advanced Usage
```bash
# Search with tags
notesmd-cli search --tag "project"
# Append to existing note
notesmd-cli append "Note Name" "Additional content"
```
## Configuration
### Environment Variables
- `OBSIDIAN_VAULT`: Default vault path
### Command Line Options
Run `notesmd-cli --help` for a complete list of options.
## Build Information
- **Version**: 0.3.0
- **Language**: Go
- **License**: MIT
- **Source**: [GitHub](https://github.com/Yakitrak/notesmd-cli)
- **Vendor Hash**: null (no external dependencies)
## Platform Support
- Linux
- macOS (Unix systems)
## Notes
- No vendor dependencies (pure Go stdlib)
- The binary is named `notesmd-cli` (not `notesmd`)
- This is the community CLI, not the official Obsidian CLI
## Related
- [Obsidian](https://obsidian.md) - The Obsidian application
- [Adding Packages](../guides/adding-packages.md) - How to add new packages
- [Quick Start](../QUICKSTART.md) - Getting started guide

View File

@@ -56,13 +56,6 @@
inputs = inputs;
};
# Individual overlays for more granular control
additions = final: prev:
import ./pkgs {
pkgs = final;
inputs = inputs;
};
modifications = final: prev: import ./overlays/mods {inherit prev;};
};
@@ -84,10 +77,7 @@
};
# Library functions - helper utilities for your configuration
lib = forAllSystems (system: let
pkgs = pkgsFor system;
in
import ./lib {lib = pkgs.lib;});
lib = forAllSystems (system: import ./lib {lib = nixpkgs.lib;});
# Development shells for various programming environments
# Usage: nix develop .#<shell-name>
@@ -111,6 +101,9 @@
${pkgs.alejandra}/bin/alejandra --check ${./.}
touch $out
'';
# Lib unit tests
lib-agents = import ./tests/lib/agents-test.nix;
lib-coding-rules = import ./tests/lib/coding-rules-test.nix;
});
# Templates for creating new packages/modules

View File

@@ -1,4 +1,4 @@
# Opencode rules management utilities
# Coding 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
@@ -26,7 +26,7 @@
#
# 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
# - A `coding-rules.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.
@@ -46,7 +46,7 @@
#
# Returns:
# An attribute set containing:
# - shellHook: Bash code to create symlink and opencode.json
# - shellHook: Bash code to create symlink and coding-rules.json
# - instructions: List of rule file paths (relative to project root)
#
# Example:
@@ -82,9 +82,8 @@
],
frameworks ? [],
extraInstructions ? [],
rulesDir ? ".opencode-rules",
}: 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 =
@@ -93,8 +92,8 @@
++ (map (f: "${rulesDir}/frameworks/${f}.md") frameworks)
++ extraInstructions;
# Generate JSON configuration for Opencode
opencodeConfig = {
# Generate JSON configuration for coding rules
rulesConfig = {
"$schema" = "https://opencode.ai/config.json";
inherit instructions;
};
@@ -102,15 +101,15 @@
inherit instructions;
# Shell hook to set up rules in the project
# Creates a symlink to the AGENTS rules directory and generates opencode.json
# Creates a symlink to the AGENTS rules directory and generates coding-rules.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
# Generate coding-rules.json configuration file
cat > coding-rules.json <<'RULES_EOF'
${builtins.toJSON rulesConfig}
RULES_EOF
'';
};

View File

@@ -3,88 +3,87 @@
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";
}: let
shared = import ./shared-options.nix {inherit lib;};
in
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 = ''
agentsInput = shared.mkAgentsInputOption ''
The `agents` flake input (your personal AGENTS repo).
When set, agents are rendered from canonical agent.toml files
and symlinked to ~/.claude/agents/.
'';
modelOverrides = shared.mkModelOverridesOption;
externalSkills = shared.externalSkillsOption;
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.
'';
};
};
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";
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;
}
'';
};
);
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.
'';
};
};
# 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";
};
config = mkIf cfg.enable (let
agentsLib = (import ../../../../lib {inherit lib;}).agents;
# Skills (merged from personal AGENTS repo + optional external skills)
home.file.".claude/skills" = mkIf (cfg.agentsInput != null) {
source = cfg.agentsInput.lib.mkOpencodeSkills {
inherit pkgs;
customSkills = "${cfg.agentsInput}/skills";
externalSkills = shared.mapExternalSkills cfg.externalSkills;
};
};
# 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}";
};
});
}
# Rendered settings.json with permissions + MCP servers
home.file.".claude/settings.json" = mkIf (settingsJson != null) {
source = "${settingsJson}";
};
});
}

View File

@@ -3,111 +3,67 @@
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";
}: let
shared = import ./shared-options.nix {inherit lib;};
in
with lib; {
options.coding.agents.opencode = {
enable = mkEnableOption "OpenCode agent management via canonical agent.toml definitions";
agentsInput = mkOption {
type = types.nullOr types.anything;
default = null;
description = ''
agentsInput = shared.mkAgentsInputOption ''
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 = shared.mkModelOverridesOption;
externalSkills = shared.externalSkillsOption;
};
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.
'';
config = mkIf config.coding.agents.opencode.enable {
# Rendered agent files symlinked to ~/.config/opencode/agents/
xdg.configFile."opencode/agents" = let
cfg = config.coding.agents.opencode;
in
mkIf (cfg.agentsInput != null) {
source = (import ../../../../lib {inherit lib;}).agents.renderForOpencode {
inherit pkgs;
canonical = cfg.agentsInput.lib.loadAgents;
modelOverrides = cfg.modelOverrides;
};
};
});
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" = let
cfg = config.coding.agents.opencode;
in
mkIf (cfg.agentsInput != null) {
source = cfg.agentsInput.lib.mkOpencodeSkills {
inherit pkgs;
customSkills = "${cfg.agentsInput}/skills";
externalSkills = shared.mapExternalSkills cfg.externalSkills;
};
};
# 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" = let
cfg = config.coding.agents.opencode;
in
mkIf (cfg.agentsInput != null) {
source = "${cfg.agentsInput}/context";
};
xdg.configFile."opencode/commands" = let
cfg = config.coding.agents.opencode;
in
mkIf (cfg.agentsInput != null) {
source = "${cfg.agentsInput}/commands";
};
xdg.configFile."opencode/prompts" = let
cfg = config.coding.agents.opencode;
in
mkIf (cfg.agentsInput != null) {
source = "${cfg.agentsInput}/prompts";
};
};
# 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";
};
};
}
}

View File

@@ -3,232 +3,225 @@
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";
}: let
shared = import ./shared-options.nix {inherit lib;};
in
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.
'';
};
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 = ''
agentsInput = shared.mkAgentsInputOption ''
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"; }
'';
};
modelOverrides = shared.mkModelOverridesOption;
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.";
};
};
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.
'';
};
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
);
externalSkills = shared.externalSkillsOption;
piSettings = filterNulls cfg.settings;
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.
'';
};
# 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;
defaultProvider = mkOption {
type = types.nullOr types.str;
default = null;
description = "Default LLM provider (e.g. 'anthropic', 'openai', 'zai').";
};
# 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;};
})
defaultModel = mkOption {
type = types.nullOr types.str;
default = null;
description = "Default model ID.";
};
# ── ~/.pi/agent/settings.json ──────────────────────────────────
{
".pi/agent/settings.json".text = builtins.toJSON piSettings;
}
defaultThinkingLevel = mkOption {
type = types.nullOr (types.enum ["off" "minimal" "low" "medium" "high" "xhigh"]);
default = null;
description = "Default extended thinking level.";
};
# ── AGENTS.md — agent descriptions and specialist listing ──────
(mkIf (cfg.agentsInput != null) {
".pi/agent/AGENTS.md".source = "${rendered}/AGENTS.md";
})
theme = mkOption {
type = types.nullOr types.str;
default = null;
description = "Pi theme name.";
};
# ── SYSTEM.md — primary agent's system prompt ──────────────────
(mkIf (cfg.agentsInput != null) {
".pi/agent/SYSTEM.md".source = "${rendered}/SYSTEM.md";
})
hideThinkingBlock = mkOption {
type = types.nullOr types.bool;
default = null;
description = "Hide thinking blocks in output.";
};
# ── Agents — pi-subagents .md files ────────────────────────────
agentFiles
quietStartup = mkOption {
type = types.nullOr types.bool;
default = null;
description = "Hide startup header.";
};
# ── Skills symlinked from AGENTS repo ──────────────────────────
(mkIf (cfg.agentsInput != null) {
".pi/agent/skills".source = cfg.agentsInput.lib.mkOpencodeSkills {
inherit pkgs;
customSkills = "${cfg.agentsInput}/skills";
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 + external skills ────────
(mkIf (cfg.agentsInput != null) {
".pi/agent/skills".source = cfg.agentsInput.lib.mkOpencodeSkills {
inherit pkgs;
customSkills = "${cfg.agentsInput}/skills";
externalSkills = shared.mapExternalSkills cfg.externalSkills;
};
})
];
});
}

View File

@@ -0,0 +1,75 @@
# Shared option definitions for agent modules.
# Prevents copy-pasting the externalSkills submodule across opencode/claude-code/pi.
{lib}: let
inherit (lib) mkOption mkEnableOption types literalExpression;
in {
# Common agentsInput option used by all agent modules.
mkAgentsInputOption = description: mkOption {
type = types.nullOr types.anything;
default = null;
inherit description;
};
# Common modelOverrides option.
mkModelOverridesOption = 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";
}
'';
};
# External skills submodule — used by opencode, claude-code, and pi modules.
externalSkillsOption = 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.basecamp; }
]
'';
};
# Helper to map externalSkills from module config to mkOpencodeSkills format.
mapExternalSkills = cfgEntries:
map (
entry:
{inherit (entry) src skillsDir;}
// lib.optionalAttrs (entry.selectSkills != null) {inherit (entry) selectSkills;}
) cfgEntries;
}

View File

@@ -1,70 +0,0 @@
{inputs, ...}: {
# This one brings our custom packages from the 'pkgs' directory
additions = final: prev:
(import ../pkgs {pkgs = final;})
# // (inputs.hyprpanel.overlay final prev)
// {rose-pine-hyprcursor = inputs.rose-pine-hyprcursor.packages.${prev.stdenv.hostPlatform.system}.default;};
# This one contains whatever you want to overlay
# You can change versions, add patches, set compilation flags, anything really.
# https://nixos.wiki/wiki/Overlays
modifications = final: prev:
# Import all package modifications from mods directory
(import ./mods/default.nix {inherit prev;})
// {
# Direct configuration overrides
brave = prev.brave.override {
commandLineArgs = "--password-store=gnome-libsecret";
};
# nodejs_24 = inputs.nixpkgs-stable.legacyPackages.${prev.system}.nodejs_24;
# paperless-ngx = inputs.nixpkgs-45570c2.legacyPackages.${prev.system}.paperless-ngx;
# anytype-heart = inputs.nixpkgs-9e58ed7.legacyPackages.${prev.system}.anytype-heart;
# trezord = inputs.nixpkgs-2744d98.legacyPackages.${prev.system}.trezord;
# mesa = inputs.nixpkgs-master.legacyPackages.${prev.system}.mesa;
# hyprpanel = inputs.hyprpanel.packages.${prev.system}.default.overrideAttrs (prev: {
# version = "latest"; # or whatever version you want
# src = final.fetchFromGitHub {
# owner = "Jas-SinghFSU";
# repo = "HyprPanel";
# rev = "master"; # or a specific commit hash
# hash = "sha256-l623fIVhVCU/ylbBmohAtQNbK0YrWlEny0sC/vBJ+dU=";
# };
# });
};
temp-packages = final: _prev: {
temp = import inputs.nixpkgs-9e9486b {
system = final.stdenv.hostPlatform.system;
config.allowUnfree = true;
};
};
stable-packages = final: _prev: {
stable = import inputs.nixpkgs-stable {
system = final.stdenv.hostPlatform.system;
config.allowUnfree = true;
};
};
pinned-packages = final: _prev: {
pinned = import inputs.nixpkgs-9472de4 {
system = final.stdenv.hostPlatform.system;
config.allowUnfree = true;
};
};
locked-packages = final: _prev: {
locked = import inputs.nixpkgs-locked {
system = final.stdenv.hostPlatform.system;
config.allowUnfree = true;
};
};
master-packages = final: _prev: {
master = import inputs.nixpkgs-master {
system = final.stdenv.hostPlatform.system;
config.allowUnfree = true;
};
};
}

View File

@@ -1,16 +0,0 @@
{prev}:
prev.beads.overrideAttrs (oldAttrs: rec {
version = "0.47.1";
src = prev.fetchFromGitHub {
owner = "steveyegge";
repo = "beads";
tag = "v${version}";
hash = "sha256-DwIR/r1TJnpVd/CT1E2OTkAjU7k9/KHbcVwg5zziFVg=";
};
vendorHash = "sha256-pY5m5ODRgqghyELRwwxOr+xlW41gtJWLXaW53GlLaFw=";
# Tests require git worktree operations that fail in Nix sandbox
doCheck = false;
})

View File

@@ -2,9 +2,6 @@
# Package modifications
# This overlay contains package overrides and modifications
# n8n = import ./n8n.nix {inherit prev;};
# beads = import ./beads.nix {inherit prev;};
# Add more modifications here as needed
# example-package = prev.example-package.override { ... };
}

View File

@@ -1,18 +0,0 @@
{prev}:
prev.n8n.overrideAttrs (oldAttrs: rec {
version = "2.4.1";
src = prev.fetchFromGitHub {
owner = "n8n-io";
repo = "n8n";
rev = "n8n@${version}";
hash = "sha256-EQP9ZI8kt30SUYE1+/UUpxQXpavzKqDu8qE24zsNifg=";
};
pnpmDeps = prev.pnpm_10.fetchDeps {
pname = oldAttrs.pname;
inherit version src;
fetcherVersion = 1;
hash = "sha256-Q30IuFEQD3896Hg0HCLd38YE2i8fJn74JY0o95LKJis=";
};
})

View File

@@ -3,6 +3,7 @@
inputs,
...
}: let
# Used only for flake input pass-throughs (basecamp, openspec, opencode-desktop)
system = pkgs.stdenv.hostPlatform.system;
in {
# Custom packages registry

View File

@@ -8,10 +8,10 @@
nix-update-script,
}: let
pname = "eigent";
version = "0.0.89";
version = "0.0.90";
src = fetchurl {
url = "https://github.com/eigent-ai/eigent/releases/download/v${version}/Eigent-${version}.AppImage";
hash = "sha256-9KuiFjegfXhCu1W/FCinWX4ae/DsNPudeBcXFfW18Hc=";
hash = "sha256-mwCBx+D6mgGqQa8bDuUpo3h49EwFVkwasJwaYc6aXFE=";
};
appimageContents = appimageTools.extractType2 {inherit pname version src;};
in

View File

@@ -31,7 +31,7 @@
# Upstream is missing outputHashes for git dependencies
# Also fix stale npm deps hash in upstream node_modules FOD
fixedNodeModules = opencode.node_modules.overrideAttrs {
outputHash = "sha256-LRhPPrOKCGUSCEWTpAxPdWKTKVNkg82WrvD25cP3jts=";
outputHash = "sha256-285KZ7rZLRoc6XqCZRHc25NE+mmpGh/BVeMpv8aPQtQ=";
};
opencode-desktop = rustPlatform.buildRustPackage (finalAttrs: {

26
tests/lib/agents-test.nix Normal file
View File

@@ -0,0 +1,26 @@
let
lib = import <nixpkgs/lib>;
agentsLib = (import ../../lib {inherit lib;}).agents;
# Test 1: renderForTool throws for unknown tools
testUnknownTool = let
result = builtins.tryEval (
agentsLib.renderForTool {
pkgs = {};
agentsInput = {};
tool = "unknown-tool";
}
);
in
assert result.success == false; {result = "pass";};
# Test 2: loadCanonical extracts loadAgents from input
testLoadCanonical = let
fakeInput = {lib.loadAgents = {test = {description = "test";};};};
result = agentsLib.loadCanonical {agentsInput = fakeInput;};
in
assert result == {test = {description = "test";};}; {result = "pass";};
in {
unknown-tool-throws = testUnknownTool;
load-canonical = testLoadCanonical;
}

View File

@@ -0,0 +1,48 @@
let
lib = import <nixpkgs/lib>;
codingRulesLib = (import ../../lib {inherit lib;}).coding-rules;
# Test 1: instructions are generated correctly with custom rulesDir
testInstructions = let
rules = codingRulesLib.mkCodingRules {
agents = "/tmp/fake-agents";
languages = ["python"];
concerns = ["naming"];
rulesDir = ".coding-rules";
};
in
assert rules.instructions
== [
".coding-rules/concerns/naming.md"
".coding-rules/languages/python.md"
]; {result = "pass";};
# Test 2: default rulesDir is .opencode-rules
testDefaultRulesDir = let
rules = codingRulesLib.mkCodingRules {
agents = "/tmp/fake-agents";
};
hasCorrectPrefix = builtins.all (s: builtins.substring 0 15 s == ".opencode-rules") rules.instructions;
in
assert hasCorrectPrefix == true; {result = "pass";};
# Test 3: backward-compat alias exists
testBackwardCompat = assert codingRulesLib.mkOpencodeRules == codingRulesLib.mkCodingRules; {result = "pass";};
# Test 4: shellHook contains both the symlink command and the config generation
testShellHook = let
rules = codingRulesLib.mkCodingRules {
agents = "/tmp/fake-agents";
};
hook = rules.shellHook;
hasSymlink = builtins.match ".*ln -sfn.*" hook != null;
hasConfigGen = builtins.match ".*coding-rules.json.*" hook != null;
in
assert hasSymlink;
assert hasConfigGen; {result = "pass";};
in {
instructions-correct = testInstructions;
default-rules-dir = testDefaultRulesDir;
backward-compat = testBackwardCompat;
shell-hook = testShellHook;
}

4
tests/lib/default.nix Normal file
View File

@@ -0,0 +1,4 @@
{
coding-rules = import ./coding-rules-test.nix;
agents = import ./agents-test.nix;
}