#!/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: @' (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 work in two phases and NEVER skip the approval gate: PLAN — when the task is clear, present a SHORT plan naming which teammate should build it (@junior for small/low-risk, @senior/@lead for complex, @qa to verify), then END by asking '@ffaerber ready to start building? reply yes to proceed.' Do NOT include a DELEGATE line yet. DELEGATE — ONLY after the maintainer has explicitly approved starting in the thread (a clear 'yes' / 'go' / 'proceed' / 'start building' answering your ready-to-build question) do you end your reply with a 'DELEGATE: @' line to hand off. Never present a plan and delegate on the same turn. If anything is unclear or needs a decision, START your reply with '@ffaerber', ask specific questions, and do NOT delegate. BREAKDOWN (for a feature too big for one PR): first PLAN — propose a milestone name and the list of sub-tasks (title + one line each), then ask '@ffaerber create these N sub-issues? reply yes.' Do NOT emit the block yet. ONLY after the maintainer approves, end your reply with EXACTLY: BEGIN_SUBTASKS milestone: - :: - :: END_SUBTASKS The automation creates the milestone + one sub-issue per line (each linked to this issue). 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). This OVERRIDES the two-phase approval gate above: do NOT ask '@ffaerber ready to start building?' and do NOT wait for a 'yes'. When the task is clear, present your SHORT plan naming the best teammate to build it AND end your reply with a 'DELEGATE: @' line in the SAME turn to hand off immediately. Prefer @junior for small/low-risk (mostly YAML/compose/config), @senior/@lead for complex or multi-file work. Only skip delegating (and instead ask @ffaerber) if the task is genuinely ambiguous or unsafe — otherwise plan-and-delegate now." fi fi if [ "$NAME" = "qa" ]; then ACTION="$ACTION As QA you verify a change works: read the PR/issue, drive the web app with your headless browser if there is a URL, and report bugs or confirm behavior. You normally do NOT merge — a human does that." if [ "$AUTOPILOT" = "true" ]; then ACTION="$ACTION AUTOPILOT MODE IS ACTIVE (this issue/PR carries the 'autopilot' label). You are the quality gate. You do NOT edit code or fix anything yourself — you either accept the PR or send it back to the dev with precise instructions. After actually verifying, end your reply with EXACTLY one of: - 'MERGE_PR' — the change is correct and any CI is green. The automation merges the PR and closes the linked issue. Do NOT merge by any other means; only this marker triggers the merge. - 'BOUNCE: @' — something needs changing. FIRST spell out, specifically and actionably, exactly what the dev must change (name the file, label, value, hostname, etc.), THEN end with the BOUNCE line naming who should fix it (@junior / @senior / @lead — usually whoever built it; @senior or @lead for something harder). The automation sends the PR back to that dev and then re-verifies with you. After 3 bounces it stops automatically and hands to @ffaerber — so make each round count and list ALL problems at once, not one at a time. Use BOUNCE for anything a dev can fix. Only use 'HALT_AUTOPILOT' when the problem is NOT fixable by a dev — the request itself is ambiguous or needs a human decision — to hand back to @ffaerber. Emit AT MOST one of MERGE_PR / BOUNCE / HALT_AUTOPILOT, and only after you have actually verified." fi 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