Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55e00871f9 |
@@ -4,31 +4,6 @@ name: agent
|
|||||||
# The gate + steps run in the caller's event context (github.event.* / github.repository are the caller's).
|
# The gate + steps run in the caller's event context (github.event.* / github.repository are the caller's).
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
# Explicit secret contract so callers can map secrets by name (more reliable than
|
|
||||||
# secrets: inherit alone on some Gitea versions / cross-owner reusable workflows).
|
|
||||||
secrets:
|
|
||||||
GITEA_TOKEN:
|
|
||||||
required: true
|
|
||||||
OLLAMA_URL:
|
|
||||||
required: false
|
|
||||||
OLLAMA_CLOUD_API_KEY:
|
|
||||||
required: false
|
|
||||||
XAI_API_KEY:
|
|
||||||
required: false
|
|
||||||
TOKEN_PM:
|
|
||||||
required: false
|
|
||||||
TOKEN_SENIOR:
|
|
||||||
required: false
|
|
||||||
TOKEN_JUNIOR:
|
|
||||||
required: false
|
|
||||||
TOKEN_LEAD:
|
|
||||||
required: false
|
|
||||||
TOKEN_QA:
|
|
||||||
required: false
|
|
||||||
TOKEN_OPS:
|
|
||||||
required: false
|
|
||||||
TOKEN_INTERN:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
# Pinned opencode version — used to install it and to key the CI cache below.
|
# Pinned opencode version — used to install it and to key the CI cache below.
|
||||||
env:
|
env:
|
||||||
@@ -50,10 +25,9 @@ jobs:
|
|||||||
# 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: >
|
||||||
(github.event.comment == null && (github.event.issue.user.login == 'ffaerber' || github.event.issue.user.login == 'hermes')) ||
|
(github.event.comment == null && github.event.issue.user.login == 'ffaerber') ||
|
||||||
(github.event.comment != null &&
|
(github.event.comment != null &&
|
||||||
(github.event.comment.user.login == 'ffaerber' ||
|
(github.event.comment.user.login == 'ffaerber' ||
|
||||||
github.event.comment.user.login == 'hermes' ||
|
|
||||||
github.event.comment.user.login == 'pm' ||
|
github.event.comment.user.login == 'pm' ||
|
||||||
github.event.comment.user.login == 'junior' ||
|
github.event.comment.user.login == 'junior' ||
|
||||||
github.event.comment.user.login == 'senior' ||
|
github.event.comment.user.login == 'senior' ||
|
||||||
@@ -175,9 +149,7 @@ jobs:
|
|||||||
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 }}
|
||||||
# Accept common alternate names — empty XAI_API_KEY has bitten us when the
|
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
|
||||||
# secret was stored under a slightly different key on the caller repo.
|
|
||||||
XAI_API_KEY: ${{ secrets.XAI_API_KEY || secrets.XAI_KEY || secrets.GROK_API_KEY || secrets.XAI_TOKEN }}
|
|
||||||
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"
|
||||||
@@ -240,7 +212,8 @@ jobs:
|
|||||||
id: run
|
id: run
|
||||||
env:
|
env:
|
||||||
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||||
XAI_API_KEY: ${{ secrets.XAI_API_KEY || secrets.XAI_KEY || secrets.GROK_API_KEY || secrets.XAI_TOKEN }}
|
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
|
||||||
@@ -249,7 +222,6 @@ jobs:
|
|||||||
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 || '' }}
|
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 || '' }}
|
||||||
NAME: ${{ steps.prep.outputs.name }}
|
NAME: ${{ steps.prep.outputs.name }}
|
||||||
MODEL: ${{ steps.prep.outputs.model }}
|
MODEL: ${{ steps.prep.outputs.model }}
|
||||||
FALLBACK: ${{ steps.prep.outputs.fallback }}
|
|
||||||
VISION: ${{ steps.prep.outputs.vision }}
|
VISION: ${{ steps.prep.outputs.vision }}
|
||||||
MODE: ${{ steps.prep.outputs.mode }}
|
MODE: ${{ steps.prep.outputs.mode }}
|
||||||
WORKMODE: ${{ steps.prep.outputs.workmode }} # build | discuss (devs consulted in-thread)
|
WORKMODE: ${{ steps.prep.outputs.workmode }} # build | discuss (devs consulted in-thread)
|
||||||
|
|||||||
@@ -12,17 +12,4 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
agent:
|
agent:
|
||||||
uses: gitea/agents/.gitea/workflows/agent.yml@main
|
uses: gitea/agents/.gitea/workflows/agent.yml@main
|
||||||
# Explicit secret map (plus inherit) so XAI_API_KEY / OLLAMA_* always reach the reusable
|
secrets: inherit
|
||||||
# workflow. secrets: inherit alone has left XAI_API_KEY empty on some Gitea cross-owner calls.
|
|
||||||
secrets:
|
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
OLLAMA_URL: ${{ secrets.OLLAMA_URL }}
|
|
||||||
OLLAMA_CLOUD_API_KEY: ${{ secrets.OLLAMA_CLOUD_API_KEY }}
|
|
||||||
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
|
|
||||||
TOKEN_PM: ${{ secrets.TOKEN_PM }}
|
|
||||||
TOKEN_SENIOR: ${{ secrets.TOKEN_SENIOR }}
|
|
||||||
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
|
|
||||||
TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }}
|
|
||||||
TOKEN_QA: ${{ secrets.TOKEN_QA }}
|
|
||||||
TOKEN_OPS: ${{ secrets.TOKEN_OPS }}
|
|
||||||
TOKEN_INTERN: ${{ secrets.TOKEN_INTERN }}
|
|
||||||
|
|||||||
@@ -1,34 +1,31 @@
|
|||||||
{
|
{
|
||||||
"pm": {
|
"pm": {
|
||||||
"model": "ollama-cloud/minimax-m3:cloud",
|
"model": "ollama-cloud/minimax-m3:cloud",
|
||||||
"fallback": "xai-oc/grok-4.5",
|
|
||||||
"vision": true,
|
"vision": true,
|
||||||
"mode": "comment",
|
"mode": "comment",
|
||||||
"skills": [
|
"skills": [
|
||||||
"gitea-api"
|
"gitea-api"
|
||||||
],
|
],
|
||||||
"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. Falls back to xAI grok-4.5 if Ollama Cloud is unavailable/quota-exhausted."
|
"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",
|
||||||
"fallback": "xai-oc/grok-4.3",
|
|
||||||
"vision": false,
|
"vision": false,
|
||||||
"mode": "pr",
|
"mode": "pr",
|
||||||
"skills": [],
|
"skills": [],
|
||||||
"desc": "Junior dev — small, low-risk changes (mostly YAML/compose/config). Text-only, cannot read images. Defers complex or image tasks to @senior or @lead. Falls back to xAI grok-4.3 if Ollama Cloud fails."
|
"desc": "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": {
|
"senior": {
|
||||||
"model": "ollama-cloud/glm-5.2:cloud",
|
"model": "ollama-cloud/glm-5.2:cloud",
|
||||||
"fallback": "xai-oc/grok-4.5",
|
|
||||||
"vision": false,
|
"vision": false,
|
||||||
"mode": "pr",
|
"mode": "pr",
|
||||||
"skills": [
|
"skills": [
|
||||||
"gitea-api"
|
"gitea-api"
|
||||||
],
|
],
|
||||||
"desc": "Senior dev — complex, multi-file implementation (GLM-5.2 via Ollama Cloud, text-only). Falls back to xAI grok-4.5 if Ollama Cloud fails."
|
"desc": "Senior dev — complex, multi-file implementation (GLM-5.2 via Ollama Cloud, text-only)."
|
||||||
},
|
},
|
||||||
"lead": {
|
"lead": {
|
||||||
"model": "xai-oc/grok-4.5",
|
"model": "xai/grok-4.5",
|
||||||
"vision": true,
|
"vision": true,
|
||||||
"mode": "pr",
|
"mode": "pr",
|
||||||
"skills": [
|
"skills": [
|
||||||
@@ -38,16 +35,15 @@
|
|||||||
},
|
},
|
||||||
"qa": {
|
"qa": {
|
||||||
"model": "ollama-cloud/minimax-m3:cloud",
|
"model": "ollama-cloud/minimax-m3:cloud",
|
||||||
"fallback": "xai-oc/grok-4.5",
|
|
||||||
"vision": true,
|
"vision": true,
|
||||||
"mode": "comment",
|
"mode": "comment",
|
||||||
"skills": [
|
"skills": [
|
||||||
"gitea-api"
|
"gitea-api"
|
||||||
],
|
],
|
||||||
"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. Falls back to xAI grok-4.5 if Ollama Cloud fails."
|
"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": "xai-oc/grok-4.5",
|
"model": "anthropic/claude-opus-4-8",
|
||||||
"vision": false,
|
"vision": false,
|
||||||
"mode": "comment",
|
"mode": "comment",
|
||||||
"skills": [
|
"skills": [
|
||||||
@@ -57,10 +53,9 @@
|
|||||||
},
|
},
|
||||||
"intern": {
|
"intern": {
|
||||||
"model": "ollama/ornith:35b",
|
"model": "ollama/ornith:35b",
|
||||||
"fallback": "xai-oc/grok-4.3",
|
|
||||||
"vision": false,
|
"vision": false,
|
||||||
"mode": "pr",
|
"mode": "pr",
|
||||||
"skills": [],
|
"skills": [],
|
||||||
"desc": "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. Falls back to xAI grok-4.3 if local Ollama is down."
|
"desc": "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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (xai-oc/…) get the real dollar cost opencode computed.
|
# Metered providers (anthropic/…) 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}') ;;
|
||||||
|
|||||||
@@ -5,14 +5,6 @@
|
|||||||
# NAME SKILLS GITHUB_PATH HOME
|
# NAME SKILLS GITHUB_PATH HOME
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
# Non-secret diagnostics — prove which provider keys reached the runner (length only).
|
|
||||||
echo "provider key lengths: OLLAMA_URL=${#OLLAMA_URL} OLLAMA_CLOUD_API_KEY=${#OLLAMA_CLOUD_API_KEY} XAI_API_KEY=${#XAI_API_KEY}"
|
|
||||||
if [ -z "${XAI_API_KEY:-}" ]; then
|
|
||||||
echo "WARNING: XAI_API_KEY is empty in this job. xai-oc fallback will fail."
|
|
||||||
echo "Fix: set Actions secret XAI_API_KEY on the CALLER repo (e.g. ffaerber/homelab),"
|
|
||||||
echo "not only on gitea/agents. Name must be exact: XAI_API_KEY"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 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).
|
||||||
@@ -46,32 +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"})) )}')
|
||||||
# Three OpenAI-compatible providers: local self-hosted ollama (ornith) + Ollama Cloud
|
# Two ollama providers: local self-hosted (ornith) + Ollama Cloud (gemma4/kimi-k2.7-code/glm-5.2/minimax-m3).
|
||||||
# (gemma4/kimi-k2.7-code/glm-5.2/minimax-m3) + xAI (grok-4.5). The provider `models:` maps are
|
# The provider `models:` maps are DERIVED from agents.json (the single source of truth, shared with
|
||||||
# DERIVED from agents.json (the single source of truth, shared with route.sh) so every model an
|
# route.sh) so every model an agent is routed to is always declared in the provider config.
|
||||||
# agent is routed to is always declared in the provider config. `ollama-cloud/` prefix models go to
|
# `ollama-cloud/` prefix models go to the cloud provider; `ollama/` prefix models go to the local
|
||||||
# the cloud provider; `ollama/` prefix models go to the local provider; `xai-oc/` prefix models go
|
# provider. `xai/` prefix models go to the xAI provider (OpenAI-compatible, https://api.x.ai/v1).
|
||||||
# to our xAI shim (OpenAI-compatible, https://api.x.ai/v1). No other built-in providers remain.
|
# Built-in providers (e.g. `anthropic/claude-opus-4-8` for @ops) are not derived here.
|
||||||
#
|
|
||||||
# The provider key is `xai-oc`, NOT `xai` — opencode ships a built-in `xai` entry in its own model
|
|
||||||
# catalog (a real @ai-sdk/xai integration that implements the Responses API). Naming our custom
|
|
||||||
# openai-compatible shim `xai` collides with that catalog entry: opencode's --auto permission-check
|
|
||||||
# path then assumes the catalog's provider (which has `.responses()`) and crashes with
|
|
||||||
# "Z.responses is not a function" on every run once a `permission` block is present (i.e. always in
|
|
||||||
# this pipeline). Confirmed by reproducing locally with the production config shape on opencode
|
|
||||||
# 1.17.13 — renaming the key to `xai-oc` avoids the collision entirely. See issue #118.
|
|
||||||
# See issue #31.
|
# See issue #31.
|
||||||
AGENTS_JSON="${SCRIPTS:-$(dirname -- "$0")}/agents.json"
|
AGENTS_JSON="${SCRIPTS:-$(dirname -- "$0")}/agents.json"
|
||||||
# Include primary `.model` AND optional `.fallback` so failover models are always registered
|
CLOUD_MODELS=$(jq -r '[.[] | .model | select(startswith("ollama-cloud/")) | sub("^ollama-cloud/";"")] | map({(.):{}}) | add // {}' "$AGENTS_JSON")
|
||||||
# in opencode provider maps (issue: Ollama Cloud quota → xAI).
|
LOCAL_MODELS=$(jq -r '[.[] | .model | select(startswith("ollama/")) | sub("^ollama/";"")] | map({(.):{}}) | add // {"ornith:35b":{}}' "$AGENTS_JSON")
|
||||||
CLOUD_MODELS=$(jq -r '[.[] | (.model, .fallback) | select(type=="string" and startswith("ollama-cloud/")) | sub("^ollama-cloud/";"")] | unique | map({(.):{}}) | add // {}' "$AGENTS_JSON")
|
XAI_MODELS=$(jq -r '[.[] | .model | select(startswith("xai/")) | sub("^xai/";"")] | map({(.):{}}) | add // {}' "$AGENTS_JSON")
|
||||||
LOCAL_MODELS=$(jq -r '[.[] | (.model, .fallback) | select(type=="string" and startswith("ollama/")) | sub("^ollama/";"")] | unique | map({(.):{}}) | add // {"ornith:35b":{}}' "$AGENTS_JSON")
|
|
||||||
XAI_MODELS=$(jq -r '[.[] | (.model, .fallback) | select(type=="string" and startswith("xai-oc/")) | sub("^xai-oc/";"")] | unique | 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" '{
|
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-oc": {npm:"@ai-sdk/openai-compatible", options:{baseURL:"https://api.x.ai/v1", apiKey:$xkey}, models:$xai}
|
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
|
||||||
|
|||||||
@@ -334,14 +334,7 @@ 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
|
||||||
if git push origin "HEAD:$BRANCH"; then
|
git push origin "HEAD:$BRANCH" || true
|
||||||
:
|
|
||||||
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")
|
||||||
@@ -367,21 +360,14 @@ 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_body=/tmp/pr_create_resp.json
|
resp=$(curl -sS -X POST "${hdr[@]}" "$API/pulls" \
|
||||||
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}')")
|
||||||
resp=$(cat "$resp_body" 2>/dev/null || true)
|
echo "PR create ($br): $resp"
|
||||||
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
|
||||||
if [ -z "$url" ]; then
|
[ -z "$url" ] && { echo "PR open/lookup failed for $br — posting reply on issue instead"; post "$(printf '%s%s' "$reply" "$activity")"; exit 0; }
|
||||||
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() {
|
||||||
|
|||||||
@@ -43,14 +43,12 @@ if [ -z "$name" ]; then
|
|||||||
# Not an agent task (e.g. the gate's contains() matched "@internal"). Skip GRACEFULLY: emit
|
# Not an agent task (e.g. the gate's contains() matched "@internal"). Skip GRACEFULLY: emit
|
||||||
# mode=skip so every later step no-ops — a red run for a non-agent comment is just noise.
|
# mode=skip so every later step no-ops — a red run for a non-agent comment is just noise.
|
||||||
echo "no known agent mentioned (word-boundary) — skipping run"
|
echo "no known agent mentioned (word-boundary) — skipping run"
|
||||||
{ echo "name=none"; echo "model=none"; echo "fallback="; echo "vision=false"; echo "mode=skip"; echo "skills=[]";
|
{ echo "name=none"; echo "model=none"; echo "vision=false"; echo "mode=skip"; echo "skills=[]";
|
||||||
echo "branch=main"; echo "new=false"; echo "autopilot=false"; echo "issnum=$NUM"; } >> "$GITHUB_OUTPUT"
|
echo "branch=main"; echo "new=false"; echo "autopilot=false"; echo "issnum=$NUM"; } >> "$GITHUB_OUTPUT"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
model=$(jq -r --arg a "$name" '.[$a].model' /tmp/agents.json)
|
model=$(jq -r --arg a "$name" '.[$a].model' /tmp/agents.json)
|
||||||
# Optional provider failover when primary is quota/network-dead (e.g. Ollama Cloud → xAI).
|
|
||||||
fallback=$(jq -r --arg a "$name" '.[$a].fallback // empty' /tmp/agents.json)
|
|
||||||
vision=$(jq -r --arg a "$name" '.[$a].vision' /tmp/agents.json)
|
vision=$(jq -r --arg a "$name" '.[$a].vision' /tmp/agents.json)
|
||||||
mode=$(jq -r --arg a "$name" '.[$a].mode' /tmp/agents.json)
|
mode=$(jq -r --arg a "$name" '.[$a].mode' /tmp/agents.json)
|
||||||
# Compact JSON array of the skills this agent may load (scopes permission.skill in install-opencode.sh).
|
# Compact JSON array of the skills this agent may load (scopes permission.skill in install-opencode.sh).
|
||||||
@@ -72,8 +70,8 @@ if [ "$mode" = "pr" ] && [ -z "$IS_PR" ]; then
|
|||||||
workmode=discuss
|
workmode=discuss
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
echo "Routing to @$name (model=$model fallback=${fallback:-none} vision=$vision mode=$mode workmode=$workmode skills=$skills)"
|
echo "Routing to @$name (model=$model vision=$vision mode=$mode workmode=$workmode skills=$skills)"
|
||||||
{ echo "name=$name"; echo "model=$model"; echo "fallback=$fallback"; echo "vision=$vision"; echo "mode=$mode"; echo "workmode=$workmode"; echo "skills=$skills"; } >> "$GITHUB_OUTPUT"
|
{ echo "name=$name"; echo "model=$model"; echo "vision=$vision"; echo "mode=$mode"; echo "workmode=$workmode"; echo "skills=$skills"; } >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
# Act as the agent's own Gitea user when its token is set; else the built-in bot.
|
# Act as the agent's own Gitea user when its token is set; else the built-in bot.
|
||||||
case "$name" in
|
case "$name" in
|
||||||
|
|||||||
@@ -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):
|
||||||
# XAI_API_KEY SELF_TOKEN NAME MODEL VISION MODE HAS_IMAGES BRANCH AUTOPILOT NUM TITLE
|
# ANTHROPIC_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).
|
||||||
@@ -186,16 +186,6 @@ rc=1
|
|||||||
# instance-wide. 20 min is far above any legitimate attempt. timeout SIGTERMs, then SIGKILLs 30s
|
# instance-wide. 20 min is far above any legitimate attempt. timeout SIGTERMs, then SIGKILLs 30s
|
||||||
# later. rc=124 (timed out) is NOT retried — a hung backend stays hung; fail fast, free the runner.
|
# later. rc=124 (timed out) is NOT retried — a hung backend stays hung; fail fast, free the runner.
|
||||||
AGENT_TIMEOUT="${AGENT_TIMEOUT:-1200}"
|
AGENT_TIMEOUT="${AGENT_TIMEOUT:-1200}"
|
||||||
# Provider failover: when primary model dies on quota/auth/provider errors, switch once to
|
|
||||||
# FALLBACK (from agents.json) and continue the retry loop. Transient rate-limits still back off
|
|
||||||
# on the current model first.
|
|
||||||
FALLBACK_MODEL="${FALLBACK:-}"
|
|
||||||
fallback_used=0
|
|
||||||
is_failover_error() {
|
|
||||||
# Ollama Cloud exhausted / provider hard-fail — switch to fallback rather than thrash.
|
|
||||||
grep -qiE 'overloaded|429|529|rate.?limit|timeout|ETIMEDOUT|ECONNRESET|EAI_AGAIN|quota|credit|balance|usage.?limit|limit.?exceed|402|403|401|insufficient|out of credits|payment.?required|model_not_found|not found|Unavailable|capacity|ENOTFOUND|ECONNREFUSED' \
|
|
||||||
/tmp/events.jsonl /tmp/agent_err.log 2>/dev/null
|
|
||||||
}
|
|
||||||
for attempt in 1 2 3; do
|
for attempt in 1 2 3; do
|
||||||
echo "opencode attempt $attempt/3 for @$NAME ($MODEL, timeout ${AGENT_TIMEOUT}s)"
|
echo "opencode attempt $attempt/3 for @$NAME ($MODEL, timeout ${AGENT_TIMEOUT}s)"
|
||||||
rc=0
|
rc=0
|
||||||
@@ -206,14 +196,6 @@ for attempt in 1 2 3; do
|
|||||||
echo "--- stderr (trace) ---"; cat /tmp/agent_err.log
|
echo "--- stderr (trace) ---"; cat /tmp/agent_err.log
|
||||||
[ $rc -eq 0 ] && break
|
[ $rc -eq 0 ] && break
|
||||||
if [ $rc -eq 124 ]; then echo "attempt timed out after ${AGENT_TIMEOUT}s — backend hung, not retrying"; break; fi
|
if [ $rc -eq 124 ]; then echo "attempt timed out after ${AGENT_TIMEOUT}s — backend hung, not retrying"; break; fi
|
||||||
if [ $fallback_used -eq 0 ] && [ -n "$FALLBACK_MODEL" ] && [ "$FALLBACK_MODEL" != "$MODEL" ] && is_failover_error; then
|
|
||||||
echo "primary model failed — failing over to fallback: $FALLBACK_MODEL"
|
|
||||||
MODEL="$FALLBACK_MODEL"
|
|
||||||
fallback_used=1
|
|
||||||
# short pause then use next attempt slot on the fallback provider
|
|
||||||
sleep 2
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
if grep -qiE 'overloaded|429|529|rate.?limit|timeout|ETIMEDOUT|ECONNRESET|EAI_AGAIN' /tmp/events.jsonl /tmp/agent_err.log; then
|
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
|
echo "transient error — backing off $((attempt*20))s"; sleep $((attempt * 20)); continue
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -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 (XAI_API_KEY, SELF_TOKEN,
|
5. Ensure the repo can run agents — the org must hold the runtime secrets (ANTHROPIC_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` | `xai-oc/grok-4.5` | yes | pr | `gitea-api` | 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 / 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` | `xai-oc/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. |
|
| `@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. |
|
||||||
| `@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,7 +111,7 @@ points `$SCRIPTS` at it. Keep the workflow and its scripts moving together on `m
|
|||||||
|
|
||||||
| Secret | For |
|
| Secret | For |
|
||||||
|--------|-----|
|
|--------|-----|
|
||||||
| `XAI_API_KEY` | `@lead`, `@ops` (and any other agent switched to a `xai-oc/…` model) |
|
| `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 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