mirror of
https://github.com/ryan4yin/nix-config.git
synced 2026-11-13 12:13:46 +01:00
refactor(agents): manage agent config in a home-manager module (#367)
This commit is contained in:
1 parent
0d2665c286
commit
afdaa3b869
15 files changed
+116
-241
No files matched your search
+23
-37
@@ -1,9 +1,10 @@
|
||||
# agents
|
||||
|
||||
Reusable, symlink-first agent resources shared across projects.
|
||||
Portable agent resources shared across projects: the global baseline rules, behavioral scenarios for
|
||||
those rules, and reference snippets for external tooling.
|
||||
|
||||
This directory is the canonical source for baseline agent rules and supporting command references.
|
||||
The primary workflow is to symlink files from here into each agent runtime/config location.
|
||||
Home Manager links the rules from here into each agent's config location, so `agents/` itself stays
|
||||
plain Markdown with no Nix dependency.
|
||||
|
||||
It is shared across projects. Repo-scoped task procedures for one repository belong elsewhere: put
|
||||
them in that repository's `.agents/skills/` (note the leading dot), which OpenCode and compatible
|
||||
@@ -20,27 +21,23 @@ Keep one canonical home for each fact; link between layers instead of copying pa
|
||||
|
||||
- `AGENTS.md`: global baseline rules for coding agents.
|
||||
- `evals/global-rules.md`: behavioral scenarios for validating changes to the global rules.
|
||||
- `install-rules.py`: installs the baseline by creating symlinks in supported agent config dirs.
|
||||
- `test_install_rules.py`: regression tests for file preservation and independent target handling.
|
||||
- `install-cli.md`: curated CLI install/update command snippets.
|
||||
- `install-skills.md`: curated `npx skills` command snippets.
|
||||
- `install-skills.md`: curated `npx skills` command snippets, plus the `npx ctx7` docs-tool setup.
|
||||
|
||||
The Nix side of the agents — deploying these rules, the agent CLIs, and their environment — lives in
|
||||
the Home Manager module [`home/base/tui/agents/`](../home/base/tui/agents/README.md).
|
||||
|
||||
## Core workflow
|
||||
|
||||
1. Maintain shared rules in `agents/AGENTS.md`.
|
||||
2. Configure permissions directly in the agent runtime; auto-approval is generally used.
|
||||
3. Run `install-rules.py` to refresh symlinks in local agent homes.
|
||||
4. Use `install-cli.md` and `install-skills.md` as reference snippets when needed.
|
||||
3. Edit the rules; Home Manager links them out-of-store, so the change reaches the next agent
|
||||
session without a rebuild. Run a Home Manager switch only when the deployed target set changes.
|
||||
4. Use `install-skills.md` as a reference when installing external skills.
|
||||
|
||||
## Install baseline rules (symlink-based)
|
||||
## Deployment
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python3 agents/install-rules.py
|
||||
```
|
||||
|
||||
Current targets:
|
||||
[`home/base/tui/agents/rules.nix`](../home/base/tui/agents/rules.nix) links `AGENTS.md` into every
|
||||
supported agent config directory as an out-of-store symlink, so edits apply without a rebuild:
|
||||
|
||||
- Codex: `AGENTS.md` -> `${CODEX_HOME:-~/.codex}/AGENTS.md`
|
||||
- OpenCode: `AGENTS.md` -> `${XDG_CONFIG_HOME:-~/.config}/opencode/AGENTS.md`
|
||||
@@ -48,30 +45,20 @@ Current targets:
|
||||
- OMP: `AGENTS.md` -> `~/.omp/agent/AGENTS.md`
|
||||
- Generic cross-tool (read by Kimi Code): `AGENTS.md` -> `~/.agents/AGENTS.md`
|
||||
|
||||
Behavior:
|
||||
The module is imported through `home/base/tui`, so it covers the hosts that import
|
||||
`home/linux/gui.nix` or the macOS `home/darwin` stack; core-only servers are unchanged.
|
||||
|
||||
- Each target is handled independently.
|
||||
- Target errors are reported while remaining targets are attempted; any error yields a nonzero exit.
|
||||
- Missing destination directories are skipped.
|
||||
- Existing regular files are preserved as `.bak` backups (numbered when a backup already exists).
|
||||
- Destination links are replaced atomically; a failed link creation leaves the destination intact.
|
||||
|
||||
The installer links only `AGENTS.md`; it does not install permission configuration, skills, or CLIs.
|
||||
The repository-root `AGENTS.md` contains guidance for this Nix configuration repository. It is not
|
||||
the global rules source and is not installed by this script.
|
||||
Only `AGENTS.md` is deployed to the agents. Permission configuration and skills are not installed by
|
||||
Nix; the CLIs themselves are, via the same module's `packages.nix`. The repository-root `AGENTS.md`
|
||||
contains guidance for this Nix configuration repository. It is not the global rules source and is
|
||||
not deployed.
|
||||
|
||||
Auto-approval controls tool prompting. The global rules still define task authorization, safety, and
|
||||
secret handling.
|
||||
|
||||
Run installer regression tests without changing installed rules:
|
||||
## About `install-skills.md`
|
||||
|
||||
```bash
|
||||
python3 -B -m unittest discover -s agents -p test_install_rules.py
|
||||
```
|
||||
|
||||
## About `install-cli.md` and `install-skills.md`
|
||||
|
||||
Use them as snippet libraries:
|
||||
Use it as a snippet library:
|
||||
|
||||
- review the commands
|
||||
- select what you need
|
||||
@@ -79,8 +66,7 @@ Use them as snippet libraries:
|
||||
|
||||
## TODO
|
||||
|
||||
Ideas worth adopting once a concrete need appears; nothing here is implemented yet, and the install
|
||||
scripts above do not cover it.
|
||||
Ideas worth adopting once a concrete need appears; nothing here is implemented yet.
|
||||
|
||||
- **Shared instructions and skills.** Deploy one rules body plus declared skills to every agent
|
||||
instead of maintaining a copy per agent. Reference: `mirkolenz/infra`
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Agent CLI Commands
|
||||
|
||||
Reference commands for installing and updating agent CLIs. Run only the commands you need.
|
||||
|
||||
## Install CLIs
|
||||
|
||||
Installed via Nix:
|
||||
|
||||
- codex
|
||||
- opencode
|
||||
- kimi-code
|
||||
- pi
|
||||
- omp
|
||||
|
||||
## Optional tooling
|
||||
|
||||
```bash
|
||||
# context7: up-to-date docs and code examples for LLMs and agents
|
||||
npx ctx7 setup
|
||||
```
|
||||
|
||||
## Update
|
||||
|
||||
The agent CLIs come from Nix (`home/base/gui/dev-tools.nix`) through the `llm-agents` flake input,
|
||||
so update them with the flake rather than npm.
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def install_one(target_dir: Path, source_file: Path, target_name: str) -> None:
|
||||
if not target_dir.exists():
|
||||
print(f"skipped {target_dir} (not found)")
|
||||
return
|
||||
|
||||
target_file = target_dir / target_name
|
||||
|
||||
if target_file.is_file() and not target_file.is_symlink():
|
||||
# Preserve the old inode without reading its contents or replacing an earlier backup.
|
||||
index = 0
|
||||
while True:
|
||||
suffix = ".bak" if index == 0 else f".bak.{index}"
|
||||
backup = target_file.with_name(target_file.name + suffix)
|
||||
try:
|
||||
os.link(target_file, backup)
|
||||
break
|
||||
except FileExistsError:
|
||||
index += 1
|
||||
print(f"backed up {target_file} -> {backup}")
|
||||
|
||||
# Create the link on the same filesystem before atomically replacing the destination.
|
||||
with tempfile.TemporaryDirectory(prefix=".install-rules-", dir=target_dir) as staging:
|
||||
link = Path(staging) / target_name
|
||||
link.symlink_to(source_file)
|
||||
link.replace(target_file)
|
||||
print(f"linked {target_file} -> {source_file}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
agents_file = script_dir / "AGENTS.md"
|
||||
|
||||
if not agents_file.is_file():
|
||||
print(f"Missing source file: {agents_file}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
codex_dir = Path(os.environ.get("CODEX_HOME", "~/.codex")).expanduser()
|
||||
xdg_config_home = Path(os.environ.get("XDG_CONFIG_HOME", "~/.config")).expanduser()
|
||||
opencode_dir = xdg_config_home / "opencode"
|
||||
pi_dir = Path("~/.pi/agent").expanduser()
|
||||
omp_dir = Path("~/.omp/agent").expanduser()
|
||||
agents_dir = Path("~/.agents").expanduser()
|
||||
targets = (
|
||||
(codex_dir, "AGENTS.md"),
|
||||
(opencode_dir, "AGENTS.md"),
|
||||
(pi_dir, "AGENTS.md"),
|
||||
(omp_dir, "AGENTS.md"),
|
||||
(agents_dir, "AGENTS.md"),
|
||||
)
|
||||
failed = False
|
||||
for target_dir, target_name in targets:
|
||||
try:
|
||||
install_one(target_dir, agents_file, target_name)
|
||||
except OSError as error:
|
||||
print(f"failed {target_dir / target_name}: {error}", file=sys.stderr)
|
||||
failed = True
|
||||
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -81,6 +81,13 @@ npx skills add anthropics/skills --skill 'pdf'
|
||||
npx skills add trailofbits/skills --skill 'codeql' --skill 'semgrep'
|
||||
```
|
||||
|
||||
## Other agent tooling (npx)
|
||||
|
||||
```bash
|
||||
# context7: up-to-date library docs and code examples for LLMs and agents
|
||||
npx ctx7 setup
|
||||
```
|
||||
|
||||
References:
|
||||
|
||||
- https://github.com/obra/superpowers
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import importlib.util
|
||||
import io
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"install_rules", Path(__file__).with_name("install-rules.py")
|
||||
)
|
||||
installer = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(installer)
|
||||
|
||||
|
||||
class InstallRulesTests(unittest.TestCase):
|
||||
def test_main_links_all_supported_agent_targets(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
home = Path(directory)
|
||||
target_names = (
|
||||
(home / ".codex", "AGENTS.md"),
|
||||
(home / ".config" / "opencode", "AGENTS.md"),
|
||||
(home / ".pi" / "agent", "AGENTS.md"),
|
||||
(home / ".omp" / "agent", "AGENTS.md"),
|
||||
(home / ".agents", "AGENTS.md"),
|
||||
)
|
||||
for target_dir, _ in target_names:
|
||||
target_dir.mkdir(parents=True)
|
||||
|
||||
with patch.dict(os.environ, {"HOME": str(home)}, clear=True):
|
||||
self.assertEqual(installer.main(), 0)
|
||||
|
||||
for target_dir, target_name in target_names:
|
||||
target = target_dir / target_name
|
||||
self.assertTrue(target.is_symlink(), target)
|
||||
self.assertEqual(target.resolve(), installer.Path(__file__).with_name("AGENTS.md"))
|
||||
|
||||
def test_target_failure_does_not_stop_remaining_targets(self):
|
||||
with (
|
||||
patch.object(
|
||||
installer,
|
||||
"install_one",
|
||||
side_effect=[PermissionError("denied"), None, None, None, None],
|
||||
) as install,
|
||||
patch("sys.stderr", new_callable=io.StringIO) as stderr,
|
||||
):
|
||||
self.assertEqual(installer.main(), 1)
|
||||
self.assertEqual(install.call_count, 5)
|
||||
self.assertIn("denied", stderr.getvalue())
|
||||
|
||||
def test_successful_targets_return_success(self):
|
||||
with patch.object(installer, "install_one") as install:
|
||||
self.assertEqual(installer.main(), 0)
|
||||
self.assertEqual(install.call_count, 5)
|
||||
|
||||
def test_existing_file_is_backed_up_without_overwriting_backup(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
source = root / "source.md"
|
||||
source.write_text("global rules")
|
||||
target = root / "AGENTS.md"
|
||||
target.write_text("personal rules")
|
||||
backup = root / "AGENTS.md.bak"
|
||||
backup.write_text("older rules")
|
||||
installer.install_one(root, source, target.name)
|
||||
self.assertEqual(target.resolve(), source)
|
||||
self.assertEqual(backup.read_text(), "older rules")
|
||||
self.assertEqual((root / "AGENTS.md.bak.1").read_text(), "personal rules")
|
||||
|
||||
def test_failed_link_creation_preserves_existing_file(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
source = root / "source.md"
|
||||
source.write_text("global rules")
|
||||
target = root / "AGENTS.md"
|
||||
target.write_text("personal rules")
|
||||
with patch.object(Path, "symlink_to", side_effect=OSError("link failed")):
|
||||
with self.assertRaises(OSError):
|
||||
installer.install_one(root, source, target.name)
|
||||
self.assertTrue(target.is_file())
|
||||
self.assertFalse(target.is_symlink())
|
||||
self.assertEqual(target.read_text(), "personal rules")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user