Compare commits
5 Commits
weekly-updates
...
kimi
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a55c42408 | |||
| 4dd0c9d241 | |||
| 5d1a81ec48 | |||
| 465b20a7c6 | |||
| 7c9b5cf4e0 |
@@ -45,6 +45,19 @@ imports = [ inputs.agent-skills.homeModules.default ];
|
||||
|
||||
`recursive = true` links files individually, so machine-local skills can coexist in the same dir. `nixos-rebuild switch` to apply/update.
|
||||
|
||||
The module also carries the user units — `pr-daemon`, `hourlog`, `week-review` — so each lives next to the script it runs. All three are off by default, because every one of them starts an agent session and a second machine enabling them would run the same job twice:
|
||||
|
||||
```nix
|
||||
programs.agentSkills = {
|
||||
machine = "yolo";
|
||||
prDaemon.enable = true;
|
||||
hourlog.enable = true;
|
||||
weekReview.enable = true;
|
||||
};
|
||||
```
|
||||
|
||||
`repoPath` (default `%h/tea/agent-skills`) is what the units execute from. Deliberately a checkout rather than a store path: the daemon and the scripts change far more often than the flake input is bumped, so a restart is enough to pick up an edit. The `systemd/` unit files stay for non-Nix machines, where `link.sh` installs them.
|
||||
|
||||
## Shared machine, many sessions
|
||||
|
||||
Several autonomous runs share one box. `skills/linear-common/scripts/gate.sh` is a machine-wide semaphore for heavy commands (full test suites, whole-project builds): bounded slots, memory + CPU cap via a systemd user scope, pinned build/test parallelism. Skills run scoped checks in the inner loop and put only the once-per-push full suite through the gate; exit 75 means it never ran and CI takes over. Policy lives in `linear-common/COMMON.md` under "Local verification budget".
|
||||
@@ -164,11 +177,11 @@ Every pick is appended to `ledger` (default
|
||||
`~/.local/state/reviewer/reviewers.jsonl`):
|
||||
|
||||
```json
|
||||
{"at":"…","pr":"gitea:yolo/rev#75","title":"rev-75-fix-race","reviewer":"pi/kimi-k3@high","author":"claude"}
|
||||
{"at":"…","pr":"gitea:yolo/rev#75","title":"rev-75-fix-race","reviewer":"pi/gpt5.6@high","author":"claude"}
|
||||
```
|
||||
|
||||
That's the raw material for rating later — group by harness, by model, or by
|
||||
effort, and `pi/kimi-k3@med` against `@high` is the cleanest comparison in
|
||||
effort, and `pi/gpt5.6@med` against `@high` is the cleanest comparison in
|
||||
there. It's append-only analytics, not routing state, so nothing the daemon
|
||||
does depends on it surviving.
|
||||
|
||||
|
||||
@@ -44,8 +44,9 @@
|
||||
{ "id": "claude/opus@med", "tool": "claude", "args": ["--model", "opus", "--effort", "medium"] },
|
||||
{ "id": "claude/opus@high", "tool": "claude", "args": ["--model", "opus", "--effort", "high"] },
|
||||
{ "id": "pi/gpt5.6@high", "tool": "pi", "args": ["--model", "openai-codex/gpt-5.6-sol:high"] },
|
||||
{ "id": "pi/kimi-k3@med", "tool": "pi", "args": ["--model", "synthetic/hf:moonshotai/Kimi-K3:medium"] },
|
||||
{ "id": "oc/glm5.2", "tool": "opencode", "args": ["--model", "synthetic/hf:zai-org/GLM-5.2"] },
|
||||
{ "id": "pi/gpt5.6@med", "tool": "pi", "args": ["--model", "openai-codex/gpt-5.6-sol:medium"] },
|
||||
{ "id": "claude/fable@high", "tool": "claude", "args": ["--model", "fable", "--effort", "high"] },
|
||||
{ "id": "oc/gpt5.6", "tool": "opencode", "args": ["--model", "openai/gpt-5.6-sol"] },
|
||||
{ "id": "codex/gpt5.6@high", "tool": "codex", "enabled": false, "args": ["-c", "model_reasoning_effort=high"] }
|
||||
]
|
||||
}
|
||||
|
||||
+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.
|
||||
|
||||
+11
-3
@@ -61,9 +61,11 @@ fi
|
||||
Gitea equivalent — issue comments plus reviews:
|
||||
|
||||
```bash
|
||||
{ curl -sS -H "Authorization: token $GITEA_TOKEN" "$BASE/api/v1/repos/$REPO/issues/$N/comments"; \
|
||||
curl -sS -H "Authorization: token $GITEA_TOKEN" "$BASE/api/v1/repos/$REPO/pulls/$N/reviews"; } 2>/dev/null \
|
||||
| jq -r '.[]?.id' > "$seen" || : > "$seen"
|
||||
{ curl -sS -H "Authorization: token $GITEA_TOKEN" "$BASE/api/v1/repos/$REPO/issues/$N/comments" | jq -r '.[]?.id'
|
||||
for r in $(curl -sS -H "Authorization: token $GITEA_TOKEN" "$BASE/api/v1/repos/$REPO/pulls/$N/reviews" | jq -r '.[]?.id'); do
|
||||
echo "$r"
|
||||
curl -sS -H "Authorization: token $GITEA_TOKEN" "$BASE/api/v1/repos/$REPO/pulls/$N/reviews/$r/comments" | jq -r '.[]?.id'
|
||||
done; } 2>/dev/null > "$seen" || : > "$seen"
|
||||
```
|
||||
|
||||
**Request the Copilot review** (github only, once). Its comments then
|
||||
@@ -111,6 +113,12 @@ and act on what's left:
|
||||
echo "$rid" >> "$seen"
|
||||
```
|
||||
|
||||
Gitea has no reply endpoint, so that lands as a loose PR comment. To
|
||||
answer a code comment inside its own thread, post a review instead
|
||||
whose `comments[]` entry repeats the same `path` and `new_position` —
|
||||
gitea groups code comments by position into one conversation. Record
|
||||
the review id and its comment ids.
|
||||
|
||||
- **Resolve the thread** (github only — gitea has no per-thread
|
||||
resolve, so a short confirming reply plus the pushed fix is the
|
||||
signal):
|
||||
|
||||
+40
-15
@@ -62,43 +62,68 @@ than reviewing hunks in isolation.
|
||||
|
||||
**Write the findings** to `<git-dir>/pr-<N>-findings.md` — in the git
|
||||
dir, not the working tree, so nothing lands in the branch under review.
|
||||
One finding per entry: `path:line`, what's wrong, what to do. No praise,
|
||||
no summary of what the PR does, no severity theatre. If you find
|
||||
nothing, say so in one line.
|
||||
One finding per entry: `path:line`, what's wrong, what to do — and mark
|
||||
whether it anchors to a diff line or is a loose remark about the change
|
||||
as a whole, which decides where it goes in §2. No praise, no summary of
|
||||
what the PR does, no severity theatre. If you find nothing, say so in
|
||||
one line.
|
||||
|
||||
Then follow the mode: post (gitea) or report the file to the user and
|
||||
stop (github).
|
||||
|
||||
## 2. Posting
|
||||
|
||||
Post **one review** per pass, never a stream of separate comments. A
|
||||
review carries two kinds of finding at once:
|
||||
|
||||
- **Anchored** — the finding is about a specific line in the diff. It
|
||||
belongs in `comments[]` with a `path` and a line, so it renders on
|
||||
the code.
|
||||
- **Loose** — the finding is about the change as a whole, or about code
|
||||
the diff doesn't touch, or it has no single line to sit on. It goes
|
||||
in the review `body`.
|
||||
|
||||
Anchor whatever can be anchored. Writing `path:line` into prose when
|
||||
the API would have put the comment on that line is the failure mode
|
||||
this section exists to prevent.
|
||||
|
||||
Only after the user's go-ahead on gated repos. Record every id you post
|
||||
in the same step, or the next hint reads your own review as new
|
||||
feedback:
|
||||
|
||||
```bash
|
||||
# gitea
|
||||
rid=$(curl -sS -X POST -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \
|
||||
"$BASE/api/v1/repos/$REPO/issues/$N/comments" -d "$(jq -nc --arg body "<finding>" '{body:$body}')" | jq -r .id)
|
||||
# gitea — body is the loose findings, comments[] the anchored ones
|
||||
# new_position = line in the new file; use old_position for a removed line
|
||||
rid=$(jq -nc \
|
||||
--arg body "<loose findings, or empty>" \
|
||||
--argjson comments '[{"path":"path/to/file.ts","new_position":11,"body":"<finding>"}]' \
|
||||
'{event:"COMMENT", body:$body, comments:$comments}' \
|
||||
| curl -sS -X POST -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \
|
||||
"$BASE/api/v1/repos/$REPO/pulls/$N/reviews" -d @- | jq -r .id)
|
||||
echo "$rid" >> "$seen"
|
||||
curl -sS -H "Authorization: token $GITEA_TOKEN" \
|
||||
"$BASE/api/v1/repos/$REPO/pulls/$N/reviews/$rid/comments" | jq -r '.[].id' >> "$seen"
|
||||
```
|
||||
|
||||
```bash
|
||||
# github, after approval
|
||||
rid=$(gh api repos/<OWNER>/<REPO>/pulls/<N>/comments -f body="<finding>" \
|
||||
-f commit_id=<sha> -f path=<path> -F line=<line> --jq .id)
|
||||
echo "$rid" >> "$seen"
|
||||
# github, after approval — same shape, `line` instead of new_position
|
||||
jq -nc --arg body "<loose findings, or empty>" \
|
||||
--argjson comments '[{"path":"path/to/file.ts","line":11,"body":"<finding>"}]' \
|
||||
'{event:"COMMENT", commit_id:"<sha>", body:$body, comments:$comments}' \
|
||||
| gh api repos/<OWNER>/<REPO>/pulls/<N>/reviews --input - --jq .id >> "$seen"
|
||||
gh api repos/<OWNER>/<REPO>/pulls/<N>/comments --jq '.[].id' >> "$seen"
|
||||
```
|
||||
|
||||
Prefer one review with several comments over a stream of separate
|
||||
comments. **Never approve and never request changes as a review
|
||||
decision** — that's the user's call on someone else's PR, and it carries
|
||||
weight your findings don't.
|
||||
`event: "COMMENT"` is the only event either forge should see from you.
|
||||
**Never approve and never request changes as a review decision** —
|
||||
that's the user's call on someone else's PR, and it carries weight your
|
||||
findings don't.
|
||||
|
||||
## 3. Handling a hint
|
||||
|
||||
| reason | what to do |
|
||||
| --- | --- |
|
||||
| `comments` | Read comments not in the seen file. Someone replying to a finding gets an answer; a new comment thread may need a fresh look at that code. Record every id you handle or post. |
|
||||
| `comments` | Read comments not in the seen file. Someone replying to a finding gets an answer; a new comment thread may need a fresh look at that code. Reply in the thread it came from: on github, `POST /pulls/<N>/comments/<cid>/replies`; on gitea there is no reply endpoint, so post a review whose `comments[]` entry carries the same `path` and line — gitea groups code comments by position into one conversation. A loose reply goes to `POST /issues/<N>/comments`. Record every id you handle or post. |
|
||||
| `ci` | New head SHA: the author pushed. Re-read the diff for the new commits only, and check whether your open findings are addressed. Do not investigate their CI failures — not your PR. |
|
||||
| `state` | Merged or closed: write the outcome to the state file and stop. Draft flips: nothing to do. |
|
||||
| `conflicts` | Nothing to do. The author resolves conflicts on their own branch. |
|
||||
|
||||
Reference in New Issue
Block a user