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
13 changes: 13 additions & 0 deletions 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 Down
3 changes: 3 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 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
4 changes: 3 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
34 changes: 34 additions & 0 deletions bot/coordinator/operator-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
## Operator's notes

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

### 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