Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6cafaf81c | ||
|
|
60f9e95ecc | ||
|
|
dd25c0ac2b | ||
|
|
8ae6abd9d0 | ||
|
|
b30db8a4c9 | ||
|
|
6955bc4f71 | ||
|
|
0a06c813bc | ||
|
|
c4fb117b93 | ||
|
|
eeae1fdaaa | ||
|
|
7abbbb1b8d | ||
|
|
95bc254640 | ||
|
|
75a2493dee | ||
|
|
53ac2b7ba8 | ||
|
|
4b73d0b8e9 |
+47
-19
@@ -5,6 +5,10 @@ name: agent
|
|||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
|
||||||
|
# Pinned opencode version — used to install it and to key the CI cache below.
|
||||||
|
env:
|
||||||
|
OPENCODE_VERSION: "1.17.13"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
agent:
|
agent:
|
||||||
@@ -44,17 +48,6 @@ jobs:
|
|||||||
# job must never hold the single runner slot for hours.
|
# job must never hold the single runner slot for hours.
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
steps:
|
steps:
|
||||||
- name: Acknowledge with 👀
|
|
||||||
env:
|
|
||||||
GT: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
CID: ${{ github.event.comment.id }}
|
|
||||||
NUM: ${{ github.event.issue.number }}
|
|
||||||
run: |
|
|
||||||
B="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/issues"
|
|
||||||
if [ -n "$CID" ]; then R="$B/comments/$CID/reactions"; else R="$B/$NUM/reactions"; fi
|
|
||||||
curl -sS -X POST -H "Authorization: token $GT" -H "Content-Type: application/json" \
|
|
||||||
"$R" -d '{"content":"eyes"}' -w '\nreact -> HTTP %{http_code}\n' || true
|
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -119,12 +112,44 @@ jobs:
|
|||||||
TOKEN_INTERN: ${{ secrets.TOKEN_INTERN }}
|
TOKEN_INTERN: ${{ secrets.TOKEN_INTERN }}
|
||||||
run: bash "$SCRIPTS/route.sh"
|
run: bash "$SCRIPTS/route.sh"
|
||||||
|
|
||||||
|
- name: Acknowledge with 👀 (as the routed agent)
|
||||||
|
if: steps.prep.outputs.mode != 'skip'
|
||||||
|
env:
|
||||||
|
SELF_TOKEN: ${{ steps.prep.outputs.name == 'pm' && secrets.TOKEN_PM || steps.prep.outputs.name == 'junior' && secrets.TOKEN_JUNIOR || steps.prep.outputs.name == 'senior' && secrets.TOKEN_SENIOR || steps.prep.outputs.name == 'lead' && secrets.TOKEN_LEAD || steps.prep.outputs.name == 'qa' && secrets.TOKEN_QA || steps.prep.outputs.name == 'ops' && secrets.TOKEN_OPS || steps.prep.outputs.name == 'intern' && secrets.TOKEN_INTERN || '' }}
|
||||||
|
CID: ${{ github.event.comment.id }}
|
||||||
|
NUM: ${{ github.event.issue.number }}
|
||||||
|
run: |
|
||||||
|
[ -n "$SELF_TOKEN" ] || { echo "no agent token — skipping 👀"; exit 0; }
|
||||||
|
B="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/issues"
|
||||||
|
if [ -n "$CID" ]; then R="$B/comments/$CID/reactions"; else R="$B/$NUM/reactions"; fi
|
||||||
|
curl -sS -X POST -H "Authorization: token $SELF_TOKEN" -H "Content-Type: application/json" \
|
||||||
|
"$R" -d '{"content":"eyes"}' -w '\nreact -> HTTP %{http_code}\n' || true
|
||||||
|
|
||||||
|
- name: Cache opencode CLI
|
||||||
|
if: steps.prep.outputs.mode != 'skip'
|
||||||
|
continue-on-error: true # a cache backend hiccup must never fail an agent run
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.opencode
|
||||||
|
key: opencode-${{ runner.os }}-${{ env.OPENCODE_VERSION }}
|
||||||
|
|
||||||
|
- name: Cache Playwright browsers + npm (browser agents only)
|
||||||
|
if: steps.prep.outputs.mode != 'skip' && (steps.prep.outputs.name == 'senior' || steps.prep.outputs.name == 'lead' || steps.prep.outputs.name == 'qa')
|
||||||
|
continue-on-error: true
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/ms-playwright
|
||||||
|
~/.npm
|
||||||
|
key: playwright-npm-${{ runner.os }}-v1
|
||||||
|
|
||||||
- name: Install opencode + provider config (+ Playwright MCP for browser agents)
|
- name: Install opencode + provider config (+ Playwright MCP for browser agents)
|
||||||
if: steps.prep.outputs.mode != 'skip'
|
if: steps.prep.outputs.mode != 'skip'
|
||||||
env:
|
env:
|
||||||
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||||
OLLAMA_URL: ${{ secrets.OLLAMA_URL }}
|
OLLAMA_URL: ${{ secrets.OLLAMA_URL }}
|
||||||
OLLAMA_CLOUD_API_KEY: ${{ secrets.OLLAMA_CLOUD_API_KEY }}
|
OLLAMA_CLOUD_API_KEY: ${{ secrets.OLLAMA_CLOUD_API_KEY }}
|
||||||
|
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
|
||||||
NAME: ${{ steps.prep.outputs.name }}
|
NAME: ${{ steps.prep.outputs.name }}
|
||||||
SKILLS: ${{ steps.prep.outputs.skills }} # JSON array of skills this agent may load
|
SKILLS: ${{ steps.prep.outputs.skills }} # JSON array of skills this agent may load
|
||||||
run: bash "$SCRIPTS/install-opencode.sh"
|
run: bash "$SCRIPTS/install-opencode.sh"
|
||||||
@@ -187,7 +212,7 @@ jobs:
|
|||||||
id: run
|
id: run
|
||||||
env:
|
env:
|
||||||
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
|
||||||
# SELF_TOKEN = the RUNNING agent's OWN token (TOKEN_PM for @pm, TOKEN_OPS for @ops, …).
|
# SELF_TOKEN = the RUNNING agent's OWN token (TOKEN_PM for @pm, TOKEN_OPS for @ops, …).
|
||||||
# Only this agent's token is placed in its process env, so no agent can act as another.
|
# Only this agent's token is placed in its process env, so no agent can act as another.
|
||||||
# Powers the gitea-api / gitea-admin skills — each agent calls Gitea as itself. Every
|
# Powers the gitea-api / gitea-admin skills — each agent calls Gitea as itself. Every
|
||||||
@@ -266,24 +291,27 @@ jobs:
|
|||||||
run: bash "$SCRIPTS/rescue-pr.sh" || true
|
run: bash "$SCRIPTS/rescue-pr.sh" || true
|
||||||
|
|
||||||
- name: Mark done with 🚀 (remove 👀)
|
- name: Mark done with 🚀 (remove 👀)
|
||||||
|
if: steps.prep.outputs.mode != 'skip'
|
||||||
env:
|
env:
|
||||||
GT: ${{ secrets.GITEA_TOKEN }}
|
SELF_TOKEN: ${{ steps.prep.outputs.name == 'pm' && secrets.TOKEN_PM || steps.prep.outputs.name == 'junior' && secrets.TOKEN_JUNIOR || steps.prep.outputs.name == 'senior' && secrets.TOKEN_SENIOR || steps.prep.outputs.name == 'lead' && secrets.TOKEN_LEAD || steps.prep.outputs.name == 'qa' && secrets.TOKEN_QA || steps.prep.outputs.name == 'ops' && secrets.TOKEN_OPS || steps.prep.outputs.name == 'intern' && secrets.TOKEN_INTERN || '' }}
|
||||||
CID: ${{ github.event.comment.id }}
|
CID: ${{ github.event.comment.id }}
|
||||||
NUM: ${{ github.event.issue.number }}
|
NUM: ${{ github.event.issue.number }}
|
||||||
run: |
|
run: |
|
||||||
|
[ -n "$SELF_TOKEN" ] || exit 0
|
||||||
B="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/issues"
|
B="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/issues"
|
||||||
if [ -n "$CID" ]; then R="$B/comments/$CID/reactions"; else R="$B/$NUM/reactions"; fi
|
if [ -n "$CID" ]; then R="$B/comments/$CID/reactions"; else R="$B/$NUM/reactions"; fi
|
||||||
curl -sS -X DELETE -H "Authorization: token $GT" -H "Content-Type: application/json" "$R" -d '{"content":"eyes"}' || true
|
curl -sS -X DELETE -H "Authorization: token $SELF_TOKEN" -H "Content-Type: application/json" "$R" -d '{"content":"eyes"}' || true
|
||||||
curl -sS -X POST -H "Authorization: token $GT" -H "Content-Type: application/json" "$R" -d '{"content":"rocket"}' -w '\nreact -> HTTP %{http_code}\n' || true
|
curl -sS -X POST -H "Authorization: token $SELF_TOKEN" -H "Content-Type: application/json" "$R" -d '{"content":"rocket"}' -w '\nreact -> HTTP %{http_code}\n' || true
|
||||||
|
|
||||||
- name: Mark failed with 😕 (remove 👀)
|
- name: Mark failed with 😕 (remove 👀)
|
||||||
if: failure()
|
if: failure() && steps.prep.outputs.mode != 'skip'
|
||||||
env:
|
env:
|
||||||
GT: ${{ secrets.GITEA_TOKEN }}
|
SELF_TOKEN: ${{ steps.prep.outputs.name == 'pm' && secrets.TOKEN_PM || steps.prep.outputs.name == 'junior' && secrets.TOKEN_JUNIOR || steps.prep.outputs.name == 'senior' && secrets.TOKEN_SENIOR || steps.prep.outputs.name == 'lead' && secrets.TOKEN_LEAD || steps.prep.outputs.name == 'qa' && secrets.TOKEN_QA || steps.prep.outputs.name == 'ops' && secrets.TOKEN_OPS || steps.prep.outputs.name == 'intern' && secrets.TOKEN_INTERN || '' }}
|
||||||
CID: ${{ github.event.comment.id }}
|
CID: ${{ github.event.comment.id }}
|
||||||
NUM: ${{ github.event.issue.number }}
|
NUM: ${{ github.event.issue.number }}
|
||||||
run: |
|
run: |
|
||||||
|
[ -n "$SELF_TOKEN" ] || exit 0
|
||||||
B="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/issues"
|
B="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/issues"
|
||||||
if [ -n "$CID" ]; then R="$B/comments/$CID/reactions"; else R="$B/$NUM/reactions"; fi
|
if [ -n "$CID" ]; then R="$B/comments/$CID/reactions"; else R="$B/$NUM/reactions"; fi
|
||||||
curl -sS -X DELETE -H "Authorization: token $GT" -H "Content-Type: application/json" "$R" -d '{"content":"eyes"}' || true
|
curl -sS -X DELETE -H "Authorization: token $SELF_TOKEN" -H "Content-Type: application/json" "$R" -d '{"content":"eyes"}' || true
|
||||||
curl -sS -X POST -H "Authorization: token $GT" -H "Content-Type: application/json" "$R" -d '{"content":"confused"}' -w '\nreact -> HTTP %{http_code}\n' || true
|
curl -sS -X POST -H "Authorization: token $SELF_TOKEN" -H "Content-Type: application/json" "$R" -d '{"content":"confused"}' -w '\nreact -> HTTP %{http_code}\n' || true
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"desc": "Senior dev — complex, multi-file implementation (GLM-5.2 via Ollama Cloud, text-only)."
|
"desc": "Senior dev — complex, multi-file implementation (GLM-5.2 via Ollama Cloud, text-only)."
|
||||||
},
|
},
|
||||||
"lead": {
|
"lead": {
|
||||||
"model": "anthropic/claude-opus-4-8",
|
"model": "ollama-cloud/kimi-k3:cloud",
|
||||||
"vision": true,
|
"vision": true,
|
||||||
"mode": "pr",
|
"mode": "pr",
|
||||||
"skills": [
|
"skills": [
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
"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."
|
"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": "xai/grok-4.5",
|
||||||
"vision": false,
|
"vision": false,
|
||||||
"mode": "comment",
|
"mode": "comment",
|
||||||
"skills": [
|
"skills": [
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ COST=${COST:-0}; INP=${INP:-0}; OUT=${OUT:-0}; CR=${CR:-0}; CW=${CW:-0}; RE=${RE
|
|||||||
IN_TOTAL=$(( INP + CR + CW )) # total input context processed
|
IN_TOTAL=$(( INP + CR + CW )) # total input context processed
|
||||||
# Cost label: ollama / ollama-cloud models are SUBSCRIPTION-billed (GPU-time against the plan, no
|
# 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.
|
# $/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.
|
# Metered providers (xai/…) get the real dollar cost opencode computed.
|
||||||
case "${MODEL:-}" in
|
case "${MODEL:-}" in
|
||||||
ollama*|*"/ollama"*) COSTF="subscription" ;;
|
ollama*|*"/ollama"*) COSTF="subscription" ;;
|
||||||
*) COSTF=$(awk -v c="$COST" 'BEGIN{printf "$%.4f", c+0}') ;;
|
*) COSTF=$(awk -v c="$COST" 'BEGIN{printf "$%.4f", c+0}') ;;
|
||||||
|
|||||||
@@ -26,3 +26,9 @@ jq -rs '
|
|||||||
"### comment by \($who):\n\(.body | gsub("\\s*<!-- 🤖 agent reply — do not trigger -->"; ""))\n"' \
|
"### comment by \($who):\n\(.body | gsub("\\s*<!-- 🤖 agent reply — do not trigger -->"; ""))\n"' \
|
||||||
/tmp/thread_pages.json > /tmp/thread.md 2>/dev/null || : > /tmp/thread.md
|
/tmp/thread_pages.json > /tmp/thread.md 2>/dev/null || : > /tmp/thread.md
|
||||||
echo "thread comments fetched: $(grep -c '^### comment by ' /tmp/thread.md 2>/dev/null || echo 0) (newest 100 kept)"
|
echo "thread comments fetched: $(grep -c '^### comment by ' /tmp/thread.md 2>/dev/null || echo 0) (newest 100 kept)"
|
||||||
|
|
||||||
|
# Record the newest comment id on the thread BEFORE the agent runs. publish.sh compares against
|
||||||
|
# it to detect an agent that self-posted its reply mid-run (via the gitea-api skill, despite the
|
||||||
|
# prompt telling it not to) and skips the duplicate framework reply. Ids are monotonic — no dates.
|
||||||
|
jq -rs '[ (add // [])[].id ] | max // 0' /tmp/thread_pages.json > /tmp/thread_max_cid 2>/dev/null || echo 0 > /tmp/thread_max_cid
|
||||||
|
echo "pre-run newest comment id: $(cat /tmp/thread_max_cid)"
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Install opencode + provider config (+ Playwright MCP for browser agents).
|
# Install opencode + provider config (+ Playwright MCP for browser agents).
|
||||||
#
|
#
|
||||||
# Required env (provided by the workflow step): OLLAMA_URL OLLAMA_CLOUD_API_KEY NAME SKILLS
|
# Required env (provided by the workflow step): OLLAMA_URL OLLAMA_CLOUD_API_KEY XAI_API_KEY
|
||||||
# GITHUB_PATH HOME
|
# NAME SKILLS GITHUB_PATH HOME
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
# PIN the opencode version: an unpinned `latest` means a breaking release (CLI flags, or the
|
# 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
|
# --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).
|
# at once. Bump deliberately by changing this default (or set OPENCODE_VERSION in the step env).
|
||||||
OPENCODE_VERSION="${OPENCODE_VERSION:-1.17.13}"
|
OPENCODE_VERSION="${OPENCODE_VERSION:-1.17.13}"
|
||||||
|
# Skip the download when a cache hit already restored the pinned binary (see the Cache
|
||||||
|
# opencode CLI step in agent.yml). The installer always re-fetches otherwise.
|
||||||
|
OC_BIN="$HOME/.opencode/bin/opencode"
|
||||||
|
if [ -x "$OC_BIN" ] && "$OC_BIN" --version 2>/dev/null | grep -qF "$OPENCODE_VERSION"; then
|
||||||
|
echo "opencode $OPENCODE_VERSION already present (cache hit) — skipping install"
|
||||||
|
else
|
||||||
curl -fsSL https://opencode.ai/install | bash -s -- --version "$OPENCODE_VERSION"
|
curl -fsSL https://opencode.ai/install | bash -s -- --version "$OPENCODE_VERSION"
|
||||||
|
fi
|
||||||
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
|
||||||
@@ -31,19 +38,22 @@ esac
|
|||||||
SKILLS="${SKILLS:-[]}"
|
SKILLS="${SKILLS:-[]}"
|
||||||
PERM=$(jq -nc --argjson s "$SKILLS" '
|
PERM=$(jq -nc --argjson s "$SKILLS" '
|
||||||
{skill: ( {"*":"deny"} + (reduce $s[] as $k ({}; . + {($k):"allow"})) )}')
|
{skill: ( {"*":"deny"} + (reduce $s[] as $k ({}; . + {($k):"allow"})) )}')
|
||||||
# Two ollama providers: local self-hosted (ornith) + Ollama Cloud (gemma4/kimi-k2.7-code/glm-5.2/minimax-m3).
|
# Three OpenAI-compatible providers: local self-hosted ollama (ornith) + Ollama Cloud
|
||||||
# The provider `models:` maps are DERIVED from agents.json (the single source of truth, shared with
|
# (gemma4/kimi-k2.7-code/kimi-k3/glm-5.2/minimax-m3) + xAI (grok-4.5). The provider `models:` maps are
|
||||||
# route.sh) so every model an agent is routed to is always declared in the provider config.
|
# DERIVED from agents.json (the single source of truth, shared with route.sh) so every model an
|
||||||
# `ollama-cloud/` prefix models go to the cloud provider; `ollama/` prefix models go to the local
|
# agent is routed to is always declared in the provider config. `ollama-cloud/` prefix models go to
|
||||||
# provider. Built-in providers (e.g. `anthropic/claude-opus-4-8` for @lead) are not derived here.
|
# the cloud provider; `ollama/` prefix models go to the local provider; `xai/` prefix models go to
|
||||||
|
# the xAI provider (OpenAI-compatible, https://api.x.ai/v1). No other built-in providers remain.
|
||||||
# See issue #31.
|
# See issue #31.
|
||||||
AGENTS_JSON="${SCRIPTS:-$(dirname -- "$0")}/agents.json"
|
AGENTS_JSON="${SCRIPTS:-$(dirname -- "$0")}/agents.json"
|
||||||
CLOUD_MODELS=$(jq -r '[.[] | .model | select(startswith("ollama-cloud/")) | sub("^ollama-cloud/";"")] | map({(.):{}}) | add // {}' "$AGENTS_JSON")
|
CLOUD_MODELS=$(jq -r '[.[] | .model | select(startswith("ollama-cloud/")) | sub("^ollama-cloud/";"")] | map({(.):{}}) | add // {}' "$AGENTS_JSON")
|
||||||
LOCAL_MODELS=$(jq -r '[.[] | .model | select(startswith("ollama/")) | sub("^ollama/";"")] | map({(.):{}}) | add // {"ornith:35b":{}}' "$AGENTS_JSON")
|
LOCAL_MODELS=$(jq -r '[.[] | .model | select(startswith("ollama/")) | sub("^ollama/";"")] | map({(.):{}}) | add // {"ornith:35b":{}}' "$AGENTS_JSON")
|
||||||
jq -n --argjson mcp "$MCP" --argjson perm "$PERM" --argjson cloud "$CLOUD_MODELS" --argjson local "$LOCAL_MODELS" --arg url "$OLLAMA_URL" --arg ckey "$OLLAMA_CLOUD_API_KEY" '{
|
XAI_MODELS=$(jq -r '[.[] | .model | select(startswith("xai/")) | sub("^xai/";"")] | map({(.):{}}) | add // {}' "$AGENTS_JSON")
|
||||||
|
jq -n --argjson mcp "$MCP" --argjson perm "$PERM" --argjson cloud "$CLOUD_MODELS" --argjson local "$LOCAL_MODELS" --argjson xai "$XAI_MODELS" --arg url "$OLLAMA_URL" --arg ckey "$OLLAMA_CLOUD_API_KEY" --arg xkey "$XAI_API_KEY" '{
|
||||||
provider: {
|
provider: {
|
||||||
ollama: {npm:"@ai-sdk/openai-compatible", options:{baseURL:($url+"/v1")}, models:$local},
|
ollama: {npm:"@ai-sdk/openai-compatible", options:{baseURL:($url+"/v1")}, models:$local},
|
||||||
"ollama-cloud": {npm:"@ai-sdk/openai-compatible", options:{baseURL:"https://ollama.com/v1", apiKey:$ckey}, models:$cloud}
|
"ollama-cloud": {npm:"@ai-sdk/openai-compatible", options:{baseURL:"https://ollama.com/v1", apiKey:$ckey}, models:$cloud},
|
||||||
|
xai: {npm:"@ai-sdk/openai-compatible", options:{baseURL:"https://api.x.ai/v1", apiKey:$xkey}, models:$xai}
|
||||||
},
|
},
|
||||||
permission: $perm,
|
permission: $perm,
|
||||||
mcp: $mcp
|
mcp: $mcp
|
||||||
|
|||||||
@@ -195,7 +195,31 @@ if [ "$MODE" != "pr" ]; then
|
|||||||
done < /tmp/subtasks.txt
|
done < /tmp/subtasks.txt
|
||||||
subtext=$(printf '\n\n---\nCreated sub-issues%s (mention an agent on each when ready):%b' "${ms:+ under milestone **$ms**}" "$links")
|
subtext=$(printf '\n\n---\nCreated sub-issues%s (mention an agent on each when ready):%b' "${ms:+ under milestone **$ms**}" "$links")
|
||||||
fi
|
fi
|
||||||
|
# DEDUP GUARD (issue: @pm double-posts its report). Prompt-level "do not self-post" is ignored
|
||||||
|
# by some models, so enforce it here: if the agent ALREADY posted a comment on this thread
|
||||||
|
# during the run (any comment by $NAME newer than the pre-run newest id from fetch-thread.sh),
|
||||||
|
# its self-post IS the reply — skip the duplicate framework comment. Markers (CLOSE_ISSUE,
|
||||||
|
# DELEGATE, MERGE_PR, subtasks) were already processed above and are unaffected.
|
||||||
|
# FAIL OPEN: if the pre-run marker is missing (fetch-thread hiccup), pre_cid=0 would make the
|
||||||
|
# agent's comments from PREVIOUS runs count as self-posts and wrongly suppress the reply.
|
||||||
|
# Without the marker, skip the guard and post normally.
|
||||||
|
pre_cid=$(cat /tmp/thread_max_cid 2>/dev/null || echo "")
|
||||||
|
selfposts=0
|
||||||
|
if [ -n "$pre_cid" ]; then
|
||||||
|
: > /tmp/all_comments.json
|
||||||
|
for pg in $(seq 1 10); do
|
||||||
|
cpg=$(curl -sS "${hdr[@]}" "$API/issues/$NUM/comments?limit=50&page=$pg" 2>/dev/null) || cpg='[]'
|
||||||
|
cn=$(printf '%s' "$cpg" | jq 'if type=="array" then length else 0 end' 2>/dev/null || echo 0)
|
||||||
|
[ "${cn:-0}" -gt 0 ] && printf '%s\n' "$cpg" >> /tmp/all_comments.json
|
||||||
|
[ "${cn:-0}" -lt 50 ] && break
|
||||||
|
done
|
||||||
|
selfposts=$(jq -rs --arg n "$NAME" --argjson c "${pre_cid:-0}" '[ (add // [])[] | select(.user.login==$n) | select(.id > $c) ] | length' /tmp/all_comments.json 2>/dev/null || echo 0)
|
||||||
|
fi
|
||||||
|
if [ "${selfposts:-0}" -gt 0 ]; then
|
||||||
|
echo "agent @$NAME already posted ${selfposts} comment(s) on #$NUM during this run — skipping duplicate framework reply"
|
||||||
|
else
|
||||||
post "$(printf '%s%s%s' "$msg" "$subtext" "$activity")"
|
post "$(printf '%s%s%s' "$msg" "$subtext" "$activity")"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- @pm autopilot merge: @pm is the ONLY agent that merges, and ONLY under the autopilot label ---
|
# --- @pm autopilot merge: @pm is the ONLY agent that merges, and ONLY under the autopilot label ---
|
||||||
# (@qa never merges — it approves and hands back here.) Merge with the PAT (TTOK), not the built-in
|
# (@qa never merges — it approves and hands back here.) Merge with the PAT (TTOK), not the built-in
|
||||||
@@ -310,7 +334,14 @@ if [ -n "$(git status --porcelain)" ]; then
|
|||||||
git add -A
|
git add -A
|
||||||
git commit -m "@$NAME: issue #$NUM"
|
git commit -m "@$NAME: issue #$NUM"
|
||||||
fi
|
fi
|
||||||
git push origin "HEAD:$BRANCH" || true
|
if git push origin "HEAD:$BRANCH"; then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
status=$?
|
||||||
|
echo "git push failed for $BRANCH (exit $status)"
|
||||||
|
post "$(printf '⚠️ Push to branch `%s` failed (git exit %s). The PR will not open until the push succeeds. Please check the Actions log.%s' "$BRANCH" "$status" "$activity")"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
git fetch -q origin 2>/dev/null || true
|
git fetch -q origin 2>/dev/null || true
|
||||||
|
|
||||||
prbody=$(printf '%s\n\n---\nResolves #%s' "$prdesc" "$NUM")
|
prbody=$(printf '%s\n\n---\nResolves #%s' "$prdesc" "$NUM")
|
||||||
@@ -336,14 +367,21 @@ url=$(printf '%s' "$resp" | jq -r '.html_url // empty' 2>/dev/null)
|
|||||||
prnum=$(printf '%s' "$resp" | jq -r '.number // empty' 2>/dev/null)
|
prnum=$(printf '%s' "$resp" | jq -r '.number // empty' 2>/dev/null)
|
||||||
if [ -z "$url" ]; then
|
if [ -z "$url" ]; then
|
||||||
title="@$NAME: $TITLE"
|
title="@$NAME: $TITLE"
|
||||||
resp=$(curl -sS -X POST "${hdr[@]}" "$API/pulls" \
|
resp_body=/tmp/pr_create_resp.json
|
||||||
|
http_status=$(curl -sS -o "$resp_body" -w '%{http_code}' -X POST "${hdr[@]}" "$API/pulls" \
|
||||||
-d "$(jq -nc --arg t "$title" --arg h "$br" --arg b "$prbody" \
|
-d "$(jq -nc --arg t "$title" --arg h "$br" --arg b "$prbody" \
|
||||||
'{title:$t, head:$h, base:"main", body:$b}')")
|
'{title:$t, head:$h, base:"main", body:$b}')")
|
||||||
echo "PR create ($br): $resp"
|
resp=$(cat "$resp_body" 2>/dev/null || true)
|
||||||
|
echo "PR create ($br): HTTP $http_status — $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)
|
prnum=$(printf '%s' "$resp" | jq -r '.number // empty' 2>/dev/null)
|
||||||
fi
|
fi
|
||||||
[ -z "$url" ] && { echo "PR open/lookup failed for $br — posting reply on issue instead"; post "$(printf '%s%s' "$reply" "$activity")"; exit 0; }
|
if [ -z "$url" ]; then
|
||||||
|
err_msg=$(printf '%s' "$resp" | jq -r 'if type=="object" and .message then .message else "(no error message in response)" end' 2>/dev/null)
|
||||||
|
echo "PR open/lookup failed for $br — HTTP $http_status — response: $resp"
|
||||||
|
post "$(printf '⚠️ Failed to open PR for branch `%s`.\n\nHTTP status: %s\nGitea message: %s%s' "$br" "$http_status" "$err_msg" "$activity")"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# Posts to the PR thread when we have a PR number, else to the origin issue ($NUM).
|
# Posts to the PR thread when we have a PR number, else to the origin issue ($NUM).
|
||||||
prpost() {
|
prpost() {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# plain-text reply (/tmp/agent_out.md) plus the raw event stream (/tmp/events.jsonl).
|
# plain-text reply (/tmp/agent_out.md) plus the raw event stream (/tmp/events.jsonl).
|
||||||
#
|
#
|
||||||
# Required env (provided by the workflow step):
|
# Required env (provided by the workflow step):
|
||||||
# ANTHROPIC_API_KEY SELF_TOKEN NAME MODEL VISION MODE HAS_IMAGES BRANCH AUTOPILOT NUM TITLE
|
# XAI_API_KEY SELF_TOKEN NAME MODEL VISION MODE HAS_IMAGES BRANCH AUTOPILOT NUM TITLE
|
||||||
# IBODY CMT
|
# IBODY CMT
|
||||||
# FILES (the opencode -f image flags, from the imgs step output)
|
# 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).
|
# AUTOPILOT is 'true' when the issue carries the `autopilot` label (label-gated autopilot mode).
|
||||||
@@ -149,6 +149,12 @@ ${LEARN}
|
|||||||
shown by Gitea. Do NOT begin your reply with your own name, an '@${NAME}' header, or a '🤖/🔨 @you'
|
shown by Gitea. Do NOT begin your reply with your own name, an '@${NAME}' header, or a '🤖/🔨 @you'
|
||||||
line; just write the content directly.
|
line; just write the content directly.
|
||||||
|
|
||||||
|
Do NOT use the gitea-api skill to post your reply, report, or any comment on THIS thread
|
||||||
|
yourself. The automation already posts your reply exactly once — self-posting it too is what
|
||||||
|
creates the duplicate comments you must avoid. On this thread, use gitea-api only to READ, or to
|
||||||
|
take an explicit action you were asked for (add/remove a label, close the issue, merge the PR).
|
||||||
|
Your report or answer IS your reply text — write it as your reply; do not post it via the API.
|
||||||
|
|
||||||
TEAM ROSTER (who does what — hand off if a task isn't yours):
|
TEAM ROSTER (who does what — hand off if a task isn't yours):
|
||||||
${ROSTER}
|
${ROSTER}
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ curl -sS -X POST -H "Authorization: token $SELF_TOKEN" "$API/orgs/{org}/labels"
|
|||||||
3. Commit the standard caller so it gets the agents — `PUT /repos/{owner}/{repo}/contents/.gitea/workflows/ai-agent.yml`
|
3. Commit the standard caller so it gets the agents — `PUT /repos/{owner}/{repo}/contents/.gitea/workflows/ai-agent.yml`
|
||||||
with base64 `content`, `message`, `branch:"main"` (copy the exact caller from the `agents` repo README).
|
with base64 `content`, `message`, `branch:"main"` (copy the exact caller from the `agents` repo README).
|
||||||
4. Add the agent bot users as collaborators: `PUT /repos/{owner}/{repo}/collaborators/{username}` (`{"permission":"write"}`).
|
4. Add the agent bot users as collaborators: `PUT /repos/{owner}/{repo}/collaborators/{username}` (`{"permission":"write"}`).
|
||||||
5. Ensure the repo can run agents — the org must hold the runtime secrets (ANTHROPIC_API_KEY, SELF_TOKEN,
|
5. Ensure the repo can run agents — the org must hold the runtime secrets (XAI_API_KEY, SELF_TOKEN,
|
||||||
TOKEN_* , OLLAMA_URL, OLLAMA_CLOUD_API_KEY); set any missing via the secrets calls above.
|
TOKEN_* , OLLAMA_URL, OLLAMA_CLOUD_API_KEY); set any missing via the secrets calls above.
|
||||||
|
|
||||||
## Packages / container registry
|
## Packages / container registry
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ Shared **AI dev-team** workflow for Gitea Actions, reusable across repos. It giv
|
|||||||
| `@pm` | `ollama-cloud/minimax-m3: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. |
|
| `@pm` | `ollama-cloud/minimax-m3: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` | 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` | Tech lead — the hardest problems, architecture, and final calls. |
|
| `@lead` | `ollama-cloud/kimi-k3:cloud` | yes | pr | `gitea-api` | Tech lead — the hardest problems, architecture, and final calls. |
|
||||||
| `@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. |
|
| `@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` | `xai/grok-4.5` | 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. |
|
||||||
| `@intern` | `ollama/ornith:35b` | no | pr | — | Intern — very basic tasks only, routed to the local Ollama model (`ornith:35b`). Text-only, cannot read images. Escalates anything non-trivial to `@junior`, `@senior` or `@lead`. |
|
| `@intern` | `ollama/ornith:35b` | no | pr | — | Intern — very basic tasks only, routed to the local Ollama model (`ornith:35b`). Text-only, cannot read images. Escalates anything non-trivial to `@junior`, `@senior` or `@lead`. |
|
||||||
|
|
||||||
The registry `.gitea/workflows/scripts/agents.json` is the source of truth for this mapping — if you
|
The registry `.gitea/workflows/scripts/agents.json` is the source of truth for this mapping — if you
|
||||||
@@ -111,8 +111,8 @@ points `$SCRIPTS` at it. Keep the workflow and its scripts moving together on `m
|
|||||||
|
|
||||||
| Secret | For |
|
| Secret | For |
|
||||||
|--------|-----|
|
|--------|-----|
|
||||||
| `ANTHROPIC_API_KEY` | `@lead` (and `@pm`/`@senior`/`@qa` if on Claude) |
|
| `XAI_API_KEY` | `@ops` (and any other agent switched to a `xai/…` model) |
|
||||||
| `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, kimi-k3, 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 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_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`. |
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user