This repository has been archived on 2026-10-27. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
agents/README.md
T
Felix FaerberandClaude Opus 5 e29b8141f8
ci / lint (pull_request) Successful in 17s
Let caller-provided skills receive their secrets again
#122 gave the reusable workflow an explicit workflow_call secret contract so
XAI_API_KEY would stop arriving empty. That fixed the model key and quietly
broke every caller-provided skill: SECRETS_JSON is toJSON(secrets), so it now
contains only the secrets this contract declares, and a skill's setup.sh finds
nothing under the names its own repo uses.

homelab's node1-ssh skill is the live casualty. Its setup.sh reads SWARM_HOST,
SWARM_USER and SSH_PRIV_KEY_B64, takes the "not set -- skipping node1 SSH alias"
path, and exits 0; the agent then loads the skill, is told it may run
`ssh node1 ...`, and gets nothing. @senior hit exactly this while diagnosing the
immich crash-loop in homelab #301 and had to fall back to a blind version
revert. The failure is silent by design -- the skip is what lets a fork run
without deploy secrets -- so nothing in the run says the capability is gone.

Declaring the three optional here is the smallest fix that keeps the contract.
It is not a general one: secret names for caller skills are by definition known
only to the caller, and an explicit contract cannot express that, so each new
caller-skill secret has to be added alongside these.

Also fixes two pieces of drift that would spread the same breakage: the header
still told callers to use `secrets: inherit`, and README's copy-me stub still
showed it, so the next repo onboarded would have arrived with no model key.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYFnHida4AdiNDek2pzgXx
2026-09-15 10:09:19 +03:00

144 lines
9.7 KiB
Markdown

# 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/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`. |
| `@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. |
| `@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. |
| `@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
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).
### Discussion vs building
Mentioning a dev agent is a **conversation by default**: it reads what it needs and replies in the
thread — no branch, no PR. `@pm` can consult devs the same way with an `ASK: @<dev> <question>`
marker (gather feasibility/effort input before planning). **Building starts only on the explicit
signals**: `@pm`'s delegation (*"please proceed with issue …"* — a human can write the same phrase
to start a build directly), a `@qa` bounce (*"please address my review …"*), or any comment on the
PR thread itself (resuming existing work).
### Retros — the learning loop
Ask `@pm` for a retrospective on any issue (e.g. **"@pm run a retro"**, typically when merging). The
automation opens a `retro: issue #N` issue and assigns `@senior`, who reads the full issue + PR
threads (via the `gitea-api` skill), distills what went wrong or slow, and appends one-line
`symptom → rule` bullets to **`LEARNINGS.md`** at the repo root — through the normal PR choreography,
so the retro itself gets reviewed. `LEARNINGS.md` is injected into **every agent's prompt** on every
run, so the lessons actually change future behavior (better delegation, fewer repeated misses).
### 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`](.gitea/workflows/ai-agent.yml) verbatim into the consuming repo —
it is the source of truth, and `agents` itself uses the same file:
```yaml
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
# Explicit map, not `secrets: inherit`: inherit alone has left XAI_API_KEY empty on some Gitea
# cross-owner calls. Copy the full list from .gitea/workflows/ai-agent.yml, which is the stub
# this file abbreviates -- a secret missing from the map does not reach the workflow at all.
secrets:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
# ... and the OLLAMA_*, XAI_API_KEY, TOKEN_* and caller-skill entries, verbatim.
```
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 |
|--------|-----|
| `XAI_API_KEY` | `@lead`, `@ops` (and any other agent switched to a `xai-oc/…` model) |
| `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_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 trigger comments
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
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 (copy `AGENTS.template.md` from here and adapt). The
agent reads the *caller* repo's `AGENTS.md`, so each repo can differ.
- The bot users (`pm`,`senior`,…) as **collaborators** (needed on private repos, and enables
`@name` autocomplete).
## Maintaining
Change agent behavior once, here. Callers pin `@main` (or pin a tag for stability). History is the
changelog — see `git log`.