From daa841475bee3779791d70afad68e87bc2945c23 Mon Sep 17 00:00:00 2001 From: Joseph Marrero Date: Fri, 2 Oct 2026 19:57:46 -0400 Subject: [PATCH] bot-coordinator: Use opencode for second-opinion reviews and well-defined work All of the coordinator's workers and reviewers were Claude subagents. Give it bot-opencode, which runs one task with opencode on a GitHub Copilot model (the jmarrero-bot-copilot-auth secret, BOT_OPENCODE_MODEL), logged under ~/.local/state/bot-opencode. The operator's notes (~/.config/bot-harness/notes.md, operator-notes.md by default), which render-skills now appends to the coordinator skill, say when: a review from a different model family on every forge PR, besides the reviewer subagent, and items well-defined enough to need no design choices, under the same review as any worker's. Co-Authored-By: Claude Opus 5.5 --- bot/SETUP.md | 4 +++ bot/coordinator/Containerfile | 3 +- bot/coordinator/README.md | 13 +++++++ bot/coordinator/bot-coordinator.container | 3 ++ bot/coordinator/bot-opencode | 43 +++++++++++++++++++++++ bot/coordinator/entrypoint.sh | 4 ++- bot/coordinator/operator-notes.md | 34 ++++++++++++++++++ bot/coordinator/render-skills.py | 12 +++++-- bot/test_render.py | 8 ++++- 9 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 bot/coordinator/bot-opencode create mode 100644 bot/coordinator/operator-notes.md diff --git a/bot/SETUP.md b/bot/SETUP.md index 3f9459d..052dbc2 100644 --- a/bot/SETUP.md +++ b/bot/SETUP.md @@ -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 diff --git a/bot/coordinator/Containerfile b/bot/coordinator/Containerfile index 3331408..e46acd1 100644 --- a/bot/coordinator/Containerfile +++ b/bot/coordinator/Containerfile @@ -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 diff --git a/bot/coordinator/README.md b/bot/coordinator/README.md index d504d54..e85a1be 100644 --- a/bot/coordinator/README.md +++ b/bot/coordinator/README.md @@ -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/ diff --git a/bot/coordinator/bot-coordinator.container b/bot/coordinator/bot-coordinator.container index 952d87c..5c78800 100644 --- a/bot/coordinator/bot-coordinator.container +++ b/bot/coordinator/bot-coordinator.container @@ -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 diff --git a/bot/coordinator/bot-opencode b/bot/coordinator/bot-opencode new file mode 100644 index 0000000..a352c12 --- /dev/null +++ b/bot/coordinator/bot-opencode @@ -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 diff --git a/bot/coordinator/entrypoint.sh b/bot/coordinator/entrypoint.sh index 389bc2d..c3c4710 100644 --- a/bot/coordinator/entrypoint.sh +++ b/bot/coordinator/entrypoint.sh @@ -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 diff --git a/bot/coordinator/operator-notes.md b/bot/coordinator/operator-notes.md new file mode 100644 index 0000000..6305795 --- /dev/null +++ b/bot/coordinator/operator-notes.md @@ -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). diff --git a/bot/coordinator/render-skills.py b/bot/coordinator/render-skills.py index 6ba1298..810ca51 100644 --- a/bot/coordinator/render-skills.py +++ b/bot/coordinator/render-skills.py @@ -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//... (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. """ @@ -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() @@ -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: @@ -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']}; " diff --git a/bot/test_render.py b/bot/test_render.py index b31ae38..5a97c63 100644 --- a/bot/test_render.py +++ b/bot/test_render.py @@ -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"))) @@ -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): @@ -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"))