Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bot/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,10 @@ homegit's skills rendered for this operator, with the goals in
`~/.config/bot-harness/goals.md`:

install -m 644 bot/coordinator/operator-goals.md ~/.config/bot-harness/goals.md
install -m 644 bot/coordinator/operator-notes.md ~/.config/bot-harness/notes.md

The notes also tell it when to hand reviews and well-defined work to
opencode (`bot-opencode`, with the Copilot login from §5).

podman build -t localhost/bot-coordinator bot/coordinator/
mkdir -p ~/.config/containers/systemd
Expand Down
3 changes: 2 additions & 1 deletion bot/coordinator/Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ FROM localhost/bootc-bot-agent:latest

COPY --chmod=755 entrypoint.sh /usr/local/bin/bot-coordinator-entrypoint
COPY --chmod=755 render-skills.py /usr/local/bin/render-skills
COPY operator-goals.md /usr/local/share/bot-coordinator/operator-goals.md
COPY operator-goals.md operator-notes.md /usr/local/share/bot-coordinator/
COPY --chmod=755 bot-opencode /usr/local/bin/bot-opencode
ENV PATH=/home/agent/.local/bin:/home/agent/.cargo/bin:/usr/local/bin:/usr/bin:/bin \
BOT_LOGIN=jmarrero-bot \
HOMEGIT_URL=https://github.com/jmarrero-forge/homegit.git
Expand Down
16 changes: 15 additions & 1 deletion bot/coordinator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ writes it, so the fork is easy to update. If a homegit change moves a
section the renderer replaces, it stops with an error rather than render
his goals as yours; `bot/test_render.py` checks the result.

## opencode for reviews and well-defined work

The coordinator also has `bot-opencode`, which runs one task with opencode
on a GitHub Copilot model (`BOT_OPENCODE_MODEL`, with the
`jmarrero-bot-copilot-auth` secret; usage counts against the license
holder's premium requests). The operator's notes
(`~/.config/bot-harness/notes.md`, default
[`operator-notes.md`](operator-notes.md), appended to the rendered
coordinator skill) say when: a second, different-model review of every
forge PR, and items well-defined enough to need no design choices. Edit the
notes and restart the coordinator to change that. Runs are logged in
`~/.local/state/bot-opencode/` in the container.

## Use

podman build -t localhost/bot-coordinator bot/coordinator/
Expand All @@ -59,7 +72,8 @@ the bot."); comment it out to have it wait for you. If the Claude session
ends (`/exit`, or by accident), systemd restarts it after 60 seconds; stop it
for good with `systemctl --user stop bot-coordinator`. Attach to watch or
talk to it, and detach with Ctrl-b d. Its usage counts against the Claude
Max subscription.
Max subscription. It runs `claude-opus-5-5` at `high` effort
(`COORDINATOR_MODEL`, `COORDINATOR_EFFORT` in the `.container` file).

The coordinator only hears of news while `bot-poll` runs in its background,
and it sometimes stops it and doesn't start it again. The entrypoint's
Expand Down
7 changes: 7 additions & 0 deletions bot/coordinator/bot-coordinator.container
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ Image=localhost/bot-coordinator:latest
# Credentials as podman secrets (see bot/SETUP.md), never in the image.
Secret=jmarrero-bot-gh-token,type=env,target=GH_TOKEN
Secret=jmarrero-bot-claude-token,type=env,target=CLAUDE_CODE_OAUTH_TOKEN
# The Copilot login for bot-opencode (bot/copilot-login.sh), and its model.
Secret=jmarrero-bot-copilot-auth,type=env,target=OPENCODE_AUTH_CONTENT
Environment=BOT_OPENCODE_MODEL=github-copilot/gpt-6.1-sol
# The persistent home: clones, Claude's sessions, caches, devspace keys.
# No keep-id: the container's users map to subordinate ids, not to you.
Volume=bot-coordinator-home:/home/agent
Expand All @@ -21,6 +24,10 @@ Volume=%h/.config/bot-harness:/home/agent/.config/bot-harness:ro,z
# botd answers "@BOT <command> ..." at the start of a line; bot-notify leaves
# those to it. Keep in step with botd's agents ([agents.*]) plus help.
Environment="BOT_NOTIFY_SKIP_COMMANDS=claude opencode help"
# The coordinator's model, pinned by full name (an alias like "opus" moves
# with new releases), and its effort (low, medium, high, xhigh, max).
Environment=COORDINATOR_MODEL=claude-opus-5-5
Environment=COORDINATOR_EFFORT=high
# Network through the host (pasta), which is on the tailnet.
PodmanArgs=--memory=16g --cpus=4 --pids-limit=8192
NoNewPrivileges=true
Expand Down
43 changes: 43 additions & 0 deletions bot/coordinator/bot-opencode
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/bin/bash
# Run one task with opencode on a GitHub Copilot model, for the coordinator:
# a second model family for reviews, and well-defined work. See the operator
# notes in the coordinator skill for when to use it.
#
# bot-opencode [--model PROVIDER/MODEL] [--dir DIR] [--name NAME] PROMPT
#
# Runs `opencode run --auto` in DIR (default: the current directory) and
# prints its final output; the whole run is kept in
# ~/.local/state/bot-opencode/NAME-TIMESTAMP.log. Start it with the Bash
# tool's run_in_background, so its exit wakes the session. The login comes
# from OPENCODE_AUTH_CONTENT (a podman secret); the model from --model or
# BOT_OPENCODE_MODEL.
set -euo pipefail

model=${BOT_OPENCODE_MODEL:-github-copilot/gpt-6.1-sol}
dir=$PWD
name=task
while [ $# -gt 1 ]; do
case $1 in
--model) model=$2; shift 2 ;;
--dir) dir=$2; shift 2 ;;
--name) name=$2; shift 2 ;;
--) shift; break ;;
-*) echo "bot-opencode: unknown option $1" >&2; exit 2 ;;
*) break ;;
esac
done
[ $# -eq 1 ] || { sed -n '6,7p' "$0" | sed 's/^# //' >&2; exit 2; }
: "${OPENCODE_AUTH_CONTENT:?no Copilot login (OPENCODE_AUTH_CONTENT)}"
[[ "$name" =~ ^[A-Za-z0-9._-]+$ ]] || { echo "bot-opencode: bad --name" >&2; exit 2; }

logdir=$HOME/.local/state/bot-opencode
mkdir -p "$logdir"
log=$logdir/$name-$(date -u +%Y%m%dT%H%M%SZ).log
echo "bot-opencode: $model in $dir; log $log" >&2
cd "$dir"
set +e
opencode run --auto -m "$model" "$1" > >(tee "$log") 2>>"$log"
rc=$?
set -e
echo "bot-opencode: exit $rc (log $log)" >&2
exit $rc
8 changes: 7 additions & 1 deletion bot/coordinator/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ fi
# the model sometimes copies). Rendered again on every start, after the pull.
goals=$HOME/.config/bot-harness/goals.md
test -s "$goals" || goals=/usr/local/share/bot-coordinator/operator-goals.md
notes=$HOME/.config/bot-harness/notes.md
test -s "$notes" || notes=/usr/local/share/bot-coordinator/operator-notes.md
rendered=$HOME/.local/share/bot-skills
render-skills "$homegit" "$rendered" "$goals"
render-skills "$homegit" "$rendered" "$goals" "$notes"
ln -sfn "$rendered/skills" "$HOME/.agents/skills"
rm -f "$HOME"/.claude/skills/*
for skill in "$rendered"/skills/*/; do
Expand Down Expand Up @@ -71,6 +73,10 @@ fi
# are skipped (it runs unattended, in this container); COORDINATOR_PROMPT,
# if set, is the first message, e.g. "Load the coordinator skill and run".
cmd=(claude --dangerously-skip-permissions)
# Its model and effort (COORDINATOR_MODEL, COORDINATOR_EFFORT); unset means
# Claude Code's defaults. Subagents inherit the model unless they set one.
[ -n "${COORDINATOR_MODEL:-}" ] && cmd+=(--model "$COORDINATOR_MODEL")
[ -n "${COORDINATOR_EFFORT:-}" ] && cmd+=(--effort "$COORDINATOR_EFFORT")
[ -n "${COORDINATOR_PROMPT:-}" ] && cmd+=("$COORDINATOR_PROMPT")
cd "$clones"
tmux new-session -d -s coordinator -x 200 -y 50 "$(printf '%q ' "${cmd[@]}")"
Expand Down
46 changes: 46 additions & 0 deletions bot/coordinator/operator-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
## Operator's notes

These come from jmarrero (`~/.config/bot-harness/notes.md`) and add to the
rest of this skill.

### After a restart

A session that ends takes its workers with it: subagents and `bot-opencode`
runs die mid-task, while their items stay In Progress on the board. So at
the start of every session, right after the first `bot-poll --once`, run
`bot-board list --status "In Progress"` (and check the board for items
without a priority too). For each item, look at its PR, branch and Why to
see how far it got, then resume it with a new worker (or `bot-opencode`)
briefed with that state, or set it to Needs human with the reason. Say
which in your start-up summary. Never leave an item In Progress without a
worker on it.

### Second opinions and well-defined work with opencode

`bot-opencode` runs one task with opencode on a GitHub Copilot model (a
different model family from yours; usage counts against jmarrero's Copilot
premium requests). Always start it with the Bash tool's
`run_in_background`, so its exit wakes you, and give it `--name` (e.g. the
item or PR) and `--dir` (a worktree of its own). Run at most two at once.

- **Reviews.** Every forge PR also gets a review from opencode, besides the
reviewer subagent: brief it like a reviewer, starting with "Read
~/.agents/skills/coordinator/reviewer-preamble.md and follow it", plus the
PR URL and head SHA, and have it post its verdict as a review comment on
the PR (it has the bot's `gh` login). Weigh both verdicts; when they
disagree, say so on the PR and in the item's Why, and treat the stricter
one as standing until resolved.
- **Well-defined work.** An item is well-defined when its issue says what to
change and how to check it, with no design choices left: dependency
bumps, mechanical refactors and renames, a test for a described case, a
clear compiler, clippy or CI fix, docs that follow a stated change. Send
those to opencode instead of a Claude worker: brief it starting with
"Read ~/.agents/skills/coordinator/worker-preamble.md and follow it",
plus the item URL, the repository and the worktree. Everything else
(design, ambiguous asks, debugging an unknown cause, anything touching
sign-off, credentials or the sandbox) stays with Claude workers.
- Its result gets the same review as any worker's: a reviewer subagent, and
you before it reaches jmarrero. If it fails or wanders off, hand the item
to a Claude worker and note it in the Why field.
- The default model is `BOT_OPENCODE_MODEL`; `--model` overrides it
(`opencode models github-copilot` lists them).
12 changes: 10 additions & 2 deletions bot/coordinator/render-skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
link), so the coordinator gets copies with this operator's values filled in.
homegit stays as cgwalters writes it, which keeps the fork easy to update.

render-skills.py HOMEGIT DEST GOALS
render-skills.py HOMEGIT DEST GOALS [NOTES]

writes DEST/skills/<name>/... (from dotfiles/.agents/skills), DEST/AGENTS.md
and DEST/agents/builder.md, with values from `bot-operator --json`. GOALS is
a Markdown file replacing cgwalters' goals and priority rules. What still
a Markdown file replacing cgwalters' goals and priority rules; NOTES, the
operator's own notes, is appended to the coordinator skill. What still
names cgwalters afterwards (links to his own repositories, issues and the
coordination channel, kept on purpose) is listed on stderr.
"""
Expand Down Expand Up @@ -137,6 +138,7 @@ def stash(m):

def main():
homegit, dest, goals_file = sys.argv[1:4]
notes = open(sys.argv[4]).read() if len(sys.argv) > 4 else ""
cfg = json.loads(subprocess.run(["bot-operator", "--json"], check=True,
capture_output=True, text=True).stdout)
goals = open(goals_file).read()
Expand All @@ -160,6 +162,9 @@ def main():
sys.exit(f"render-skills: expected one match of {pattern[:60]!r} in {rel}, found {n}")
open(path, "w").write(new)

if notes.strip():
# After rendering, so the operator's text is taken as written.
notes_path = os.path.join(tmp, "skills/coordinator/SKILL.md")
leftovers = []
for root, _, files in os.walk(tmp):
for name in files:
Expand All @@ -172,6 +177,9 @@ def main():
if re.search(r"(?i)cgwalters|colin|verbum", line):
leftovers.append(f"{os.path.relpath(path, tmp)}:{i}: {line.strip()[:120]}")

if notes.strip():
with open(notes_path, "a") as f:
f.write("\n" + notes.strip() + "\n")
shutil.rmtree(dest, ignore_errors=True)
os.rename(tmp, dest)
print(f"render-skills: rendered for {cfg['operator']['login']}/{cfg['bot']['login']}; "
Expand Down
8 changes: 7 additions & 1 deletion bot/test_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
HERE = os.path.dirname(os.path.abspath(__file__))
RENDER = os.path.join(HERE, "coordinator", "render-skills.py")
GOALS = os.path.join(HERE, "coordinator", "operator-goals.md")
NOTES = os.path.join(HERE, "coordinator", "operator-notes.md")
HOMEGIT = os.environ.get("HOMEGIT", os.path.expanduser("~/development/github/jmarrero-forge/homegit"))
CONFIG = json.load(open(os.path.join(HERE, "operator.json")))

Expand All @@ -30,7 +31,7 @@ def setUpClass(cls):
os.chmod(stub, stat.S_IRWXU)
cls.out = os.path.join(cls.tmp, "rendered")
env = dict(os.environ, PATH=f"{cls.tmp}:{os.environ['PATH']}")
cls.proc = subprocess.run([sys.executable, RENDER, HOMEGIT, cls.out, GOALS],
cls.proc = subprocess.run([sys.executable, RENDER, HOMEGIT, cls.out, GOALS, NOTES],
env=env, capture_output=True, text=True)

def read(self, rel):
Expand Down Expand Up @@ -69,6 +70,11 @@ def test_peers_are_cgwalters_harness(self):
self.assertIn("coordination questions from cgwalters-bot and cgwalters", self.read("skills/bot-notify/SKILL.md"))
self.assertIn("the channel with cgwalters' harness", self.read("skills/coordinator/worker-preamble.md"))

def test_operator_notes_are_appended(self):
coordinator = self.read("skills/coordinator/SKILL.md")
self.assertTrue(coordinator.rstrip().endswith(open(NOTES).read().strip()))
self.assertIn("bot-opencode", coordinator)

def test_paths_point_at_the_rendered_skills(self):
self.assertIn("~/.agents/skills/coordinator/", self.read("skills/coordinator/SKILL.md"))
self.assertNotIn("homegit/dotfiles/.agents/skills", self.read("skills/coordinator/SKILL.md"))
Expand Down