Merge pull request 'refactor/remove-pi-agent-cleanup' (#14) from refactor/remove-pi-agent-cleanup into master
Some checks failed
Update Nix Packages with nix-update / nix-update (push) Failing after 4m2s

Reviewed-on: #14
This commit was merged in pull request #14.
This commit is contained in:
2026-04-19 18:16:50 +02:00
27 changed files with 1300 additions and 798 deletions

2
.gitignore vendored
View File

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

View File

@@ -1,3 +1 @@
{ null
"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."
}

View File

@@ -1,3 +1,3 @@
{ {
"timestamp": "2026-04-11T04:21:36.939Z" "timestamp": "2026-04-19T15:52:39.989Z"
} }

View File

@@ -1,3 +1,3 @@
{ {
"timestamp": "2026-04-11T04:21:36.940Z" "timestamp": "2026-04-19T15:42:10.963Z"
} }

View File

@@ -1,10 +1,5 @@
# m3ta-nixpkgs Knowledge Base # 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 **Generated:** 2026-02-14
**Commit:** dc2f3b6 **Commit:** dc2f3b6
**Branch:** master **Branch:** master
@@ -114,8 +109,8 @@ Port management utilities. See [Port Management](#port-management).
### `lib.agents` ### `lib.agents`
Harness-agnostic agent management. Reads canonical `agent.toml` from the AGENTS Harness-agnostic agent management. Reads canonical `agent.toml` +
flake input and renders tool-specific configs. `system-prompt.md` from the AGENTS flake input and renders tool-specific configs.
**Functions:** **Functions:**
@@ -124,17 +119,18 @@ flake input and renders tool-specific configs.
| `loadCanonical { agentsInput }` | Load canonical agents from AGENTS flake | | `loadCanonical { agentsInput }` | Load canonical agents from AGENTS flake |
| `renderForOpencode { pkgs, canonical, modelOverrides }` | Render to OpenCode file-based agents | | `renderForOpencode { pkgs, canonical, modelOverrides }` | Render to OpenCode file-based agents |
| `renderForClaudeCode { pkgs, canonical, modelOverrides }` | Render to Claude Code agents + settings.json | | `renderForClaudeCode { pkgs, canonical, modelOverrides }` | Render to Claude Code agents + settings.json |
| `renderForPi { pkgs, canonical }` | Render to Pi AGENTS.md + SYSTEM.md | | `renderForPi { pkgs, canonical, modelOverrides, primaryAgent }` | Render to Pi AGENTS.md + SYSTEM.md + agents/ |
| `renderForTool { pkgs, agentsInput, tool, modelOverrides }` | Dispatch to correct renderer | | `renderForTool { pkgs, agentsInput, tool, modelOverrides }` | Dispatch to correct renderer by tool name |
| `shellHookForTool { pkgs, agentsInput, tool, modelOverrides }` | Generate devShell shellHook | | `shellHookForTool { pkgs, agentsInput, tool, modelOverrides }` | Generate devShell shellHook (symlinks rendered files) |
### `lib.coding-rules` ### `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 | | 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` | | `mkOpencodeRules` | Backward-compat alias for `mkCodingRules` |
## PORT MANAGEMENT ## PORT MANAGEMENT
@@ -188,38 +184,34 @@ Types: `feat`, `fix`, `docs`, `style`, `refactor`, `chore`
## Task Management ## Task Management
This project uses **td** for tracking tasks across AI coding sessions. **td** is an optional task-tracking package. See `docs/packages/td.md` for details.
Run `td usage --new-session` at conversation start to see current work.
Use `td usage -q` for subsequent reads.
**Quick reference:** ## Agent System Architecture
- `td usage --new-session` - Start new session and view tasks The agent system uses harness-agnostic canonical definitions stored as
- `td usage -q` - Quick view of current tasks (subsequent reads) `agent.toml` + `system-prompt.md` in the AGENTS repository. Renderers in
- `td version` - Check version `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 ### Key files in this repo
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` — renderers, dispatcher, shellHook generator
- `lib/coding-rules.nix` — coding rules injection (`mkCodingRules`)
- **`lib/agents.nix`**: New — 3 renderers (OpenCode, Claude Code, Pi) + dispatcher + shellHook - `modules/home-manager/coding/agents/` — per-tool HM sub-modules (opencode, claude-code, pi)
- **`lib/coding-rules.nix`**: Renamed from `opencode-rules.nix`, `mkCodingRules` replaces `mkOpencodeRules` - `modules/home-manager/coding/opencode.nix` — OpenCode HM module (slimmed, agents handled separately)
- **`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`

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

@@ -0,0 +1,637 @@
# m3ta-nixpkgs: Cleanup & Improvements Plan
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
**Goal:** Address 10 issues identified in codebase review — reduce duplication, improve naming consistency, extract inline scripts, add testing, and update documentation.
**Architecture:** Incremental improvements across lib/, modules/, overlays/, docs/, and CI. Each change is self-contained and can be merged independently. No breaking changes to public API (backward-compat aliases preserved where needed).
**Repo:** `gitea@code.m3ta.dev:m3tam3re/nixpkgs.git` (master branch)
---
## Phase 1: Deduplication & Naming (Low Risk)
### Task 1: Remove duplicate opencode-rules.nix file
**Objective:** Eliminate the duplicate file import. The `coding-rules.nix` is the canonical source; `opencode-rules.nix` is an identical copy. Make the alias a one-liner in `lib/default.nix`.
**Files:**
- Delete: `lib/opencode-rules.nix`
- Modify: `lib/default.nix`
**Step 1: Update lib/default.nix to alias directly**
```nix
{lib}: {
ports = import ./ports.nix {inherit lib;};
coding-rules = import ./coding-rules.nix {inherit lib;};
# Backward-compat alias: opencode-rules → coding-rules
opencode-rules = import ./coding-rules.nix {inherit lib;};
opencode = import ./coding-rules.nix {inherit lib;};
}
```
**Step 2: Delete the duplicate file**
```bash
git rm lib/opencode-rules.nix
```
**Step 3: Verify nothing breaks**
```bash
nix flake check
```
**Step 4: Commit**
```bash
git commit -m "refactor: remove duplicate opencode-rules.nix, use alias in default.nix"
```
---
### Task 2: Tool-agnostic naming in coding-rules.nix internals
**Objective:** Rename internal variables and output artifacts in `coding-rules.nix` from opencode-specific names to generic names, while keeping the backward-compat alias `mkOpencodeRules`.
**Files:**
- Modify: `lib/coding-rules.nix`
**Step 1: Rename internal symbols**
In `lib/coding-rules.nix`, rename:
- `rulesDir` stays `.opencode-rules` (this is a filesystem path used by existing projects, changing it would break)
- `opencodeConfig``rulesConfig`
- `opencode.json` output → `coding-rules.json` (add a comment noting it was renamed)
- Add `rulesDir` option to function signature with default `.opencode-rules`
Updated function:
```nix
{lib}: let
mkCodingRules = {
agents,
languages ? [],
concerns ? [
"coding-style"
"naming"
"documentation"
"testing"
"git-workflow"
"project-structure"
],
frameworks ? [],
extraInstructions ? [],
rulesDir ? ".opencode-rules",
}: let
instructions =
(map (c: "${rulesDir}/concerns/${c}.md") concerns)
++ (map (l: "${rulesDir}/languages/${l}.md") languages)
++ (map (f: "${rulesDir}/frameworks/${f}.md") frameworks)
++ extraInstructions;
rulesConfig = {
"$schema" = "https://opencode.ai/config.json";
inherit instructions;
};
in {
inherit instructions;
shellHook = ''
# Create/update symlink to AGENTS rules directory
ln -sfn ${agents}/rules ${rulesDir}
# Generate coding-rules configuration file
cat > coding-rules.json <<'RULES_EOF'
${builtins.toJSON rulesConfig}
RULES_EOF
'';
};
# Backward-compat alias
mkOpencodeRules = mkCodingRules;
in {
inherit mkCodingRules mkOpencodeRules;
};
```
**Step 2: Update shellHook comment in AGENTS.md**
In `AGENTS.md`, update the coding-rules section to mention the new `rulesDir` parameter and the `coding-rules.json` output file.
**Step 3: Verify**
```bash
nix flake check
```
**Step 4: Commit**
```bash
git commit -m "refactor: tool-agnostic naming in coding-rules.nix internals"
```
---
### Task 3: Remove redundant overlays entry in flake.nix
**Objective:** The `default` and `additions` overlays in `flake.nix` produce identical output. Remove `additions` if not referenced elsewhere, or document why both exist.
**Files:**
- Modify: `flake.nix`
- Check: all consumer repos for references to `overlays.additions`
**Step 1: Search for consumers of overlays.additions**
```bash
# Check nixos-config and other repos
grep -r "overlays.additions" /data/.hermes/repos/nixos-config/
grep -r "additions" /data/.hermes/repos/nixos-config/ --include="*.nix" | grep overlay
```
**Step 2: If no consumers found, remove additions**
In `flake.nix`, simplify overlays to:
```nix
overlays = {
default = final: prev:
import ./pkgs {
pkgs = final;
inputs = inputs;
};
modifications = final: prev: import ./overlays/mods {inherit prev;};
};
```
**Step 3: Verify**
```bash
nix flake check
```
**Step 4: Commit**
```bash
git commit -m "refactor: remove redundant 'additions' overlay (identical to 'default')"
```
**Note:** If `additions` IS used elsewhere, add a comment explaining the convention and skip this task.
---
## Phase 2: Extract Inline Scripts (Medium Risk)
### Task 4: Extract pi-agent runner script to standalone file
**Objective:** Move the ~200-line inline bash script in `modules/nixos/pi-agent.nix` (the `runner` variable) to a separate file `modules/nixos/pi-agent-runner.sh` that gets imported via `builtins.readFile` + `pkgs.writeShellApplication`.
**Files:**
- Create: `modules/nixos/pi-agent-runner.sh`
- Modify: `modules/nixos/pi-agent.nix`
**Step 1: Create the runner script file**
Extract the body of the `runner` script (everything inside the `pkgs.writeShellScriptBin cfg.wrapper.runnerName '' ... ''`) into `modules/nixos/pi-agent-runner.sh`.
The script uses Nix-style variable interpolation (`${...}`). We need to keep Nix template variables as `${...}` and convert runtime bash variables to use `$` prefix. Since the script already uses Nix `escapeShellArg` and `escapeShellArg` calls, the cleanest approach is:
Create `modules/nixos/pi-agent-runner.sh` as a template that `pkgs.substituteAll` or `builtins.readFile` + string replacement can process. However, given the heavy Nix interpolation, the pragmatic approach is to use `pkgs.writeShellApplication` with the script body inline but extracted to a `let` binding:
```nix
# In pi-agent.nix, replace the inline runner with:
let
runnerScript = builtins.readFile ./pi-agent-runner.sh;
# ... or keep as let binding but move the body to a separate derivation
```
**Important caveat:** The script has ~30 Nix variable interpolations (`${cfg.user}`, `${escapeShellArg ...}`, etc.). Full extraction to a .sh file would require either:
- (a) `substituteAll` with `--replace` for each variable — unwieldy at 30+ substitutions
- (b) Converting to env vars passed at runtime — cleaner but changes security posture
- (c) Keeping the Nix interpolation but extracting to a `let` block in a separate `.nix` file
**Recommended approach: Option (c)** — Create `modules/nixos/pi-agent-runner.nix` as a function that takes `cfg` and returns the script:
```nix
# modules/nixos/pi-agent-runner.nix
{cfg, pkgs, lib, ...}:
with lib; let
# ... all the helper variables from pi-agent.nix ...
in
pkgs.writeShellScriptBin cfg.wrapper.runnerName ''
# ... the script body ...
'';
```
Then in `pi-agent.nix`:
```nix
runner = import ./pi-agent-runner.nix {inherit cfg pkgs lib;};
```
**Step 2: Similarly extract the wrapper script**
Create `modules/nixos/pi-agent-wrapper.nix` for the `wrapper` variable.
**Step 3: Verify**
```bash
nix flake check
# Also test in a nixos-rebuild if possible
```
**Step 4: Commit**
```bash
git add modules/nixos/pi-agent-runner.nix modules/nixos/pi-agent-wrapper.nix
git commit -m "refactor: extract pi-agent runner and wrapper to separate files"
```
---
## Phase 3: Testing (Higher Value)
### Task 5: Add basic lib function tests
**Objective:** Add `nix eval`-based tests for `lib/agents.nix` parseRule logic and `lib/coding-rules.nix` instruction generation.
**Files:**
- Create: `tests/lib/agents-test.nix`
- Create: `tests/lib/coding-rules-test.nix`
- Modify: `flake.nix` (add checks)
**Step 1: Create test infrastructure**
```nix
# tests/lib/default.nix
{
agents = import ./agents-test.nix;
coding-rules = import ./coding-rules-test.nix;
}
```
**Step 2: Write agents.nix parseRule test**
```nix
# tests/lib/agents-test.nix
let
lib = import <nixpkgs/lib>;
agentsLib = (import ../../lib {inherit lib;}).agents;
# Test parseRule helper
test1 = let
result = builtins.tryEval (
let
# We can't directly test parseRule since it's internal.
# Instead, test the renderer with minimal input.
canonical = {
test-agent = {
description = "Test agent";
mode = "primary";
systemPrompt = "You are a test.";
permissions = {
bash = { intent = "allow"; };
edit = { intent = "ask"; rules = ["rm -rf *:deny"]; };
};
};
};
pkgs = import <nixpkgs> { system = "x86_64-linux"; };
rendered = agentsLib.renderForOpencode {
inherit pkgs canonical;
};
in
# Verify the derivation builds
builtins.pathExists "${rendered}/test-agent.md"
);
in assert result.value == true; true;
in {
parseRule-basic = test1;
}
```
**Step 3: Write coding-rules test**
```nix
# tests/lib/coding-rules-test.nix
let
lib = import <nixpkgs/lib>;
codingRulesLib = (import ../../lib {inherit lib;}).coding-rules;
rules = codingRulesLib.mkCodingRules {
agents = "/tmp/fake-agents";
languages = ["python"];
concerns = ["naming"];
rulesDir = ".coding-rules";
};
# Verify instructions are generated correctly
test1 = assert rules.instructions == [
".coding-rules/concerns/naming.md"
".coding-rules/languages/python.md"
]; true;
# Verify backward-compat alias exists
test2 = assert codingRulesLib.mkOpencodeRules == codingRulesLib.mkCodingRules; true;
in {
instructions-correct = test1;
backward-compat = test2;
}
```
**Step 4: Add to flake.nix checks**
In `flake.nix`, extend the `checks` attribute:
```nix
checks = forAllSystems (system: let
pkgs = pkgsFor system;
packages = import ./pkgs {inherit pkgs inputs;};
in
builtins.mapAttrs (name: pkg: pkgs.lib.hydraJob pkg) packages
// {
formatting = pkgs.runCommand "check-formatting" {} ''
${pkgs.alejandra}/bin/alejandra --check ${./.}
touch $out
'';
lib-tests = pkgs.runCommand "lib-tests" {} ''
${pkgs.nix}/bin/nix-instantiate --eval ${./tests/lib/default.nix}
touch $out
'';
});
```
**Step 5: Verify**
```bash
nix flake check
```
**Step 6: Commit**
```bash
git add tests/
git commit -m "test: add basic lib function tests for agents and coding-rules"
```
---
### Task 6: Add NixOS VM test for pi-agent module
**Objective:** Add a basic NixOS VM test that verifies the pi-agent module can be evaluated and the wrapper/runner scripts exist.
**Files:**
- Create: `tests/nixos/pi-agent-test.nix`
- Modify: `flake.nix` (add to checks)
**Step 1: Write the VM test**
```nix
# tests/nixos/pi-agent-test.nix
{pkgs, ...}: {
name = "pi-agent";
nodes.machine = {config, ...}: {
imports = [
${(pkgs.path + "/nixos/modules/module-list.nix")}
];
# Minimal pi-agent config
m3ta.pi-agent = {
enable = true;
package = pkgs.writeScriptBin "pi-agent" ''
#!/bin/sh
echo "pi-agent mock"
'';
createUser = true;
hostUsers = {
testuser = {
projectRoots = ["/tmp/test-project"];
};
};
};
users.users.testuser = {
isNormalUser = true;
};
};
testScript = ''
machine.start()
machine.wait_for_unit("multi-user.target")
# Verify user was created
machine.succeed("id pi-agent")
# Verify wrapper exists
machine.succeed("which pi")
# Verify state directory
machine.succeed("test -d /var/lib/pi-agent")
machine.succeed("test -d /var/lib/pi-agent/.pi")
'';
}
```
**Step 2: Add to flake.nix checks**
```nix
# In the checks attrset:
pi-agent-vm-test = pkgs.nixosTest (import ./tests/nixos/pi-agent-test.nix {inherit pkgs;});
```
**Step 3: Verify**
```bash
nix build .#checks.x86_64-linux.pi-agent-vm-test
```
**Step 4: Commit**
```bash
git add tests/nixos/
git commit -m "test: add NixOS VM test for pi-agent module"
```
---
## Phase 4: Documentation (Low Risk, High Value)
### Task 7: Update AGENTS.md to reflect current state
**Objective:** Remove outdated migration sections, update function signatures, and align with current code.
**Files:**
- Modify: `AGENTS.md`
**Step 1: Update the AGENTS REWORK migration section**
The section starting with `## MIGRATION: Agent System (OpenCode → Canonical TOML)` describes a completed migration. Convert it to a brief "Architecture" section that describes the current state, not the migration path.
**Step 2: Update lib.agents function table**
Verify that the function signatures and descriptions in the AGENTS.md table match the actual functions in `lib/agents.nix`. Specifically:
- `loadCanonical` takes `{agentsInput}` — confirm docs match
- `renderForPi` now has `primaryAgent` parameter — confirm documented
- `shellHookForTool` exists — confirm documented
**Step 3: Update coding-rules documentation**
Replace references to `mkOpencodeRules` with `mkCodingRules` as primary, `mkOpencodeRules` as backward-compat alias. Document the new `rulesDir` parameter.
**Step 4: Update overlay documentation**
Remove or annotate the `additions` overlay depending on Task 3 outcome.
**Step 5: Commit**
```bash
git commit -m "docs: update AGENTS.md to reflect current codebase state"
```
---
### Task 8: Add CHANGELOG.md
**Objective:** Create a changelog that captures recent work (from git log) so consumers can track changes.
**Files:**
- Create: `CHANGELOG.md`
**Step 1: Generate changelog from git history**
```bash
cd /data/.hermes/repos/nixpkgs-review
git log --oneline --no-merges master | head -30
```
**Step 2: Write CHANGELOG.md**
Structure as Keep a Changelog format:
```markdown
# 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]
## [0.4.0] - 2026-04-15
### Added
- Pi agent wrapper with per-host-user policy enforcement (`m3ta.pi-agent` NixOS module)
- `coding.agents.pi` Home Manager module with settings, MCP, and skills support
- `coding.agents.claude-code` Home Manager module with MCP integration
- Automated package updates via Gitea Actions (`nix-update` workflow)
- `lib.agents.renderForPi` with primaryAgent selection and pi-subagents format
- `pkgs/td` - Task management CLI for AI coding sessions
### Changed
- Renamed `lib.opencode-rules``lib.coding-rules` (backward-compat alias preserved)
- Agent system migrated to harness-agnostic canonical format
- Pi settings sync now merges host and Nix-managed values via deep_merge
### Fixed
- Pi settings sync race condition on first run
```
**Step 3: Commit**
```bash
git commit -m "docs: add CHANGELOG.md"
```
---
## Phase 5: Minor Cleanups (Low Risk)
### Task 9: Clean up pkgs/default.nix unused `system` binding
**Objective:** The `system = pkgs.stdenv.hostPlatform.system;` binding in `pkgs/default.nix` is only used for the two input-pass-throughs. If those are the only consumers, it's fine, but add a clarifying comment.
**Files:**
- Modify: `pkgs/default.nix`
**Step 1: Add clarifying comment**
```nix
{
pkgs,
inputs,
...
}: let
# Only used for flake input pass-throughs below
system = pkgs.stdenv.hostPlatform.system;
in {
...
```
**Step 2: Commit**
```bash
git commit -m "docs: clarify system binding in pkgs/default.nix"
```
---
### Task 10: Remove commented-out overlay entries in overlays/default.nix
**Objective:** Clean up the large block of commented-out code in `overlays/default.nix` (nodejs_24, paperless-ngx, anytype-heart, hyprpanel, etc.). These belong in git history, not in active code.
**Files:**
- Modify: `overlays/default.nix`
**Step 1: Remove commented-out blocks**
Remove:
- The `rose-pine-hyprcursor` addition from `additions` (if it's unused — check with grep)
- The commented-out `nodejs_24`, `paperless-ngx`, `anytype-heart`, `trezord`, `mesa`, `hyprpanel` blocks from `modifications`
- The commented-out overlay inputs (`temp-packages`, `stable-packages`, `pinned-packages`, `locked-packages`, `master-packages`) if they reference inputs not in `flake.nix`
Actually, `nixpkgs-stable`, `nixpkgs-9e9486b`, `nixpkgs-9472de4`, `nixpkgs-locked`, `nixpkgs-master` are NOT in the current `flake.nix` inputs. These overlays will fail if referenced. They should either be removed or the inputs should be added.
**Action:**
- Keep `master-packages` IF `nixpkgs-master` is in flake.nix inputs (it IS — good)
- Remove `temp-packages`, `pinned-packages`, `locked-packages` (inputs don't exist)
- Keep `stable-packages` IF `nixpkgs-stable` exists in inputs (check — it does NOT currently exist)
- Keep `additions` with `rose-pine-hyprcursor` IF `rose-pine-hyprcursor` input exists (check)
**Step 2: Verify**
```bash
nix flake check
```
**Step 3: Commit**
```bash
git commit -m "chore: remove dead overlay entries for non-existent flake inputs"
```
---
## Execution Order & Priority
| Task | Risk | Effort | Impact | Dependencies |
|------|------|--------|--------|-------------|
| T1: Remove opencode-rules.nix | Low | 5min | Clean | None |
| T2: Tool-agnostic naming | Low | 15min | Consistency | None |
| T3: Remove redundant overlay | Low | 10min | Clean | Check consumers |
| T9: Clarify system binding | Low | 2min | Docs | None |
| T10: Remove dead overlays | Low | 10min | Clean | None |
| T7: Update AGENTS.md | Low | 20min | Docs | After T1, T2 |
| T8: Add CHANGELOG.md | Low | 15min | Docs | None |
| T4: Extract pi-agent scripts | Medium | 45min | Maintainability | None |
| T5: Lib function tests | Medium | 30min | Quality | None |
| T6: NixOS VM test | Medium | 45min | Quality | None |
**Recommended order:** T1 → T9 → T10 → T3 → T2 → T7 → T8 → T5 → T4 → T6
**Branching strategy:** Create a feature branch `chore/cleanup-review` from master, implement all tasks, open PR for review before merging.

View File

@@ -56,13 +56,6 @@
inputs = inputs; 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;}; modifications = final: prev: import ./overlays/mods {inherit prev;};
}; };
@@ -84,10 +77,7 @@
}; };
# Library functions - helper utilities for your configuration # Library functions - helper utilities for your configuration
lib = forAllSystems (system: let lib = forAllSystems (system: import ./lib {lib = nixpkgs.lib;});
pkgs = pkgsFor system;
in
import ./lib {lib = pkgs.lib;});
# Development shells for various programming environments # Development shells for various programming environments
# Usage: nix develop .#<shell-name> # Usage: nix develop .#<shell-name>
@@ -111,6 +101,9 @@
${pkgs.alejandra}/bin/alejandra --check ${./.} ${pkgs.alejandra}/bin/alejandra --check ${./.}
touch $out 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 # 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 # This module provides functions to configure Opencode agent rules across
# multiple projects. Rules are defined in the AGENTS repository and can be # multiple projects. Rules are defined in the AGENTS repository and can be
@@ -26,7 +26,7 @@
# #
# The shellHook creates: # The shellHook creates:
# - A `.opencode-rules/` symlink pointing to the AGENTS repository rules directory # - 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 # The instructions list contains paths relative to the project root, all prefixed
# with `.opencode-rules/`, making them portable across different project locations. # with `.opencode-rules/`, making them portable across different project locations.
@@ -46,7 +46,7 @@
# #
# Returns: # Returns:
# An attribute set containing: # 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) # - instructions: List of rule file paths (relative to project root)
# #
# Example: # Example:
@@ -82,9 +82,8 @@
], ],
frameworks ? [], frameworks ? [],
extraInstructions ? [], extraInstructions ? [],
rulesDir ? ".opencode-rules",
}: let }: let
rulesDir = ".opencode-rules";
# Build instructions list by mapping concerns, languages, frameworks to their file paths # Build instructions list by mapping concerns, languages, frameworks to their file paths
# All paths are relative to project root via the rulesDir symlink # All paths are relative to project root via the rulesDir symlink
instructions = instructions =
@@ -93,8 +92,8 @@
++ (map (f: "${rulesDir}/frameworks/${f}.md") frameworks) ++ (map (f: "${rulesDir}/frameworks/${f}.md") frameworks)
++ extraInstructions; ++ extraInstructions;
# Generate JSON configuration for Opencode # Generate JSON configuration for coding rules
opencodeConfig = { rulesConfig = {
"$schema" = "https://opencode.ai/config.json"; "$schema" = "https://opencode.ai/config.json";
inherit instructions; inherit instructions;
}; };
@@ -102,15 +101,15 @@
inherit instructions; inherit instructions;
# Shell hook to set up rules in the project # 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 = '' shellHook = ''
# Create/update symlink to AGENTS rules directory # Create/update symlink to AGENTS rules directory
ln -sfn ${agents}/rules ${rulesDir} ln -sfn ${agents}/rules ${rulesDir}
# Generate opencode.json configuration file # Generate coding-rules.json configuration file
cat > opencode.json <<'OPENCODE_EOF' cat > coding-rules.json <<'RULES_EOF'
${builtins.toJSON opencodeConfig} ${builtins.toJSON rulesConfig}
OPENCODE_EOF RULES_EOF
''; '';
}; };

View File

@@ -1,116 +0,0 @@
# 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.opencode-rules.mkOpencodeRules {
# 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}: {
# 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:
# mkOpencodeRules {
# 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"
# # ];
# # }
mkOpencodeRules = {
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
'';
};
}

View File

@@ -3,88 +3,87 @@
lib, lib,
pkgs, pkgs,
... ...
}: }: let
with lib; let shared = import ./shared-options.nix {inherit lib;};
cfg = config.coding.agents.claude-code; in
mcpCfg = config.programs.mcp or null; with lib; let
in { cfg = config.coding.agents.claude-code;
options.coding.agents.claude-code = { mcpCfg = config.programs.mcp or null;
enable = mkEnableOption "Claude Code agent management via canonical agent.toml definitions"; in {
options.coding.agents.claude-code = {
enable = mkEnableOption "Claude Code agent management via canonical agent.toml definitions";
agentsInput = mkOption { agentsInput = shared.mkAgentsInputOption ''
type = types.nullOr types.anything;
default = null;
description = ''
The `agents` flake input (your personal AGENTS repo). The `agents` flake input (your personal AGENTS repo).
When set, agents are rendered from canonical agent.toml files When set, agents are rendered from canonical agent.toml files
and symlinked to ~/.claude/agents/. 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 { config = mkIf cfg.enable (let
type = types.attrsOf types.str; agentsLib = (import ../../../../lib {inherit lib;}).agents;
default = {};
description = '' # Rendered agents + permissions (only if agentsInput is set)
Per-agent model overrides. Maps agent slug to model alias or ID. rendered = mkIf (cfg.agentsInput != null) (
Example: { chiron = "claude-sonnet-4-20250514"; } agentsLib.renderForClaudeCode {
''; inherit pkgs;
example = literalExpression '' canonical = cfg.agentsInput.lib.loadAgents;
{ modelOverrides = cfg.modelOverrides;
chiron = "claude-sonnet-4-20250514";
"chiron-forge" = "claude-sonnet-4-20250514";
} }
''; );
};
mcpServers = mkOption { # Merge MCP servers into the rendered settings.json.
type = types.attrsOf types.anything; # The renderer produces { permissions: { allow, deny } }.
default = if mcpCfg != null then mcpCfg.servers else {}; # We add mcpServers on top.
defaultText = literalExpression "config.programs.mcp.servers"; settingsJson =
description = '' if cfg.agentsInput != null
MCP server configurations for Claude Code. then let
Merged into ~/.claude/settings.json alongside permissions. renderedSettings = builtins.fromJSON (builtins.readFile "${rendered}/.claude/settings.json");
Automatically inherits from config.programs.mcp.servers. 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 # Skills (merged from personal AGENTS repo + optional external skills)
agentsLib = (import ../../../../lib {inherit lib;}).agents; 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 settings.json with permissions + MCP servers
rendered = mkIf (cfg.agentsInput != null) ( home.file.".claude/settings.json" = mkIf (settingsJson != null) {
agentsLib.renderForClaudeCode { source = "${settingsJson}";
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}";
};
});
}

View File

@@ -3,111 +3,67 @@
lib, lib,
pkgs, pkgs,
... ...
}: }: let
with lib; let shared = import ./shared-options.nix {inherit lib;};
cfg = config.coding.agents.opencode; in
in { with lib; {
options.coding.agents.opencode = { options.coding.agents.opencode = {
enable = mkEnableOption "OpenCode agent management via canonical agent.toml definitions"; enable = mkEnableOption "OpenCode agent management via canonical agent.toml definitions";
agentsInput = mkOption { agentsInput = shared.mkAgentsInputOption ''
type = types.nullOr types.anything;
default = null;
description = ''
The `agents` flake input (your personal AGENTS repo). The `agents` flake input (your personal AGENTS repo).
When set, agents are rendered from canonical agent.toml files When set, agents are rendered from canonical agent.toml files
and symlinked to ~/.config/opencode/agents/. and symlinked to ~/.config/opencode/agents/.
''; '';
modelOverrides = shared.mkModelOverridesOption;
externalSkills = shared.externalSkillsOption;
}; };
modelOverrides = mkOption { config = mkIf config.coding.agents.opencode.enable {
type = types.attrsOf types.str; # Rendered agent files symlinked to ~/.config/opencode/agents/
default = {}; xdg.configFile."opencode/agents" = let
description = '' cfg = config.coding.agents.opencode;
Per-agent model overrides. Maps agent slug to model string. in
Example: { chiron = "anthropic/claude-sonnet-4"; } mkIf (cfg.agentsInput != null) {
''; source = (import ../../../../lib {inherit lib;}).agents.renderForOpencode {
example = literalExpression '' inherit pkgs;
{ canonical = cfg.agentsInput.lib.loadAgents;
chiron = "anthropic/claude-sonnet-4"; modelOverrides = cfg.modelOverrides;
"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 { # Skills (merged from personal AGENTS repo + optional external skills)
# Rendered agent files symlinked to ~/.config/opencode/agents/ xdg.configFile."opencode/skills" = let
xdg.configFile."opencode/agents" = mkIf (cfg.agentsInput != null) { cfg = config.coding.agents.opencode;
source = (import ../../../../lib {inherit lib;}).agents.renderForOpencode { in
inherit pkgs; mkIf (cfg.agentsInput != null) {
canonical = cfg.agentsInput.lib.loadAgents; source = cfg.agentsInput.lib.mkOpencodeSkills {
modelOverrides = cfg.modelOverrides; inherit pkgs;
}; customSkills = "${cfg.agentsInput}/skills";
}; externalSkills = shared.mapExternalSkills cfg.externalSkills;
};
};
# Skills (merged from personal AGENTS repo + optional external skills) # Static config dirs from AGENTS repo
xdg.configFile."opencode/skills" = mkIf (cfg.agentsInput != null) { xdg.configFile."opencode/context" = let
source = cfg.agentsInput.lib.mkOpencodeSkills { cfg = config.coding.agents.opencode;
inherit pkgs; in
customSkills = "${cfg.agentsInput}/skills"; mkIf (cfg.agentsInput != null) {
externalSkills = source = "${cfg.agentsInput}/context";
map ( };
entry: xdg.configFile."opencode/commands" = let
{inherit (entry) src skillsDir;} cfg = config.coding.agents.opencode;
// optionalAttrs (entry.selectSkills != null) {inherit (entry) selectSkills;} in
) mkIf (cfg.agentsInput != null) {
cfg.externalSkills; 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, lib,
pkgs, pkgs,
... ...
}: }: let
with lib; let shared = import ./shared-options.nix {inherit lib;};
cfg = config.coding.agents.pi; in
mcpCfg = config.programs.mcp or null; with lib; let
in { cfg = config.coding.agents.pi;
options.coding.agents.pi = { mcpCfg = config.programs.mcp or null;
enable = mkEnableOption "Pi agent management via canonical agent.toml definitions"; in {
options.coding.agents.pi = {
enable = mkEnableOption "Pi agent management via canonical agent.toml definitions";
mcpServers = mkOption { mcpServers = mkOption {
type = types.attrsOf types.anything; type = types.attrsOf types.anything;
default = if mcpCfg != null then mcpCfg.servers else {}; default = if mcpCfg != null then mcpCfg.servers else {};
defaultText = literalExpression "config.programs.mcp.servers"; defaultText = literalExpression "config.programs.mcp.servers";
description = '' description = ''
MCP server configurations for Pi (pi-mcp-adapter). MCP server configurations for Pi (pi-mcp-adapter).
Written to ~/.pi/agent/mcp.json. Written to ~/.pi/agent/mcp.json.
Automatically inherits from config.programs.mcp.servers. Automatically inherits from config.programs.mcp.servers.
''; '';
}; };
agentsInput = mkOption { agentsInput = shared.mkAgentsInputOption ''
type = types.nullOr types.anything;
default = null;
description = ''
The `agents` flake input (your personal AGENTS repo). The `agents` flake input (your personal AGENTS repo).
When set, the primary agent's system prompt is rendered as SYSTEM.md, 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. all agents are listed in AGENTS.md, and subagent .md files are deployed.
''; '';
};
modelOverrides = mkOption { modelOverrides = shared.mkModelOverridesOption;
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 { primaryAgent = mkOption {
type = types.nullOr types.str; type = types.nullOr types.str;
default = null; default = null;
description = '' description = ''
Override which canonical agent is used as primary for SYSTEM.md. Override which canonical agent is used as primary for SYSTEM.md.
When null, the first agent with mode="primary" is used. 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 externalSkills = shared.externalSkillsOption;
# 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; 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) defaultProvider = mkOption {
rendered = type = types.nullOr types.str;
if cfg.agentsInput != null default = null;
then description = "Default LLM provider (e.g. 'anthropic', 'openai', 'zai').";
(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 defaultModel = mkOption {
agentFiles = type = types.nullOr types.str;
if cfg.agentsInput != null default = null;
then description = "Default model ID.";
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 ────────────────────────────────── defaultThinkingLevel = mkOption {
{ type = types.nullOr (types.enum ["off" "minimal" "low" "medium" "high" "xhigh"]);
".pi/agent/settings.json".text = builtins.toJSON piSettings; default = null;
} description = "Default extended thinking level.";
};
# ── AGENTS.md — agent descriptions and specialist listing ────── theme = mkOption {
(mkIf (cfg.agentsInput != null) { type = types.nullOr types.str;
".pi/agent/AGENTS.md".source = "${rendered}/AGENTS.md"; default = null;
}) description = "Pi theme name.";
};
# ── SYSTEM.md — primary agent's system prompt ────────────────── hideThinkingBlock = mkOption {
(mkIf (cfg.agentsInput != null) { type = types.nullOr types.bool;
".pi/agent/SYSTEM.md".source = "${rendered}/SYSTEM.md"; default = null;
}) description = "Hide thinking blocks in output.";
};
# ── Agents — pi-subagents .md files ──────────────────────────── quietStartup = mkOption {
agentFiles type = types.nullOr types.bool;
default = null;
description = "Hide startup header.";
};
# ── Skills symlinked from AGENTS repo ────────────────────────── compaction = mkOption {
(mkIf (cfg.agentsInput != null) { type = types.nullOr (types.submodule {
".pi/agent/skills".source = cfg.agentsInput.lib.mkOpencodeSkills { options = {
inherit pkgs; enabled = mkOption {
customSkills = "${cfg.agentsInput}/skills"; 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 # Package modifications
# This overlay contains package overrides and 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 # Add more modifications here as needed
# example-package = prev.example-package.override { ... }; # 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, inputs,
... ...
}: let }: let
# Used only for flake input pass-throughs (basecamp, openspec, opencode-desktop)
system = pkgs.stdenv.hostPlatform.system; system = pkgs.stdenv.hostPlatform.system;
in { in {
# Custom packages registry # Custom packages registry

View File

@@ -8,10 +8,10 @@
nix-update-script, nix-update-script,
}: let }: let
pname = "eigent"; pname = "eigent";
version = "0.0.89"; version = "0.0.90";
src = fetchurl { src = fetchurl {
url = "https://github.com/eigent-ai/eigent/releases/download/v${version}/Eigent-${version}.AppImage"; 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;}; appimageContents = appimageTools.extractType2 {inherit pname version src;};
in in

View File

@@ -11,6 +11,7 @@
node-gyp, node-gyp,
cctools, cctools,
xcbuild, xcbuild,
dart-sass,
libkrb5, libkrb5,
libmongocrypt, libmongocrypt,
libpq, libpq,
@@ -25,20 +26,20 @@
in in
stdenv.mkDerivation (finalAttrs: { stdenv.mkDerivation (finalAttrs: {
pname = "n8n"; pname = "n8n";
version = "stable"; version = "2.16.1";
src = fetchFromGitHub { src = fetchFromGitHub {
owner = "n8n-io"; owner = "n8n-io";
repo = "n8n"; repo = "n8n";
tag = "${finalAttrs.version}"; tag = "n8n@${finalAttrs.version}";
hash = "sha256-/atba0ymCqhh5Rt61UxwC2xf8SGrRsEKtlsDCIkg37Y="; hash = "sha256-5y00RY8WWVgpxC3TNPFS9XxshgZKTlShpw+HiJVQvmM=";
}; };
pnpmDeps = fetchPnpmDeps { pnpmDeps = fetchPnpmDeps {
inherit (finalAttrs) pname version src; inherit (finalAttrs) pname version src;
pnpm = pnpm_10; pnpm = pnpm_10;
fetcherVersion = 3; fetcherVersion = 3;
hash = "sha256-YGplNNvIOIY1BthWmejAzucXujq8AkgPJus774GmWCA="; hash = "sha256-qyD+zlsBiJLwrazEclVkDmUp+wAxvdH3P6oWpmiX5rc=";
}; };
nativeBuildInputs = nativeBuildInputs =
@@ -61,6 +62,17 @@ in
libpq libpq
]; ];
preBuild = ''
# Force sass-embedded to use our dart-sass instead of bundled binaries.
# The bundled Dart binary can't run in the Nix sandbox (no /lib64/ld-linux-x86-64.so.2).
for dep in node_modules/.pnpm/sass-embedded@*; do
substituteInPlace "$dep/node_modules/sass-embedded/dist/lib/src/compiler-path.js" \
--replace-fail \
'compilerCommand = (() => {' \
'compilerCommand = (() => { return ["${lib.getExe dart-sass}"];'
done
'';
buildPhase = '' buildPhase = ''
runHook preBuild runHook preBuild

View File

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

View File

@@ -4,10 +4,10 @@
lib, lib,
}: let }: let
pname = "vibetyper"; pname = "vibetyper";
version = "1.2.2"; version = "1.2.3";
src = fetchurl { src = fetchurl {
url = "https://cdn.vibetyper.com/releases/linux/VibeTyper.AppImage"; url = "https://cdn.vibetyper.com/releases/linux/VibeTyper.AppImage";
sha256 = "sha256-AUjrSVxyaI8Ok4pnoqaW4fGAd4GtSc0mEjDhkqdifY0="; sha256 = "sha256-6uGXw2nxb0sGkcMDTWBlL3PuwBfVodhgqfgZT1Ncs40=";
}; };
appimageContents = appimageTools.extractType2 {inherit pname version src;}; appimageContents = appimageTools.extractType2 {inherit pname version src;};
in in

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;
}