e818cfea8b
The reviewer pool was the only place naming harness+model combos, and blitz kept its own table inline. `reviewers` becomes `agents`, gains `roles` and `tiers`, and blitz routes from it via scripts/roster.sh. Config path moves to ~/.config/agent-skills/; the reviewer path stays readable so a box migrates with a mv. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
274 lines
10 KiB
Nix
274 lines
10 KiB
Nix
# Home-manager module: link centralized agent skills into Claude Code, Codex,
|
|
# Pi and opencode. Usage: add this flake as an input, then import this module
|
|
# in your home config.
|
|
#
|
|
# inputs.agent-skills.url = "git+https://git.naps.pt/yolo/agent-skills.git";
|
|
# # in home.nix imports: inputs.agent-skills.homeModules.default
|
|
# # and pick a machine profile:
|
|
# programs.agentSkills.machine = "yolo";
|
|
#
|
|
# recursive=true links each FILE individually, so machine-local skills can still
|
|
# live alongside the managed ones in the same dir (a whole-dir symlink would not).
|
|
{ agent-skills }:
|
|
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.programs.agentSkills;
|
|
|
|
# Pi and opencode have no `@file` imports in context files, so the shared
|
|
# fragments are concatenated into one AGENTS.md per tool.
|
|
concatMd =
|
|
name: files:
|
|
pkgs.writeText name (lib.concatMapStringsSep "\n" builtins.readFile files);
|
|
|
|
# A user unit gets almost no PATH by default; the units below shell out to
|
|
# aoe, git and tmux, which live in the profile dirs.
|
|
toolPath = lib.concatStringsSep ":" [
|
|
"%h/.local/bin"
|
|
"%h/.nix-profile/bin"
|
|
"/etc/profiles/per-user/${config.home.username}/bin"
|
|
"/run/current-system/sw/bin"
|
|
];
|
|
|
|
# These run from the working checkout, not the store: the scripts and the
|
|
# daemon are edited far more often than the flake input is bumped, and a
|
|
# restart is meant to be enough to pick a change up.
|
|
repo = cfg.repoPath;
|
|
|
|
mkEnable = what: lib.mkEnableOption "the ${what} user unit";
|
|
|
|
# Codex reads $CODEX_HOME/skills and nothing else -- ~/.agents/skills reaches
|
|
# pi and opencode but not codex, which is why codex review sessions reported
|
|
# review-pr as an unavailable skill. One symlink per skill dir rather than
|
|
# recursive=true: codex skips any skill whose SKILL.md is itself a symlink,
|
|
# which is exactly what a per-file link produces. Per-skill also leaves
|
|
# codex's own ~/.codex/skills/.system in place, which a whole-dir link would
|
|
# replace.
|
|
skillNames = builtins.attrNames (
|
|
lib.filterAttrs (_: type: type == "directory") (builtins.readDir "${agent-skills}/skills")
|
|
);
|
|
codexSkills = lib.listToAttrs (
|
|
map (name: lib.nameValuePair ".codex/skills/${name}" {
|
|
source = "${agent-skills}/skills/${name}";
|
|
}) skillNames
|
|
);
|
|
in
|
|
{
|
|
options.programs.agentSkills = {
|
|
machine = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "default";
|
|
example = "yolo";
|
|
description = ''
|
|
Which claude-md/machines/<name>.md to link as ~/.claude/machine.md.
|
|
The shared entry file @imports it, so it always has to resolve; the
|
|
"default" profile is the conservative one (no passwordless root).
|
|
'';
|
|
};
|
|
|
|
repoPath = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "%h/tea/agent-skills";
|
|
description = ''
|
|
Checkout the units run from, as a systemd unit specifier path. Not a
|
|
store path: the units are pointed at working copies so an edit takes
|
|
effect on restart instead of requiring a flake bump and a rebuild.
|
|
'';
|
|
};
|
|
|
|
# Off by default, and that matters: every one of these starts an agent
|
|
# session, so enabling them on a second machine would run the same job twice.
|
|
prDaemon.enable = mkEnable "PR daemon";
|
|
hourlog.enable = mkEnable "Friday hour log timer";
|
|
weekReview.enable = mkEnable "weekly review timer";
|
|
};
|
|
|
|
config.home.file = codexSkills // {
|
|
".claude/skills" = {
|
|
source = "${agent-skills}/skills";
|
|
recursive = true;
|
|
};
|
|
".agents/skills" = {
|
|
source = "${agent-skills}/skills";
|
|
recursive = true;
|
|
};
|
|
".claude/commands" = {
|
|
source = "${agent-skills}/commands";
|
|
recursive = true;
|
|
};
|
|
".claude/hooks" = {
|
|
source = "${agent-skills}/hooks";
|
|
recursive = true;
|
|
};
|
|
# Referenced by hook commands in settings.json, which this module does not
|
|
# own — so these have to exist under the same path on every machine.
|
|
".claude/scripts" = {
|
|
source = "${agent-skills}/scripts";
|
|
recursive = true;
|
|
};
|
|
|
|
# CLAUDE.md fragments land in ~/.claude root, pulled in by `@name.md` imports.
|
|
# Listed one by one: recursive on ~/.claude would fight every other tool
|
|
# writing there (settings.json, projects/, file-history/).
|
|
".claude/writing.md".source = "${agent-skills}/claude-md/writing.md";
|
|
".claude/operating.md".source = "${agent-skills}/claude-md/operating.md";
|
|
".claude/intercomms.md".source = "${agent-skills}/claude-md/intercomms.md";
|
|
|
|
# Per-machine section: what this box permits (sudo, network exposure).
|
|
".claude/machine.md".source = "${agent-skills}/claude-md/machines/${cfg.machine}.md";
|
|
|
|
# Path-scoped: loads only when Claude reads a matching source file.
|
|
".claude/rules/code-comments.md".source = "${agent-skills}/claude-md/code-comments.md";
|
|
".claude/RTK.md".source = "${agent-skills}/claude-md/RTK.md";
|
|
|
|
# Entry files: machine-local sections + @imports of the fragments above.
|
|
".claude/CLAUDE.md".source = "${agent-skills}/entry/CLAUDE.md";
|
|
".codex/AGENTS.md".source = "${agent-skills}/entry/codex-AGENTS.md";
|
|
|
|
# Pi: skills need no wiring — pi reads ~/.agents/skills, linked above.
|
|
# Settings stay unmanaged: pi writes ~/.pi/agent/settings.json itself.
|
|
".pi/agent/AGENTS.md".source = concatMd "pi-AGENTS.md" [
|
|
"${agent-skills}/claude-md/machines/${cfg.machine}.md"
|
|
"${agent-skills}/claude-md/operating.md"
|
|
"${agent-skills}/claude-md/writing.md"
|
|
"${agent-skills}/claude-md/code-comments.md"
|
|
"${agent-skills}/claude-md/intercomms.md"
|
|
"${agent-skills}/claude-md/RTK.md"
|
|
];
|
|
|
|
# opencode auto-loads skills from ~/.claude/skills and ~/.agents/skills,
|
|
# so only the rules file and commands need linking here.
|
|
".config/opencode/AGENTS.md".source = concatMd "opencode-AGENTS.md" [
|
|
"${agent-skills}/claude-md/opencode-header.md"
|
|
"${agent-skills}/claude-md/machines/${cfg.machine}.md"
|
|
"${agent-skills}/claude-md/operating.md"
|
|
"${agent-skills}/claude-md/writing.md"
|
|
"${agent-skills}/claude-md/code-comments.md"
|
|
"${agent-skills}/claude-md/intercomms.md"
|
|
"${agent-skills}/claude-md/RTK.md"
|
|
];
|
|
".config/opencode/commands" = {
|
|
source = "${agent-skills}/commands";
|
|
recursive = true;
|
|
};
|
|
};
|
|
|
|
# Unit definitions live here, next to the scripts they run; a machine opts in
|
|
# with `programs.agentSkills.<name>.enable`. Nothing is enabled by default --
|
|
# each of these starts an agent session, and two machines running the same
|
|
# timer means the same job twice.
|
|
config.systemd.user.services = lib.mkMerge [
|
|
(lib.mkIf cfg.prDaemon.enable {
|
|
pr-daemon = {
|
|
Unit = {
|
|
Description = "pr-daemon — watches GitHub/Gitea PRs and routes them to aoe sessions";
|
|
Documentation = [ "https://git.naps.pt/yolo/agent-skills" ];
|
|
After = [ "network.target" ];
|
|
# Not in the store: the config names the repos and the roster. A
|
|
# missing one would crash-loop against Restart=always. `|` makes
|
|
# these triggering conditions, so either path satisfies the pair --
|
|
# the config moved out of ~/.config/reviewer once blitz read it too.
|
|
ConditionPathExists = [
|
|
"|%h/.config/agent-skills/config.json"
|
|
"|%h/.config/reviewer/config.json"
|
|
];
|
|
# MUST stay 0: at RestartSec=5 a fast-crashing daemon burns the
|
|
# default 5-starts-per-10s budget and systemd parks the unit in
|
|
# `failed` until a manual `systemctl --user reset-failed`.
|
|
StartLimitIntervalSec = 0;
|
|
};
|
|
Service = {
|
|
Type = "simple";
|
|
WorkingDirectory = "%h";
|
|
ExecStart = "${pkgs.bun}/bin/bun ${repo}/bin/reviewer-poll.ts";
|
|
# Optional on both paths, for the same migration: whichever exists
|
|
# carries the read-only forge tokens.
|
|
EnvironmentFile = [ "-%h/.config/agent-skills/env" "-%h/.config/reviewer/env" ];
|
|
Environment = [
|
|
"PATH=${toolPath}"
|
|
# Without this the daemon reaches a different tmux server than the
|
|
# shell and TUI do, so sessions it starts are invisible where you
|
|
# look for them.
|
|
"TMUX_TMPDIR=%t"
|
|
];
|
|
Restart = "always";
|
|
RestartSec = 5;
|
|
# The agent tmux sessions this daemon starts land in its cgroup, so
|
|
# the default control-group kill takes every running agent down with
|
|
# a daemon restart.
|
|
KillMode = "process";
|
|
};
|
|
Install.WantedBy = [ "default.target" ];
|
|
};
|
|
})
|
|
|
|
(lib.mkIf cfg.hourlog.enable {
|
|
hourlog = {
|
|
Unit = {
|
|
Description = "Start the Friday hour log in a tmux session";
|
|
Documentation = [ "https://git.naps.pt/yolo/agent-skills" ];
|
|
ConditionPathIsDirectory = repo;
|
|
};
|
|
Service = {
|
|
Type = "oneshot";
|
|
ExecStart = "${repo}/bin/hourlog-session.sh";
|
|
Environment = [ "PATH=${toolPath}" ];
|
|
# This unit may be what starts the tmux server; the default cgroup
|
|
# kill would take it back down as soon as ExecStart returns.
|
|
KillMode = "process";
|
|
};
|
|
};
|
|
})
|
|
|
|
(lib.mkIf cfg.weekReview.enable {
|
|
week-review = {
|
|
Unit = {
|
|
Description = "Start the weekly agent-skills review in a tmux session";
|
|
Documentation = [ "https://git.naps.pt/yolo/agent-skills" ];
|
|
ConditionPathIsDirectory = repo;
|
|
};
|
|
Service = {
|
|
Type = "oneshot";
|
|
ExecStart = "${repo}/bin/week-review-session.sh";
|
|
Environment = [ "PATH=${toolPath}" ];
|
|
KillMode = "process";
|
|
};
|
|
};
|
|
})
|
|
];
|
|
|
|
config.systemd.user.timers = lib.mkMerge [
|
|
(lib.mkIf cfg.hourlog.enable {
|
|
hourlog = {
|
|
Unit.Description = "Friday hour log, 18:00 Europe/Lisbon";
|
|
Timer = {
|
|
# Zone suffix pinned because the machine clock is UTC; keeps it at
|
|
# 18:00 wall time across DST.
|
|
OnCalendar = "Fri 18:00 Europe/Lisbon";
|
|
Persistent = true;
|
|
AccuracySec = "1min";
|
|
};
|
|
Install.WantedBy = [ "timers.target" ];
|
|
};
|
|
})
|
|
|
|
(lib.mkIf cfg.weekReview.enable {
|
|
week-review = {
|
|
Unit.Description = "Weekly agent-skills review, Fridays 17:00 Europe/Lisbon";
|
|
Timer = {
|
|
OnCalendar = "Fri 17:00 Europe/Lisbon";
|
|
Persistent = true;
|
|
AccuracySec = "1min";
|
|
};
|
|
Install.WantedBy = [ "timers.target" ];
|
|
};
|
|
})
|
|
];
|
|
}
|
|
# Hook wiring lives in ~/.claude/settings.json, which this module does not own.
|
|
# See hooks/README.md for the snippet.
|