feat(nix): ship the pr-daemon, hourlog and week-review units
The units ran from nixos-config while the scripts they execute live here, so a fix like KillMode had to be made in the repo that does not contain the daemon. They are defined here now and each machine opts in with programs.agentSkills.<name>.enable, which keeps the property that nothing starts a session unless a host asks for it. ExecStart still points at the checkout, not the store: an edit should take effect on restart rather than needing a flake bump. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+152
-9
@@ -24,17 +24,51 @@ let
|
||||
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";
|
||||
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).
|
||||
'';
|
||||
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 = {
|
||||
@@ -103,6 +137,115 @@ in
|
||||
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" ];
|
||||
# Neither is in the store: the config names the repos, the env file
|
||||
# holds the read-only forge tokens. A missing config would crash-loop
|
||||
# against Restart=always.
|
||||
ConditionPathExists = [
|
||||
"%h/.config/reviewer/config.json"
|
||||
"%h/.config/reviewer/env"
|
||||
];
|
||||
# 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";
|
||||
EnvironmentFile = "%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.
|
||||
|
||||
Reference in New Issue
Block a user