`${VAR:-{}}` appends a stray '}' when VAR is set (bash brace-matching), so the
JSON handed to a caller skill's setup.sh was corrupted and its jq failed with
"Unmatched '}'" — install-caller-skills.sh then caught the non-zero exit and
skipped the skill. Default SECRETS_JSON in two safe steps and pass it as a plain
var. This is why node1-ssh never installed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agents
Shared AI dev-team workflow for Gitea Actions, reusable across repos. It gives any repo the
@pm / @junior / @senior / @lead / @qa agents driven from issues and comments.
Agents
| 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. |
@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). |
@lead |
anthropic/claude-opus-4-8 |
yes | pr | gitea-api, node1-ssh |
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. |
@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
or an agent's skills there, update this table too.
Per-agent skill scoping
Skills load on-demand: only a skill's one-line description ever appears in an agent's
<available_skills> list, and the full SKILL.md body (curl/API how-to) is fetched only when
the agent calls the skill tool — it is never baked into any system prompt. On top of that, each
agent's skills list in the registry drives an OpenCode permission.skill block that denies all
skills by default and allows only the listed ones. A denied skill is hidden entirely (its name and
description are omitted), so e.g. @junior never sees gitea-api — it just knows from the roster
that @senior/@lead can reach the Gitea API and asks them to. This keeps the "how it's done"
detail out of agents that shouldn't act on it while still letting them know the capability exists.
Use it in a repo
The standard caller is one file, identical in every repo. Copy this repo's own
.gitea/workflows/ai-agent.yml verbatim into the consuming repo —
it is the source of truth, and agents itself uses the same file:
name: ai-agent
run-name: "ai-agent · #${{ github.event.issue.number }}" # quotes required: bare # starts a YAML comment
# Standard caller for the shared AI-agent workflow (gitea/agents). Copy this file VERBATIM into
# any repo that should get the agents — it is identical in every repo. All logic + scripts live in
# agents/.gitea/workflows/; scripts are fetched from @main at run time. The `jobs.agent` wrapper is
# required: a reusable (workflow_call) workflow can only be invoked from a caller job, not top-level.
# `run-name` titles each run by the triggering issue (e.g. "ai-agent · #42") in the Actions list.
on:
issue_comment:
types: [created]
issues:
types: [opened]
jobs:
agent:
uses: gitea/agents/.gitea/workflows/agent.yml@main
secrets: inherit
That's the whole per-repo footprint, and it's the minimum a caller can be: the on: triggers must
live in each repo (a reusable workflow can't declare its callers' triggers) and the jobs.agent
wrapper is mandatory for workflow_call. Everything else (agent registry, routing, delegation,
reactions, PR/issue plumbing) lives here in agent.yml.
Repo layout
agent.yml is kept thin: each step's shell lives in its own file under
.gitea/workflows/scripts/ (route.sh, install-opencode.sh, skill-node1-ssh.sh,
skill-gitea-api.sh, fetch-images.sh, fetch-thread.sh, run-agent.sh,
build-activity-log.sh, publish.sh), invoked as bash "$SCRIPTS/<name>.sh".
Because this is a reusable workflow (workflow_call), a caller run checks out the caller's
repo, not this one — so those script files aren't on disk by default. agent.yml therefore checks
this repo out into .agents-workflow/ (pinned to @main, matching the caller's uses: …@main) and
points $SCRIPTS at it. Keep the workflow and its scripts moving together on main.
Required secrets (per repo, or org-level for all)
| Secret | For |
|---|---|
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) |
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_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
SELF_TOKEN (never another agent's), and publish.sh uses the same token for the delegation/autopilot
trigger comment and @qa merges — 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
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
org / user level so every repo inherits them via secrets: inherit.
Also add to each consuming repo
AGENTS.md— the repo's own conventions (copyAGENTS.template.mdfrom here and adapt). The agent reads the caller repo'sAGENTS.md, so each repo can differ.- The bot users (
pm,senior,…) as collaborators (needed on private repos, and enables@nameautocomplete).
Maintaining
Change agent behavior once, here. Callers pin @main (or pin a tag for stability). History is the
changelog — see git log.