Files
agents/.gitea/workflows/scripts/run-agent.sh
T
Felix FaerberandClaude Opus 4.8 84cec444ef agents: PM-orchestrated flow — @pm hub, @qa reviews, human/@pm merges
Rework the delegation/trigger choreography so @pm orchestrates end-to-end from
the issue thread while @qa reviews on the PR and never merges.

Flow: issue → @pm plans (asks creator; autopilot skips) → dev builds PR → dev
pings @pm on the issue → @pm hands to @qa → @qa reviews ON THE PR (recommends /
BOUNCE:@dev, direct qa↔dev loop, 3 max) → @qa APPROVEs → verdict on the ISSUE →
@pm → normal: tells creator "ready to merge" (human merges); autopilot: @pm
MERGE_PR. @pm never reads or comments on the PR (small context); @qa never merges.

- run-agent.sh: @pm prompt = 3 phases (plan / hand PR to @qa / finalize),
  autopilot only drops the approval gate + adds @pm MERGE_PR. @qa prompt =
  reviewer, ends with APPROVE / BOUNCE:@dev / HALT; identical in both flows.
- publish.sh: shared post_to()/trig()/resolve_pr() helpers + ISSN; @qa routes
  reply to PR (bounce) vs issue (approve) and fires @pm/@dev; @pm handles
  DELEGATE:@dev, DELEGATE:@qa (resolves the PR), and autopilot MERGE_PR; dev
  NEW=true fires @pm on the issue, NEW=false fires @qa on the PR. Strip
  APPROVE/HALT markers.

Tokens re-minted to match: TOKEN_PM gains write:repository (it merges now),
TOKEN_QA drops to read:repository (never merges).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:06:39 +03:00

157 lines
9.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Run the agent: build the full prompt, invoke opencode with retries, and reconstruct the
# plain-text reply (/tmp/agent_out.md) plus the raw event stream (/tmp/events.jsonl).
#
# Required env (provided by the workflow step):
# ANTHROPIC_API_KEY SELF_TOKEN NAME MODEL VISION MODE HAS_IMAGES BRANCH AUTOPILOT NUM TITLE
# IBODY CMT
# FILES (the opencode -f image flags, from the imgs step output)
# AUTOPILOT is 'true' when the issue carries the `autopilot` label (label-gated autopilot mode).
set -u
[ -z "$CMT" ] && CMT="(a new issue was just opened — assess it)"
THREAD=$(cat /tmp/thread.md 2>/dev/null); [ -z "$THREAD" ] && THREAD="(no prior comments)"
DESC=$(jq -r --arg a "$NAME" '.[$a].desc' /tmp/agents.json)
# Include each teammate's registry skills so an agent (esp. @pm) can route by capability — e.g. only
# skill-holders should get a task that needs that skill. Skill *names* only; the scoped how-to detail
# stays hidden per the permission.skill allow-list. (Caller-provided skills from a repo's
# .gitea/agent-skills/ are not in this roster — document that routing in the caller's AGENTS.md.)
ROSTER=$(jq -r 'to_entries | map("- @\(.key): \(.value.desc) (vision: \(.value.vision); skills: \(.value.skills | if length>0 then join(", ") else "none" end))") | join("\n")' /tmp/agents.json)
if [ "$VISION" = "true" ]; then CAP="You CAN read images attached to the issue."; else CAP="You CANNOT read images — you are a text-only model."; fi
NOTE=""
if [ "$VISION" != "true" ] && [ "${HAS_IMAGES:-0}" -gt 0 ]; then
NOTE="IMPORTANT: this issue has image attachment(s) you cannot read. Do NOT guess their contents — say so and tell the maintainer to re-run with a vision-capable teammate (@senior, @lead, or @pm)."
fi
if [ "$MODE" = "comment" ]; then
ACTION="You do NOT edit files, create branches, or write a PR description. Respond with your analysis,
plan, research, or clarifying questions — your reply becomes a comment on the issue.
To hand work to a teammate, end your reply with EXACTLY one line: 'DELEGATE: @<agent>' (one of
@junior @senior @lead @qa) — but ONLY when you are ready to hand off AND need nothing further from the
maintainer. If you are asking @ffaerber to confirm or decide ANYTHING, do NOT include a DELEGATE line;
just ask and wait. Never ask for confirmation and delegate in the same reply. Mentioning a teammate in
prose does NOT delegate — only the DELEGATE line does.
To CLOSE the issue (the maintainer says it is not needed / a duplicate / won't-do), briefly note why
and end your reply with EXACTLY one line: 'CLOSE_ISSUE'. Only close when clearly instructed or it is
obviously not needed; when in doubt, ask instead."
if [ "$NAME" = "pm" ]; then
ACTION="$ACTION
As PM you ORCHESTRATE this issue from the ISSUE THREAD ONLY — you never read or comment on the PR
(keep your context on the issue). Read the thread and act for the CURRENT phase:
PHASE 1 — PLAN (a fresh request; no dev is building yet). Present a SHORT plan naming which
teammate should build it (@junior small/low-risk YAML/compose/config; @senior/@lead complex or
multi-file). Then END by asking '@ffaerber ready to start building? reply yes to proceed.' — do
NOT delegate yet. ONLY after an explicit 'yes'/'go'/'proceed' do you end a reply with a
'DELEGATE: @<dev>' line to hand off. Never plan and delegate in the same reply.
PHASE 2 — REVIEW (a dev has reported 'PR #<n> is ready'). Do NOT re-plan. Briefly acknowledge and
hand the PR to QA: end your reply with EXACTLY 'DELEGATE: @qa'. (The automation tells @qa which PR
to review; @qa reviews it on the PR, not here — you never see the diff.)
PHASE 3 — FINALIZE (@qa has reported the PR is approved / 'code OK'). Tell the issue creator it is
ready: e.g. 'PR #<n> is reviewed and ready to merge, @ffaerber.' Do NOT delegate and do NOT merge —
the human merges.
If anything is unclear or needs a decision at any phase, START your reply with '@ffaerber', ask
specific questions, and do NOT emit a marker. Mentioning a teammate in prose does NOT act — only a
DELEGATE line does.
BREAKDOWN (a feature too big for one PR): in PHASE 1, propose a milestone name and the sub-task
list, then ask '@ffaerber create these N sub-issues? reply yes.' ONLY after approval, end with:
BEGIN_SUBTASKS
milestone: <feature name>
- <task title> :: <one-line description>
- <task title> :: <one-line description>
END_SUBTASKS
The automation creates the milestone + one sub-issue per line (each linked here); it does NOT
auto-start any dev — the maintainer @mentions an agent on each sub-issue when ready."
if [ "$AUTOPILOT" = "true" ]; then
ACTION="$ACTION
AUTOPILOT MODE IS ACTIVE (this issue carries the 'autopilot' label) — it changes exactly TWO
things for you; everything else above is unchanged:
- PHASE 1: do NOT ask '@ffaerber ready to build?'. Present your SHORT plan AND end with a
'DELEGATE: @<dev>' line in the SAME reply. Only skip delegating (and ask @ffaerber) if the task
is genuinely ambiguous or unsafe.
- PHASE 3: do NOT ask the human to merge. When @qa has approved, end your reply with EXACTLY
'MERGE_PR' — the automation merges the PR and closes this issue. You are the ONLY agent that
merges, and only here."
fi
fi
if [ "$NAME" = "qa" ]; then
ACTION="$ACTION
As QA you are the REVIEWER — you NEVER edit code and NEVER merge. @pm points you at a PR; review
it: read the diff, drive the web app with your headless browser if there is a URL, and put your
detailed, specific recommendations ON THE PR (the automation posts your reply to the PR thread).
After actually verifying, end your reply with EXACTLY one of:
- 'APPROVE' — the change is correct and any CI is green. The automation records your verdict on the
issue and hands back to @pm (who tells the creator, or in autopilot merges). You do NOT merge.
- 'BOUNCE: @<dev>' — something needs changing. FIRST spell out, specifically and actionably, exactly
what to change (file, label, value, hostname, …), THEN end with the BOUNCE line naming who fixes
it (@junior / @senior / @lead — usually whoever built it). The automation sends the PR back and
re-verifies with you. After 3 rounds it stops and hands to @ffaerber — so list ALL problems at
once, not one at a time.
- 'HALT' — the problem is NOT something a dev can fix (the request is ambiguous / needs a human
decision). Hands back to @ffaerber.
Emit AT MOST one marker, and only after you have actually verified."
fi
else
ACTION="You start on git branch '${BRANCH}', with git and push credentials already configured.
FIRST read AGENTS.md at the repo root and FOLLOW IT EXACTLY — it defines the golden rules,
branch naming, how to split work into multiple small independently-mergeable PRs, commit/push
style, and the required PR-description format (the BEGIN_PR_DESCRIPTION block the automation
extracts). Do all work on branches (never in the issue), commit and push as you go, and do NOT
open pull requests yourself — that is automated for every branch you push.
If the task is genuinely unclear, make NO changes and reply with specific questions instead."
fi
PROMPT="You are @${NAME}, a member of an AI dev team working on this Gitea repository.
YOUR ROLE: ${DESC}
YOUR CAPABILITIES: model ${MODEL}. ${CAP}
${NOTE}
Your reply is posted as a comment already attributed to you (@${NAME}) — your name and avatar are
shown by Gitea. Do NOT begin your reply with your own name, an '@${NAME}' header, or a '🤖/🔨 @you'
line; just write the content directly.
TEAM ROSTER (who does what — hand off if a task isn't yours):
${ROSTER}
${ACTION}
If a task needs expertise or a capability you lack, do NOT guess — say which
teammate should handle it. The task is fully described below; do not search the
repo for an 'issue' file.
TASK (issue #${NUM} \"${TITLE}\"):
${IBODY}
FULL CONVERSATION THREAD SO FAR (every comment on this issue, oldest first — including your
OWN previous replies and the maintainer's answers). READ IT CAREFULLY. Do NOT repeat questions
that have already been answered; build on what has already been decided. If the maintainer has
answered your earlier questions, ACT on those answers — do not re-ask.
${THREAD}
LATEST INSTRUCTION FROM MAINTAINER:
${CMT}"
echo "opencode version: $(opencode --version 2>&1)"
# Capture the raw JSON event stream (--format json) so the activity log can be built
# from it afterwards. The plain --auto reply text == concatenation of all assistant
# "text" parts, so reconstruct /tmp/agent_out.md from those — the Publish step below
# keeps reading agent_out.md exactly as before. Success is exit code 0: the agent may
# make tool-only changes with no text summary, so DO NOT treat empty output as failure.
rc=1
for attempt in 1 2 3; do
echo "opencode attempt $attempt/3 for @$NAME ($MODEL)"
rc=0
opencode run --model "$MODEL" --auto --format json "$PROMPT" ${FILES:-} \
>/tmp/events.jsonl 2>/tmp/agent_err.log || rc=$?
echo "rc=$rc"; echo "--- events ($(wc -l < /tmp/events.jsonl 2>/dev/null || echo 0) lines) ---"
echo "--- stderr (trace) ---"; cat /tmp/agent_err.log
[ $rc -eq 0 ] && break
if grep -qiE 'overloaded|429|529|rate.?limit|timeout|ETIMEDOUT|ECONNRESET|EAI_AGAIN' /tmp/events.jsonl /tmp/agent_err.log; then
echo "transient error — backing off $((attempt*20))s"; sleep $((attempt * 20)); continue
fi
echo "non-transient failure (rc=$rc) — not retrying"; break
done
[ $rc -eq 0 ] || { echo "agent failed"; exit 1; }
# Reconstruct the plain-text reply from assistant text parts (== what plain --auto prints).
jq -r 'select(.type=="text") | .part.text // ""' /tmp/events.jsonl > /tmp/agent_out.md 2>/dev/null || true
echo "reconstructed reply ($(wc -l < /tmp/agent_out.md 2>/dev/null || echo 0) lines):"; cat /tmp/agent_out.md