agents: fix review findings — thread attribution, races, rescue stall, docs, CI
ci / lint (push) Skipped
ci / lint (push) Skipped
Fixes from a full repo review: - fetch-thread.sh: attribute每 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
09c716a8c1
@@ -6,8 +6,14 @@ on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
|
||||
|
||||
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
|
||||
# defense against malicious-issue prompt injection — do not loosen it.
|
||||
if: >
|
||||
@@ -190,6 +196,7 @@ jobs:
|
||||
env:
|
||||
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||
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"
|
||||
|
||||
- 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": [
|
||||
"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": {
|
||||
"model": "ollama-cloud/kimi-k2.7-code:cloud",
|
||||
@@ -40,7 +40,7 @@
|
||||
"skills": [
|
||||
"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": {
|
||||
"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)
|
||||
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
|
||||
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"
|
||||
|
||||
{
|
||||
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"
|
||||
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"
|
||||
else
|
||||
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
|
||||
# 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
|
||||
set -eu
|
||||
@@ -7,9 +10,8 @@ set -eu
|
||||
API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||
curl -sS -H "Authorization: token $GT" "$API/issues/$NUM/comments?limit=100" 2>/dev/null \
|
||||
| jq -r '.[] |
|
||||
( if (.body | test("delegated by")) then "an automated delegation"
|
||||
elif (.user.login == "ffaerber") then "ffaerber (the maintainer / you)"
|
||||
else "an AI teammate — the specific one is named in the 🤖 @name line at the top of the comment"
|
||||
end ) as $who |
|
||||
"### comment by \($who):\n\(.body)\n"' > /tmp/thread.md 2>/dev/null || true
|
||||
( if (.user.login == "ffaerber") then "@ffaerber (the maintainer)"
|
||||
else "@" + .user.login end ) as $who |
|
||||
"### comment by \($who):\n\(.body | gsub("\\s*<!-- 🤖 agent reply — do not trigger -->"; ""))\n"' \
|
||||
> /tmp/thread.md 2>/dev/null || true
|
||||
echo "thread comments fetched: $(grep -c '^### comment by ' /tmp/thread.md 2>/dev/null || echo 0)"
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
# GITHUB_PATH HOME
|
||||
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"
|
||||
mkdir -p ~/.config/opencode
|
||||
# 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.
|
||||
#
|
||||
# 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
|
||||
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.
|
||||
case "$NAME" in
|
||||
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
|
||||
[ -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}"
|
||||
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)
|
||||
url=$(printf '%s' "$resp" | jq -r '.html_url // empty' 2>/dev/null)
|
||||
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" \
|
||||
-d "$(jq -nc --arg t "@$NAME: $TITLE" --arg h "$BRANCH" --arg b "$body" \
|
||||
'{title:$t, head:$h, base:"main", body:$b}')")
|
||||
echo "rescue PR create ($BRANCH): $resp"
|
||||
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
|
||||
|
||||
if [ -n "$url" ]; then
|
||||
# Status note on the issue (marked — must not trigger)…
|
||||
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
|
||||
# …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
|
||||
echo "rescue: could not open/find a PR for $BRANCH"
|
||||
fi
|
||||
|
||||
@@ -27,6 +27,10 @@ cp "$AGENTS_JSON" /tmp/agents.json
|
||||
# here — see agent.yml: this reusable workflow sees it as 'workflow_call'.)
|
||||
if [ -n "$CID" ]; then scan="$BODY"; else scan="$IBODY"; fi
|
||||
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
|
||||
case "$scan" in *"@$a"*) name=$a; break;; esac
|
||||
done
|
||||
|
||||
Reference in New Issue
Block a user