agents: fix review findings — thread attribution, races, rescue stall, docs, CI
ci / lint (push) Skipped
ci / lint (pull_request) Successful in 11s
ci / lint (push) Skipped
ci / lint (pull_request) Successful in 11s
Fixes from a full repo review: - fetch-thread.sh: attribute every comment to its REAL author (@pm/@qa/… — agents post as their own users now); the old "🤖 @name line at the top" hint pointed at headers we removed, leaving every teammate comment anonymous. Also strip the hidden loop-prevention marker from bodies (prompt noise). - agent.yml: per-issue concurrency group (queued, no cancel) — two quick comments on one issue no longer race the same ai/issue-N branch. - rescue-pr.sh: after opening a rescue PR, hand it back into the flow with an unmarked @pm trigger (the pm→qa choreography previously stalled silently on any rescued run); drop the old "🤖 **@name**" header style; add ops to the token case; mark the status note with the hidden marker. - README: token table said "@qa merges / TOKEN_QA needs write:repository" — inverted since the PM-orchestrated flow (@pm merges, autopilot only; @qa is read-only). Updated the agent table (descs, node1-ssh moved to homelab) and added a "How a task flows" section. - agents.json: pm/qa descs now describe the orchestrator/reviewer roles (these feed the roster prompt agents route by). - NEW ci.yml: bash -n + shellcheck(-S error) on every script, YAML-parse on every workflow, agents.json schema check — the ${x:-{}} brace bug would have been caught here before it shipped. - install-opencode.sh: pin opencode (default 1.17.13, override via OPENCODE_VERSION) — a breaking release no longer takes down every agent. - build-activity-log.sh: ollama/ollama-cloud models are subscription-billed (no $/token exists) — label the footer "subscription" instead of a misleading $0.0000; metered (anthropic) models keep the real dollar cost. - route.sh: document that mention-priority is list-order and load-bearing for the flow's trigger comments. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
88237d3f4d
commit
9b9e1e945d
@@ -8,6 +8,12 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
agent:
|
agent:
|
||||||
|
# One run at a time PER ISSUE: two quick comments on the same issue would otherwise race —
|
||||||
|
# both checking out ai/issue-N, pushing (non-fast-forward loss) and double-posting. Queued
|
||||||
|
# runs wait (no cancel) so every trigger is still processed, just serially.
|
||||||
|
concurrency:
|
||||||
|
group: ai-agent-${{ github.repository }}-${{ github.event.issue.number }}
|
||||||
|
cancel-in-progress: false
|
||||||
# Trusted author only, and only when a known agent is mentioned. This gate is the main
|
# Trusted author only, and only when a known agent is mentioned. This gate is the main
|
||||||
# defense against malicious-issue prompt injection — do not loosen it.
|
# defense against malicious-issue prompt injection — do not loosen it.
|
||||||
if: >
|
if: >
|
||||||
@@ -190,6 +196,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||||
MODE: ${{ steps.prep.outputs.mode }}
|
MODE: ${{ steps.prep.outputs.mode }}
|
||||||
|
MODEL: ${{ steps.prep.outputs.model }} # ollama-cloud models are subscription-billed (no $/token)
|
||||||
run: bash "$SCRIPTS/build-activity-log.sh"
|
run: bash "$SCRIPTS/build-activity-log.sh"
|
||||||
|
|
||||||
- name: Publish — PR (dev agents) or comment (pm), always reply in the issue
|
- name: Publish — PR (dev agents) or comment (pm), always reply in the issue
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
name: ci
|
||||||
|
run-name: "ci · ${{ github.event.pull_request.title || github.sha }}"
|
||||||
|
# Lint the very scripts every agent run executes. A single unchecked shell bug here breaks ALL
|
||||||
|
# agents in ALL repos at once (e.g. the ${VAR:-{}} brace bug shellcheck flags as SC1083/SC2321),
|
||||||
|
# so PRs must pass: bash -n + shellcheck on every script, YAML-parse on every workflow, and a
|
||||||
|
# schema check on agents.json (the routing registry).
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ci-runner
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install linters
|
||||||
|
run: |
|
||||||
|
command -v shellcheck >/dev/null || (apt-get update -qq && apt-get install -y -qq shellcheck) || \
|
||||||
|
sudo sh -c 'apt-get update -qq && apt-get install -y -qq shellcheck' || true
|
||||||
|
python3 -c 'import yaml' 2>/dev/null || pip3 install --quiet pyyaml || \
|
||||||
|
(apt-get install -y -qq python3-yaml || sudo apt-get install -y -qq python3-yaml) || true
|
||||||
|
|
||||||
|
- name: bash -n (syntax) — every script
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
for f in .gitea/workflows/scripts/*.sh; do bash -n "$f" && echo "OK $f"; done
|
||||||
|
|
||||||
|
- name: shellcheck — every script
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
if command -v shellcheck >/dev/null; then
|
||||||
|
# error-severity only: the scripts intentionally use unquoted word-splitting in places;
|
||||||
|
# errors (real breakage like the ${x:-{}} brace bug) must fail the build.
|
||||||
|
shellcheck -S error .gitea/workflows/scripts/*.sh && echo "shellcheck clean (severity=error)"
|
||||||
|
else
|
||||||
|
echo "shellcheck unavailable on runner — skipped"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: YAML-parse every workflow
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
python3 - <<'EOF'
|
||||||
|
import glob, sys, yaml
|
||||||
|
for f in sorted(glob.glob('.gitea/workflows/*.yml')):
|
||||||
|
yaml.safe_load(open(f))
|
||||||
|
print('OK', f)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Validate agents.json (registry schema)
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
jq -e 'to_entries | all(.value | (.model|type=="string") and (.mode=="pr" or .mode=="comment")
|
||||||
|
and (.vision|type=="boolean") and (.skills|type=="array") and (.desc|type=="string"))' \
|
||||||
|
.gitea/workflows/scripts/agents.json >/dev/null && echo "agents.json OK"
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
"skills": [
|
"skills": [
|
||||||
"gitea-api"
|
"gitea-api"
|
||||||
],
|
],
|
||||||
"desc": "Product manager — research, plan, ask clarifying questions, and decide which dev should do the work. Comments only; never edits files."
|
"desc": "Product manager & orchestrator — plans and picks the dev, hands each finished PR to @qa for review, and reports back to the issue creator (in autopilot it merges approved PRs itself). Works from the issue thread only — comments only, never edits files, never reads the PR diff."
|
||||||
},
|
},
|
||||||
"junior": {
|
"junior": {
|
||||||
"model": "ollama-cloud/kimi-k2.7-code:cloud",
|
"model": "ollama-cloud/kimi-k2.7-code:cloud",
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"skills": [
|
"skills": [
|
||||||
"gitea-api"
|
"gitea-api"
|
||||||
],
|
],
|
||||||
"desc": "QA — verifies things work. Drives a headless browser (Playwright) to open a URL/web app, click through it, screenshot, and report bugs or confirm behavior. Comments findings; opens no PRs."
|
"desc": "QA / reviewer — reviews PRs: reads the diff, drives a headless browser (Playwright) to verify behavior, posts specific recommendations on the PR and the pass/fail verdict on the issue. Never edits code, never merges."
|
||||||
},
|
},
|
||||||
"ops": {
|
"ops": {
|
||||||
"model": "anthropic/claude-opus-4-8",
|
"model": "anthropic/claude-opus-4-8",
|
||||||
|
|||||||
@@ -36,14 +36,20 @@ read -r COST INP OUT CR CW RE < <(jq -rs '
|
|||||||
| @tsv' "$E" 2>/dev/null)
|
| @tsv' "$E" 2>/dev/null)
|
||||||
COST=${COST:-0}; INP=${INP:-0}; OUT=${OUT:-0}; CR=${CR:-0}; CW=${CW:-0}; RE=${RE:-0}
|
COST=${COST:-0}; INP=${INP:-0}; OUT=${OUT:-0}; CR=${CR:-0}; CW=${CW:-0}; RE=${RE:-0}
|
||||||
IN_TOTAL=$(( INP + CR + CW )) # total input context processed
|
IN_TOTAL=$(( INP + CR + CW )) # total input context processed
|
||||||
COSTF=$(awk -v c="$COST" 'BEGIN{printf "$%.4f", c+0}')
|
# Cost label: ollama / ollama-cloud models are SUBSCRIPTION-billed (GPU-time against the plan, no
|
||||||
|
# $/token price exists), so a "$0.0000" there would be misleading — label it a subscription instead.
|
||||||
|
# Metered providers (anthropic/…) get the real dollar cost opencode computed.
|
||||||
|
case "${MODEL:-}" in
|
||||||
|
ollama*|*"/ollama"*) COSTF="subscription" ;;
|
||||||
|
*) COSTF=$(awk -v c="$COST" 'BEGIN{printf "$%.4f", c+0}') ;;
|
||||||
|
esac
|
||||||
echo "usage: in=$IN_TOTAL out=$OUT cost=$COSTF (fresh=$INP cache_r=$CR cache_w=$CW reasoning=$RE); tools=$n"
|
echo "usage: in=$IN_TOTAL out=$OUT cost=$COSTF (fresh=$INP cache_r=$CR cache_w=$CW reasoning=$RE); tools=$n"
|
||||||
|
|
||||||
{
|
{
|
||||||
if [ "$n" -gt 0 ]; then
|
if [ "$n" -gt 0 ]; then
|
||||||
printf '\n\n<details>\n<summary>🔧 %s tool calls · in %s · out %s · %s</summary>\n\n' "$n" "$IN_TOTAL" "$OUT" "$COSTF"
|
printf '\n\n<details>\n<summary>🔧 %s tool calls · in %s · out %s · %s</summary>\n\n' "$n" "$IN_TOTAL" "$OUT" "$COSTF"
|
||||||
cat /tmp/tools.md
|
cat /tmp/tools.md
|
||||||
printf '\n\n<sub>tokens — input %s (fresh %s · cache %sw / %sr) · output %s · reasoning %s · **cost %s**</sub>\n</details>' \
|
printf '\n\n<sub>tokens — input %s (fresh %s · cache %sw / %sr) · output %s · reasoning %s · **%s**</sub>\n</details>' \
|
||||||
"$IN_TOTAL" "$INP" "$CW" "$CR" "$OUT" "$RE" "$COSTF"
|
"$IN_TOTAL" "$INP" "$CW" "$CR" "$OUT" "$RE" "$COSTF"
|
||||||
else
|
else
|
||||||
printf '\n\n<sub>💰 **%s** · in %s · out %s tokens (cache %sw / %sr)</sub>' "$COSTF" "$IN_TOTAL" "$OUT" "$CW" "$CR"
|
printf '\n\n<sub>💰 **%s** · in %s · out %s tokens (cache %sw / %sr)</sub>' "$COSTF" "$IN_TOTAL" "$OUT" "$CW" "$CR"
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Fetch the full issue thread (shared memory) into /tmp/thread.md.
|
# Fetch the full issue thread (shared memory) into /tmp/thread.md.
|
||||||
|
# Agents post as their OWN Gitea users, so .user.login IS the agent name — attribute each comment
|
||||||
|
# to its real author (@pm/@qa/@junior/…). Strip the hidden `<!-- 🤖 … -->` loop-prevention marker
|
||||||
|
# from bodies — it's plumbing, not conversation, and would just waste prompt tokens.
|
||||||
#
|
#
|
||||||
# Required env (provided by the workflow step): GT NUM GITHUB_SERVER_URL GITHUB_REPOSITORY
|
# Required env (provided by the workflow step): GT NUM GITHUB_SERVER_URL GITHUB_REPOSITORY
|
||||||
set -eu
|
set -eu
|
||||||
@@ -7,9 +10,8 @@ set -eu
|
|||||||
API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
curl -sS -H "Authorization: token $GT" "$API/issues/$NUM/comments?limit=100" 2>/dev/null \
|
curl -sS -H "Authorization: token $GT" "$API/issues/$NUM/comments?limit=100" 2>/dev/null \
|
||||||
| jq -r '.[] |
|
| jq -r '.[] |
|
||||||
( if (.body | test("delegated by")) then "an automated delegation"
|
( if (.user.login == "ffaerber") then "@ffaerber (the maintainer)"
|
||||||
elif (.user.login == "ffaerber") then "ffaerber (the maintainer / you)"
|
else "@" + .user.login end ) as $who |
|
||||||
else "an AI teammate — the specific one is named in the 🤖 @name line at the top of the comment"
|
"### comment by \($who):\n\(.body | gsub("\\s*<!-- 🤖 agent reply — do not trigger -->"; ""))\n"' \
|
||||||
end ) as $who |
|
> /tmp/thread.md 2>/dev/null || true
|
||||||
"### comment by \($who):\n\(.body)\n"' > /tmp/thread.md 2>/dev/null || true
|
|
||||||
echo "thread comments fetched: $(grep -c '^### comment by ' /tmp/thread.md 2>/dev/null || echo 0)"
|
echo "thread comments fetched: $(grep -c '^### comment by ' /tmp/thread.md 2>/dev/null || echo 0)"
|
||||||
|
|||||||
@@ -5,7 +5,11 @@
|
|||||||
# GITHUB_PATH HOME
|
# GITHUB_PATH HOME
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
curl -fsSL https://opencode.ai/install | bash
|
# PIN the opencode version: an unpinned `latest` means a breaking release (CLI flags, or the
|
||||||
|
# --format json event schema that build-activity-log.sh parses) breaks every agent in every repo
|
||||||
|
# at once. Bump deliberately by changing this default (or set OPENCODE_VERSION in the step env).
|
||||||
|
OPENCODE_VERSION="${OPENCODE_VERSION:-1.17.13}"
|
||||||
|
curl -fsSL https://opencode.ai/install | bash -s -- --version "$OPENCODE_VERSION"
|
||||||
echo "$HOME/.opencode/bin" >> "$GITHUB_PATH"
|
echo "$HOME/.opencode/bin" >> "$GITHUB_PATH"
|
||||||
mkdir -p ~/.config/opencode
|
mkdir -p ~/.config/opencode
|
||||||
# Playwright browser MCP only for agents that need to drive a web app
|
# Playwright browser MCP only for agents that need to drive a web app
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# the run. Comment-only roles (pm/qa) push nothing, so they are skipped.
|
# the run. Comment-only roles (pm/qa) push nothing, so they are skipped.
|
||||||
#
|
#
|
||||||
# Required env (provided by the workflow step):
|
# Required env (provided by the workflow step):
|
||||||
# GT TOKEN_PM TOKEN_SENIOR TOKEN_JUNIOR TOKEN_LEAD TOKEN_QA
|
# GT TOKEN_PM TOKEN_SENIOR TOKEN_JUNIOR TOKEN_LEAD TOKEN_QA TOKEN_OPS
|
||||||
# NAME MODE NUM TITLE BRANCH GITHUB_SERVER_URL GITHUB_REPOSITORY
|
# NAME MODE NUM TITLE BRANCH GITHUB_SERVER_URL GITHUB_REPOSITORY
|
||||||
set +e
|
set +e
|
||||||
|
|
||||||
@@ -17,9 +17,11 @@ set +e
|
|||||||
# Post/PR as the agent's OWN Gitea user when its token is configured; else the built-in bot.
|
# Post/PR as the agent's OWN Gitea user when its token is configured; else the built-in bot.
|
||||||
case "$NAME" in
|
case "$NAME" in
|
||||||
pm) TOK="$TOKEN_PM";; senior) TOK="$TOKEN_SENIOR";; junior) TOK="$TOKEN_JUNIOR";;
|
pm) TOK="$TOKEN_PM";; senior) TOK="$TOKEN_SENIOR";; junior) TOK="$TOKEN_JUNIOR";;
|
||||||
lead) TOK="$TOKEN_LEAD";; qa) TOK="$TOKEN_QA";; *) TOK="";;
|
lead) TOK="$TOKEN_LEAD";; qa) TOK="$TOKEN_QA";; ops) TOK="$TOKEN_OPS";; *) TOK="";;
|
||||||
esac
|
esac
|
||||||
[ -z "$TOK" ] && TOK="$GT"
|
[ -z "$TOK" ] && TOK="$GT"
|
||||||
|
# Trigger token: the @pm hand-back below must FIRE a new run, which the built-in token cannot.
|
||||||
|
TTOK="$TOK"; [ "$TTOK" = "$GT" ] && TTOK=""
|
||||||
API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
hdr=(-H "Authorization: token $TOK" -H "Content-Type: application/json")
|
hdr=(-H "Authorization: token $TOK" -H "Content-Type: application/json")
|
||||||
|
|
||||||
@@ -38,18 +40,30 @@ resp=$(curl -sS "${hdr[@]}" "$API/pulls?state=open&limit=50" \
|
|||||||
| jq -r --arg br "$BRANCH" 'if type=="array" then (map(select(.head.ref==$br)) | .[0] // empty) else empty end' 2>/dev/null)
|
| jq -r --arg br "$BRANCH" 'if type=="array" then (map(select(.head.ref==$br)) | .[0] // empty) else empty end' 2>/dev/null)
|
||||||
url=$(printf '%s' "$resp" | jq -r '.html_url // empty' 2>/dev/null)
|
url=$(printf '%s' "$resp" | jq -r '.html_url // empty' 2>/dev/null)
|
||||||
if [ -z "$url" ]; then
|
if [ -z "$url" ]; then
|
||||||
body=$(printf 'The run failed before it could publish, but pushed work exists on this branch — opening a PR so it is not lost.\n\n---\nResolves #%s · 🤖 @%s (auto-rescued after a failed run)' "$NUM" "$NAME")
|
body=$(printf 'The run failed before it could publish, but pushed work exists on this branch — opening a PR so it is not lost.\n\n---\nResolves #%s (auto-rescued after a failed run)' "$NUM")
|
||||||
resp=$(curl -sS -X POST "${hdr[@]}" "$API/pulls" \
|
resp=$(curl -sS -X POST "${hdr[@]}" "$API/pulls" \
|
||||||
-d "$(jq -nc --arg t "@$NAME: $TITLE" --arg h "$BRANCH" --arg b "$body" \
|
-d "$(jq -nc --arg t "@$NAME: $TITLE" --arg h "$BRANCH" --arg b "$body" \
|
||||||
'{title:$t, head:$h, base:"main", body:$b}')")
|
'{title:$t, head:$h, base:"main", body:$b}')")
|
||||||
echo "rescue PR create ($BRANCH): $resp"
|
echo "rescue PR create ($BRANCH): $resp"
|
||||||
url=$(printf '%s' "$resp" | jq -r '.html_url // empty' 2>/dev/null)
|
url=$(printf '%s' "$resp" | jq -r '.html_url // empty' 2>/dev/null)
|
||||||
|
prnum=$(printf '%s' "$resp" | jq -r '.number // empty' 2>/dev/null)
|
||||||
|
else
|
||||||
|
prnum=$(printf '%s' "$resp" | jq -r '.number // empty' 2>/dev/null)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$url" ]; then
|
if [ -n "$url" ]; then
|
||||||
|
# Status note on the issue (marked — must not trigger)…
|
||||||
curl -sS -X POST "${hdr[@]}" "$API/issues/$NUM/comments" \
|
curl -sS -X POST "${hdr[@]}" "$API/issues/$NUM/comments" \
|
||||||
-d "$(jq -nc --arg b "$(printf '🤖 **@%s** — ⚠️ the run failed, but your pushed work was not lost — a PR was opened for branch \`%s\`:\n- %s' "$NAME" "$BRANCH" "$url")" '{body:$b}')" \
|
-d "$(jq -nc --arg b "$(printf '⚠️ The run failed, but the pushed work was not lost — a PR was opened for branch \`%s\`:\n- %s\n\n<!-- 🤖 agent reply — do not trigger -->' "$BRANCH" "$url")" '{body:$b}')" \
|
||||||
-w '\nrescue comment -> HTTP %{http_code}\n' || true
|
-w '\nrescue comment -> HTTP %{http_code}\n' || true
|
||||||
|
# …then hand the rescued PR back into the flow: without this, the pm→qa choreography would stall
|
||||||
|
# here (the normal "PR ready" trigger never fired). Unmarked + PAT so it starts @pm's run.
|
||||||
|
if [ -n "$TTOK" ]; then
|
||||||
|
curl -sS -X POST -H "Authorization: token $TTOK" -H "Content-Type: application/json" \
|
||||||
|
"$API/issues/$NUM/comments" \
|
||||||
|
-d "$(jq -nc --arg b "@pm — PR #${prnum:-?} was auto-rescued after a failed run (issue #$NUM). Please route it for review." '{body:$b}')" \
|
||||||
|
-w '\nrescue trigger @pm -> HTTP %{http_code}\n' || true
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "rescue: could not open/find a PR for $BRANCH"
|
echo "rescue: could not open/find a PR for $BRANCH"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ cp "$AGENTS_JSON" /tmp/agents.json
|
|||||||
# here — see agent.yml: this reusable workflow sees it as 'workflow_call'.)
|
# here — see agent.yml: this reusable workflow sees it as 'workflow_call'.)
|
||||||
if [ -n "$CID" ]; then scan="$BODY"; else scan="$IBODY"; fi
|
if [ -n "$CID" ]; then scan="$BODY"; else scan="$IBODY"; fi
|
||||||
name=""
|
name=""
|
||||||
|
# FIRST MATCH IN THIS LIST ORDER WINS when a comment mentions several agents. The order is
|
||||||
|
# load-bearing for the flow's trigger comments: "@pm — @qa approved …" must route to @pm (pm is
|
||||||
|
# checked first), while "@junior please address @qa's review …" must route to the dev (devs are
|
||||||
|
# checked before qa). If you add an agent or reword a trigger in publish.sh, re-check this order.
|
||||||
for a in pm junior senior lead qa ops; do
|
for a in pm junior senior lead qa ops; do
|
||||||
case "$scan" in *"@$a"*) name=$a; break;; esac
|
case "$scan" in *"@$a"*) name=$a; break;; esac
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -7,15 +7,33 @@ Shared **AI dev-team** workflow for Gitea Actions, reusable across repos. It giv
|
|||||||
|
|
||||||
| Agent | Model | Vision | Mode | Skills | Role |
|
| Agent | Model | Vision | Mode | Skills | Role |
|
||||||
|-------|-------|:------:|------|--------|------|
|
|-------|-------|:------:|------|--------|------|
|
||||||
| `@pm` | `ollama-cloud/gemma4:cloud` | yes | comment | `gitea-api` | Product manager — research, plan, ask clarifying questions, and decide which dev should do the work. Comments only; never edits files. |
|
| `@pm` | `ollama-cloud/gemma4:cloud` | yes | comment | `gitea-api` | Product manager & orchestrator — plans, picks the dev, hands finished PRs to `@qa`, reports back to the issue creator (autopilot: merges approved PRs itself). Issue thread only; never edits files, never reads the PR diff. |
|
||||||
| `@junior` | `ollama-cloud/kimi-k2.7-code:cloud` | no | pr | — | Junior dev — small, low-risk changes (mostly YAML/compose/config). Text-only, cannot read images. Defers complex or image tasks to `@senior` or `@lead`. |
|
| `@junior` | `ollama-cloud/kimi-k2.7-code:cloud` | no | pr | — | Junior dev — small, low-risk changes (mostly YAML/compose/config). Text-only, cannot read images. Defers complex or image tasks to `@senior` or `@lead`. |
|
||||||
| `@senior` | `ollama-cloud/glm-5.2:cloud` | no | pr | `gitea-api`, `node1-ssh` | Senior dev — complex, multi-file implementation (GLM-5.2 via Ollama Cloud, text-only). |
|
| `@senior` | `ollama-cloud/glm-5.2:cloud` | no | pr | `gitea-api` | Senior dev — complex, multi-file implementation (GLM-5.2 via Ollama Cloud, text-only). |
|
||||||
| `@lead` | `anthropic/claude-opus-4-8` | yes | pr | `gitea-api`, `node1-ssh` | Tech lead — the hardest problems, architecture, and final calls. |
|
| `@lead` | `anthropic/claude-opus-4-8` | yes | pr | `gitea-api` | Tech lead — the hardest problems, architecture, and final calls. |
|
||||||
| `@qa` | `ollama-cloud/minimax-m3:cloud` | yes | comment | `gitea-api` | QA — verifies things work. Drives a headless browser (Playwright) to open a URL/web app, click through it, screenshot, and report bugs or confirm behavior. Comments findings; opens no PRs. |
|
| `@qa` | `ollama-cloud/minimax-m3:cloud` | yes | comment | `gitea-api` | QA / reviewer — reads the PR diff, drives a headless browser (Playwright) to verify behavior; recommendations on the PR, pass/fail verdict on the issue. Never edits code, never merges. |
|
||||||
| `@ops` | `anthropic/claude-opus-4-8` | no | comment | `gitea-admin` | Gitea operator — administers the instance itself (create orgs/users/repos, labels, secrets, scoped per-user tokens, bootstrap repos). Comments only; never edits code. Confirms before destructive actions. |
|
| `@ops` | `anthropic/claude-opus-4-8` | no | comment | `gitea-admin` | Gitea operator — administers the instance itself (create orgs/users/repos, labels, secrets, scoped per-user tokens, bootstrap repos). Comments only; never edits code. Confirms before destructive actions. |
|
||||||
|
|
||||||
`agent.yml`'s agent registry is the source of truth for this mapping — if you change a model
|
The registry `.gitea/workflows/scripts/agents.json` is the source of truth for this mapping — if you
|
||||||
or an agent's skills there, update this table too.
|
change a model or an agent's skills there, update this table too. (Repo-specific skills, e.g. a
|
||||||
|
deploy-host SSH skill, live in the consuming repo under `.gitea/agent-skills/` — not in this table.)
|
||||||
|
|
||||||
|
## How a task flows
|
||||||
|
|
||||||
|
`@pm` orchestrates from the **issue thread**; the review happens on the **PR**; `@pm` never reads the PR
|
||||||
|
(keeps its context small) and `@qa` never merges.
|
||||||
|
|
||||||
|
1. **Issue opened** → `@pm` plans and names a dev, then asks the creator *"ready? reply yes"*
|
||||||
|
(with the `autopilot` label it skips the question and delegates immediately).
|
||||||
|
2. **Dev builds** on `ai/issue-N`, a PR opens automatically, and the dev pings `@pm` on the issue.
|
||||||
|
3. `@pm` hands the PR to **`@qa`**.
|
||||||
|
4. `@qa` reviews **on the PR** — either recommendations + `BOUNCE: @dev` (dev fixes → `@qa`
|
||||||
|
re-verifies, direct loop, max 3 rounds) or `APPROVE`.
|
||||||
|
5. On approval `@qa` posts the verdict **on the issue** → `@pm` tells the creator *"ready to merge"*
|
||||||
|
and a **human merges** — or, with the `autopilot` label, `@pm` merges and closes the issue itself.
|
||||||
|
|
||||||
|
`@pm` is the only agent that ever merges, and only under the `autopilot` label (its kill switch:
|
||||||
|
remove the label mid-flight and the next step reverts to human control).
|
||||||
|
|
||||||
### Per-agent skill scoping
|
### Per-agent skill scoping
|
||||||
|
|
||||||
@@ -76,12 +94,13 @@ points `$SCRIPTS` at it. Keep the workflow and its scripts moving together on `m
|
|||||||
|--------|-----|
|
|--------|-----|
|
||||||
| `ANTHROPIC_API_KEY` | `@lead` (and `@pm`/`@senior`/`@qa` if on Claude) |
|
| `ANTHROPIC_API_KEY` | `@lead` (and `@pm`/`@senior`/`@qa` if on Claude) |
|
||||||
| `OLLAMA_URL`, `OLLAMA_CLOUD_API_KEY` | local ornith / Ollama Cloud (gemma4, kimi-k2.7-code, glm-5.2, minimax-m3) |
|
| `OLLAMA_URL`, `OLLAMA_CLOUD_API_KEY` | local ornith / Ollama Cloud (gemma4, kimi-k2.7-code, glm-5.2, minimax-m3) |
|
||||||
| `TOKEN_PM`,`TOKEN_SENIOR`,`TOKEN_JUNIOR`,`TOKEN_LEAD`,`TOKEN_QA` | **primary** — each agent's own Gitea-user PAT. The running agent gets *only its own* token (as `SELF_TOKEN`) so it posts, commits, comments, and (for `@qa` autopilot) merges as itself, and its `gitea-api` skill acts with its own scopes. `TOKEN_QA` needs `write:repository` to merge. |
|
| `TOKEN_PM`,`TOKEN_SENIOR`,`TOKEN_JUNIOR`,`TOKEN_LEAD`,`TOKEN_QA` | **primary** — each agent's own Gitea-user PAT. The running agent gets *only its own* token (as `SELF_TOKEN`) so it posts, commits and comments as itself, and its `gitea-api` skill acts with its own scopes. Scopes: devs + `TOKEN_PM` carry `write:repository` (`@pm` is the only agent that merges, autopilot only); `TOKEN_QA` is `read:repository` + `write:issue` (reviews, never merges). |
|
||||||
| `TOKEN_OPS` | `@ops` only — the admin PAT behind the `gitea-admin` skill (create orgs/users/repos, manage labels & secrets, mint scoped tokens). Injected into the agent process only when the agent is `@ops`. |
|
| `TOKEN_OPS` | `@ops` only — the admin PAT behind the `gitea-admin` skill (create orgs/users/repos, manage labels & secrets, mint scoped tokens). Injected into the agent process only when the agent is `@ops`. |
|
||||||
|
|
||||||
Each agent authenticates as **itself**: the Run-agent step selects that agent's `TOKEN_*` into
|
Each agent authenticates as **itself**: the Run-agent step selects that agent's `TOKEN_*` into
|
||||||
`SELF_TOKEN` (never another agent's), and `publish.sh` uses the same token for the delegation/autopilot
|
`SELF_TOKEN` (never another agent's), and `publish.sh` uses the same token for the trigger comments
|
||||||
trigger comment and `@qa` merges — the two things the built-in `GITEA_TOKEN` can't do (it won't start
|
that drive the flow (delegation, `@qa` hand-offs, bounces) and for `@pm`'s autopilot merge — the two
|
||||||
|
things the built-in `GITEA_TOKEN` can't do (it won't start
|
||||||
new runs, and a merge under it won't fire downstream deploys). So **every consuming repo must carry the
|
new runs, and a merge under it won't fire downstream deploys). So **every consuming repo must carry the
|
||||||
per-agent `TOKEN_*` secrets** (org-level for `gitea/*`, user-level for `ffaerber/*`); there is no shared
|
per-agent `TOKEN_*` secrets** (org-level for `gitea/*`, user-level for `ffaerber/*`); there is no shared
|
||||||
fallback token. `GITEA_TOKEN` is auto-provided (used for reads). Tip: set the `TOKEN_*` once at the
|
fallback token. `GITEA_TOKEN` is auto-provided (used for reads). Tip: set the `TOKEN_*` once at the
|
||||||
|
|||||||
Reference in New Issue
Block a user