Compare commits

..

22 Commits

Author SHA1 Message Date
m3tm3re
cae2f73a12 Merge branch 'chore/update-gitignore-and-changes' 2026-04-17 11:12:28 +02:00
14d7dc47a5 chore: update .gitignore, remove tracked .pi-lens files, and sync pending changes
- Add .cache to .gitignore
- Remove .pi-lens/ from git tracking (already ignored)
- Update pi-agent-runner, pi-agent-wrapper modules
- Update claude-code agent module
- Update overlays, opencode-desktop package, and tests
2026-04-17 11:07:08 +02:00
853c644446 fix: propagate TERM/locale through sudo for correct UTF-8 handling, remove broken VM test
- Pass TERM, LANG, LC_ALL, LC_CTYPE, COLORTERM through sudo in wrapper
- Propagate these vars to systemd-run in runner for correct PTY/UTF-8
- Add activationScript to fix stateDir ownership after useradd
- Remove pi-agent VM test (ownership race condition with createHome)
2026-04-16 08:13:24 +02:00
Chiron
9a8107ea90 test: add NixOS VM test for pi-agent module 2026-04-15 18:47:34 +00:00
Chiron
4935fcb9ee refactor: extract pi-agent runner and wrapper to separate files 2026-04-15 18:46:21 +00:00
Chiron
a2f08671a6 docs: update AGENTS.md to reflect current codebase state 2026-04-15 18:45:25 +00:00
Chiron
b708b2a05f test: add basic lib function tests for agents and coding-rules 2026-04-15 18:43:00 +00:00
Chiron
41fbe75abc docs: add CHANGELOG.md 2026-04-15 18:27:42 +00:00
Chiron
778192e5e6 refactor: remove redundant 'additions' overlay (identical to 'default') 2026-04-15 18:26:41 +00:00
Chiron
1f149155b4 refactor: tool-agnostic naming in coding-rules.nix internals 2026-04-15 18:26:02 +00:00
Chiron
a2cb2b6319 chore: remove dead overlay entries for non-existent flake inputs 2026-04-15 18:24:12 +00:00
Chiron
6ff6deb4e3 docs: clarify system binding in pkgs/default.nix 2026-04-15 18:23:43 +00:00
Chiron
5d9fe6afb7 refactor: remove duplicate opencode-rules.nix, use alias in default.nix 2026-04-15 18:23:18 +00:00
Chiron
b2208277c4 docs: add cleanup and improvements plan 2026-04-15 18:22:10 +00:00
9adfa185bb Merge pull request 'fix: pi settings sync' (#8) from fix/pi-sync-settings into master
Some checks failed
Update Nix Packages with nix-update / nix-update (push) Failing after 3m21s
Reviewed-on: #8
2026-04-15 11:48:51 +02:00
sascha.koenig
a1b6950e93 fix: pi settings sync
chore: eigent update
2026-04-15 11:38:25 +02:00
25a44e79fa Merge pull request 'fix: pi settings sync' (#7) from fix/pi-sync-settings into master
Some checks failed
Update Nix Packages with nix-update / nix-update (push) Failing after 7m1s
Reviewed-on: #7
2026-04-14 20:16:38 +02:00
sascha.koenig
c615eb5c1e fix: pi settings sync 2026-04-14 20:15:20 +02:00
aa084be01a Merge pull request 'feat: pi-agent wrapper' (#6) from feature/pi-agent-wrapper into master
Reviewed-on: #6
2026-04-14 18:52:02 +02:00
m3tm3re
3794500230 feat: pi-agent wrapper 2026-04-14 18:36:13 +02:00
0867492170 Merge pull request 'feat: containerized pi agent' (#5) from feature/pi-agent-containerized into master
Some checks failed
Update Nix Packages with nix-update / nix-update (push) Failing after 3m25s
Reviewed-on: #5
2026-04-13 21:30:41 +02:00
m3tm3re
cab1f73c89 feat: containerized pi agent 2026-04-13 21:27:09 +02:00
60 changed files with 2369 additions and 2003 deletions

73
.beads/.gitignore vendored
View File

@@ -1,73 +0,0 @@
# Dolt database (managed by Dolt, not git)
dolt/
embeddeddolt/
# Runtime files
bd.sock
bd.sock.startlock
sync-state.json
last-touched
.exclusive-lock
# Daemon runtime (lock, log, pid)
daemon.*
# Interactions log (runtime, not versioned)
interactions.jsonl
# Push state (runtime, per-machine)
push-state.json
# Lock files (various runtime locks)
*.lock
# Credential key (encryption key for federation peer auth — never commit)
.beads-credential-key
# Local version tracking (prevents upgrade notification spam after git ops)
.local_version
# Worktree redirect file (contains relative path to main repo's .beads/)
# Must not be committed as paths would be wrong in other clones
redirect
# Sync state (local-only, per-machine)
# These files are machine-specific and should not be shared across clones
.sync.lock
export-state/
export-state.json
# Ephemeral store (SQLite - wisps/molecules, intentionally not versioned)
ephemeral.sqlite3
ephemeral.sqlite3-journal
ephemeral.sqlite3-wal
ephemeral.sqlite3-shm
# Dolt server management (auto-started by bd)
dolt-server.pid
dolt-server.log
dolt-server.lock
dolt-server.port
dolt-server.activity
# Corrupt backup directories (created by bd doctor --fix recovery)
*.corrupt.backup/
# Backup data (auto-exported JSONL, local-only)
backup/
# Per-project environment file (Dolt connection config, GH#2520)
.env
# Legacy files (from pre-Dolt versions)
*.db
*.db?*
*.db-journal
*.db-wal
*.db-shm
db.sqlite
bd.db
# NOTE: Do NOT add negation patterns here.
# They would override fork protection in .git/info/exclude.
# Config files (metadata.json, config.yaml) are tracked by git by default
# since no pattern above ignores them.

View File

@@ -1,81 +0,0 @@
# Beads - AI-Native Issue Tracking
Welcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code.
## What is Beads?
Beads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git.
**Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads)
## Quick Start
### Essential Commands
```bash
# Create new issues
bd create "Add user authentication"
# View all issues
bd list
# View issue details
bd show <issue-id>
# Update issue status
bd update <issue-id> --claim
bd update <issue-id> --status done
# Sync with Dolt remote
bd dolt push
```
### Working with Issues
Issues in Beads are:
- **Git-native**: Stored in Dolt database with version control and branching
- **AI-friendly**: CLI-first design works perfectly with AI coding agents
- **Branch-aware**: Issues can follow your branch workflow
- **Always in sync**: Auto-syncs with your commits
## Why Beads?
**AI-Native Design**
- Built specifically for AI-assisted development workflows
- CLI-first interface works seamlessly with AI coding agents
- No context switching to web UIs
🚀 **Developer Focused**
- Issues live in your repo, right next to your code
- Works offline, syncs when you push
- Fast, lightweight, and stays out of your way
🔧 **Git Integration**
- Automatic sync with git commits
- Branch-aware issue tracking
- Dolt-native three-way merge resolution
## Get Started with Beads
Try Beads in your own projects:
```bash
# Install Beads
curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash
# Initialize in your repo
bd init
# Create your first issue
bd create "Try out Beads"
```
## Learn More
- **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs)
- **Quick Start Guide**: Run `bd quickstart`
- **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples)
---
*Beads: Issue tracking that moves at the speed of thought*

View File

@@ -1,56 +0,0 @@
# Beads Configuration File
# This file configures default behavior for all bd commands in this repository
# All settings can also be set via environment variables (BD_* prefix)
# or overridden with command-line flags
# Issue prefix for this repository (used by bd init)
# If not set, bd init will auto-detect from directory name
# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc.
# issue-prefix: ""
# Use no-db mode: JSONL-only, no Dolt database
# When true, bd will use .beads/issues.jsonl as the source of truth
# no-db: false
# Enable JSON output by default
# json: false
# Feedback title formatting for mutating commands (create/update/close/dep/edit)
# 0 = hide titles, N > 0 = truncate to N characters
# output:
# title-length: 255
# Default actor for audit trails (overridden by BEADS_ACTOR or --actor)
# actor: ""
# Export events (audit trail) to .beads/events.jsonl on each flush/sync
# When enabled, new events are appended incrementally using a high-water mark.
# Use 'bd export --events' to trigger manually regardless of this setting.
# events-export: false
# Multi-repo configuration (experimental - bd-307)
# Allows hydrating from multiple repositories and routing writes to the correct database
# repos:
# primary: "." # Primary repo (where this database lives)
# additional: # Additional repos to hydrate from (read-only)
# - ~/beads-planning # Personal planning repo
# - ~/work-planning # Work planning repo
# JSONL backup (periodic export for off-machine recovery)
# Auto-enabled when a git remote exists. Override explicitly:
# backup:
# enabled: false # Disable auto-backup entirely
# interval: 15m # Minimum time between auto-exports
# git-push: false # Disable git push (export locally only)
# git-repo: "" # Separate git repo for backups (default: project repo)
# Integration settings (access with 'bd config get/set')
# These are stored in the database, not in this file:
# - jira.url
# - jira.project
# - linear.url
# - linear.api-key
# - github.org
# - github.repo
sync.remote: "git+ssh://gitea@code.m3ta.dev/m3tam3re/nixpkgs.git"

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env sh
# --- BEGIN BEADS INTEGRATION v1.0.2 ---
# This section is managed by beads. Do not remove these markers.
if command -v bd >/dev/null 2>&1; then
export BD_GIT_HOOK=1
_bd_timeout=${BEADS_HOOK_TIMEOUT:-300}
if command -v timeout >/dev/null 2>&1; then
timeout "$_bd_timeout" bd hooks run post-checkout "$@"
_bd_exit=$?
if [ $_bd_exit -eq 124 ]; then
echo >&2 "beads: hook 'post-checkout' timed out after ${_bd_timeout}s — continuing without beads"
_bd_exit=0
fi
else
bd hooks run post-checkout "$@"
_bd_exit=$?
fi
if [ $_bd_exit -eq 3 ]; then
echo >&2 "beads: database not initialized — skipping hook 'post-checkout'"
_bd_exit=0
fi
if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
fi
# --- END BEADS INTEGRATION v1.0.2 ---

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env sh
# --- BEGIN BEADS INTEGRATION v1.0.2 ---
# This section is managed by beads. Do not remove these markers.
if command -v bd >/dev/null 2>&1; then
export BD_GIT_HOOK=1
_bd_timeout=${BEADS_HOOK_TIMEOUT:-300}
if command -v timeout >/dev/null 2>&1; then
timeout "$_bd_timeout" bd hooks run post-merge "$@"
_bd_exit=$?
if [ $_bd_exit -eq 124 ]; then
echo >&2 "beads: hook 'post-merge' timed out after ${_bd_timeout}s — continuing without beads"
_bd_exit=0
fi
else
bd hooks run post-merge "$@"
_bd_exit=$?
fi
if [ $_bd_exit -eq 3 ]; then
echo >&2 "beads: database not initialized — skipping hook 'post-merge'"
_bd_exit=0
fi
if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
fi
# --- END BEADS INTEGRATION v1.0.2 ---

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env sh
# --- BEGIN BEADS INTEGRATION v1.0.2 ---
# This section is managed by beads. Do not remove these markers.
if command -v bd >/dev/null 2>&1; then
export BD_GIT_HOOK=1
_bd_timeout=${BEADS_HOOK_TIMEOUT:-300}
if command -v timeout >/dev/null 2>&1; then
timeout "$_bd_timeout" bd hooks run pre-commit "$@"
_bd_exit=$?
if [ $_bd_exit -eq 124 ]; then
echo >&2 "beads: hook 'pre-commit' timed out after ${_bd_timeout}s — continuing without beads"
_bd_exit=0
fi
else
bd hooks run pre-commit "$@"
_bd_exit=$?
fi
if [ $_bd_exit -eq 3 ]; then
echo >&2 "beads: database not initialized — skipping hook 'pre-commit'"
_bd_exit=0
fi
if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
fi
# --- END BEADS INTEGRATION v1.0.2 ---

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env sh
# --- BEGIN BEADS INTEGRATION v1.0.2 ---
# This section is managed by beads. Do not remove these markers.
if command -v bd >/dev/null 2>&1; then
export BD_GIT_HOOK=1
_bd_timeout=${BEADS_HOOK_TIMEOUT:-300}
if command -v timeout >/dev/null 2>&1; then
timeout "$_bd_timeout" bd hooks run pre-push "$@"
_bd_exit=$?
if [ $_bd_exit -eq 124 ]; then
echo >&2 "beads: hook 'pre-push' timed out after ${_bd_timeout}s — continuing without beads"
_bd_exit=0
fi
else
bd hooks run pre-push "$@"
_bd_exit=$?
fi
if [ $_bd_exit -eq 3 ]; then
echo >&2 "beads: database not initialized — skipping hook 'pre-push'"
_bd_exit=0
fi
if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
fi
# --- END BEADS INTEGRATION v1.0.2 ---

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env sh
# --- BEGIN BEADS INTEGRATION v1.0.2 ---
# This section is managed by beads. Do not remove these markers.
if command -v bd >/dev/null 2>&1; then
export BD_GIT_HOOK=1
_bd_timeout=${BEADS_HOOK_TIMEOUT:-300}
if command -v timeout >/dev/null 2>&1; then
timeout "$_bd_timeout" bd hooks run prepare-commit-msg "$@"
_bd_exit=$?
if [ $_bd_exit -eq 124 ]; then
echo >&2 "beads: hook 'prepare-commit-msg' timed out after ${_bd_timeout}s — continuing without beads"
_bd_exit=0
fi
else
bd hooks run prepare-commit-msg "$@"
_bd_exit=$?
fi
if [ $_bd_exit -eq 3 ]; then
echo >&2 "beads: database not initialized — skipping hook 'prepare-commit-msg'"
_bd_exit=0
fi
if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
fi
# --- END BEADS INTEGRATION v1.0.2 ---

View File

@@ -1 +0,0 @@
{"id":"nixpkgs-ng1","title":"Configure agent git identity in nixpkgs repo","description":"Git commits are using p@m3ta.dev instead of m3ta-chiron@agentmail.to. The GIT_AUTHOR_NAME and GIT_AUTHOR_EMAIL environment variables are not set in this environment. Need to configure the agent git identity for this repository following the pattern in AGENTS.md","status":"open","priority":2,"issue_type":"task","owner":"p@m3ta.dev","created_at":"2026-04-27T18:16:17Z","created_by":"m3tm3re","updated_at":"2026-04-27T18:16:17Z","dependency_count":0,"dependent_count":0,"comment_count":0}

View File

@@ -1,7 +0,0 @@
{
"database": "dolt",
"backend": "dolt",
"dolt_mode": "embedded",
"dolt_database": "nixpkgs",
"project_id": "b57a167a-6526-4211-a6c1-51686e431912"
}

Binary file not shown.

7
.gitignore vendored
View File

@@ -43,10 +43,5 @@ flake.lock.bak
.sidecar-start.sh
.sidecar-base
.td-root
.pi-lens
.cache
.pi*
# Beads / Dolt files (added by bd init)
.dolt/
*.db
.beads-credential-key

261
AGENTS.md
View File

@@ -1,84 +1,217 @@
# Agent Instructions
# m3ta-nixpkgs Knowledge Base
This project uses **bd** (beads) for issue tracking. Run `bd prime` for full workflow context.
**Generated:** 2026-02-14
**Commit:** dc2f3b6
**Branch:** master
## Quick Reference
## OVERVIEW
```bash
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --claim # Claim work atomically
bd close <id> # Complete work
bd dolt push # Push beads data to remote
Personal Nix flake: custom packages, overlays, NixOS/Home Manager modules, dev shells. Flakes-only (no channels).
## STRUCTURE
```
.
├── flake.nix # Entry: packages, overlays, modules, shells, lib
├── pkgs/ # Custom packages (one dir each, callPackage registry)
├── modules/
│ ├── nixos/ # System modules (ports.nix)
│ └── home-manager/ # User modules by category (cli/, coding/, ports.nix)
├── lib/ # Shared utilities (ports.nix)
├── shells/ # Dev environments (default, python, devops)
├── overlays/mods/ # Package modifications (n8n version bump)
├── templates/ # Boilerplate for new packages/modules
├── examples/ # Usage examples
└── .gitea/workflows/ # CI/CD workflows (nix-update automation)
```
## Non-Interactive Shell Commands
## WHERE TO LOOK
**ALWAYS use non-interactive flags** with file operations to avoid hanging on confirmation prompts.
| Task | Location | Notes |
| -------------------- | ---------------------------------- | ------------------------------------- |
| Add package | `pkgs/<name>/default.nix` | Register in `pkgs/default.nix` |
| Add NixOS module | `modules/nixos/<name>.nix` | Import in `modules/nixos/default.nix` |
| Add HM module | `modules/home-manager/<category>/` | Category: cli, coding, or root |
| Override nixpkgs pkg | `overlays/mods/<name>.nix` | Import in `overlays/mods/default.nix` |
| Add dev shell | `shells/<name>.nix` | Register in `shells/default.nix` |
| Use port management | `config.m3ta.ports.get "service"` | Host-specific via `hostOverrides` |
| CI/CD workflows | `.gitea/workflows/<name>.yml` | Automated package updates (nix-update) |
Shell commands like `cp`, `mv`, and `rm` may be aliased to include `-i` (interactive) mode on some systems, causing the agent to hang indefinitely waiting for y/n input.
## CONVENTIONS
**Use these forms instead:**
```bash
# Force overwrite without prompting
cp -f source dest # NOT: cp source dest
mv -f source dest # NOT: mv source dest
rm -f file # NOT: rm file
**Formatter**: `nix fmt` before commit (alejandra)
# For recursive operations
rm -rf directory # NOT: rm -r directory
cp -rf source dest # NOT: cp -r source dest
**Naming**:
- Packages: `lowercase-hyphen` (e.g., `hyprpaper-random`)
- Variables: `camelCase` (e.g., `portHelpers`)
- Module options: `m3ta.*` namespace
**Imports**: Multi-line, trailing commas:
```nix
{
lib,
stdenv,
fetchFromGitHub,
}:
```
**Other commands that may prompt:**
- `scp` - use `-o BatchMode=yes` for non-interactive
- `ssh` - use `-o BatchMode=yes` to fail instead of prompting
- `apt-get` - use `-y` flag
- `brew` - use `HOMEBREW_NO_AUTO_UPDATE=1` env var
**Modules**: Standard pattern:
<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:ca08a54f -->
## Beads Issue Tracker
This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands.
### Quick Reference
```bash
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --claim # Claim work
bd close <id> # Complete work
```nix
{ config, lib, pkgs, ... }:
with lib; let
cfg = config.m3ta.myModule;
in {
options.m3ta.myModule = {
enable = mkEnableOption "description";
};
config = mkIf cfg.enable { ... };
}
```
### Rules
**Meta**: Always include all fields:
- Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists
- Run `bd prime` for detailed command reference and session close protocol
- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files
```nix
meta = with lib; {
description = "...";
homepage = "...";
license = licenses.mit;
platforms = platforms.linux;
mainProgram = "...";
};
```
## Session Completion
## PACKAGE PATTERNS
**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds.
**Rust**: `rustPlatform.buildRustPackage rec { cargoLock.lockFile = src + "/Cargo.lock"; }`
**MANDATORY WORKFLOW:**
**Shell**: `writeShellScriptBin "name" ''script''` or `mkDerivation` with custom `installPhase`
1. **File issues for remaining work** - Create issues for anything that needs follow-up
2. **Run quality gates** (if code changed) - Tests, linters, builds
3. **Update issue status** - Close finished work, update in-progress items
4. **PUSH TO REMOTE** - This is MANDATORY:
```bash
git pull --rebase
bd dolt push
git push
git status # MUST show "up to date with origin"
```
5. **Clean up** - Clear stashes, prune remote branches
6. **Verify** - All changes committed AND pushed
7. **Hand off** - Provide context for next session
**AppImage**: `appimageTools.wrapType2 { ... }`
**CRITICAL RULES:**
- Work is NOT complete until `git push` succeeds
- NEVER stop before pushing - that leaves work stranded locally
- NEVER say "ready to push when you are" - YOU must push
- If push fails, resolve and retry until it succeeds
<!-- END BEADS INTEGRATION -->
**Custom fetcher**: `fetchFromGitea { domain = "code.m3ta.dev"; owner = "m3tam3re"; ... }`
## MODULE PATTERNS
**Simple**: `options.cli.name = { enable = mkEnableOption "..."; }; config = mkIf cfg.enable { ... };`
**Multiple**: `config = mkMerge [ (mkIf cfg.x.enable { ... }) (mkIf cfg.y.enable { ... }) ];`
**Shared lib**: `portsLib = import ../../lib/ports.nix { inherit lib; }; portHelpers = portsLib.mkPortHelpers { ... };`
## LIBRARY FUNCTIONS
### `lib.ports`
Port management utilities. See [Port Management](#port-management).
### `lib.agents`
Harness-agnostic agent management. Reads canonical `agent.toml` +
`system-prompt.md` from the AGENTS flake input and renders tool-specific configs.
**Functions:**
| Function | Purpose |
|----------|--------|
| `loadCanonical { agentsInput }` | Load canonical agents from AGENTS flake |
| `renderForOpencode { pkgs, canonical, modelOverrides }` | Render to OpenCode file-based agents |
| `renderForClaudeCode { pkgs, canonical, modelOverrides }` | Render to Claude Code agents + settings.json |
| `renderForPi { pkgs, canonical, 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. 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, rulesDir }` | Generate rules config + shellHook. `rulesDir` defaults to `.opencode-rules` |
| `mkOpencodeRules` | Backward-compat alias for `mkCodingRules` |
## PORT MANAGEMENT
Central port management: `config.m3ta.ports.get "service"` with host-specific via `hostOverrides`
Generated: `/etc/m3ta/ports.json` (NixOS), `~/.config/m3ta/ports.json` (HM)
## COMMANDS
```bash
nix flake check # Validate flake
nix fmt # Format (alejandra)
nix build .#<pkg> # Build package
nix flake show # List outputs
nix develop # Enter dev shell
nix develop .#python # Python shell
nix develop .#devops # DevOps shell
# In dev shell only:
statix check . # Lint
deadnix . # Find dead code
```
## ANTI-PATTERNS
| Don't | Do Instead |
| ------------------------- | ------------------------------------------------------------------- |
| `lib.fakeHash` in commits | Get real hash: `nix build`, copy from error |
| Flat module files | Organize by category (`cli/`, `coding/`) |
| Hardcode ports | Use `m3ta.ports` module |
| Skip meta fields | Include all: description, homepage, license, platforms, mainProgram |
| `with pkgs;` in modules | Explicit `pkgs.package` or `with pkgs; [ ... ]` in lists only |
## COMMIT FORMAT
```
type: brief description
```
Types: `feat`, `fix`, `docs`, `style`, `refactor`, `chore`
## NOTES
- **Hash fetching**: Use `lib.fakeHash` initially, build to get real hash
- **HM modules**: Category subdirs (`cli/`, `coding/`) have own `default.nix` aggregators
- **Ports module**: Different for NixOS vs HM (HM adds `generateEnvVars` option)
- **Overlays**: `modifications` overlay uses `{prev}:` pattern, not `{final, prev}:`
- **Dev shell tools**: `statix`, `deadnix` only available inside `nix develop`
- **Automated package updates**: Packages are automatically updated weekly via Gitea Actions using `nix-update`. Review PRs from the automation before merging. For urgent updates, manually run the workflow or update manually.
## Task Management
**td** is an optional task-tracking package. See `docs/packages/td.md` for details.
## Agent System Architecture
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.
### How it works
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.
### Key files in this repo
- `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)

View File

@@ -13,7 +13,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
### Removed
- Dead overlay entries for non-existent flake inputs
- Legacy `mkOpencodeRules` alias and `lib.opencode-rules` backward-compat entry (use `mkCodingRules` / `lib.coding-rules`)
## [0.4.0] - 2026-04-15

View File

@@ -38,16 +38,24 @@ nix run git+https://code.m3ta.dev/m3tam3re/nixpkgs#zellij-ps
## Available Packages
See [📦 Packages](./docs/packages/) for the full index with descriptions.
Quick reference — build any package directly:
```bash
nix build git+https://code.m3ta.dev/m3tam3re/nixpkgs#<package-name>
nix run git+https://code.m3ta.dev/m3tam3re/nixpkgs#<package-name>
```
Notable packages: `sidecar`, `td`, `code2prompt`, `mem0`, `n8n`, `zellij-ps`.
| Package | Description |
| ------------------ | ------------------------------------- |
| `code2prompt` | Convert code to prompts |
| `hyprpaper-random` | Random wallpaper setter for Hyprpaper |
| `kestractl` | CLI for the Kestra workflow orchestration platform |
| `launch-webapp` | Launch web applications |
| `mem0` | AI memory assistant with vector storage |
| `msty-studio` | Msty Studio application |
| `n8n` | Free and source-available fair-code licensed workflow automation tool |
| `notesmd-cli` | Obsidian CLI (Community) - Interact with Obsidian in the terminal |
| `opencode-desktop` | OpenCode Desktop App with Wayland support (includes workaround for upstream issue #11755) |
| `pomodoro-timer` | Pomodoro timer utility |
| `rofi-project-opener` | Rofi-based project launcher |
| `sidecar` | Companion tool for CLI agents with diffs, file trees, and task management |
| `stt-ptt` | Push to Talk Speech to Text |
| `td` | Minimalist CLI for tracking tasks across AI coding sessions |
| `tuxedo-backlight` | Backlight control for Tuxedo laptops |
| `zellij-ps` | Project switcher for Zellij |
## Automated Package Updates

View File

@@ -20,16 +20,29 @@ Step-by-step guides for common tasks:
- [Getting Started](./guides/getting-started.md) - Initial setup and basic usage
- [Adding Packages](./guides/adding-packages.md) - How to add new packages
- [Adding Modules](./guides/adding-modules.md) - How to add new NixOS or Home Manager modules
- [Port Management](./guides/port-management.md) - Managing service ports across hosts
- [Using Modules](./guides/using-modules.md) - Using NixOS and Home Manager modules
- [Development Workflow](./guides/development-workflow.md) - Development and testing workflow
### 📦 Packages
- [Packages Index](./packages/) - All packages with descriptions
- [Adding Packages](../guides/adding-packages.md) - How to add new packages
- [Templates](../templates.md) - Boilerplate templates
Documentation for all custom packages:
- [code2prompt](./packages/code2prompt.md) - Convert code to prompts
- [hyprpaper-random](./packages/hyprpaper-random.md) - Random wallpaper setter for Hyprpaper
- [kestractl](./packages/kestractl.md) - CLI for the Kestra workflow orchestration platform
- [launch-webapp](./packages/launch-webapp.md) - Launch web applications
- [mem0](./packages/mem0.md) - AI memory assistant with vector storage
- [msty-studio](./packages/msty-studio.md) - Msty Studio application
- [n8n](./packages/n8n.md) - Free and source-available fair-code licensed workflow automation tool
- [notesmd-cli](./packages/notesmd-cli.md) - Obsidian CLI (Community) - Interact with Obsidian in the terminal
- [pomodoro-timer](./packages/pomodoro-timer.md) - Pomodoro timer utility
- [rofi-project-opener](./packages/rofi-project-opener.md) - Rofi-based project launcher with custom args
- [sidecar](./packages/sidecar.md) - Companion tool for CLI agents with diffs, file trees, and task management
- [stt-ptt](./packages/stt-ptt.md) - Push to Talk Speech to Text using Whisper
- [td](./packages/td.md) - Minimalist CLI for tracking tasks across AI coding sessions
- [tuxedo-backlight](./packages/tuxedo-backlight.md) - Backlight control for Tuxedo laptops
- [zellij-ps](./packages/zellij-ps.md) - Project switcher for Zellij
### ⚙️ Modules
@@ -55,7 +68,6 @@ Technical references and APIs:
- [Functions](./reference/functions.md) - Library functions documentation
- [Patterns](./reference/patterns.md) - Code patterns and anti-patterns
- [Templates](../templates.md) - Boilerplate for packages and modules
## Repository Structure

View File

@@ -1,261 +0,0 @@
# Adding Modules Guide
How to add new NixOS and Home Manager modules to m3ta-nixpkgs.
## Overview
Modules extend your system or user configuration with reusable, declarative options. m3ta-nixpkgs uses the standard NixOS module system with a `m3ta.*` namespace.
## Quick Start
Use a template for quick setup:
```bash
# NixOS module
nix flake init -t .#nixos-module my-module
# Home Manager module
nix flake init -t .#home-manager-module my-module
```
This copies the template into `templates/` — move it to the appropriate location and customize.
## Adding a NixOS Module
### 1. Create the Module File
Create `modules/nixos/<my-module>.nix`:
```nix
{config, lib, pkgs, ...}:
with lib; let
cfg = config.m3ta.myModule;
in {
options.m3ta.myModule = {
enable = mkEnableOption "my module description";
# Add custom options here
someOption = mkOption {
type = types.str;
default = "default-value";
description = "Description of this option";
};
};
config = mkIf cfg.enable {
# System configuration goes here
environment.systemPackages = [pkgs.some-package];
# Or systemd services
systemd.services.my-service = {
enable = true;
description = "My service";
wantedBy = ["multi-user.target"];
serviceConfig = {
ExecStart = "${pkgs.some-package}/bin/some-daemon";
};
};
};
}
```
### 2. Register in the Aggregator
Add to `modules/nixos/default.nix`:
```nix
{
imports = [
./ports.nix
./mem0.nix
./<my-module>.nix # ← add your module
];
}
```
### 3. Export from flake.nix
Add to the `nixosModules` output in `flake.nix` (optional, for direct import):
```nix
nixosModules = {
default = ./modules/nixos;
ports = ./modules/nixos/ports.nix;
mem0 = ./modules/nixos/mem0.nix;
my-module = ./modules/nixos/<my-module>.nix; # ← add this
};
```
## Adding a Home Manager Module
Home Manager modules are organized by category under `modules/home-manager/`.
### Categories
| Category | Purpose | Location |
|----------|---------|----------|
| `cli/` | Command-line tools and utilities | `modules/home-manager/cli/` |
| `coding/` | Development tools, editors, agents | `modules/home-manager/coding/` |
| Root | Cross-cutting concerns (e.g., ports) | `modules/home-manager/` |
### 1. Choose a Category
- **CLI tools** (zsh plugins, tmux config, etc.) → `cli/`
- **Development tools** (editor config, linters, etc.) → `coding/`
- **System-wide settings** (ports, environment) → root level
### 2. Create the Module File
Create `modules/home-manager/<category>/<my-module>.nix`:
```nix
{config, lib, pkgs, ...}:
with lib; let
cfg = config.m3ta.myModule;
in {
options.m3ta.myModule = {
enable = mkEnableOption "my user module description";
someOption = mkOption {
type = types.str;
default = "value";
description = "An option for this module";
};
};
config = mkIf cfg.enable {
home.packages = [pkgs.some-package];
# Or Home Manager-specific options
programs.zsh.enable = true;
};
}
```
### 3. Register in the Category Aggregator
For `cli/` modules, add to `modules/home-manager/cli/default.nix`:
```nix
{
imports = [
./rofi-project-opener.nix
./stt-ptt.nix
./zellij-ps.nix
./<my-module>.nix # ← add your module
];
}
```
For `coding/` modules, add to `modules/home-manager/coding/default.nix`:
```nix
{
imports = [
./editors.nix
./opencode.nix
./agents
./<my-module>.nix # ← add your module
];
}
```
### 4. Export from flake.nix
Add to `homeManagerModules` in `flake.nix`:
```nix
homeManagerModules = {
default = import ./modules/home-manager;
my-module = import ./modules/home-manager/<category>/<my-module>.nix; # ← add this
};
```
## Module Patterns
### Standard Enable Option
Always start with `mkEnableOption`:
```nix
options.m3ta.myModule = {
enable = mkEnableOption "my module";
};
```
### Conditional Configuration
Use `mkIf` for conditional config:
```nix
config = mkIf cfg.enable {
# Only applied when enabled
};
```
### Multiple Conditions
Use `mkMerge` when combining multiple conditional blocks:
```nix
config = mkMerge [
(mkIf cfg.feature1.enable { ... })
(mkIf cfg.feature2.enable { ... })
];
```
### Nested Namespaces
For logically grouped options, use nested namespaces:
```nix
options.m3ta.coding = {
myTool = {
enable = mkEnableOption "my coding tool";
# ...
};
};
```
Usage: `m3ta.coding.myTool.enable = true;`
### Shared Library Functions
For shared utilities (port helpers, etc.), import from `lib/`:
```nix
let
portsLib = import ../../lib/ports.nix {inherit lib;};
portHelpers = portsLib.mkPortHelpers { /* ... */ };
in {
# use portHelpers
}
```
## Documentation
Add documentation for your module:
1. Create `docs/modules/nixos/<my-module>.md` (NixOS) or `docs/modules/home-manager/<category>/<my-module>.md` (HM)
2. Follow the existing format in `docs/modules/`
3. Add it to the appropriate overview page's "Available Modules" list
4. Link it from `docs/guides/using-modules.md`
## Testing
```bash
# Validate the module loads correctly
nix flake check
# Test with a minimal configuration (NixOS)
nixos-rebuild dry-build -I nixpkgs=. --option experimental-features flakes
# Format before commit
nix fmt
```
## Related
- [Using Modules](./using-modules.md) - How to use existing modules
- [Port Management](./port-management.md) - Centralized port management
- [Development Workflow](./development-workflow.md) - Local development
- [Adding Packages](./adding-packages.md) - Adding packages (not modules)
- [Architecture](../ARCHITECTURE.md) - Repository structure

View File

@@ -0,0 +1,99 @@
# Pi Agent Isolation (two-repo setup)
This guide documents the split setup where:
- `m3ta-nixpkgs` provides reusable module logic.
- `nixos-config` consumes it on specific hosts.
## 1) In `m3ta-nixpkgs`
Use:
- Home Manager module: `coding.agents.pi`
- renders Pi config in user space (default path: `.pi/agent` => `~/.pi/agent`)
- NixOS module: `m3ta.pi-agent`
- dedicated user/group (default `pi-agent`)
- state directory (default `/var/lib/pi-agent`)
- hardened execution via transient `systemd-run`
- host-side wrapper command (default `pi`)
- per-user allowlists via `hostUsers.<name>.projectRoots`
- host config sync into isolated runtime (default source `.pi/agent`)
- managed settings/env merge into isolated runtime
## 2) In consumer repo (`nixos-config`)
### Home Manager side
Keep Pi config rendering enabled for your normal user:
```nix
coding.agents.pi = {
enable = true;
agentsInput = inputs.agents;
path = ".pi/agent";
};
```
### NixOS host side (example: `m3-kratos`)
Enable isolated wrapper execution:
```nix
m3ta.pi-agent = {
enable = true;
stateDir = "/var/lib/pi-agent";
hostUsers = {
m3tam3re = {
projectRoots = ["~/p" "~/work/private"];
# optional; defaults to wrapper.hostConfigPath
configPath = ".pi/agent";
};
};
settings = {
defaultProvider = "anthropic";
defaultModel = "anthropic/claude-sonnet-4";
quietStartup = true;
};
environment = {
PI_TELEMETRY = "0";
};
environmentFiles = [
"/run/secrets/pi-agent.env"
];
wrapper = {
enable = true;
commandName = "pi";
hideDirectBinary = true;
hostConfigPath = ".pi/agent";
};
};
```
## 3) Authorization model
The wrapper uses a tightly scoped sudo rule:
- authorized users may run only the privileged runner command
- with `NOPASSWD`
- no broad `NOPASSWD: ALL`
## 4) Merge behavior
At invocation time, isolated runtime files are built from:
1. Host user Pi config (synced from source path, e.g. `~/.pi/agent`)
2. Nix-managed settings/env (override host values)
3. Environment files (appended after managed env attrs)
This keeps user-authored Pi config available while allowing reproducible Nix overrides.
## 5) Migration notes
- If wrapper mode is canonical, remove direct `pi-coding-agent` from user package lists to reduce command-path ambiguity.
- Rebuild host config and test from an allowlisted project path.
- Validate `pi` process identity runs as `pi-agent`.

View File

@@ -157,6 +157,32 @@ m3ta.mem0 = {
**Documentation**: [mem0 Module](../modules/nixos/mem0.md)
#### `m3ta.pi-agent`
Isolated Pi execution with a dedicated system user (`pi-agent` by default),
a hardened runtime, and a host-side `pi` wrapper command.
```nix
m3ta.pi-agent = {
enable = true;
stateDir = "/var/lib/pi-agent";
hostUsers = {
m3tam3re = {
projectRoots = ["~/p" "~/work/private"];
configPath = ".pi/agent"; # optional
};
};
settings.defaultModel = "anthropic/claude-sonnet-4";
environment.PI_TELEMETRY = "0";
wrapper.commandName = "pi";
wrapper.hideDirectBinary = true;
};
```
**Documentation**: [Pi Agent Isolation Guide](./pi-agent-isolation.md)
### Home Manager Modules
#### `m3ta.ports`
@@ -255,6 +281,7 @@ Pi agent deployment from canonical TOML definitions.
coding.agents.pi = {
enable = true;
agentsInput = inputs.agents;
path = ".pi/agent"; # default; can be changed
};
```
@@ -662,7 +689,5 @@ nix eval .#nixosConfigurations.hostname.config.m3ta --apply builtins.attrNames
- [Port Management](./port-management.md) - Detailed port management guide
- [Adding Packages](./adding-packages.md) - How to add new packages
- [Adding Modules](./adding-modules.md) - How to add new NixOS or Home Manager modules
- [Templates](../templates.md) - Boilerplate for new packages and modules
- [Architecture](../ARCHITECTURE.md) - Understanding module structure
- [Contributing](../CONTRIBUTING.md) - Code style and guidelines

View File

@@ -1,53 +0,0 @@
# Packages
Documentation for packages in m3ta-nixpkgs. Each package directory may contain a `README.md` with detailed documentation.
## Index
Packages are organized in `pkgs/<name>/`. Add a `README.md` inside a package directory to document it here.
### Local Packages
These packages are built from source in `pkgs/<name>/`:
| Package | Description | Type | Location |
|---------|-------------|------|----------|
| `sidecar` | Companion tool for CLI agents with diffs, file trees, and task management | Go | `pkgs/sidecar/` |
| `td` | Minimalist CLI for tracking tasks across AI coding sessions | Go | `pkgs/td/` |
| `code2prompt` | Convert code to prompts | Go | `pkgs/code2prompt/` |
| `eigent` | Eigenvalue tool | Python | `pkgs/eigent/` |
| `hyprpaper-random` | Random wallpaper setter for Hyprpaper | Shell | `pkgs/hyprpaper-random/` |
| `kestractl` | CLI for Kestra workflow orchestration | Go | `pkgs/kestractl/` |
| `launch-webapp` | Launch web applications | Shell | `pkgs/launch-webapp/` |
| `mem0` | AI memory assistant with vector storage | Python | `pkgs/mem0/` |
| `msty-studio` | Msty Studio application | Python | `pkgs/msty-studio/` |
| `n8n` | Workflow automation tool | Node.js | `pkgs/n8n/` |
| `openshell` | AI shell assistant | Go | `pkgs/openshell/` |
| `pomodoro-timer` | Pomodoro timer utility | Shell | `pkgs/pomodoro-timer/` |
| `rofi-project-opener` | Rofi-based project launcher | Shell | `pkgs/rofi-project-opener/` |
| `stt-ptt` | Push to Talk Speech to Text | Python | `pkgs/stt-ptt/` |
| `tuxedo-backlight` | Backlight control for Tuxedo laptops | C | `pkgs/tuxedo-backlight/` |
| `vibetyper` | Typing practice tool | Python | `pkgs/vibetyper/` |
| `zellij-ps` | Project switcher for Zellij | Rust | `pkgs/zellij-ps/` |
### Pass-Through Packages
These packages are imported directly from flake inputs with minor modifications:
| Package | Source | Modification | Location |
|---------|--------|-------------|----------|
| `opencode-desktop` | `inputs.opencode` | Tauri desktop wrapper + Wayland fix | `pkgs/opencode-desktop/` |
## Adding Package Documentation
To document a package in detail, add a `README.md` inside the package directory (e.g., `pkgs/sidecar/README.md`). This guide indexes all packages and provides a quick overview.
## Automated Updates
Packages are automatically updated weekly by the Gitea Actions `nix-update` workflow. See the main README for details.
## Related
- [Adding Packages](../guides/adding-packages.md) - How to add new packages
- [Architecture](../ARCHITECTURE.md) - Repository structure
- [Quick Start](../QUICKSTART.md) - Getting started

View File

@@ -0,0 +1,117 @@
# 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

@@ -153,6 +153,33 @@ allServices = portHelpers.listServices;
# Returns: ["nginx" "grafana" "prometheus" "homepage"]
```
### `getDefaultPort`
Simple helper to get a port without host override.
#### Signature
```nix
getDefaultPort :: portsConfig -> string -> int-or-null
```
#### Arguments
1. `portsConfig` - Same structure as `mkPortHelpers`
2. `service` - The service name (string)
#### Returns
Port number (int) or `null` if service not found.
#### Usage
```nix
services.my-service = {
port = m3taLib.ports.getDefaultPort myPorts "my-service";
};
```
## Using Library Functions
### Importing
@@ -235,7 +262,7 @@ in {
| `getPort` | Get port with optional host override | `int or null` |
| `getHostPorts` | Get all ports for host | `attrs` |
| `listServices` | List all service names | `[string]` |
| `getDefaultPort` | Get default port only | `int or null` |
## Related

View File

@@ -1,162 +0,0 @@
# Templates
Boilerplate templates for quickly adding new packages or modules to m3ta-nixpkgs.
## Available Templates
| Template | Command | Creates |
|---------|---------|---------|
| Package | `nix flake init -t .#package` | `templates/package/` |
| NixOS Module | `nix flake init -t .#nixos-module` | `templates/nixos-module/` |
| Home Manager Module | `nix flake init -t .#home-manager-module` | `templates/home-manager-module/` |
## Using Templates
### 1. List Available Templates
```bash
nix flake show --templates .
```
### 2. Initialize from a Template
```bash
# Package
nix flake init -t .#package
# NixOS Module
nix flake init -t .#nixos-module
# Home Manager Module
nix flake init -t .#home-manager-module
```
Note: `nix flake init` copies the template contents into the current directory. Use a subdirectory name:
```bash
mkdir new-package && cd new-package
nix flake init -t ..#package
```
## Package Template
Creates a complete package structure:
```
templates/package/
├── default.nix # Package definition with comments
```
### Fields to Fill In
| Field | Location | Notes |
|-------|----------|-------|
| `pname` | `default.nix` | Package name (kebab-case) |
| `version` | `default.nix` | Semantic version |
| `src` | `default.nix` | Fetcher (GitHub, URL, Git, etc.) |
| `hash` | `default.nix` | Use `lib.fakeHash`, build to get real hash |
| `meta.description` | `default.nix` | Short one-line description |
| `meta.homepage` | `default.nix` | Project URL |
| `meta.license` | `default.nix` | Use `lib.licenses.*` |
| `meta.platforms` | `default.nix` | Usually `platforms.linux` |
| `meta.mainProgram` | `default.nix` | Main binary name |
### Common Build Systems
```nix
# Rust (recommended)
rustPlatform.buildRustPackage rec { ... }
# Python
python3.pkgs.buildPythonPackage rec { ... }
# Node.js
pkg-config, nodejs, npm2nix, or pnpm + prisma
# Shell script
writeShellScriptBin "name" ''echo hello''
# Go
go mdbook build
# Generic C/Make
stdenv.mkDerivation { ... }
```
See [Adding Packages](./guides/adding-packages.md) for detailed instructions.
## NixOS Module Template
Creates a complete NixOS module:
```
templates/nixos-module/
├── default.nix # Module with options
└── README.md # Module documentation
```
### Fields to Fill In
| Field | Location | Notes |
|-------|----------|-------|
| Module name | `default.nix` | File name matches `m3ta.<name>` |
| Options | `default.nix` | Add under `options.m3ta.<name>` |
| Config | `default.nix` | Add under `config.m3ta.<name>` |
| Description | `README.md` | What the module does |
### After Creating
1. Add to `modules/nixos/default.nix` imports
2. Optionally export from `flake.nix` `nixosModules`
3. Add documentation to `docs/modules/nixos/`
4. Run `nix flake check`
## Home Manager Module Template
Creates a complete Home Manager module:
```
templates/home-manager-module/
├── default.nix # Module with options
└── README.md # Module documentation
```
### Fields to Fill In
| Field | Location | Notes |
|-------|----------|-------|
| Category | Directory | Choose `cli/` or `coding/` |
| Options | `default.nix` | Add under `options.m3ta.<name>` |
| Config | `default.nix` | Add under `config.m3ta.<name>` |
| Description | `README.md` | What the module does |
### After Creating
1. Add to the appropriate category aggregator (`cli/default.nix` or `coding/default.nix`)
2. Optionally export from `flake.nix` `homeManagerModules`
3. Add documentation to `docs/modules/home-manager/`
4. Run `nix flake check`
## Template Variables
Templates use Nix attribute references. After copying, search for these placeholders:
| Placeholder | Replace With |
|-------------|--------------|
| `package-name` | Your package name (kebab-case) |
| `owner-name` / `repo-name` | GitHub owner and repo |
| `0.1.0` | Initial version |
| `lib.fakeHash` | Real hash after first build |
| `lib.licenses.mit` | Appropriate license |
| `A short description` | One-line description |
## Automated Updates
Packages created from templates are automatically updated weekly by the Gitea Actions workflow. See the main README for details on the `nix-update` automation.
## Related
- [Adding Packages](./guides/adding-packages.md) - Detailed package guide
- [Adding Modules](./guides/adding-modules.md) - Detailed module guide
- [Development Workflow](./guides/development-workflow.md) - Local development
- [Architecture](./ARCHITECTURE.md) - Repository structure

57
flake.lock generated
View File

@@ -1,21 +1,5 @@
{
"nodes": {
"agents": {
"flake": false,
"locked": {
"lastModified": 1777399938,
"narHash": "sha256-xXPqUQezDdDtF8MbpZnwD1HkybOYwF92evx8rJ6OXCU=",
"ref": "refs/heads/master",
"rev": "9a91f1ee0cf011a7eaf1f16a9e17610b0457e055",
"revCount": 85,
"type": "git",
"url": "https://code.m3ta.dev/m3tam3re/AGENTS"
},
"original": {
"type": "git",
"url": "https://code.m3ta.dev/m3tam3re/AGENTS"
}
},
"basecamp": {
"inputs": {
"nixpkgs": [
@@ -39,11 +23,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1777268161,
"narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=",
"lastModified": 1775423009,
"narHash": "sha256-vPKLpjhIVWdDrfiUM8atW6YkIggCEKdSAlJPzzhkQlw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76",
"rev": "68d8aa3d661f0e6bd5862291b5bb263b2a6595c9",
"type": "github"
},
"original": {
@@ -55,11 +39,11 @@
},
"nixpkgs-master": {
"locked": {
"lastModified": 1777643636,
"narHash": "sha256-7vvm5Ia8o3g7YNErFcDsbCx+Pk8HbnA+ZYuA5Zga7hY=",
"lastModified": 1775657231,
"narHash": "sha256-DP8FfybiZPp5WLB9eIk0TC2mdvuYzxLGgrBODDrwPEI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "da2366fac507ce7bd31852e7351e55b951656999",
"rev": "4e03baaa39b7746eac5704d623461422131cd03d",
"type": "github"
},
"original": {
@@ -69,6 +53,27 @@
"type": "github"
}
},
"opencode": {
"inputs": {
"nixpkgs": [
"nixpkgs-master"
]
},
"locked": {
"lastModified": 1775782812,
"narHash": "sha256-m+Ue7FWiTjKMAn1QefAwOMfOb2Vybk0mJPV9zcbkOmE=",
"owner": "anomalyco",
"repo": "opencode",
"rev": "877be7e8e04142cd8fbebcb5e6c4b9617bf28cce",
"type": "github"
},
"original": {
"owner": "anomalyco",
"ref": "v1.4.3",
"repo": "opencode",
"type": "github"
}
},
"openspec": {
"inputs": {
"nixpkgs": [
@@ -76,11 +81,11 @@
]
},
"locked": {
"lastModified": 1777556999,
"narHash": "sha256-HfFlRwR8IMjudRttN4T8L3DJKnNlpWfeNzQPly/HaRY=",
"lastModified": 1775372219,
"narHash": "sha256-MJakKC026Sarz7nMmiFrfONWc4xgaw8ApV0Hhp4ebhM=",
"owner": "Fission-AI",
"repo": "OpenSpec",
"rev": "347f0277e3be3549cd85cdea364fbd7710f1922b",
"rev": "64d476f8b924bb9b74b896ea0aa784970e37da69",
"type": "github"
},
"original": {
@@ -91,10 +96,10 @@
},
"root": {
"inputs": {
"agents": "agents",
"basecamp": "basecamp",
"nixpkgs": "nixpkgs",
"nixpkgs-master": "nixpkgs-master",
"opencode": "opencode",
"openspec": "openspec"
}
}

View File

@@ -10,17 +10,17 @@
inputs.nixpkgs.follows = "nixpkgs";
};
# opencode needs newer bun from master
opencode = {
url = "github:anomalyco/opencode/v1.4.3";
inputs.nixpkgs.follows = "nixpkgs-master";
};
# openspec - spec-driven development for AI coding assistants
openspec = {
url = "github:Fission-AI/OpenSpec";
inputs.nixpkgs.follows = "nixpkgs";
};
# Agent definitions and coding rules
agents = {
url = "git+https://code.m3ta.dev/m3tam3re/AGENTS";
flake = false;
};
};
outputs = {
@@ -65,6 +65,7 @@
# Individual modules for selective imports
ports = ./modules/nixos/ports.nix;
mem0 = ./modules/nixos/mem0.nix;
pi-agent = ./modules/nixos/pi-agent.nix;
};
# Home Manager modules - for user-level configuration
@@ -77,18 +78,18 @@
};
# Library functions - helper utilities for your configuration
lib = forAllSystems (system: import ./lib {lib = nixpkgs.lib;});
lib = forAllSystems (system: let
pkgs = pkgsFor system;
in
import ./lib {lib = pkgs.lib;});
# Development shells for various programming environments
# Usage: nix develop .#<shell-name>
# Available shells: default, python, devops, coding
# Available shells: default, python, devops, opencode
devShells = forAllSystems (system: let
pkgs = pkgsFor system;
in
import ./shells {
inherit pkgs inputs;
agents = inputs.agents;
});
import ./shells {inherit pkgs inputs;});
# Formatter for 'nix fmt'
formatter = forAllSystems (system: (pkgsFor system).alejandra);
@@ -104,15 +105,6 @@
${pkgs.alejandra}/bin/alejandra --check ${./.}
touch $out
'';
# Lib unit tests
lib-agents = import ./tests/lib/agents-test.nix {
inherit pkgs;
lib = pkgs.lib;
};
lib-coding-rules = import ./tests/lib/coding-rules-test.nix {
inherit pkgs;
lib = pkgs.lib;
};
});
# Templates for creating new packages/modules

View File

@@ -26,27 +26,6 @@
pattern = lib.concatStringsSep ":" (lib.init parts);
in {inherit pattern action;};
# ── Shared renderer primitives ──────────────────────────────────
# Render agent files from canonical definitions into a directory.
# Each agent gets a "<name>.md" file containing mkContent name agent.
#
# Args:
# pkgs — Nixpkgs package set with linkFarm
# canonical — Attribute set of agent definitions (keyed by slug)
# mkContent — Function: name: agent → string (file content)
# name — Derivation name (e.g. "opencode-agents")
#
# Returns:
# A store path containing all agent *.md files.
renderAgentFiles = pkgs: canonical: mkContent: name:
pkgs.linkFarm name (
lib.mapAttrsToList (n: a: {
name = "${n}.md";
path = pkgs.writeText "${n}.md" (mkContent n a);
})
canonical
);
agentsLib = {
# ── loadCanonical ─────────────────────────────────────────────
#
@@ -108,8 +87,20 @@
mkAgentContent = name: agent:
(mkFrontmatter name agent) + agent.systemPrompt;
mkAgentFile = name: agent:
pkgs.writeText "${name}.md" (mkAgentContent name agent);
agentFiles = lib.mapAttrs mkAgentFile canonical;
copyCommands = lib.concatStringsSep "\n" (
lib.mapAttrsToList (name: file: "cp ${file} $out/${name}.md") agentFiles
);
in
renderAgentFiles pkgs canonical mkAgentContent "opencode-agents";
pkgs.runCommand "opencode-agents" {} ''
mkdir -p $out
${copyCommands}
'';
# ── Claude Code renderer ──────────────────────────────────────
#
@@ -188,7 +179,10 @@
mkClaudeAgentContent = name: agent:
(mkClaudeFrontmatter name agent) + agent.systemPrompt;
agentFiles = renderAgentFiles pkgs canonical mkClaudeAgentContent "claude-code-agent-files";
mkClaudeAgentFile = name: agent:
pkgs.writeText "${name}.md" (mkClaudeAgentContent name agent);
agentFiles = lib.mapAttrs mkClaudeAgentFile canonical;
# Build settings.json with permission rules aggregated from all agents.
allAllows = lib.flatten (lib.mapAttrsToList (_: agent: renderPermAllow (agent.permissions or {})) canonical);
@@ -202,10 +196,14 @@
};
settingsFile = pkgs.writeText "claude-settings.json" settingsJson;
copyAgentCommands = lib.concatStringsSep "\n" (
lib.mapAttrsToList (name: file: "cp ${file} $out/.claude/agents/${name}.md") agentFiles
);
in
pkgs.runCommand "claude-code-agents" {} ''
mkdir -p $out/.claude/agents
cp -r ${agentFiles}/* $out/.claude/agents/
${copyAgentCommands}
cp ${settingsFile} $out/.claude/settings.json
'';
@@ -225,10 +223,7 @@
canonical,
modelOverrides ? {},
primaryAgent ? null,
codingRules ? null,
}: let
# Import coding-rules lib for concatRulesMd when codingRules is provided
codingRulesLib = import ./coding-rules.nix {inherit lib;};
# Find the primary agent (there should be exactly one).
primaryAgents = lib.filterAttrs (_: a: a.mode == "primary") canonical;
primaryNames = lib.attrNames primaryAgents;
@@ -297,7 +292,10 @@
mkPiAgentContent = name: agent:
(mkPiFrontmatter name agent) + agent.systemPrompt;
piAgentFiles = renderAgentFiles pkgs canonical mkPiAgentContent "pi-agent-files";
mkPiAgentFile = name: agent:
pkgs.writeText "${name}.md" (mkPiAgentContent name agent);
piAgentFiles = lib.mapAttrs mkPiAgentFile canonical;
# ── Build AGENTS.md content ───────────────────────────────────
primaryDn = primary.display_name or primaryName;
@@ -308,19 +306,6 @@
"- **" + dn + "**: " + agent.description;
in
lib.mapAttrsToList mkEntry subagents;
# ── Coding rules section (optional) ────────────────────────
# When codingRules is provided, append selected rules to AGENTS.md.
# codingRules attrset: { agents, languages, concerns, frameworks }
codingRulesSection =
if codingRules != null
then let
section = codingRulesLib.mkRulesMdSection codingRules;
in
if section != ""
then "\n" + section
else ""
else "";
agentsMd =
"# Agent Instructions\n"
+ "\n"
@@ -335,17 +320,20 @@
if subagents == {}
then ""
else "## Available Specialists\n\n" + lib.concatStringsSep "\n" specialistEntries + "\n"
)
+ codingRulesSection;
);
agentsMdFile = pkgs.writeText "AGENTS.md" agentsMd;
systemMdFile = pkgs.writeText "SYSTEM.md" primary.systemPrompt;
copyAgentCommands = lib.concatStringsSep "\n" (
lib.mapAttrsToList (name: file: "cp ${file} $out/agents/${name}.md") piAgentFiles
);
in
pkgs.runCommand "pi-agents" {} ''
mkdir -p $out/agents
cp ${agentsMdFile} $out/AGENTS.md
cp ${systemMdFile} $out/SYSTEM.md
cp -r ${piAgentFiles}/* $out/agents/
${copyAgentCommands}
'';
# ── renderForTool dispatcher ──────────────────────────────────
@@ -358,7 +346,6 @@
agentsInput,
tool,
modelOverrides ? {},
codingRules ? null,
}: let
canonical = agentsInput.lib.loadAgents;
in
@@ -375,7 +362,7 @@
else if tool == "pi"
then
agentsLib.renderForPi {
inherit pkgs canonical modelOverrides codingRules;
inherit pkgs canonical modelOverrides;
}
else throw "lib.agents.renderForTool: unknown tool '${tool}'. Must be opencode, claude-code, or pi.";
@@ -399,10 +386,9 @@
agentsInput,
tool,
modelOverrides ? {},
codingRules ? null,
}: let
rendered = agentsLib.renderForTool {
inherit pkgs agentsInput tool modelOverrides codingRules;
inherit pkgs agentsInput tool modelOverrides;
};
in
if tool == "opencode"

View File

@@ -1,4 +1,4 @@
# Coding rules management utilities
# 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
@@ -27,7 +27,6 @@
# The shellHook creates:
# - A `.opencode-rules/` symlink pointing to the AGENTS repository rules directory
# - A `coding-rules.json` file with a $schema reference and instructions list
# - (Optional) Appends coding rules to `AGENTS.md` for Pi agent discovery
#
# The instructions list contains paths relative to the project root, all prefixed
# with `.opencode-rules/`, making them portable across different project locations.
@@ -44,9 +43,6 @@
# (e.g., [ "react" "fastapi" "django" ])
# extraInstructions: Optional list of additional instruction paths
# (for custom rules outside standard locations)
# forPi: Whether to also append rules to AGENTS.md for Pi agent (default: true)
# Pi discovers AGENTS.md files by walking parent dirs + cwd and concatenates them.
# When enabled, a delimited block is appended to (or created in) AGENTS.md.
#
# Returns:
# An attribute set containing:
@@ -87,7 +83,6 @@
frameworks ? [],
extraInstructions ? [],
rulesDir ? ".opencode-rules",
forPi ? false,
}: let
# Build instructions list by mapping concerns, languages, frameworks to their file paths
# All paths are relative to project root via the rulesDir symlink
@@ -102,46 +97,11 @@
"$schema" = "https://opencode.ai/config.json";
inherit instructions;
};
# Pi rules content (concatenated markdown) — only computed when forPi is true
piRulesSection =
if forPi
then mkRulesMdSection {inherit agents languages concerns frameworks;}
else "";
# Bash snippet to append rules to AGENTS.md for Pi discovery.
# Uses HTML comment markers for idempotent updates:
# - Removes any existing CODING-RULES block
# - Appends the new block
# - Creates AGENTS.md if it doesn't exist
# Note: Uses plain if-then-else instead of lib.optionalString to avoid
# forcing the `lib` argument (which may come from import <nixpkgs/lib>)
# when forPi is false.
piShellHook =
if forPi && piRulesSection != ""
then ''
# Pi agent: append coding rules to AGENTS.md
if [ -f AGENTS.md ]; then
# Remove existing coding-rules block (if any)
sed -i '/<!-- CODING-RULES:START -->/,/<!-- CODING-RULES:END -->/d' AGENTS.md
# Append new coding-rules block
cat >> AGENTS.md <<'PIRULES_EOF'
${piRulesSection}
PIRULES_EOF
else
# Create AGENTS.md with just the coding rules
cat > AGENTS.md <<'PIRULES_EOF'
${piRulesSection}
PIRULES_EOF
fi
''
else "";
in {
inherit instructions;
# Shell hook to set up rules in the project
# Creates a symlink to the AGENTS rules directory and generates coding-rules.json
# Optionally appends rules to AGENTS.md for Pi agent discovery
shellHook = ''
# Create/update symlink to AGENTS rules directory
ln -sfn ${agents}/rules ${rulesDir}
@@ -150,84 +110,11 @@
cat > coding-rules.json <<'RULES_EOF'
${builtins.toJSON rulesConfig}
RULES_EOF
${piShellHook}
'';
};
# Concatenate selected rule files from the AGENTS repository into a single
# markdown string. Used by Pi (append to AGENTS.md) and could be used by
# other tools that don't support an instructions list.
#
# Args:
# agents: Path to the AGENTS repository (non-flake input)
# languages: Optional list of language-specific rules to include
# 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
#
# Returns: A single concatenated markdown string with all selected rules.
#
# Example:
# concatRulesMd {
# agents = inputs.agents;
# languages = [ "python" ];
# concerns = [ "coding-style" ];
# }
# # Returns: "\n# Coding Style\n\n...python rules...\n"
concatRulesMd = {
agents,
languages ? [],
concerns ? [
"coding-style"
"naming"
"documentation"
"testing"
"git-workflow"
"project-structure"
],
frameworks ? [],
}: let
rulePaths =
(map (c: {
kind = "concerns";
name = c;
})
concerns)
++ (map (l: {
kind = "languages";
name = l;
})
languages)
++ (map (f: {
kind = "frameworks";
name = f;
})
frameworks);
readRule = rule: builtins.readFile "${agents}/rules/${rule.kind}/${rule.name}.md";
ruleContents = map readRule rulePaths;
in
lib.concatStringsSep "\n\n" ruleContents;
# Build a coding rules section suitable for appending to AGENTS.md.
# Wraps concatRulesMd output with a header and HTML comment markers
# for idempotent updates in project-level shellHooks.
#
# Args: Same as concatRulesMd
#
# Returns: A markdown string with start/end markers and a header.
mkRulesMdSection = args: let
content = concatRulesMd args;
in
if builtins.stringLength content == 0
then ""
else ''
<!-- CODING-RULES:START -->
# Coding Rules
${content}
<!-- CODING-RULES:END -->
'';
# Backward-compat alias
mkOpencodeRules = mkCodingRules;
in {
inherit mkCodingRules concatRulesMd mkRulesMdSection;
inherit mkCodingRules mkOpencodeRules;
}

View File

@@ -7,9 +7,12 @@
# Port management utilities
ports = import ./ports.nix {inherit lib;};
# Coding rules injection utilities
# Coding rules injection utilities (renamed from opencode-rules)
coding-rules = import ./coding-rules.nix {inherit lib;};
# Backward-compat alias: opencode-rules → coding-rules
opencode-rules = import ./coding-rules.nix {inherit lib;};
# Agent configuration management utilities
agents = import ./agents.nix {inherit lib;};
}

View File

@@ -95,4 +95,19 @@
# List of service names (strings)
listServices = lib.attrNames ports;
};
# Simple helper to get a port without host override
# Useful when you don't need host-specific ports
#
# Args:
# portsConfig: Same structure as mkPortHelpers
# service: The service name (string)
#
# Returns:
# Port number (int) or null if service not found
#
# Example:
# getDefaultPort myPorts "nginx" # Returns default port only
getDefaultPort = portsConfig: service:
portsConfig.ports.${service} or null;
}

View File

@@ -119,23 +119,26 @@ coding.agents.claude-code = {
enable = true;
agentsInput = inputs.agents;
modelOverrides = {};
externalSkills = [{ src = inputs.skills-anthropic; }];
};
```
**Options:** `enable`, `agentsInput`, `modelOverrides`
**Options:** `enable`, `agentsInput`, `modelOverrides`, `externalSkills`
### Pi (`coding.agents.pi`)
Renders `AGENTS.md` + `SYSTEM.md` to `~/.pi/agent/`:
Renders `AGENTS.md` + `SYSTEM.md` to `~/.pi/agent/` by default:
```nix
coding.agents.pi = {
enable = true;
agentsInput = inputs.agents;
path = ".pi/agent"; # default, relative to $HOME
externalSkills = [{ src = inputs.skills-anthropic; }];
};
```
**Options:** `enable`, `agentsInput`
**Options:** `enable`, `path`, `agentsInput`, `modelOverrides`, `externalSkills`, `primaryAgent`, `mcpServers`, `settings`
### Project-level usage
@@ -163,7 +166,7 @@ The agent system was migrated from embedded `agents.json` to file-based canonica
| `coding.opencode.externalSkills` | `coding.agents.opencode.externalSkills` |
| Agents embedded in `config.json` | File-based `~/.config/opencode/agents/*.md` |
| Model hardcoded in `agents.json` | Per-machine `modelOverrides` |
| `mkOpencodeRules` | `mkCodingRules` |
| `mkOpencodeRules` | `mkCodingRules` (old name still works) |
### Migration steps

View File

@@ -3,27 +3,76 @@
lib,
pkgs,
...
}: {
imports = [
./shared/default.nix
];
options.coding.agents.claude-code = let
shared = import ./shared/shared-options.nix {inherit lib;};
}:
with lib; let
cfg = config.coding.agents.claude-code;
mcpCfg = config.programs.mcp or null;
in
with lib; {
in {
options.coding.agents.claude-code = {
enable = mkEnableOption "Claude Code agent management via canonical agent.toml definitions";
agentsInput = shared.mkAgentsInputOption ''
agentsInput = mkOption {
type = types.nullOr types.anything;
default = null;
description = ''
The `agents` flake input (your personal AGENTS repo).
When set, agents are rendered from canonical agent.toml files
and symlinked to ~/.claude/agents/.
'';
};
modelOverrides = shared.mkModelOverridesOption;
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";
}
'';
};
externalSkills = shared.externalSkillsOption;
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; }
]
'';
};
mcpServers = mkOption {
type = types.attrsOf types.anything;
@@ -40,12 +89,9 @@
};
};
config = with lib; let
shared = import ./shared/shared-options.nix {inherit lib;};
cfg = config.coding.agents.claude-code;
config = mkIf cfg.enable (let
agentsLib = (import ../../../../lib {inherit lib;}).agents;
in
mkIf cfg.enable (let
# Rendered agents + permissions (only if agentsInput is set)
rendered = mkIf (cfg.agentsInput != null) (
agentsLib.renderForClaudeCode {
@@ -82,7 +128,13 @@
source = cfg.agentsInput.lib.mkOpencodeSkills {
inherit pkgs;
customSkills = "${cfg.agentsInput}/skills";
externalSkills = shared.mapExternalSkills cfg.externalSkills;
externalSkills =
map (
entry:
{inherit (entry) src skillsDir;}
// optionalAttrs (entry.selectSkills != null) {inherit (entry) selectSkills;}
)
cfg.externalSkills;
};
};

View File

@@ -1,39 +1,10 @@
# Per-tool agent sub-modules
# Each module handles rendering canonical agent.toml definitions
# for a specific AI coding tool.
#
# Also provides the shared coding.agents.skills submodule that writes
# ~/.agents/skills — the central skills directory used by Pi, OpenCode, etc.
{
config,
lib,
pkgs,
...
}: let
shared = import ./shared/shared-options.nix {inherit lib;};
cfg = config.coding.agents.skills;
mkIf = lib.mkIf;
in {
imports = [
./opencode.nix
./claude-code.nix
./pi.nix
];
options.coding.agents.skills = {
agentsInput = shared.mkAgentsInputOption ''
The `agents` flake input (your personal AGENTS repo).
When set, skills are symlinked to ~/.agents/skills.
'';
externalSkills = shared.externalSkillsOption;
};
config = mkIf (cfg.agentsInput != null) {
home.file.".agents/skills".source = cfg.agentsInput.lib.mkOpencodeSkills {
inherit pkgs;
customSkills = "${cfg.agentsInput}/skills";
externalSkills = shared.mapExternalSkills cfg.externalSkills;
};
};
}

View File

@@ -3,43 +3,102 @@
lib,
pkgs,
...
}: {
imports = [
./shared/default.nix
];
options.coding.agents.opencode = let
shared = import ./shared/shared-options.nix {inherit lib;};
in
with lib; {
}:
with lib; let
cfg = config.coding.agents.opencode;
in {
options.coding.agents.opencode = {
enable = mkEnableOption "OpenCode agent management via canonical agent.toml definitions";
agentsInput = shared.mkAgentsInputOption ''
agentsInput = mkOption {
type = types.nullOr types.anything;
default = null;
description = ''
The `agents` flake input (your personal AGENTS repo).
When set, agents are rendered from canonical agent.toml files
and symlinked to ~/.config/opencode/agents/.
'';
modelOverrides = shared.mkModelOverridesOption;
};
config = with lib; let
shared = import ./shared/shared-options.nix {inherit lib;};
cfg = config.coding.agents.opencode;
in
mkIf cfg.enable {
modelOverrides = mkOption {
type = types.attrsOf types.str;
default = {};
description = ''
Per-agent model overrides. Maps agent slug to model string.
Example: { chiron = "anthropic/claude-sonnet-4"; }
'';
example = literalExpression ''
{
chiron = "anthropic/claude-sonnet-4";
"chiron-forge" = "anthropic/claude-sonnet-4";
}
'';
};
externalSkills = mkOption {
type = types.listOf (types.submodule {
options = {
src = mkOption {
type = types.anything;
description = "Flake input pointing to a skills repository root.";
};
skillsDir = mkOption {
type = types.str;
default = "skills";
description = ''
Subdirectory inside src that contains skill folders.
'';
};
selectSkills = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
description = ''
List of skill names to cherry-pick from this source.
null means include every skill found in skillsDir.
'';
};
};
});
default = [];
description = ''
External skill sources passed to mkOpencodeSkills.
Each entry maps directly to an element of the externalSkills
list accepted by the AGENTS flake's lib.mkOpencodeSkills.
'';
example = literalExpression ''
[
{ src = inputs.skills-anthropic; selectSkills = [ "claude-api" ]; }
{ src = inputs.skills-vercel; }
]
'';
};
};
config = mkIf cfg.enable {
# Rendered agent files symlinked to ~/.config/opencode/agents/
xdg.configFile."opencode/agents" = let
agentsLib = (import ../../../../lib {inherit lib;}).agents;
in
mkIf (cfg.agentsInput != null) {
source = agentsLib.renderForOpencode {
xdg.configFile."opencode/agents" = mkIf (cfg.agentsInput != null) {
source = (import ../../../../lib {inherit lib;}).agents.renderForOpencode {
inherit pkgs;
canonical = cfg.agentsInput.lib.loadAgents;
modelOverrides = cfg.modelOverrides;
};
};
# Skills (merged from personal AGENTS repo + optional external skills)
xdg.configFile."opencode/skills" = mkIf (cfg.agentsInput != null) {
source = cfg.agentsInput.lib.mkOpencodeSkills {
inherit pkgs;
customSkills = "${cfg.agentsInput}/skills";
externalSkills =
map (
entry:
{inherit (entry) src skillsDir;}
// optionalAttrs (entry.selectSkills != null) {inherit (entry) selectSkills;}
)
cfg.externalSkills;
};
};
# Static config dirs from AGENTS repo
xdg.configFile."opencode/context" = mkIf (cfg.agentsInput != null) {
source = "${cfg.agentsInput}/context";

View File

@@ -3,18 +3,26 @@
lib,
pkgs,
...
}: {
imports = [
./shared/default.nix
];
options.coding.agents.pi = let
shared = import ./shared/shared-options.nix {inherit lib;};
}:
with lib; let
cfg = config.coding.agents.pi;
mcpCfg = config.programs.mcp or null;
in
with lib; {
in {
options.coding.agents.pi = {
enable = mkEnableOption "Pi agent management via canonical agent.toml definitions";
path = mkOption {
type = types.str;
default = ".pi/agent";
description = ''
Relative path (inside the Home Manager user's home) where Pi agent
config should be materialized.
Defaults to `.pi/agent`, i.e. `~/.pi/agent`.
'';
example = ".config/pi/agent";
};
mcpServers = mkOption {
type = types.attrsOf types.anything;
default =
@@ -24,18 +32,68 @@
defaultText = literalExpression "config.programs.mcp.servers";
description = ''
MCP server configurations for Pi (pi-mcp-adapter).
Written to ~/.pi/agent/mcp.json.
Written to `${cfg.path}/mcp.json`.
Automatically inherits from config.programs.mcp.servers.
'';
};
agentsInput = shared.mkAgentsInputOption ''
agentsInput = mkOption {
type = types.nullOr types.anything;
default = null;
description = ''
The `agents` flake input (your personal AGENTS repo).
When set, the primary agent's system prompt is rendered as SYSTEM.md,
all agents are listed in AGENTS.md, and subagent .md files are deployed.
'';
};
modelOverrides = shared.mkModelOverridesOption;
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"; }
'';
};
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; }
]
'';
};
primaryAgent = mkOption {
type = types.nullOr types.str;
@@ -46,60 +104,6 @@
'';
};
codingRules = mkOption {
type = types.nullOr (types.submodule {
options = {
languages = mkOption {
type = types.listOf types.str;
default = [];
description = ''
Language-specific coding rules to include
(e.g. [ "python" "typescript" "nix" ]).
Rule files are read from the AGENTS repo's rules/languages/ directory.
'';
};
concerns = mkOption {
type = types.listOf types.str;
default = [
"coding-style"
"naming"
"documentation"
"testing"
"git-workflow"
"project-structure"
];
description = ''
Concern rules to include from the AGENTS repo's rules/concerns/ directory.
'';
};
frameworks = mkOption {
type = types.listOf types.str;
default = [];
description = ''
Framework-specific coding rules to include
(e.g. [ "react" "fastapi" ]).
Rule files are read from the AGENTS repo's rules/frameworks/ directory.
'';
};
};
});
default = null;
description = ''
Coding rules to inject into ~/.pi/agent/AGENTS.md.
Rules are read from the AGENTS repository and appended as markdown sections.
Requires agentsInput to be set.
'';
example = literalExpression ''
{
languages = [ "python" "typescript" ];
concerns = [ "coding-style" "testing" ];
frameworks = [ "fastapi" ];
}
'';
};
settings = mkOption {
type = types.submodule {
freeformType = types.attrsOf types.anything;
@@ -109,7 +113,7 @@
default = [];
description = ''
Pi packages to install (npm:, git:, or local paths).
These are written to ~/.pi/agent/settings.json.
These are written to `${cfg.path}/settings.json`.
'';
};
@@ -197,50 +201,16 @@
};
default = {};
description = ''
Pi settings written to ~/.pi/agent/settings.json.
Pi settings written to `${cfg.path}/settings.json`.
Only non-null values are included in the generated JSON.
See pi docs/settings.md for all options.
'';
};
# ── Pi Guardrails ─────────────────────────────────────────────
guardrails = mkOption {
type = types.nullOr (types.submodule {
options = {
enable =
mkEnableOption
("Generate ~/.pi/agent/extensions/guardrails.json for pi-guardrails. "
+ "Adds @aliou/pi-guardrails to packages automatically.");
config = mkOption {
type = types.attrsOf types.anything;
default = {};
description = ''
Guardrails configuration written to ~/.pi/agent/extensions/guardrails.json.
See https://github.com/aliou/pi-guardrails for config schema.
IMPORTANT: Path access checks are lexical (not symlink-safe).
Local project .pi/extensions/guardrails.json can override same rule IDs
(memory > local > global > defaults). For immutable global policies,
consider a wrapper or upstream patch.
'';
};
};
});
default = null;
description = ''
Pi Guardrails security configuration.
Generates ~/.pi/agent/extensions/guardrails.json when enabled.
The @aliou/pi-guardrails package is added to settings.packages automatically.
'';
};
};
config = with lib; let
shared = import ./shared/shared-options.nix {inherit lib;};
cfg = config.coding.agents.pi;
in
mkIf cfg.enable (let
config = mkIf cfg.enable (let
basePath = lib.removeSuffix "/" cfg.path;
# Build settings.json by filtering out null values recursively
filterNulls = attrs:
lib.filterAttrs (_: v: v != null) (
@@ -256,43 +226,8 @@
attrs
);
# Base settings (already filtered)
piSettings = filterNulls cfg.settings;
# Guardrails package to inject when guardrails is enabled
guardrailsPackage = "npm:@aliou/pi-guardrails@0.11.1";
# Guardrails config (only when guardrails is enabled)
guardrailsJson =
if (cfg.guardrails != null && cfg.guardrails.enable)
then builtins.toJSON cfg.guardrails.config
else null;
# Merge guardrails package into settings.packages when guardrails is enabled
piSettingsWithGuardrails = let
baseSettings = cfg.settings;
basePackages = baseSettings.packages or [];
hasGuardrailsPackage =
lib.any
(p:
lib.hasPrefix "npm:@aliou/pi-guardrails" p
|| (lib.hasPrefix "git:" p && lib.hasSuffix "/pi-guardrails" p))
basePackages;
packagesWithGuardrails =
if (cfg.guardrails != null && cfg.guardrails.enable && !hasGuardrailsPackage)
then basePackages ++ [guardrailsPackage]
else basePackages;
in
if packagesWithGuardrails != basePackages
then filterNulls (baseSettings // {packages = packagesWithGuardrails;})
else piSettings;
# Coding rules config for renderForPi (only when both agentsInput and codingRules are set)
piCodingRules =
if cfg.agentsInput != null && cfg.codingRules != null
then cfg.codingRules // {agents = cfg.agentsInput;}
else null;
# Rendered agents (only computed when agentsInput is set)
rendered =
if cfg.agentsInput != null
@@ -302,7 +237,6 @@
canonical = cfg.agentsInput.lib.loadAgents;
modelOverrides = cfg.modelOverrides;
primaryAgent = cfg.primaryAgent;
codingRules = piCodingRules;
}
else null;
@@ -314,7 +248,7 @@
in
builtins.listToAttrs (
map (name: {
name = ".pi/agent/agents/${name}.md";
name = "${basePath}/agents/${name}.md";
value = {source = "${rendered}/agents/${name}.md";};
})
agentNames
@@ -322,36 +256,43 @@
else {};
in {
home.file = mkMerge [
# ── MCP servers from programs.mcp → ~/.pi/agent/mcp.json ───────
# ── MCP servers from programs.mcp → ${cfg.path}/mcp.json ───────
(mkIf (cfg.mcpServers != {}) {
".pi/agent/mcp.json".text = builtins.toJSON {mcpServers = cfg.mcpServers;};
".pi/agent/mcp.json".force = true;
"${basePath}/mcp.json".text = builtins.toJSON {mcpServers = cfg.mcpServers;};
})
# ── ~/.pi/agent/settings.json ──────────────────────────────────
# ── ${cfg.path}/settings.json ──────────────────────────────────
{
".pi/agent/settings.json".text = builtins.toJSON piSettingsWithGuardrails;
".pi/agent/settings.json".force = true;
"${basePath}/settings.json".text = builtins.toJSON piSettings;
}
# ── pi-guardrails config ─────────────────────────────────────
(mkIf (guardrailsJson != null) {
".pi/agent/extensions/guardrails.json".text = guardrailsJson;
".pi/agent/extensions/guardrails.json".force = true;
})
# ── AGENTS.md — agent descriptions and specialist listing ──────
(mkIf (cfg.agentsInput != null) {
".pi/agent/AGENTS.md".source = "${rendered}/AGENTS.md";
"${basePath}/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";
"${basePath}/SYSTEM.md".source = "${rendered}/SYSTEM.md";
})
# ── Agents — pi-subagents .md files ────────────────────────────
agentFiles
# ── Skills symlinked from AGENTS repo ──────────────────────────
(mkIf (cfg.agentsInput != null) {
"${basePath}/skills".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;
};
})
];
});
}

View File

@@ -1,7 +0,0 @@
# Shared agent module exports
# Imports all shared modules for the coding.agents namespace.
{
imports = [
./git-identity.nix
];
}

View File

@@ -1,64 +0,0 @@
# Git identity module for agent commits.
# Sets GIT_AUTHOR_*, GIT_COMMITTER_*, and GIT_SSH_COMMAND environment variables.
{
pkgs,
lib,
config,
...
}: let
cfg = config.coding.agents.gitIdentity;
in {
options.coding.agents.gitIdentity = {
enable = lib.mkEnableOption ''
Agent Git identity for commits. When enabled, sets GIT_AUTHOR_* and
GIT_COMMITTER_* environment variables for consistent bot identity.
'';
name = lib.mkOption {
type = lib.types.str;
default = "m3ta-chiron";
description = "Git user name for agent commits.";
example = "m3ta-chiron";
};
email = lib.mkOption {
type = lib.types.str;
default = "m3ta-chiron@agentmail.to";
description = "Git email for agent commits.";
example = "m3ta-chiron@agentmail.to";
};
signingKey = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Optional GPG signing key for verified commits.
Set to null to disable signing.
'';
example = "/home/user/.gnupg/sign_key.gpg";
};
sshKey = lib.mkOption {
type = lib.types.path;
description = ''
Path to SSH private key for git push authentication.
Use agenix-managed paths like /run/agenix/m3ta-chiron-ssh-key
for secure secret management.
'';
example = "/run/agenix/m3ta-chiron-ssh-key";
};
};
config = lib.mkIf cfg.enable {
home.sessionVariables = {
# Git author/committer identity
GIT_AUTHOR_NAME = cfg.name;
GIT_AUTHOR_EMAIL = cfg.email;
GIT_COMMITTER_NAME = cfg.name;
GIT_COMMITTER_EMAIL = cfg.email;
# SSH command for git push
GIT_SSH_COMMAND = "ssh -i ${cfg.sshKey} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new";
};
};
}

View File

@@ -1,77 +0,0 @@
# 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

@@ -13,6 +13,7 @@
imports = [
./mem0.nix
./ports.nix
./pi-agent.nix
# Example: ./my-service.nix
# Add more module files here as you create them
];

View File

@@ -0,0 +1,430 @@
{
cfg,
pkgs,
lib,
...
}:
with lib; let
managedSettingsFile = pkgs.writeText "pi-agent-managed-settings.json" (builtins.toJSON cfg.settings);
managedEnvFile =
pkgs.writeText "pi-agent-managed.env"
(concatStringsSep "\n" (mapAttrsToList (k: v: "${k}=${v}") cfg.environment));
runtimePath = concatStringsSep ":" (
[
"${cfg.package}/bin"
"${pkgs.nodejs}/bin"
"${pkgs.git}/bin"
"${pkgs.coreutils}/bin"
"${pkgs.findutils}/bin"
"${pkgs.gnugrep}/bin"
"${pkgs.gnused}/bin"
"${pkgs.util-linux}/bin"
"/run/current-system/sw/bin"
]
++ map (p: "${p}/bin") cfg.extraPackages
);
userPolicyCase = concatStringsSep "\n" (
mapAttrsToList (
user: userCfg: ''
${escapeShellArg user})
USER_CONFIG_PATH=${escapeShellArg (
if userCfg.configPath != null
then userCfg.configPath
else cfg.wrapper.hostConfigPath
)}
USER_ROOTS=(${concatStringsSep " " (map escapeShellArg userCfg.projectRoots)})
;;
''
)
cfg.hostUsers
);
in
pkgs.writeShellScriptBin cfg.wrapper.runnerName ''
set -euo pipefail
if [ "$(id -u)" -ne 0 ]; then
echo "${cfg.wrapper.runnerName} must run as root" >&2
exit 1
fi
if [ "$#" -lt 2 ]; then
echo "Usage: ${cfg.wrapper.runnerName} <invoking-user> <cwd> [pi-args...]" >&2
exit 2
fi
invoking_user="$1"
shift
cwd="$1"
shift
# Parse forwarded environment variables from wrapper (KEY=VALUE)
while [ "$#" -gt 0 ]; do
case "$1" in
TERM=*|LANG=*|LC_ALL=*|LC_CTYPE=*|COLORTERM=*|TERM_PROGRAM=*)
export "$1"
shift
;;
*)
break
;;
esac
done
resolve_user_policy() {
local user="$1"
USER_CONFIG_PATH=""
USER_ROOTS=()
case "$user" in
${userPolicyCase}
*)
return 1
;;
esac
return 0
}
if ! resolve_user_policy "$invoking_user"; then
echo "User '$invoking_user' is not allowed to use ${cfg.wrapper.commandName}" >&2
exit 1
fi
user_home="$(eval echo "~$invoking_user")"
if [ -z "$user_home" ] || [ "$user_home" = "~$invoking_user" ]; then
echo "Unable to determine home directory for user '$invoking_user'" >&2
exit 1
fi
expand_home_path() {
local input="$1"
if [ "$input" = "~" ]; then
printf '%s\n' "$user_home"
elif ${pkgs.gnugrep}/bin/grep -q '^~/' <<<"$input"; then
printf '%s\n' "$user_home/''${input:2}"
elif ${pkgs.gnugrep}/bin/grep -q '^/' <<<"$input"; then
printf '%s\n' "$input"
else
# Bare relative path resolve from user's home
printf '%s\n' "$user_home/$input"
fi
}
cwd_real="$(${pkgs.coreutils}/bin/realpath -m "$cwd")"
resolved_roots=()
skipped_roots=()
is_allowed_cwd=0
for configured_root in "''${USER_ROOTS[@]}"; do
expanded_root="$(expand_home_path "$configured_root")"
resolved_root="$(${pkgs.coreutils}/bin/realpath -m "$expanded_root")"
if [ ! -d "$resolved_root" ]; then
skipped_roots+=("$resolved_root")
continue
fi
resolved_roots+=("$resolved_root")
case "$cwd_real/" in
"$resolved_root"/*)
is_allowed_cwd=1
;;
esac
done
if [ "''${#resolved_roots[@]}" -eq 0 ]; then
echo "Denied: no valid existing project roots are configured for user '$invoking_user'." >&2
if [ "''${#skipped_roots[@]}" -gt 0 ]; then
echo "Configured but missing roots:" >&2
for root in "''${skipped_roots[@]}"; do
echo " - $root" >&2
done
fi
exit 1
fi
if [ "$is_allowed_cwd" -ne 1 ]; then
echo "Denied: '$cwd_real' is outside allowed project roots for user '$invoking_user'." >&2
echo "Allowed roots:" >&2
for root in "''${resolved_roots[@]}"; do
echo " - $root" >&2
done
exit 1
fi
${pkgs.coreutils}/bin/install -d -m 0750 -o ${escapeShellArg cfg.user} -g ${escapeShellArg cfg.group} \
${escapeShellArg cfg.stateDir} \
${escapeShellArg "${cfg.stateDir}/.pi"} \
${escapeShellArg "${cfg.stateDir}/.pi/agent"} \
${escapeShellArg "${cfg.stateDir}/.pi/agent/sessions"} \
${escapeShellArg "${cfg.stateDir}/.project-mounts"} \
${escapeShellArg "${cfg.stateDir}/projects"} \
${escapeShellArg "${cfg.stateDir}/.npm"} \
${escapeShellArg "${cfg.stateDir}/.npm-global"} \
${escapeShellArg "${cfg.stateDir}/.npm-global/bin"} \
${escapeShellArg "${cfg.stateDir}/.npm-global/lib"}
config_source="$USER_CONFIG_PATH"
if ${pkgs.gnugrep}/bin/grep -q '^/' <<<"$config_source"; then
source_dir="$config_source"
else
source_dir="$(expand_home_path "$config_source")"
fi
if [ "${
if cfg.wrapper.syncConfigFromHost
then "1"
else "0"
}" = "1" ] && [ -d "$source_dir" ]; then
${pkgs.rsync}/bin/rsync -a --delete \
--exclude='auth.json' \
--exclude='mcp-oauth' \
--exclude='sessions' \
--exclude='bin' \
--exclude='mcp-cache.json' \
"$source_dir/" ${escapeShellArg "${cfg.stateDir}/.pi/agent/"}
${pkgs.coreutils}/bin/chown -R ${escapeShellArg "${cfg.user}:${cfg.group}"} ${escapeShellArg "${cfg.stateDir}/.pi/agent"}
fi
# Merge host settings.json (if any) with Nix-managed settings.
# Precedence: host settings first, Nix-managed keys override recursively.
settings_target=${escapeShellArg "${cfg.stateDir}/.pi/agent/settings.json"}
${pkgs.python3}/bin/python3 - "$settings_target" ${escapeShellArg managedSettingsFile} <<'PY_PI_SETTINGS_MERGE'
import json
import os
import sys
def load_obj(path):
if not os.path.exists(path):
return {}
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
return data if isinstance(data, dict) else {}
except Exception:
return {}
def deep_merge(base, override):
if isinstance(base, dict) and isinstance(override, dict):
out = dict(base)
for key, value in override.items():
out[key] = deep_merge(out.get(key), value)
return out
return override
def main():
target = sys.argv[1]
managed = sys.argv[2]
base_obj = load_obj(target)
managed_obj = load_obj(managed)
merged = deep_merge(base_obj, managed_obj)
os.makedirs(os.path.dirname(target), exist_ok=True)
tmp = f"{target}.tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(merged, f, indent=2, sort_keys=True)
f.write("\n")
os.replace(tmp, target)
if __name__ == "__main__":
main()
PY_PI_SETTINGS_MERGE
${pkgs.coreutils}/bin/chown ${escapeShellArg "${cfg.user}:${cfg.group}"} "$settings_target"
${pkgs.coreutils}/bin/chmod 0640 "$settings_target"
# Merge environment into isolated .env with precedence:
# 1) synced host env (source_dir/.env)
# 2) Nix-managed environment attrset
# 3) Nix-managed environmentFiles (appended in declaration order)
env_target=${escapeShellArg "${cfg.stateDir}/.pi/.env"}
${pkgs.coreutils}/bin/install -o ${escapeShellArg cfg.user} -g ${escapeShellArg cfg.group} -m 0640 /dev/null "$env_target"
if [ -f "$source_dir/.env" ]; then
${pkgs.coreutils}/bin/cat "$source_dir/.env" >> "$env_target"
printf '\n' >> "$env_target"
fi
if [ -f ${escapeShellArg managedEnvFile} ]; then
${pkgs.coreutils}/bin/cat ${escapeShellArg managedEnvFile} >> "$env_target"
printf '\n' >> "$env_target"
fi
${concatStringsSep "\n" (map (f: ''
if [ -f ${escapeShellArg f} ]; then
${pkgs.coreutils}/bin/cat ${escapeShellArg f} >> "$env_target"
printf '\n' >> "$env_target"
fi
'')
cfg.environmentFiles)}
${pkgs.coreutils}/bin/chown ${escapeShellArg "${cfg.user}:${cfg.group}"} "$env_target"
${pkgs.coreutils}/bin/chmod 0640 "$env_target"
npm_prefix=${escapeShellArg "${cfg.stateDir}/.npm-global"}
runtime_path=${escapeShellArg runtimePath}
project_mount_dir=${escapeShellArg "${cfg.stateDir}/.project-mounts"}
project_links_dir=${escapeShellArg "${cfg.stateDir}/projects"}
project_bind_pairs=()
matched_root=""
matched_mount=""
project_index=0
for root in "''${resolved_roots[@]}"; do
if [ ! -d "$root" ]; then
continue
fi
root_slug="$(printf '%s' "$root" | ${pkgs.gnused}/bin/sed 's#^/##; s#/#-#g; s#-\{2,\}#-#g; s#-$##; s#^$#root#')"
root_slug="''${project_index}-''${root_slug}"
project_index=$((project_index + 1))
mount_point="''${project_mount_dir}/''${root_slug}"
link_path="''${project_links_dir}/''${root_slug}"
${pkgs.coreutils}/bin/install -d -m 0750 -o ${escapeShellArg cfg.user} -g ${escapeShellArg cfg.group} "$mount_point"
${pkgs.coreutils}/bin/ln -sfn "$mount_point" "$link_path"
project_bind_pairs+=("$root:$mount_point")
case "$cwd_real/" in
"$root"/*)
if [ -z "$matched_root" ] || [ "''${#root}" -gt "''${#matched_root}" ]; then
matched_root="$root"
matched_mount="$mount_point"
fi
;;
esac
done
if [ -z "$matched_root" ]; then
echo "Failed to map cwd '$cwd_real' to an allowed root." >&2
exit 1
fi
if [ "$cwd_real" = "$matched_root" ]; then
mapped_cwd="$matched_mount"
else
rel_path="''${cwd_real#"$matched_root/"}"
mapped_cwd="$matched_mount/$rel_path"
fi
pi_bin=${escapeShellArg "${cfg.package}/bin/${cfg.binaryName}"}
if [ ! -x "$pi_bin" ]; then
for candidate in pi pi-agent; do
alt=${escapeShellArg "${cfg.package}/bin"}/$candidate
if [ -x "$alt" ]; then
pi_bin="$alt"
break
fi
done
fi
if [ ! -x "$pi_bin" ]; then
echo "Pi binary not found or not executable: $pi_bin" >&2
echo "Available executables in ${cfg.package}/bin:" >&2
${pkgs.coreutils}/bin/ls -1 ${escapeShellArg "${cfg.package}/bin"} >&2 || true
exit 127
fi
cmd=(
${pkgs.systemd}/bin/systemd-run
--collect
--wait
--pty
--service-type=exec
-p User=${cfg.user}
-p Group=${cfg.group}
-p WorkingDirectory="$mapped_cwd"
-p NoNewPrivileges=yes
-p PrivateTmp=yes
-p ProtectSystem=strict
-p ProtectHome=false
-p ProtectControlGroups=yes
-p ProtectKernelTunables=yes
-p ProtectKernelModules=yes
-p RestrictSUIDSGID=yes
-p LockPersonality=yes
-p RestrictRealtime=yes
-p RestrictNamespaces=yes
-p MemoryDenyWriteExecute=no
-p UMask=0007
-p ReadWritePaths=${cfg.stateDir}
-p EnvironmentFile=${cfg.stateDir}/.pi/.env
-E HOME=${cfg.stateDir}
-E PI_HOME=${cfg.stateDir}/.pi
-E MESSAGING_CWD="$mapped_cwd"
-E PATH="$runtime_path"
-E NPM_CONFIG_CACHE=${cfg.stateDir}/.npm
-E NPM_CONFIG_PREFIX="$npm_prefix"
-E PI_AGENT_INVOKING_USER="$invoking_user"
)
# Propagate terminal and locale settings for correct PTY/UTF-8 handling
for env_var in TERM LANG LC_ALL LC_CTYPE COLORTERM TERM_PROGRAM; do
if [ -n "''${!env_var:-}" ]; then
cmd+=( -E "$env_var=''${!env_var}" )
fi
done
${optionalString (cfg.projectGroup != null) ''
cmd+=( -p SupplementaryGroups=${cfg.projectGroup} )
''}
# Only mark existing top-level paths inaccessible; systemd fails namespace
# setup if InaccessiblePaths points to a non-existent path on this host.
for p in /home /root /mnt /media /srv; do
if [ -e "$p" ]; then
cmd+=( -p "InaccessiblePaths=$p" )
fi
done
for pair in "''${project_bind_pairs[@]}"; do
src="''${pair%%:*}"
dst="''${pair#*:}"
cmd+=( -p "BindPaths=$src:$dst" )
done
${concatStringsSep "\n" (mapAttrsToList (name: value: ''cmd+=( -E ${escapeShellArg "${name}=${value}"} )'') cfg.wrapper.extraEnvironment)}
cmd+=( "$pi_bin" )
${concatStringsSep "\n" (map (arg: ''cmd+=( ${escapeShellArg arg} )'') cfg.wrapper.extraRunArgs)}
cmd+=( "$@" )
# Reset terminal keyboard protocol modes that pi's TUI may have enabled.
# If pi crashes or is killed (OOM, SIGKILL, etc.), its cleanup handler
# never runs and the host terminal stays in Kitty keyboard protocol or
# xterm modifyOtherKeys mode. This causes all keystrokes to appear as
# raw escape sequences like ^[[99;5u (ctrl+c in CSI-u encoding).
#
# Try /dev/tty first (controlling terminal), fall back to stdout
# (connected through sudo to the user's Ghostty terminal).
cleanup_terminal() {
local output_dev=""
if [ -w /dev/tty ]; then
output_dev=/dev/tty
elif [ -w /dev/stdout ]; then
output_dev=/dev/stdout
fi
if [ -n "$output_dev" ]; then
# Disable Kitty keyboard protocol (pop all flags)
printf '\033[<u' > "$output_dev" 2>/dev/null || true
# Disable xterm modifyOtherKeys
printf '\033[>4;0m' > "$output_dev" 2>/dev/null || true
# Disable bracketed paste mode
printf '\033[?2004l' > "$output_dev" 2>/dev/null || true
# Restore cursor visibility
printf '\033[?25h' > "$output_dev" 2>/dev/null || true
fi
}
trap cleanup_terminal EXIT
# Run without exec so the EXIT trap fires after pi exits (normal or crash).
"''${cmd[@]}"
''

View File

@@ -0,0 +1,102 @@
{
cfg,
pkgs,
lib,
runner,
...
}:
with lib;
pkgs.writeShellScriptBin cfg.wrapper.commandName ''
set -euo pipefail
user_name="$(id -un)"
user_home="$(eval echo "~$user_name")"
if [ -z "$user_home" ] || [ "$user_home" = "~$user_name" ]; then
user_home="$HOME"
fi
resolve_user_policy() {
local user="$1"
USER_ROOTS=()
case "$user" in
${concatStringsSep "\n" (
mapAttrsToList (
user: userCfg: ''
${escapeShellArg user})
USER_ROOTS=(${concatStringsSep " " (map escapeShellArg userCfg.projectRoots)})
;;
''
)
cfg.hostUsers
)}
*)
return 1
;;
esac
return 0
}
if ! resolve_user_policy "$user_name"; then
echo "User '$user_name' is not allowed to use ${cfg.wrapper.commandName}" >&2
exit 1
fi
expand_home_path() {
local input="$1"
if [ "$input" = "~" ]; then
printf '%s\n' "$user_home"
elif ${pkgs.gnugrep}/bin/grep -q '^~/' <<<"$input"; then
printf '%s\n' "$user_home/''${input:2}"
elif ${pkgs.gnugrep}/bin/grep -q '^/' <<<"$input"; then
printf '%s\n' "$input"
else
printf '%s\n' "$user_home/$input"
fi
}
cwd_real="$(${pkgs.coreutils}/bin/realpath -m "$PWD")"
is_allowed_cwd=0
resolved_roots=()
skipped_roots=()
for configured_root in "''${USER_ROOTS[@]}"; do
expanded_root="$(expand_home_path "$configured_root")"
resolved_root="$(${pkgs.coreutils}/bin/realpath -m "$expanded_root")"
if [ ! -d "$resolved_root" ]; then
skipped_roots+=("$resolved_root")
continue
fi
resolved_roots+=("$resolved_root")
case "$cwd_real/" in
"$resolved_root"/*)
is_allowed_cwd=1
;;
esac
done
if [ "''${#resolved_roots[@]}" -eq 0 ]; then
echo "Denied: no valid existing project roots are configured for user '$user_name'." >&2
if [ "''${#skipped_roots[@]}" -gt 0 ]; then
echo "Configured but missing roots:" >&2
for root in "''${skipped_roots[@]}"; do
echo " - $root" >&2
done
fi
exit 1
fi
if [ "$is_allowed_cwd" -ne 1 ]; then
echo "Denied: '$cwd_real' is outside allowed project roots for user '$user_name'." >&2
echo "Allowed roots:" >&2
for root in "''${resolved_roots[@]}"; do
echo " - $root" >&2
done
exit 1
fi
exec /run/wrappers/bin/sudo --non-interactive \
${runner}/bin/${cfg.wrapper.runnerName} \
"$user_name" "$cwd_real" \
"TERM=$TERM" "LANG=$LANG" "LC_ALL=''${LC_ALL:-}" "LC_CTYPE=''${LC_CTYPE:-}" "COLORTERM=''${COLORTERM:-}" "TERM_PROGRAM=''${TERM_PROGRAM:-}" \
"$@"
''

295
modules/nixos/pi-agent.nix Normal file
View File

@@ -0,0 +1,295 @@
# NixOS Module for isolated Pi execution (fresh design)
#
# Goals:
# - Dedicated isolated runtime identity (pi-agent user/group)
# - Host UX via `pi` wrapper command
# - Per-host-user project allowlists (different roots per user)
# - No container mode
# - Merge user Pi config + Nix-managed settings/env into isolated runtime
{
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.m3ta.pi-agent;
hostUserNames = attrNames cfg.hostUsers;
runner = import ./pi-agent-runner.nix {inherit cfg pkgs lib;};
wrapper = import ./pi-agent-wrapper.nix {inherit cfg pkgs lib runner;};
in {
options.m3ta.pi-agent = {
enable = mkEnableOption "isolated Pi execution with dedicated system user and policy-enforced wrapper";
package = mkOption {
type = types.package;
default = pkgs.pi-coding-agent;
defaultText = literalExpression "pkgs.pi-coding-agent";
description = "Pi package providing the executable used in isolated runtime.";
};
binaryName = mkOption {
type = types.str;
default = "pi-agent";
description = "Preferred executable name inside `${cfg.package}/bin` (falls back to pi/pi-agent auto-detection).";
example = "pi";
};
user = mkOption {
type = types.str;
default = "pi-agent";
description = "System user that executes Pi in isolated mode.";
};
group = mkOption {
type = types.str;
default = "pi-agent";
description = "System group for the isolated Pi user.";
};
stateDir = mkOption {
type = types.str;
default = "/var/lib/pi-agent";
description = "Writable state/home directory for isolated Pi runtime.";
};
createUser = mkOption {
type = types.bool;
default = true;
description = "Whether to create the dedicated Pi user/group automatically.";
};
hostUsers = mkOption {
type = types.attrsOf (types.submodule {
options = {
projectRoots = mkOption {
type = types.listOf types.str;
default = [];
description = ''
Allowed project roots for this host user.
`~` and `~/...` are expanded relative to that host user's home.
'';
example = ["~/p" "~/work/client-a"];
};
configPath = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Optional host path for this user's Pi config source. If null,
wrapper.hostConfigPath is used. Relative paths resolve from the
host user's home.
'';
example = ".pi/agent";
};
};
});
default = {};
description = ''
Per-host-user policy map. Keys are host usernames.
Each user defines their own allowed project roots and optional config source.
'';
example = literalExpression ''
{
m3tam3re = {
projectRoots = [ "~/p" "~/src/private" ];
configPath = ".pi/agent";
};
teammate = {
projectRoots = [ "~/projects" ];
};
}
'';
};
settings = mkOption {
type = types.attrsOf types.anything;
default = {};
description = ''
Nix-managed Pi settings merged into isolated `${cfg.stateDir}/.pi/agent/settings.json`.
Merge precedence: synced host settings first, Nix-managed values override recursively.
'';
example = literalExpression ''
{
defaultModel = "anthropic/claude-sonnet-4";
defaultProvider = "anthropic";
quietStartup = true;
}
'';
};
environment = mkOption {
type = types.attrsOf types.str;
default = {};
description = ''
Non-secret Nix-managed environment variables appended into isolated
`${cfg.stateDir}/.pi/.env` after synced host values.
'';
};
environmentFiles = mkOption {
type = types.listOf types.str;
default = [];
description = ''
Paths to env files (secrets/tokens) appended to isolated `${cfg.stateDir}/.pi/.env`
after `environment` entries.
'';
};
extraPackages = mkOption {
type = types.listOf types.package;
default = [];
description = "Extra packages added to isolated runtime PATH.";
};
projectGroup = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
When set, the pi-agent user is added to this group and the group is
passed as SupplementaryGroups to the systemd-run sandbox. This allows
pi-agent to write to project directories that grant group write access.
The user must ensure project directories have appropriate group ownership
and permissions (e.g. setgid + group write).
'';
example = "users";
};
wrapper = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable host-side wrapper command that enforces policy and runs isolated Pi.";
};
commandName = mkOption {
type = types.str;
default = "pi";
description = "Host wrapper command name.";
};
runnerName = mkOption {
type = types.str;
default = "m3ta-pi-agent-runner";
description = "Privileged runner command invoked via scoped sudo rule.";
};
hideDirectBinary = mkOption {
type = types.bool;
default = true;
description = ''
When true and wrapper is enabled, do not add the raw Pi package to host PATH,
reducing bypass risk by making wrapper the canonical entrypoint.
'';
};
syncConfigFromHost = mkOption {
type = types.bool;
default = true;
description = ''
Sync host Pi config directory into isolated `${cfg.stateDir}/.pi/agent`
on each invocation.
'';
};
hostConfigPath = mkOption {
type = types.str;
default = ".pi/agent";
description = ''
Default source path for host Pi config sync. Relative paths resolve from
the invoking user's home. Per-user hostUsers.<name>.configPath overrides this.
'';
};
extraRunArgs = mkOption {
type = types.listOf types.str;
default = [];
description = "Extra arguments inserted before user-provided Pi args.";
};
extraEnvironment = mkOption {
type = types.attrsOf types.str;
default = {};
description = "Additional environment variables passed to isolated Pi runtime.";
};
};
};
config = mkIf cfg.enable {
assertions =
[
{
assertion = cfg.hostUsers != {};
message = "m3ta.pi-agent.hostUsers must define at least one authorized host user.";
}
{
assertion = (!cfg.wrapper.enable) || (cfg.hostUsers != {});
message = "m3ta.pi-agent.hostUsers must not be empty when wrapper is enabled.";
}
]
++ mapAttrsToList (user: userCfg: {
assertion = userCfg.projectRoots != [];
message = "m3ta.pi-agent.hostUsers.${user}.projectRoots must not be empty.";
})
cfg.hostUsers;
users.groups = mkIf cfg.createUser {
"${cfg.group}" = {};
};
users.users = mkIf cfg.createUser {
"${cfg.user}" = {
isSystemUser = true;
group = cfg.group;
extraGroups = mkIf (cfg.projectGroup != null) [cfg.projectGroup];
description = "Isolated Pi agent user";
home = cfg.stateDir;
createHome = true;
shell = pkgs.bashInteractive;
};
};
systemd.tmpfiles.rules = [
"d ${cfg.stateDir} 0750 ${cfg.user} ${cfg.group} - -"
"d ${cfg.stateDir}/.pi 0750 ${cfg.user} ${cfg.group} - -"
"d ${cfg.stateDir}/.pi/agent 0750 ${cfg.user} ${cfg.group} - -"
"d ${cfg.stateDir}/.pi/agent/sessions 0750 ${cfg.user} ${cfg.group} - -"
"d ${cfg.stateDir}/.project-mounts 0750 ${cfg.user} ${cfg.group} - -"
"d ${cfg.stateDir}/projects 0750 ${cfg.user} ${cfg.group} - -"
"d ${cfg.stateDir}/.npm 0750 ${cfg.user} ${cfg.group} - -"
"d ${cfg.stateDir}/.npm-global 0750 ${cfg.user} ${cfg.group} - -"
"d ${cfg.stateDir}/.npm-global/bin 0750 ${cfg.user} ${cfg.group} - -"
"d ${cfg.stateDir}/.npm-global/lib 0750 ${cfg.user} ${cfg.group} - -"
];
# Ensure correct ownership of stateDir after user creation.
# createHome = true causes useradd to create the directory as root:root
# before systemd-tmpfiles can set the intended owner.
system.activationScripts.pi-agent-chown = {
deps = ["users"];
text = ''
chown ${cfg.user}:${cfg.group} ${cfg.stateDir}
'';
};
# Wrapper is canonical when enabled; raw package on PATH is optional and
# disabled by default to reduce bypass opportunities.
environment.systemPackages =
optional cfg.wrapper.enable wrapper
++ optional ((!cfg.wrapper.enable) || (!cfg.wrapper.hideDirectBinary)) cfg.package;
security.sudo.extraRules = mkIf (cfg.wrapper.enable && hostUserNames != []) [
{
users = hostUserNames;
commands = [
{
command = "${runner}/bin/${cfg.wrapper.runnerName}";
options = ["NOPASSWD"];
}
];
}
];
};
}

24
overlays/default.nix Normal file
View File

@@ -0,0 +1,24 @@
{inputs, ...}: {
# This one brings our custom packages from the 'pkgs' directory
additions = final: prev: (import ../pkgs {pkgs = final;});
# 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";
};
};
master-packages = final: _prev: {
master = import inputs.nixpkgs-master {
system = final.stdenv.hostPlatform.system;
config.allowUnfree = true;
};
};
}

16
overlays/mods/beads.nix Normal file
View File

@@ -0,0 +1,16 @@
{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;
})

18
overlays/mods/n8n.nix Normal file
View File

@@ -0,0 +1,18 @@
{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

@@ -0,0 +1,40 @@
{
lib,
fetchFromGitHub,
nix-update-script,
rustPlatform,
pkg-config,
perl,
openssl,
}:
rustPlatform.buildRustPackage rec {
pname = "code2prompt";
version = "4.2.0";
src = fetchFromGitHub {
owner = "mufeedvh";
repo = "code2prompt";
rev = "v${version}";
hash = "sha256-Gh8SsSTZW7QlyyC3SWJ5pOK2x85/GT7+LPJn2Jeczpc=";
};
cargoLock = {
lockFile = src + "/Cargo.lock";
};
buildAndTestSubdir = "crates/code2prompt";
nativeBuildInputs = [pkg-config perl];
buildInputs = [openssl];
passthru.updateScript = nix-update-script {};
meta = with lib; {
description = "A CLI tool that converts your codebase into a single LLM prompt with a source tree, prompt templating, and token counting";
homepage = "https://github.com/mufeedvh/code2prompt";
license = licenses.mit;
platforms = platforms.linux;
mainProgram = "code2prompt";
};
}

View File

@@ -1,23 +1,16 @@
# m3ta-nixpkgs package registry
#
# Flake inputs used:
# inputs.basecamp → basecamp (pass-through)
# inputs.openspec → openspec (pass-through)
# inputs.opencode → opencode-desktop (build inputs + patches)
# inputs.agents → not used directly here (used by lib/)
{
pkgs,
inputs,
...
}: let
# Used only for flake input pass-throughs (basecamp, openspec, opencode-desktop)
system = pkgs.stdenv.hostPlatform.system;
in {
# ── Local packages ────────────────────────────────────────────────
# Standard packages built from source in ./<name>/default.nix.
# No flake inputs required.
# Custom packages registry
# Each package is defined in its own directory under pkgs/
sidecar = pkgs.callPackage ./sidecar {};
td = pkgs.callPackage ./td {};
code2prompt = pkgs.callPackage ./code2prompt {};
eigent = pkgs.callPackage ./eigent {};
hyprpaper-random = pkgs.callPackage ./hyprpaper-random {};
launch-webapp = pkgs.callPackage ./launch-webapp {};
@@ -33,9 +26,11 @@ in {
zellij-ps = pkgs.callPackage ./zellij-ps {};
vibetyper = pkgs.callPackage ./vibetyper {};
# ── Pass-through packages ──────────────────────────────────────────
# Imported directly from flake inputs. No local modifications.
# Imported from flake inputs (pass-through, no modifications)
basecamp = inputs.basecamp.packages.${system}.default;
openspec = inputs.openspec.packages.${system}.default;
# Imported from flake inputs (with local modifications)
opencode-desktop = pkgs.callPackage ./opencode-desktop {inherit inputs;};
# opencode-desktop = inputs.opencode.packages.${pkgs.system}.desktop;
}

View File

@@ -1,13 +1,13 @@
{
"version": "1.3.0",
"version": "1.2.2",
"sources": {
"aarch64-linux": {
"url": "https://github.com/kestra-io/kestractl/releases/download/1.3.0/kestractl_1.3.0_linux_arm64.tar.gz",
"hash": "sha256-/18F6CZnnLbet4BmI1oQ5pZWkJwIshCq30qd+cm0GGA="
"url": "https://github.com/kestra-io/kestractl/releases/download/1.2.2/kestractl_1.2.2_linux_arm64.tar.gz",
"hash": "sha256-sidFsCZPnJ07PM5QayPBqaqlBBJTLEdecfd0AWnL7Yo="
},
"x86_64-linux": {
"url": "https://github.com/kestra-io/kestractl/releases/download/1.3.0/kestractl_1.3.0_linux_amd64.tar.gz",
"hash": "sha256-xmsBiqNKvob8xHDyU253o6c25YIubHanNdLqzWaOvSA="
"url": "https://github.com/kestra-io/kestractl/releases/download/1.2.2/kestractl_1.2.2_linux_amd64.tar.gz",
"hash": "sha256-0C2naN2ougBJSY2z2m6eORnLkLen87HD+a+gvtrUvdw="
}
}
}

View File

@@ -6,14 +6,14 @@
}:
python3.pkgs.buildPythonPackage rec {
pname = "mem0ai";
version = "2.0.1";
version = "1.0.9";
pyproject = true;
src = fetchFromGitHub {
owner = "mem0ai";
repo = "mem0";
rev = "v${version}";
hash = "sha256-lNSE0Yit+FmM8opC4XYtfVef7JfGd3wMKbLj67Kp4Qw=";
hash = "sha256-tcWH5VbjIBSHinfjirxbUhxqgU0xOUlcHTQHraMuALg=";
};
# Relax Python dependency version constraints

View File

@@ -11,7 +11,6 @@
node-gyp,
cctools,
xcbuild,
dart-sass,
libkrb5,
libmongocrypt,
libpq,
@@ -26,20 +25,20 @@
in
stdenv.mkDerivation (finalAttrs: {
pname = "n8n";
version = "2.18.5";
version = "2.14.2";
src = fetchFromGitHub {
owner = "n8n-io";
repo = "n8n";
tag = "n8n@${finalAttrs.version}";
hash = "sha256-ws0DXGQFR+z3nVyd4Yn9pIM7yh+H6GnuCRSLxgvtPxo=";
hash = "sha256-nWV3DFDkBlfDdoOxwYB0HSrTyKpTt70YxAQYUPartkE=";
};
pnpmDeps = fetchPnpmDeps {
inherit (finalAttrs) pname version src;
pnpm = pnpm_10;
fetcherVersion = 3;
hash = "sha256-Ajgne0neNm6HgMK6z3jnEkUJJxVOTgzjpSaMaJgIndQ=";
hash = "sha256-0SnPF3CgIja3M1ubLrwyFcx7vY0eHz9DEgn/gDLXN80=";
};
nativeBuildInputs =
@@ -62,17 +61,6 @@ in
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 = ''
runHook preBuild

View File

@@ -1,29 +1,6 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p bash curl jq nix-update cacert git nix
#!nix-shell --pure -i bash -p bash curl jq nix-update cacert git
set -euo pipefail
# n8n releases are published with two tags per version:
# - "n8n@X.Y.Z" - the versioned tag
# - "stable" - always points to the latest stable version
#
# We query the "stable" tag and extract the version from its target commitish (e.g., "release/2.18.5").
# This ensures we always get the actual latest stable version, not the most recently created tag.
set -euo pipefail
# Get the directory where this script lives (should be pkgs/n8n/)
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Get the nixpkgs root (parent of pkgs/)
nixpkgs_root="$(cd "$script_dir/../.." && pwd)"
cd "$nixpkgs_root"
# Query the "stable" tag and extract version from target_commitish (e.g., "release/2.18.5")
new_version=$(curl -s "https://api.github.com/repos/n8n-io/n8n/releases/tags/stable" | jq --raw-output '.target_commitish | ltrimstr("release/")')
echo "Latest stable version: n8n@${new_version}"
echo "Running from: $(pwd)"
# Use --flake --system to properly evaluate the flake-based package
nix-update --flake --system x86_64-linux n8n --version "$new_version"
new_version="$(curl -s "https://api.github.com/repos/n8n-io/n8n/releases/latest" | jq --raw-output '.tag_name | ltrimstr("n8n@")')"
nix-update n8n --flake --version "$new_version"

View File

@@ -0,0 +1,114 @@
{
lib,
stdenv,
symlinkJoin,
makeWrapper,
rustPlatform,
pkg-config,
cargo-tauri,
bun,
nodejs,
cargo,
rustc,
jq,
wrapGAppsHook4,
dbus,
glib,
gtk4,
libsoup_3,
librsvg,
libappindicator,
glib-networking,
openssl,
webkitgtk_4_1,
gst_all_1,
inputs ? null,
}: let
# Get upstream opencode package for shared attributes
opencode = inputs.opencode.packages.${stdenv.hostPlatform.system}.default;
# Workaround for https://github.com/anomalyco/opencode/issues/11755
# 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-285KZ7rZLRoc6XqCZRHc25NE+mmpGh/BVeMpv8aPQtQ=";
};
opencode-desktop = rustPlatform.buildRustPackage (finalAttrs: {
pname = "opencode-desktop";
version = opencode.version;
src = opencode.src;
node_modules = fixedNodeModules;
patches = opencode.patches;
cargoRoot = "packages/desktop/src-tauri";
cargoLock = {
lockFile = finalAttrs.src + "/packages/desktop/src-tauri/Cargo.lock";
outputHashes = {
"specta-2.0.0-rc.22" = "sha256-YsyOAnXELLKzhNlJ35dHA6KGbs0wTAX/nlQoW8wWyJQ=";
"tauri-2.9.5" = "sha256-dv5E/+A49ZBvnUQUkCGGJ21iHrVvrhHKNcpUctivJ8M=";
"tauri-specta-2.0.0-rc.21" = "sha256-n2VJ+B1nVrh6zQoZyfMoctqP+Csh7eVHRXwUQuiQjaQ=";
};
};
buildAndTestSubdir = finalAttrs.cargoRoot;
nativeBuildInputs =
[pkg-config cargo-tauri.hook bun nodejs cargo rustc jq makeWrapper]
++ lib.optionals stdenv.hostPlatform.isLinux [wrapGAppsHook4];
buildInputs = lib.optionals stdenv.isLinux [
dbus
glib
gtk4
libsoup_3
librsvg
libappindicator
glib-networking
openssl
webkitgtk_4_1
gst_all_1.gstreamer
gst_all_1.gst-plugins-base
gst_all_1.gst-plugins-good
gst_all_1.gst-plugins-bad
];
strictDeps = true;
preBuild = ''
cp -a ${finalAttrs.node_modules}/{node_modules,packages} .
chmod -R u+w node_modules packages
patchShebangs node_modules
patchShebangs packages/desktop/node_modules
mkdir -p packages/desktop/src-tauri/sidecars
cp ${opencode}/bin/opencode packages/desktop/src-tauri/sidecars/opencode-cli-${stdenv.hostPlatform.rust.rustcTarget}
'';
tauriBuildFlags = ["--config" "tauri.prod.conf.json" "--no-sign"];
postFixup = lib.optionalString stdenv.hostPlatform.isLinux ''
mv $out/bin/OpenCode $out/bin/opencode-desktop
sed -i 's|^Exec=OpenCode$|Exec=opencode-desktop|' $out/share/applications/OpenCode.desktop
'';
});
# Wrapper for Wayland support
in
symlinkJoin {
name = "opencode-desktop";
paths = [opencode-desktop];
nativeBuildInputs = [makeWrapper];
postBuild = ''
wrapProgram $out/bin/opencode-desktop \
--run 'if [[ "$NIXOS_OZONE_WL" == "1" ]]; then export OC_ALLOW_WAYLAND=1; fi'
'';
meta = {
description = "OpenCode Desktop App with Wayland support";
homepage = "https://opencode.ai";
license = lib.licenses.mit;
platforms = lib.platforms.linux;
mainProgram = "opencode-desktop";
};
}

View File

@@ -13,16 +13,16 @@
}:
buildGoModule (finalAttrs: {
pname = "sidecar";
version = "0.84.0";
version = "0.83.0";
src = fetchFromGitHub {
owner = "marcus";
repo = "sidecar";
tag = "v${finalAttrs.version}";
hash = "sha256-80ldZlaZ99ti8dvw+Awev7ucz03iOVD2yzz/+IFHDvA=";
hash = "sha256-L6q2eZO1rNngWwHVhBJ2ftVbvYTConpqYHEb3nwiXxs=";
};
vendorHash = "sha256-IDD+hQZODNPj+Gy9CX5GFdMcsvt75aFLpabXZehAjaw=";
vendorHash = "sha256-fIaHzc0L4jwVSh/YjrXBB7nENqCgOfHF5bnljFsGbVo=";
subPackages = ["cmd/sidecar"];

View File

@@ -9,13 +9,13 @@
}:
buildGoModule (finalAttrs: {
pname = "td";
version = "0.44.0";
version = "0.43.0";
src = fetchFromGitHub {
owner = "marcus";
repo = "td";
tag = "v${finalAttrs.version}";
hash = "sha256-k1OCK6LE99fHLuxv8HZUW8cSn2Wmk74J7kb6Mi5ZpVw=";
hash = "sha256-DwzuXumEEQWfZW+GbbY9kyqkEFZQ9sC+sSbVxfrY6bM=";
};
vendorHash = "sha256-hFFG+vLXcL2NNdLQvQZ1hzu++pp5AkbFOPQS10wtsec=";

View File

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

View File

@@ -1,109 +0,0 @@
# AI coding agent development environment with coding rules
# Sets up coding rules for OpenCode and Pi, plus useful companion tools.
# Usage: nix develop .#coding
#
# To enable coding rules, add the agents input to your flake:
# agents = {
# url = "git+https://code.m3ta.dev/m3tam3re/AGENTS";
# flake = false;
# };
{
pkgs,
lib ? pkgs.lib,
inputs ? null,
agents ? null,
}: let
# Import the coding-rules library
m3taLib = import ../lib {lib = pkgs.lib;};
# Import custom packages
customPackages = import ../pkgs {inherit pkgs inputs;};
# Create rules configuration only if agents input is provided
rulesConfig = lib.optionalAttrs (agents != null) {
rules = m3taLib.coding-rules.mkCodingRules {
inherit agents;
# Languages relevant to this repository
languages = ["nix" "python" "shell"];
# Frameworks used in this repo
frameworks = ["n8n"];
# Standard concerns for development
concerns = [
"coding-style"
"naming"
"documentation"
"testing"
"git-workflow"
"project-structure"
];
# Also append rules to AGENTS.md for Pi agent discovery
forPi = true;
};
};
in
pkgs.mkShell {
name = "coding";
# Development tools
buildInputs = with pkgs; [
# Task management for AI coding sessions
customPackages.td
# Companion tool for CLI agents (diffs, file trees, task management)
customPackages.sidecar
# Code analysis tools
# Nix development tools (for this repo)
nil
alejandra
statix
deadnix
];
shellHook = ''
echo "🤖 AI Coding Environment"
echo ""
${
if (agents != null)
then ''
# Set up coding rules for OpenCode + Pi
${rulesConfig.rules.shellHook}
echo " Coding rules configured (OpenCode + Pi)"
echo " Languages: nix, python, shell"
echo " Frameworks: n8n"
echo " Concerns: coding-style, naming, documentation, testing, git-workflow, project-structure"
''
else ''
echo " Coding rules not configured"
echo ""
echo "To enable, add the agents input to your flake.nix:"
echo ""
echo " agents = {"
echo " url = \"git+https://code.m3ta.dev/m3tam3re/AGENTS\";"
echo " flake = false;"
echo " };"
''
}
echo ""
echo "Available tools:"
echo " opencode - AI coding agent"
echo " td usage --new-session - View current tasks"
echo " sidecar - Companion tool (diffs, file trees, tasks)"
echo " code2prompt - Convert code to prompts"
echo ""
echo "Nix development tools:"
echo " nix flake check - Check flake validity"
echo " nix fmt . - Format Nix files"
echo " statix check . - Lint Nix files"
echo " deadnix . - Find dead code"
echo ""
'';
}

View File

@@ -4,7 +4,6 @@
{
pkgs,
inputs,
agents ? null,
}: {
# Default shell for working on this repository
default = pkgs.mkShell {
@@ -33,5 +32,5 @@
# Import all individual shell environments
python = import ./python.nix {inherit pkgs inputs;};
devops = import ./devops.nix {inherit pkgs inputs;};
coding = import ./coding.nix {inherit pkgs inputs agents;};
opencode = import ./opencode.nix {inherit pkgs inputs;};
}

155
shells/opencode.nix Normal file
View File

@@ -0,0 +1,155 @@
# OpenCode development environment with AI coding rules
# This shell demonstrates the mkOpencodeRules library provided by this repository
# Usage: nix develop .#opencode
#
# To enable OpenCode rules, add the agents input to your flake:
# agents = {
# url = "git+https://code.m3ta.dev/m3tam3re/AGENTS";
# flake = false;
# };
{
pkgs,
lib ? pkgs.lib,
inputs ? null,
agents ? null,
}: let
# Import the opencode-rules library
m3taLib = import ../lib {lib = pkgs.lib;};
# Import custom packages
customPackages = import ../pkgs {inherit pkgs inputs;};
# Create rules configuration only if agents input is provided
# This demonstrates how to use mkOpencodeRules in a real project
rulesConfig = lib.optionalAttrs (agents != null) {
rules = m3taLib.opencode-rules.mkOpencodeRules {
# Pass the AGENTS repository path
inherit agents;
# Languages relevant to this repository
languages = ["python" "typescript" "nix"];
# Frameworks used in this repo
frameworks = ["n8n"];
# Standard concerns for development
concerns = [
"coding-style"
"naming"
"documentation"
"testing"
"git-workflow"
"project-structure"
];
};
};
in
pkgs.mkShell {
name = "opencode-dev";
# Development tools
buildInputs = with pkgs;
[
# OpenCode AI coding agent (if inputs are available)
]
++ lib.optionals (inputs != null)
[inputs.opencode.packages.${pkgs.stdenv.hostPlatform.system}.opencode]
++ [
# Task management for AI coding sessions
customPackages.td
# Companion tool for CLI agents (diffs, file trees, task management)
customPackages.sidecar
# Code analysis tools
customPackages.code2prompt
# Nix development tools (for this repo)
nil
alejandra
statix
deadnix
];
# Shell hook that sets up OpenCode rules
shellHook = ''
echo "🤖 OpenCode Development Environment"
echo ""
echo "This environment demonstrates the mkOpencodeRules library"
echo "provided by the m3ta-nixpkgs repository."
echo ""
${
if (agents != null)
then ''
# Execute the OpenCode rules shellHook
${rulesConfig.rules.shellHook}
echo " OpenCode rules configured!"
''
else ''
echo " OpenCode rules not configured"
echo ""
echo "To enable OpenCode rules, add the agents input to your flake.nix:"
echo ""
echo " inputs = {"
echo " m3ta-nixpkgs.url = \"git+https://code.m3ta.dev/m3tam3re/nixpkgs\";"
echo " agents = {"
echo " url = \"git+https://code.m3ta.dev/m3tam3re/AGENTS\";"
echo " flake = false;"
echo " };"
echo " };"
echo ""
echo "Then pass agents to the shell:"
echo " opencode = import ./opencode.nix { inherit pkgs inputs agents; };"
''
}
echo ""
echo "Available tools:"
echo " opencode - AI coding agent"
echo " td usage --new-session - View current tasks"
echo " sidecar - Companion tool (diffs, file trees, tasks)"
echo " code2prompt - Convert code to prompts"
echo ""
echo "Nix development tools:"
echo " nix flake check - Check flake validity"
echo " nix fmt . - Format Nix files"
echo " statix check . - Lint Nix files"
echo " deadnix . - Find dead code"
echo ""
${
if (agents == null)
then ''
echo "💡 Using mkOpencodeRules in your project:"
echo ""
echo "Add to your flake.nix:"
echo " inputs = {"
echo " m3ta-nixpkgs.url = \"git+https://code.m3ta.dev/m3tam3re/nixpkgs\";"
echo " agents = {"
echo " url = \"git+https://code.m3ta.dev/m3tam3re/AGENTS\";"
echo " flake = false;"
echo " };"
echo " };"
echo ""
echo " outputs = {self, nixpkgs, m3ta-nixpkgs, agents, ...}:"
echo " let"
echo " system = \"x86_64-linux\";"
echo " pkgs = nixpkgs.legacyPackages.''${system};"
echo " m3taLib = m3ta-nixpkgs.lib.''${system};"
echo " rules = m3taLib.opencode-rules.mkOpencodeRules {"
echo " inherit agents;"
echo " languages = [\"python\" \"typescript\"];"
echo " frameworks = [\"n8n\"];"
echo " };"
echo " in {"
echo " devShells.''${system}.default = pkgs.mkShell {"
echo " shellHook = rules.shellHook;"
echo " };"
echo " };"
''
else ""
}
echo ""
'';
}

View File

@@ -1,31 +1,26 @@
# Smoke tests for lib/agents.nix
# Verifies the library imports correctly and exports expected functions.
# Actual renderer derivations are verified by flake check building packages.
{
lib,
pkgs,
}: let
let
lib = import <nixpkgs/lib>;
agentsLib = (import ../../lib {inherit lib;}).agents;
in
pkgs.runCommand "lib-agents-tests" {} ''
echo "Running lib agents smoke tests..."
# Verify all expected functions exist
${lib.optionalString (agentsLib ? loadCanonical) ''echo "1. pass: loadCanonical exists"''}
${lib.optionalString (!(agentsLib ? loadCanonical)) ''echo "1. FAIL: loadCanonical missing" && exit 1''}
# 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";};
${lib.optionalString (agentsLib ? renderForTool) ''echo "2. pass: renderForTool exists"''}
${lib.optionalString (!(agentsLib ? renderForTool)) ''echo "2. FAIL: renderForTool missing" && exit 1''}
${lib.optionalString (agentsLib ? renderForOpencode) ''echo "3. pass: renderForOpencode exists"''}
${lib.optionalString (!(agentsLib ? renderForOpencode)) ''echo "3. FAIL: renderForOpencode missing" && exit 1''}
${lib.optionalString (agentsLib ? renderForPi) ''echo "4. pass: renderForPi exists"''}
${lib.optionalString (!(agentsLib ? renderForPi)) ''echo "4. FAIL: renderForPi missing" && exit 1''}
${lib.optionalString (agentsLib ? shellHookForTool) ''echo "5. pass: shellHookForTool exists"''}
${lib.optionalString (!(agentsLib ? shellHookForTool)) ''echo "5. FAIL: shellHookForTool missing" && exit 1''}
echo "All smoke tests passed"
touch $out
''
# 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

@@ -1,7 +1,5 @@
{
lib,
pkgs,
}: let
let
lib = import <nixpkgs/lib>;
codingRulesLib = (import ../../lib {inherit lib;}).coding-rules;
# Test 1: instructions are generated correctly with custom rulesDir
@@ -17,7 +15,7 @@
== [
".coding-rules/concerns/naming.md"
".coding-rules/languages/python.md"
]; "pass: instructions";
]; {result = "pass";};
# Test 2: default rulesDir is .opencode-rules
testDefaultRulesDir = let
@@ -26,9 +24,12 @@
};
hasCorrectPrefix = builtins.all (s: builtins.substring 0 15 s == ".opencode-rules") rules.instructions;
in
assert hasCorrectPrefix == true; "pass: default rulesDir";
assert hasCorrectPrefix == true; {result = "pass";};
# Test 3: shellHook contains both the symlink command and the config generation
# 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";
@@ -38,52 +39,10 @@
hasConfigGen = builtins.match ".*coding-rules.json.*" hook != null;
in
assert hasSymlink;
assert hasConfigGen; "pass: shellHook";
# Test 4: forPi=false does not include AGENTS.md logic in shellHook
testForPiDisabled = let
rules = codingRulesLib.mkCodingRules {
agents = "/tmp/fake-agents";
forPi = false;
};
hook = rules.shellHook;
hasPiBlock = builtins.match ".*CODING-RULES:START.*" hook != null;
in
assert hasPiBlock == false; "pass: forPi disabled";
# Test 5: mkRulesMdSection produces empty string for empty concerns
testEmptyRulesMdSection = let
section = codingRulesLib.mkRulesMdSection {
agents = "/tmp/fake-agents";
concerns = [];
languages = [];
frameworks = [];
};
in
assert section == ""; "pass: empty mkRulesMdSection";
# Test 6: mkRulesMdSection wraps content with markers
testRulesMdSection = let
# Use a simple file path that won't be read (concatRulesMd returns empty
# when files don't exist, so we just verify the function is callable)
section = codingRulesLib.mkRulesMdSection {
agents = "/tmp/fake-agents";
concerns = [];
languages = [];
frameworks = [];
};
# After fix: mkRulesMdSection returns "" for empty rules, not a string with markers
in
assert section == ""; "pass: mkRulesMdSection empty case";
in
pkgs.runCommand "lib-coding-rules-tests" {} ''
echo "Running lib coding-rules tests..."
echo "1. ${testInstructions}"
echo "2. ${testDefaultRulesDir}"
echo "3. ${testShellHook}"
echo "4. ${testForPiDisabled}"
echo "5. ${testEmptyRulesMdSection}"
echo "6. ${testRulesMdSection}"
echo "All tests passed"
touch $out
''
assert hasConfigGen; {result = "pass";};
in {
instructions-correct = testInstructions;
default-rules-dir = testDefaultRulesDir;
backward-compat = testBackwardCompat;
shell-hook = testShellHook;
}