Compare commits

...
Author SHA1 Message Date
ffaerber c792228e2c Merge pull request 'docs: README reflects per-agent SELF_TOKEN model' (#62) from docs/token-model-selftoken into main 2026-07-05 15:40:29 +02:00
Felix FaerberandClaude Opus 4.8 1c4e4ce950 docs: README reflects per-agent SELF_TOKEN model
The secret table still described AGENT_TOKEN as primary and TOKEN_* as
optional "falls back to the bot". The per-agent-token refactor inverted that:
each agent's own TOKEN_* is primary (selected into SELF_TOKEN), AGENT_TOKEN is
now only the fallback for repos without per-agent tokens. Document TOKEN_OPS,
the SELF_TOKEN selection, and that TOKEN_QA needs write:repository to merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 16:40:01 +03:00
ffaerber 738848304e Merge pull request 'agents: per-agent Gitea identity — each agent uses its own token' (#59) from feat/per-agent-tokens into main 2026-07-05 15:33:54 +02:00
Felix FaerberandClaude Opus 4.8 06f1924441 agents: per-agent Gitea identity — each agent uses its own token
Drop the shared AGENT_TOKEN as the primary credential; every agent now acts
as its own Gitea user (TOKEN_PM for @pm, TOKEN_OPS for @ops, …) for API
calls, delegation/autopilot trigger comments, and PR merges.

- agent.yml: Run-agent step injects SELF_TOKEN — a ternary selecting the
  running agent's own token by name, falling back to AGENT_TOKEN for repos
  not yet migrated to per-agent tokens (e.g. homelab). Only that one token
  enters the agent process, so no agent can act as another. The gitea-api /
  gitea-admin skill-setup steps no longer carry a token (they only write docs).
- Gate: trust the agent roster (pm/junior/senior/lead/qa/ops) as comment
  authors so an agent's own delegation/autopilot trigger comment (posted with
  its PAT, no 🤖 prefix) fires the next run. @ops added to the mention set.
- publish.sh: TOK = agent identity (comments/replies); new TTOK = trigger/merge
  token (agent PAT, else AGENT_TOKEN fallback) for delegation, autopilot @qa
  triggers, and PR merges that must fire downstream workflows.
- skill-gitea-api.sh / skill-gitea-admin.sh / run-agent.sh: AGENT_TOKEN/
  TOKEN_OPS → SELF_TOKEN in the emitted skill docs and env contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 16:20:29 +03:00
ffaerber e53e5caf8c Merge pull request 'feat(ops): @ops uses dedicated TOKEN_OPS not AGENT_TOKEN' (#58) from feat/ops-dedicated-token into main 2026-07-05 14:52:09 +02:00
Felix FaerberandClaude Opus 4.8 79ea9f68c9 feat(ops): @ops uses its dedicated TOKEN_OPS, not AGENT_TOKEN
The gitea-admin skill now authenticates with TOKEN_OPS (the ops user's admin token), and TOKEN_OPS
is injected into the agent process ONLY when name==ops. No other agent holds an admin credential in
its env. This unhooks @ops from AGENT_TOKEN, so AGENT_TOKEN can be narrowed to a non-admin token.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 15:52:07 +03:00
Felix FaerberandClaude Opus 4.8 6c753dc0a0 chore: repo moved to gitea/agents — update self-references
Repo transferred ffaerber/agents -> gitea/agents. Point the scripts checkout (agent.yml
repository:), the standard caller (uses:), and the README at gitea/agents. The trigger gate still
keys on the maintainer 'ffaerber' (unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 15:43:34 +03:00
ffaerber 23c48e66c4 Merge pull request 'ops: .env is the token source of truth (not tokens.md)' (#57) from fix/ops-env-source-of-truth into main 2026-07-05 14:01:13 +02:00
Felix Faerber 6618de9c9f ops: .env is the token source of truth (not tokens.md)
Every token value must be written to gitea/secrets/.env (the master, mirrored to org Actions secrets)
AND the Actions secret, kept in sync, removed from both on delete. Drop the tokens.md instruction —
the @ops test dutifully re-created tokens.md because the skill still referenced it.
2026-07-05 15:01:10 +03:00
ffaerber 82c4b07fea Merge pull request 'feat(ops): make @ops routable + TOKEN_OPS identity' (#56) from feat/ops-routing into main 2026-07-05 13:53:11 +02:00
Felix FaerberandClaude Opus 4.8 2f1ae61b06 feat(ops): make @ops routable + post under its own TOKEN_OPS identity
PR #55 added @ops to the registry but route.sh only looped pm/junior/senior/lead/qa, so @ops could
never be triggered, and there was no token case for it. Add 'ops' to the routing loop and the
TOKEN_OPS case in route.sh + publish.sh, and pass TOKEN_OPS in the workflow env next to TOKEN_QA
(route/publish/rescue steps) so route.sh (set -u) doesn't error on an @ops run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 14:53:09 +03:00
ffaerber 03c2bef880 Merge pull request 'feat(ops): add @ops Gitea-administrator agent' (#55) from feat/ops-agent into main
Reviewed-on: ffaerber/agents#55
2026-07-05 13:19:29 +02:00
Felix Faerber 6832d7ad6c ops: record minted tokens in the private gitea/secrets inventory
@ops now treats gitea/secrets/tokens.md as the source-of-truth inventory (readable by @ffaerber and
@ops only) and records every token it mints/rotates there, alongside storing the live value in the
matching Actions secret.
2026-07-05 14:09:13 +03:00
Felix FaerberandClaude Opus 4.8 74d3e1d229 feat(ops): add @ops agent — Gitea instance administrator
A comment-mode operator agent (opus) for administering Gitea itself from issues in gitea/ops:
create orgs/users/repos, manage labels & Actions secrets, and mint least-privilege per-user tokens.

- agents.json: new @ops role (comment-mode, skill gitea-admin, confirms before destructive ops).
- skill-gitea-admin.sh: SKILL.md documenting org/user/repo/label/secret ops + the create-user →
  mint-scoped-token → store-as-secret flow (never printing tokens). Gated on NAME=ops so the admin
  how-to is written ONLY for @ops; permission.skill also denies it to other agents.
- agent.yml: wire the skill step (uses AGENT_TOKEN — an admin PAT during bootstrap).

Bootstrap note: AGENT_TOKEN is admin for now, so every agent's process technically holds an admin
credential (skill-scoping hides the doc, not the env var). Once @ops is minting scoped per-user
tokens, narrow AGENT_TOKEN and inject a dedicated admin token only for @ops.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 14:03:57 +03:00
ffaerber 4cbbc9b2d7 Merge pull request 'fix(autopilot): merge via AGENT_TOKEN so it triggers deploy' (#52) from fix/autopilot-merge-triggers-deploy into main
Reviewed-on: ffaerber/agents#52
2026-07-05 08:05:42 +02:00
Felix FaerberandClaude Opus 4.8 cf1e7178b5 fix(autopilot): merge with AGENT_TOKEN so the merge triggers the deploy
@qa's autopilot merge used the built-in Actions token (TOKEN_QA unset -> fell back to GITEA_TOKEN).
Gitea does not fire new workflow runs for pushes made by the built-in Actions token, so the merge
landed on main but deploy.yml never ran — the change was merged-but-not-deployed. Merge with
AGENT_TOKEN (a PAT, already used for delegation and proven to trigger runs) so the push to main
triggers the deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 23:32:12 +03:00
ffaerber 5a9dba64fb Merge pull request 'autopilot(qa): bounce to dev instead of halt; stop after 3' (#51) from fix/autopilot-bounce into main 2026-07-04 22:21:51 +02:00
Felix FaerberandClaude Opus 4.8 9840c2a860 autopilot(qa): bounce back to the dev instead of halting; stop after 3 bounces
Per maintainer: @qa must never change code — it only accepts a PR or sends it back to the dev with
precise instructions. Replaces the halt-on-any-problem behavior with a bounce loop:

- QA prompt: emit MERGE_PR (approve), or `BOUNCE: @<dev>` after spelling out exactly what to change.
  HALT_AUTOPILOT is now only for problems a dev can't fix (needs a human decision).
- publish.sh: on BOUNCE, re-trigger the named dev (via AGENT_TOKEN) with "autopilot fix attempt N/3".
  Count prior attempts from the PR thread; at 3 it stops (removes the label) and hands to @ffaerber.
- publish.sh: after a dev pushes a fix (resume), auto-re-trigger @qa to re-verify — closing the loop
  (dev fix -> qa re-check -> merge or bounce again).
- Updated the initial @qa trigger wording (bounce, not halt).

Verified: BOUNCE target parsing and the 3-strike counter (attempts 1/3,2/3,3/3 then HALT).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 23:18:00 +03:00
ffaerber bb07558d70 Merge pull request '@lead: run without asking user' (#20) from ai/issue-16 into main
Reviewed-on: ffaerber/agents#20
2026-07-04 21:39:01 +02:00
lead ef06da3ffe Rename autopilot label 'auto' to 'autopilot' 2026-07-04 19:23:56 +00:00
lead 7278e06dff Rename autopilot label 'fully-automatic' to 'auto' 2026-07-04 19:23:44 +00:00
lead df0f6d6543 @lead: issue #16 — fully-automatic autopilot (label-gated @pm auto-delegate + @qa merge) 2026-07-04 19:23:20 +00:00
ffaerber 23cc3e192e Merge pull request 'fix(activity-log): tool calls only (drop prose summary)' (#50) from fix/activity-tool-calls-only into main 2026-07-04 13:41:02 +02:00
Felix FaerberandClaude Opus 4.8 0a89309ff1 fix(activity-log): tool calls only — drop the agent's prose summary
The activity <details> was labelled 'tool calls & reasoning' but included the agent's final prose
summary ('Added the whoami service... Exposed via Traefik... Pushed to branch...'), which is just a
restatement of the PR description and not a tool call. Emit only tool_use events; relabel to
'N tool calls'. The what-changed narrative lives in the PR description; this section is the record of
actions taken.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:40:59 +03:00
ffaerber cb11a6b2d1 Merge pull request 'fix(activity-log): strip PR-description block from reasoning trail' (#49) from fix/activity-log-strip-prdesc into main 2026-07-04 13:25:30 +02:00
Felix FaerberandClaude Opus 4.8 81882ee3ec fix(publish): de-dup agent comments (no repeated PR desc, no doubled header)
Batches the remaining comment-noise cleanups on top of the activity-log fix:
- Resume comment no longer re-posts the full PR description (## Summary/## Changes) — the PR body and
  diff already carry it; the comment just links the PR (the reasoning trail shows what the run did).
- Strip a leading self-header the model sometimes emits ("🤖 **@pm**" on its own line, sometimes
  twice) before we prepend our own, so headers aren't doubled.
- Also drop the BEGIN_PR_DESCRIPTION block from the plain reply text, and simplify the activity
  block's summary (it repeated "🤖 **@name** — activity log").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:24:37 +03:00
Felix FaerberandClaude Opus 4.8 1987ba792d fix(activity-log): strip the PR-description block from the reasoning trail
The 'tool calls & reasoning' activity log dumped each text part verbatim, including the agent's
BEGIN_PR_DESCRIPTION..END_PR_DESCRIPTION block — which is already published as the PR description, so
it appeared twice (e.g. homelab PR #117). Strip that block from text parts, and drop a text entry
that becomes empty after stripping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:12:32 +03:00
ffaerber a1ff1b9881 Merge pull request 'fix(route): resume existing ai/issue-N branch on re-trigger' (#47) from fix/route-resume-existing-branch into main 2026-07-04 12:21:07 +02:00
11 changed files with 411 additions and 73 deletions
+42 -15
View File
@@ -1,6 +1,6 @@
name: agent name: agent
# Reusable AI-agent workflow, shared across repos. A caller repo triggers on issue_comment/issues # Reusable AI-agent workflow, shared across repos. A caller repo triggers on issue_comment/issues
# and invokes this via: uses: ffaerber/agents/.gitea/workflows/agent.yml@main (secrets: inherit). # and invokes this via: uses: gitea/agents/.gitea/workflows/agent.yml@main (secrets: inherit).
# 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:
@@ -12,13 +12,21 @@ jobs:
# 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.comment == null && github.event.issue.user.login == 'ffaerber') ||
(github.event.comment != null && github.event.comment.user.login == 'ffaerber' && (github.event.comment != null &&
(github.event.comment.user.login == 'ffaerber' ||
github.event.comment.user.login == 'pm' ||
github.event.comment.user.login == 'junior' ||
github.event.comment.user.login == 'senior' ||
github.event.comment.user.login == 'lead' ||
github.event.comment.user.login == 'qa' ||
github.event.comment.user.login == 'ops') &&
!contains(github.event.comment.body, '🤖') && !contains(github.event.comment.body, '🤖') &&
(contains(github.event.comment.body, '@pm') || (contains(github.event.comment.body, '@pm') ||
contains(github.event.comment.body, '@junior') || contains(github.event.comment.body, '@junior') ||
contains(github.event.comment.body, '@senior') || contains(github.event.comment.body, '@senior') ||
contains(github.event.comment.body, '@lead') || contains(github.event.comment.body, '@lead') ||
contains(github.event.comment.body, '@qa'))) contains(github.event.comment.body, '@qa') ||
contains(github.event.comment.body, '@ops')))
runs-on: ci-runner runs-on: ci-runner
steps: steps:
- name: Acknowledge with 👀 - name: Acknowledge with 👀
@@ -54,7 +62,7 @@ jobs:
- name: Fetch shared agent scripts (this repo) - name: Fetch shared agent scripts (this repo)
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
repository: ffaerber/agents repository: gitea/agents
ref: main ref: main
path: .agents-workflow path: .agents-workflow
token: ${{ secrets.GITEA_TOKEN }} token: ${{ secrets.GITEA_TOKEN }}
@@ -92,6 +100,7 @@ jobs:
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }} TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }} TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }}
TOKEN_QA: ${{ secrets.TOKEN_QA }} TOKEN_QA: ${{ secrets.TOKEN_QA }}
TOKEN_OPS: ${{ secrets.TOKEN_OPS }}
run: bash "$SCRIPTS/route.sh" run: bash "$SCRIPTS/route.sh"
- name: Install opencode + provider config (+ Playwright MCP for browser agents) - name: Install opencode + provider config (+ Playwright MCP for browser agents)
@@ -120,17 +129,23 @@ jobs:
run: bash "$SCRIPTS/skill-node1-ssh.sh" run: bash "$SCRIPTS/skill-node1-ssh.sh"
- name: Set up `gitea-api` skill (let agents read/write issues, PRs, Actions across repos) - name: Set up `gitea-api` skill (let agents read/write issues, PRs, Actions across repos)
# Mirrors the node1-ssh pattern: emit an opencode Skill file under # Emits an opencode Skill file. The skill uses SELF_TOKEN — the running agent's OWN token
# ~/.config/opencode/skills/ so any dev agent discovers the capability via OpenCode's # (e.g. TOKEN_PM for @pm), injected into the Run-agent step below — so each agent talks to
# skill registry. The credential is the shared AGENT_TOKEN (a PAT whose scopes the # Gitea as itself. This step only writes the doc; permission.skill scopes who may load it.
# maintainer set at creation time — issue/repository/organization/misc read+write, cross-repo).
# Only emitted when AGENT_TOKEN is actually present, so repos without it don't get a
# broken skill. The token is passed via env and never inlined into shell.
env: env:
SCRIPTS: ${{ runner.temp }}/agents-scripts SCRIPTS: ${{ runner.temp }}/agents-scripts
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }}
run: bash "$SCRIPTS/skill-gitea-api.sh" run: bash "$SCRIPTS/skill-gitea-api.sh"
- name: Set up `gitea-admin` skill (@ops only — administer the Gitea instance)
# Instance administration (orgs/users/repos/labels/secrets/scoped tokens). The SKILL.md is
# written ONLY for @ops (skill-gitea-admin.sh gates on NAME) and permission.skill also denies
# it to every other agent. It uses SELF_TOKEN (which for @ops is TOKEN_OPS), injected into the
# Run-agent step. This step only writes the doc.
env:
SCRIPTS: ${{ runner.temp }}/agents-scripts
NAME: ${{ steps.prep.outputs.name }}
run: bash "$SCRIPTS/skill-gitea-admin.sh"
- name: Inspect / fetch image attachments (download only for vision agents) - name: Inspect / fetch image attachments (download only for vision agents)
id: imgs id: imgs
env: env:
@@ -152,16 +167,18 @@ jobs:
env: env:
SCRIPTS: ${{ runner.temp }}/agents-scripts SCRIPTS: ${{ runner.temp }}/agents-scripts
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
# AGENT_TOKEN powers the `gitea-api` skill (cross-repo issue/PR/Actions read+write). # SELF_TOKEN = the RUNNING agent's OWN token (TOKEN_PM for @pm, TOKEN_OPS for @ops, …).
# It is already a required secret for the delegation step below; exposing it here too # Only this agent's token is placed in its process env, so no agent can act as another.
# lets the agent process itself call the Gitea API on demand. # Powers the gitea-api / gitea-admin skills — each agent calls Gitea as itself. Falls back
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }} # to AGENT_TOKEN in repos that haven't been migrated to per-agent tokens (e.g. homelab).
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 || secrets.AGENT_TOKEN }}
NAME: ${{ steps.prep.outputs.name }} NAME: ${{ steps.prep.outputs.name }}
MODEL: ${{ steps.prep.outputs.model }} MODEL: ${{ steps.prep.outputs.model }}
VISION: ${{ steps.prep.outputs.vision }} VISION: ${{ steps.prep.outputs.vision }}
MODE: ${{ steps.prep.outputs.mode }} MODE: ${{ steps.prep.outputs.mode }}
HAS_IMAGES: ${{ steps.imgs.outputs.has_images }} HAS_IMAGES: ${{ steps.imgs.outputs.has_images }}
BRANCH: ${{ steps.prep.outputs.branch }} BRANCH: ${{ steps.prep.outputs.branch }}
AUTOPILOT: ${{ steps.prep.outputs.autopilot }} # 'true' when the issue carries the `autopilot` label
NUM: ${{ github.event.issue.number }} NUM: ${{ github.event.issue.number }}
TITLE: ${{ github.event.issue.title }} TITLE: ${{ github.event.issue.title }}
IBODY: ${{ github.event.issue.body }} IBODY: ${{ github.event.issue.body }}
@@ -180,18 +197,24 @@ jobs:
env: env:
SCRIPTS: ${{ runner.temp }}/agents-scripts SCRIPTS: ${{ runner.temp }}/agents-scripts
GT: ${{ secrets.GITEA_TOKEN }} GT: ${{ secrets.GITEA_TOKEN }}
# Fallback PAT for triggering comments (delegation/autopilot) + merges in repos without
# per-agent tokens. Where per-agent tokens exist, the agent's own token is used instead.
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }} AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }}
TOKEN_PM: ${{ secrets.TOKEN_PM }} TOKEN_PM: ${{ secrets.TOKEN_PM }}
TOKEN_SENIOR: ${{ secrets.TOKEN_SENIOR }} TOKEN_SENIOR: ${{ secrets.TOKEN_SENIOR }}
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }} TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }} TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }}
TOKEN_QA: ${{ secrets.TOKEN_QA }} TOKEN_QA: ${{ secrets.TOKEN_QA }}
TOKEN_OPS: ${{ secrets.TOKEN_OPS }}
NAME: ${{ steps.prep.outputs.name }} NAME: ${{ steps.prep.outputs.name }}
MODE: ${{ steps.prep.outputs.mode }} MODE: ${{ steps.prep.outputs.mode }}
NUM: ${{ github.event.issue.number }} NUM: ${{ github.event.issue.number }}
TITLE: ${{ github.event.issue.title }} TITLE: ${{ github.event.issue.title }}
BRANCH: ${{ steps.prep.outputs.branch }} BRANCH: ${{ steps.prep.outputs.branch }}
NEW: ${{ steps.prep.outputs.new }} NEW: ${{ steps.prep.outputs.new }}
IS_PR: ${{ github.event.issue.pull_request }} # set when this run is on a PR thread
AUTOPILOT: ${{ steps.prep.outputs.autopilot }} # 'true' when the origin issue carries `autopilot`
ISSNUM: ${{ steps.prep.outputs.issnum }} # origin issue number (resolved from branch on PR threads)
run: bash "$SCRIPTS/publish.sh" run: bash "$SCRIPTS/publish.sh"
# Failure-safe: if any step above failed AFTER a dev agent already pushed commits, the normal # Failure-safe: if any step above failed AFTER a dev agent already pushed commits, the normal
@@ -203,11 +226,15 @@ jobs:
env: env:
SCRIPTS: ${{ runner.temp }}/agents-scripts SCRIPTS: ${{ runner.temp }}/agents-scripts
GT: ${{ secrets.GITEA_TOKEN }} GT: ${{ secrets.GITEA_TOKEN }}
# Fallback PAT for triggering comments (delegation/autopilot) + merges in repos without
# per-agent tokens. Where per-agent tokens exist, the agent's own token is used instead.
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }}
TOKEN_PM: ${{ secrets.TOKEN_PM }} TOKEN_PM: ${{ secrets.TOKEN_PM }}
TOKEN_SENIOR: ${{ secrets.TOKEN_SENIOR }} TOKEN_SENIOR: ${{ secrets.TOKEN_SENIOR }}
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }} TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }} TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }}
TOKEN_QA: ${{ secrets.TOKEN_QA }} TOKEN_QA: ${{ secrets.TOKEN_QA }}
TOKEN_OPS: ${{ secrets.TOKEN_OPS }}
NAME: ${{ steps.prep.outputs.name }} NAME: ${{ steps.prep.outputs.name }}
MODE: ${{ steps.prep.outputs.mode }} MODE: ${{ steps.prep.outputs.mode }}
NUM: ${{ github.event.issue.number }} NUM: ${{ github.event.issue.number }}
+2 -2
View File
@@ -1,6 +1,6 @@
name: ai-agent name: ai-agent
run-name: "ai-agent · #${{ github.event.issue.number }}" # quotes required: bare # starts a YAML comment run-name: "ai-agent · #${{ github.event.issue.number }}" # quotes required: bare # starts a YAML comment
# Standard caller for the shared AI-agent workflow (ffaerber/agents). Copy this file VERBATIM into # 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 # 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 # 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. # required: a reusable (workflow_call) workflow can only be invoked from a caller job, not top-level.
@@ -11,5 +11,5 @@ on:
types: [opened] types: [opened]
jobs: jobs:
agent: agent:
uses: ffaerber/agents/.gitea/workflows/agent.yml@main uses: gitea/agents/.gitea/workflows/agent.yml@main
secrets: inherit secrets: inherit
+2 -1
View File
@@ -3,5 +3,6 @@
"junior": {"model":"ollama-cloud/kimi-k2.7-code:cloud","vision":false,"mode":"pr", "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."}, "junior": {"model":"ollama-cloud/kimi-k2.7-code:cloud","vision":false,"mode":"pr", "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."},
"senior": {"model":"ollama-cloud/glm-5.2:cloud","vision":false,"mode":"pr", "skills":["gitea-api","node1-ssh"],"desc":"Senior dev — complex, multi-file implementation (GLM-5.2 via Ollama Cloud, text-only)."}, "senior": {"model":"ollama-cloud/glm-5.2:cloud","vision":false,"mode":"pr", "skills":["gitea-api","node1-ssh"],"desc":"Senior dev — complex, multi-file implementation (GLM-5.2 via Ollama Cloud, text-only)."},
"lead": {"model":"anthropic/claude-opus-4-8","vision":true, "mode":"pr", "skills":["gitea-api","node1-ssh"],"desc":"Tech lead — the hardest problems, architecture, and final calls."}, "lead": {"model":"anthropic/claude-opus-4-8","vision":true, "mode":"pr", "skills":["gitea-api","node1-ssh"],"desc":"Tech lead — the hardest problems, architecture, and final calls."},
"qa": {"model":"ollama-cloud/minimax-m3:cloud","vision":true, "mode":"comment","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."} "qa": {"model":"ollama-cloud/minimax-m3:cloud","vision":true, "mode":"comment","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."},
"ops": {"model":"anthropic/claude-opus-4-8","vision":false,"mode":"comment","skills":["gitea-admin"],"desc":"Gitea operator — administers the Gitea instance itself: create orgs/users/repos, manage labels and secrets, mint scoped per-user tokens, bootstrap new repos with the agent caller. Comments only; never edits code. ALWAYS confirms before any destructive action (delete user/repo/org)."}
} }
@@ -1,7 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Build activity log (tool calls + reasoning) from the event stream into /tmp/activity_log.md. # Build the activity log — the list of TOOL CALLS the agent made — into /tmp/activity_log.md.
# Only dev agents (mode=pr) get an activity-log comment — comment-only roles (pm/qa) # Only dev agents (mode=pr) get an activity-log comment — comment-only roles (pm/qa) do no tool calls.
# do no tool calls, so a trail would be empty/noise. # NOTE: we deliberately DO NOT include the agent's prose text parts. That final "here's what I did"
# text is just a restatement of the PR description (already published as the PR body), not a tool
# call — so it was noise in a section titled "tool calls". The log is the record of ACTIONS taken.
# #
# Required env (provided by the workflow step): MODE # Required env (provided by the workflow step): MODE
set -u set -u
@@ -11,16 +13,12 @@ if [ "$MODE" != "pr" ]; then
fi fi
jq -r ' jq -r '
def trunc(n): if length > n then (.[0:n] + "…") else . end; def trunc(n): if length > n then (.[0:n] + "…") else . end;
select(.type=="tool_use" or .type=="text") | select(.type=="tool_use") |
if .type=="text" then
"💬 " + ((.part.text // "") | trunc(4000))
else
(.part.tool // "?") as $t | (.part.tool // "?") as $t |
((.part.state.title // (.part.state.input | tojson | trunc(160)) // "")) as $title | ((.part.state.title // (.part.state.input | tojson | trunc(160)) // "")) as $title |
"🔧 **" + $t + "**: `" + ($title | trunc(240)) + "`" "🔧 **" + $t + "**: `" + ($title | trunc(240)) + "`"
end
' /tmp/events.jsonl > /tmp/activity_log.md 2>/dev/null || true ' /tmp/events.jsonl > /tmp/activity_log.md 2>/dev/null || true
n=$(wc -l < /tmp/activity_log.md 2>/dev/null || echo 0) n=$(wc -l < /tmp/activity_log.md 2>/dev/null || echo 0)
echo "activity log: $n entries" echo "activity log: $n tool calls"
[ "$n" -eq 0 ] && : > /tmp/activity_log.md [ "$n" -eq 0 ] && : > /tmp/activity_log.md
head -3 /tmp/activity_log.md head -3 /tmp/activity_log.md
+136 -9
View File
@@ -2,30 +2,64 @@
# Publish — PR (dev agents) or comment (pm/qa), always reply in the issue. # Publish — PR (dev agents) or comment (pm/qa), always reply in the issue.
# #
# Required env (provided by the workflow step): # Required env (provided by the workflow step):
# GT AGENT_TOKEN TOKEN_PM TOKEN_SENIOR TOKEN_JUNIOR TOKEN_LEAD TOKEN_QA # GT TOKEN_PM TOKEN_SENIOR TOKEN_JUNIOR TOKEN_LEAD TOKEN_QA
# NAME MODE NUM TITLE BRANCH NEW GITHUB_SERVER_URL GITHUB_REPOSITORY # NAME MODE NUM TITLE BRANCH NEW GITHUB_SERVER_URL GITHUB_REPOSITORY
# IS_PR AUTOPILOT ISSNUM (autopilot: @qa label-gated merge/halt + auto-trigger @qa on a fresh PR)
set +e # publish is best-effort: a grep-no-match / curl non-zero must NOT kill the step set +e # publish is best-effort: a grep-no-match / curl non-zero must NOT kill the step
# Post/PR as the agent's OWN Gitea user when its token is configured; else the built-in bot. # Post/PR as the agent's OWN Gitea user when its token is configured; else the built-in bot.
case "$NAME" in case "$NAME" in
pm) TOK="$TOKEN_PM";; senior) TOK="$TOKEN_SENIOR";; junior) TOK="$TOKEN_JUNIOR";; 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 esac
[ -z "$TOK" ] && TOK="$GT" [ -z "$TOK" ] && TOK="$GT"
# Trigger token: comments that must FIRE the next workflow (delegation, autopilot) and PR merges
# cannot use the built-in GITEA_TOKEN (Gitea won't start new runs from it). Use the agent's own PAT
# when it has one; otherwise fall back to AGENT_TOKEN (repos without per-agent tokens, e.g. homelab).
TTOK="$TOK"
{ [ -z "$TTOK" ] || [ "$TTOK" = "$GT" ]; } && TTOK="${AGENT_TOKEN:-}"
git config user.name "$NAME" git config user.name "$NAME"
git config user.email "$NAME@ffaerber.duckdns.org" git config user.email "$NAME@ffaerber.duckdns.org"
API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
hdr=(-H "Authorization: token $TOK" -H "Content-Type: application/json") hdr=(-H "Authorization: token $TOK" -H "Content-Type: application/json")
post() { curl -sS -w 'comment -> HTTP %{http_code}\n' -X POST "${hdr[@]}" \ post() { curl -sS -w 'comment -> HTTP %{http_code}\n' -X POST "${hdr[@]}" \
"$API/issues/$NUM/comments" -d "$(jq -nc --arg b "$1" '{body:$b}')"; } "$API/issues/$NUM/comments" -d "$(jq -nc --arg b "$1" '{body:$b}')"; }
# Remove the 'autopilot' label from an issue by resolving its ID first (Gitea's DELETE label
# endpoint is by ID, not name). Arg $1 = issue number. Used as the autopilot kill switch.
del_autopilot_label() {
local iss="$1" lid
lid=$(curl -sS "${hdr[@]}" "$API/issues/$iss/labels" 2>/dev/null \
| jq -r 'if type=="array" then ([.[]|select(.name=="autopilot")][0].id // empty) else empty end')
if [ -n "$lid" ]; then
curl -sS -X DELETE "${hdr[@]}" "$API/issues/$iss/labels/$lid" \
-w '\nunlabel -> HTTP %{http_code}\n' || true
else
echo "no 'autopilot' label found on #$iss to remove"
fi
}
# drop machine-readable markers (DELEGATE / CLOSE_ISSUE / the BEGIN_SUBTASKS..END_SUBTASKS block) # drop machine-readable markers: DELEGATE / CLOSE_ISSUE / MERGE_PR / HALT_AUTOPILOT, and the
# BEGIN_SUBTASKS..END_SUBTASKS and BEGIN_PR_DESCRIPTION..END_PR_DESCRIPTION blocks (the PR
# description is published separately).
reply=$(awk ' reply=$(awk '
/^[[:space:]]*BEGIN_SUBTASKS/{s=1} /^[[:space:]]*BEGIN_SUBTASKS/{s=1}
/^[[:space:]]*BEGIN_PR_DESCRIPTION/{p=1}
/^[[:space:]]*DELEGATE:[[:space:]]*@/{next} /^[[:space:]]*DELEGATE:[[:space:]]*@/{next}
/^[[:space:]]*CLOSE_ISSUE[[:space:]]*$/{next} /^[[:space:]]*CLOSE_ISSUE[[:space:]]*$/{next}
/^[[:space:]]*MERGE_PR[[:space:]]*$/{next}
/^[[:space:]]*HALT_AUTOPILOT[[:space:]]*$/{next}
/^[[:space:]]*BOUNCE:[[:space:]]*@/{next}
s{ if(/^[[:space:]]*END_SUBTASKS/){s=0}; next } s{ if(/^[[:space:]]*END_SUBTASKS/){s=0}; next }
p{ if(/^[[:space:]]*END_PR_DESCRIPTION/){p=0}; next }
{print} {print}
' /tmp/agent_out.md 2>/dev/null) ' /tmp/agent_out.md 2>/dev/null)
# Strip a leading self-header the model sometimes emits ("🤖 **@pm**" on its own line) so we don't
# double it when we prepend our own. Removes a leading run of such header lines and blank lines.
reply=$(printf '%s' "$reply" | awk '
BEGIN{s=1}
s && /^[^A-Za-z0-9]*\*\*@[A-Za-z]+\*\*[[:space:]]*$/ {next}
s && /^[[:space:]]*$/ {next}
{s=0; print}
')
[ -z "$reply" ] && reply="_(Made changes without a text summary — see the diff below.)_" [ -z "$reply" ] && reply="_(Made changes without a text summary — see the diff below.)_"
# Prefer the agent's clean delimited PR description; fall back to the whole reply. # Prefer the agent's clean delimited PR description; fall back to the whole reply.
prdesc=$(awk '/BEGIN_PR_DESCRIPTION/{f=1;next} /END_PR_DESCRIPTION/{f=0} f' /tmp/agent_out.md) prdesc=$(awk '/BEGIN_PR_DESCRIPTION/{f=1;next} /END_PR_DESCRIPTION/{f=0} f' /tmp/agent_out.md)
@@ -80,17 +114,86 @@ if [ "$MODE" != "pr" ]; then
subtext=$(printf '\n\n---\n🤖 **@%s** — created sub-issues%s (mention an agent on each when ready):%b' "$NAME" "${ms:+ under milestone **$ms**}" "$links") subtext=$(printf '\n\n---\n🤖 **@%s** — created sub-issues%s (mention an agent on each when ready):%b' "$NAME" "${ms:+ under milestone **$ms**}" "$links")
fi fi
post "$(printf '🤖 **@%s**\n\n%s%s' "$NAME" "$msg" "$subtext")" post "$(printf '🤖 **@%s**\n\n%s%s' "$NAME" "$msg" "$subtext")"
# Auto-delegate: if the plan names a teammate, trigger them via AGENT_TOKEN (a PAT, so it
# --- AUTOPILOT: @qa's narrow, label-gated merge / halt authority ---
# Only @qa, only when 'autopilot' is set, and only on a PR thread. The MERGE_PR / HALT_AUTOPILOT
# markers come from the QA prompt. Merge + label ops use TOKEN_QA (the QA user's PAT, which the
# maintainer must grant write+merge scope). ISSNUM is the origin issue (resolved from the branch).
if [ "$NAME" = "qa" ] && [ "$AUTOPILOT" = "true" ]; then
if grep -qiE '^[[:space:]]*MERGE_PR[[:space:]]*$' /tmp/agent_out.md; then
if [ -z "$IS_PR" ]; then
echo "MERGE_PR marker but this run is not on a PR thread — skipping merge"
else
echo "@qa autopilot: merging PR #$NUM (origin issue #${ISSNUM:-$NUM})"
# Merge with TOK (a PAT) — NOT the built-in Actions token — so the resulting push to
# main TRIGGERS downstream workflows (e.g. deploy). A merge made with the built-in GITEA_TOKEN
# does not fire new runs (loop-prevention), which silently skips the deploy. Fall back to the
# agent's own token only if TOK isn't set (then the deploy would need a manual run).
mtok="$TTOK"
mc=$(curl -sS -o /tmp/merge_resp.txt -w '%{http_code}' -X POST \
-H "Authorization: token $mtok" -H "Content-Type: application/json" \
"$API/pulls/$NUM/merge" -d '{"Do":"merge"}')
echo "merge -> HTTP $mc"; cat /tmp/merge_resp.txt 2>/dev/null || true
case "$mc" in
200|201|204)
echo "closing origin issue #${ISSNUM:-$NUM}"
curl -sS -X PATCH "${hdr[@]}" "$API/issues/${ISSNUM:-$NUM}" \
-d '{"state":"closed"}' -w '\nclose -> HTTP %{http_code}\n' || true
post "$(printf '🤖 **@qa** — ✅ verified & merged PR #%s (autopilot). Closed issue #%s.' "$NUM" "${ISSNUM:-$NUM}")"
;;
*)
# Merge failed (checks not green, conflicts, or TOKEN_QA lacks merge scope) — do NOT
# silently proceed: drop the label so it reverts to human control and report.
del_autopilot_label "${ISSNUM:-$NUM}"
post "$(printf '🤖 **@qa** — ⚠️ tried to merge PR #%s but the API returned HTTP %s (checks not green, a conflict, or missing merge permission on TOKEN_QA). Removed the `autopilot` label — @ffaerber please take a look.' "$NUM" "$mc")"
;;
esac
fi
elif grep -qiE '^[[:space:]]*BOUNCE:[[:space:]]*@(junior|senior|lead)' /tmp/agent_out.md; then
# @qa wants the dev to fix something. Send it back — never fix it ourselves. After 3 bounces,
# stop and hand to the human. QA's feedback is already posted (the reply comment above).
if [ -z "$IS_PR" ]; then
echo "BOUNCE marker but this run is not on a PR thread — skipping"
else
target=$(grep -oiE 'BOUNCE:[[:space:]]*@(junior|senior|lead)' /tmp/agent_out.md | head -1 \
| grep -oiE '(junior|senior|lead)' | tr '[:upper:]' '[:lower:]')
[ -z "$target" ] && target=$(curl -sS "${hdr[@]}" "$API/pulls/$NUM" | jq -r '.user.login // "junior"')
# Count how many times this PR has already been bounced (marker in the trigger comment).
prior=$(curl -sS "${hdr[@]}" "$API/issues/$NUM/comments?limit=100" \
| jq -r 'if type=="array" then [.[]|select(.body|test("autopilot fix attempt"))]|length else 0 end' 2>/dev/null)
prior=${prior:-0}
if [ "$prior" -ge 3 ]; then
echo "@qa autopilot: 3 bounces already — halting"
del_autopilot_label "${ISSNUM:-$NUM}"
post "$(printf '🤖 **@qa** — 🛑 still not right after 3 fix attempts. Stopping autopilot (removed the `autopilot` label). @ffaerber please take over — details in the comments above.')"
else
n=$((prior + 1))
echo "@qa autopilot: bounce $n/3 -> @$target"
curl -sS -X POST -H "Authorization: token $TTOK" -H "Content-Type: application/json" \
"$API/issues/$NUM/comments" \
-d "$(jq -nc --arg b "@$target please address @qa's feedback above and update this PR (autopilot fix attempt $n/3)." '{body:$b}')" \
-w '\nbounce -> HTTP %{http_code}\n' || true
fi
fi
elif grep -qiE '^[[:space:]]*HALT_AUTOPILOT[[:space:]]*$' /tmp/agent_out.md; then
echo "@qa autopilot: HALT — removing 'autopilot' label from #${ISSNUM:-$NUM}"
del_autopilot_label "${ISSNUM:-$NUM}"
post "$(printf '🤖 **@qa** — 🛑 this needs a human decision (not a dev fix). Removed the `autopilot` label (back to human control). @ffaerber please decide next steps (details above).')"
fi
exit 0
fi
# Auto-delegate: if the plan names a teammate, trigger them via TOK (a PAT, so it
# fires a new workflow run — the built-in token cannot). Never targets @pm or self, so the # fires a new workflow run — the built-in token cannot). Never targets @pm or self, so the
# chain always terminates at a dev. The '🤖' guard on the trigger stops status-comment loops. # chain always terminates at a dev. The '🤖' guard on the trigger stops status-comment loops.
if [ -n "$AGENT_TOKEN" ]; then if [ -n "$TTOK" ]; then
# Only delegate on an explicit "DELEGATE: @<agent>" line — never on a prose mention, # Only delegate on an explicit "DELEGATE: @<agent>" line — never on a prose mention,
# so an agent that is asking the maintainer a question does not hand off prematurely. # so an agent that is asking the maintainer a question does not hand off prematurely.
target=$(grep -oiE 'DELEGATE:[[:space:]]*@(junior|senior|lead|qa)' /tmp/agent_out.md 2>/dev/null \ target=$(grep -oiE 'DELEGATE:[[:space:]]*@(junior|senior|lead|qa)' /tmp/agent_out.md 2>/dev/null \
| head -1 | grep -oiE '(junior|senior|lead|qa)' | tr '[:upper:]' '[:lower:]') | head -1 | grep -oiE '(junior|senior|lead|qa)' | tr '[:upper:]' '[:lower:]')
if [ -n "$target" ] && [ "$target" != "$NAME" ]; then if [ -n "$target" ] && [ "$target" != "$NAME" ]; then
echo "auto-delegating to @$target" echo "auto-delegating to @$target"
curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \ curl -sS -X POST -H "Authorization: token $TTOK" -H "Content-Type: application/json" \
"$API/issues/$NUM/comments" \ "$API/issues/$NUM/comments" \
-d "$(jq -nc --arg b "@$target please proceed with issue #$NUM per the plan above (delegated by $NAME)." '{body:$b}')" \ -d "$(jq -nc --arg b "@$target please proceed with issue #$NUM per the plan above (delegated by $NAME)." '{body:$b}')" \
-w '\ndelegate -> HTTP %{http_code}\n' || true -w '\ndelegate -> HTTP %{http_code}\n' || true
@@ -133,7 +236,7 @@ activity=""
if [ -s /tmp/activity_log.md ]; then if [ -s /tmp/activity_log.md ]; then
entries=$(wc -l < /tmp/activity_log.md 2>/dev/null || echo 0) entries=$(wc -l < /tmp/activity_log.md 2>/dev/null || echo 0)
log=$(cat /tmp/activity_log.md) log=$(cat /tmp/activity_log.md)
activity=$(printf '\n\n---\n🤖 **@%s** — activity log (%s entries):\n<details>\n<summary>tool calls & reasoning</summary>\n\n%s\n\n</details>' "$NAME" "$entries" "$log") activity=$(printf '\n\n<details>\n<summary>🔧 activity — %s tool calls</summary>\n\n%s\n\n</details>' "$entries" "$log")
fi fi
# One PR per run: publish ONLY this run's own branch ($BRANCH), never sibling # One PR per run: publish ONLY this run's own branch ($BRANCH), never sibling
@@ -174,7 +277,31 @@ prpost() {
if [ "$NEW" = "true" ]; then if [ "$NEW" = "true" ]; then
prpost "$prnum" "$(printf '🤖 **@%s** — ✅ PR ready for review — @ffaerber please review & merge:\n- %s%s' "$NAME" "$url" "$activity")" prpost "$prnum" "$(printf '🤖 **@%s** — ✅ PR ready for review — @ffaerber please review & merge:\n- %s%s' "$NAME" "$url" "$activity")"
# AUTOPILOT: hand the fresh PR to @qa automatically (via TOK, so it fires a new run).
# @qa then verifies and — if green — merges + closes via its MERGE_PR marker. The comment lands
# on the PR thread ($prnum) so the next run resolves the origin issue's label from the branch
# name. The '🤖' guard on the trigger gate stops status-comment loops.
if [ "$AUTOPILOT" = "true" ] && [ -n "$TTOK" ] && [ -n "$prnum" ]; then
echo "autopilot: auto-triggering @qa to review PR #$prnum"
curl -sS -X POST -H "Authorization: token $TTOK" -H "Content-Type: application/json" \
"$API/issues/$prnum/comments" \
-d "$(jq -nc --arg b "@qa please verify this PR (autopilot: issue #$NUM is labeled autopilot). Merge it if correct, or bounce it back to the dev with exactly what needs fixing." '{body:$b}')" \
-w '\ntrigger-qa -> HTTP %{http_code}\n' || true
fi
else else
# Resume (comment is on a PR thread): include the write-up here too. # Resume: just link the PR — its body and the diff already carry the description, so we don't
prpost "$prnum" "$(printf '🤖 **@%s** — updated branch/PR:\n- %s\n\n%s%s' "$NAME" "$url" "$prdesc" "$activity")" # repeat the full write-up in the comment (the reasoning trail below shows what this run did).
prpost "$prnum" "$(printf '🤖 **@%s** — pushed an update to the PR:\n- %s%s' "$NAME" "$url" "$activity")"
# AUTOPILOT: after a dev pushes a fix (e.g. following a @qa bounce), hand back to @qa to re-verify.
if [ "$AUTOPILOT" = "true" ] && [ -n "$TTOK" ] && [ -n "$prnum" ]; then
case "$NAME" in
junior|senior|lead)
echo "autopilot: dev pushed a fix — re-triggering @qa to re-verify PR #$prnum"
curl -sS -X POST -H "Authorization: token $TTOK" -H "Content-Type: application/json" \
"$API/issues/$prnum/comments" \
-d "$(jq -nc --arg b "@qa please re-verify this PR (autopilot). Merge it if now correct, or bounce it back with exactly what still needs fixing." '{body:$b}')" \
-w '\ntrigger-qa -> HTTP %{http_code}\n' || true
;;
esac
fi
fi fi
+19 -2
View File
@@ -27,7 +27,7 @@ cp "$AGENTS_JSON" /tmp/agents.json
# here — see agent.yml: this reusable workflow sees it as 'workflow_call'.) # here — see agent.yml: this reusable workflow sees it as 'workflow_call'.)
if [ -n "$CID" ]; then scan="$BODY"; else scan="$IBODY"; fi if [ -n "$CID" ]; then scan="$BODY"; else scan="$IBODY"; fi
name="" name=""
for a in pm junior senior lead qa; do for a in pm junior senior lead qa ops; do
case "$scan" in *"@$a"*) name=$a; break;; esac case "$scan" in *"@$a"*) name=$a; break;; esac
done done
if [ -z "$name" ]; then if [ -z "$name" ]; then
@@ -44,15 +44,17 @@ echo "Routing to @$name (model=$model vision=$vision mode=$mode skills=$skills)"
# 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
pm) TOK="$TOKEN_PM";; senior) TOK="$TOKEN_SENIOR";; junior) TOK="$TOKEN_JUNIOR";; 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 esac
[ -z "$TOK" ] && TOK="$GT" [ -z "$TOK" ] && TOK="$GT"
git config user.name "$name" git config user.name "$name"
git config user.email "$name@ffaerber.duckdns.org" git config user.email "$name@ffaerber.duckdns.org"
API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
hdr=(-H "Authorization: token $TOK" -H "Content-Type: application/json") hdr=(-H "Authorization: token $TOK" -H "Content-Type: application/json")
branch_ref=""
if [ -n "$IS_PR" ]; then # comment on a PR -> resume its branch if [ -n "$IS_PR" ]; then # comment on a PR -> resume its branch
ref=$(curl -s -H "Authorization: token $GT" "$API/pulls/$NUM" | jq -r .head.ref) ref=$(curl -s -H "Authorization: token $GT" "$API/pulls/$NUM" | jq -r .head.ref)
branch_ref="$ref"
git fetch origin "$ref" && git checkout "$ref" git fetch origin "$ref" && git checkout "$ref"
{ echo "branch=$ref"; echo "new=false"; } >> "$GITHUB_OUTPUT" { echo "branch=$ref"; echo "new=false"; } >> "$GITHUB_OUTPUT"
elif git ls-remote --exit-code --heads origin "ai/issue-$NUM" >/dev/null 2>&1; then elif git ls-remote --exit-code --heads origin "ai/issue-$NUM" >/dev/null 2>&1; then
@@ -72,3 +74,18 @@ else # comment on an issue, no branch ye
-d "$(jq -nc --arg b "🔨 **@$name** is on it — building on branch [\`ai/issue-$NUM\`]($url). I'll open a PR when it's ready." '{body:$b}')" >/dev/null || true -d "$(jq -nc --arg b "🔨 **@$name** is on it — building on branch [\`ai/issue-$NUM\`]($url). I'll open a PR when it's ready." '{body:$b}')" >/dev/null || true
fi fi
fi fi
# --- Autopilot gate: read the `autopilot` label FRESH every run. ---
# Presence of this label is the opt-in switch (and the kill switch: remove it mid-flight and the
# next run reverts to normal human-approval behavior). When @qa is triggered on a PR thread, the
# label lives on the ORIGIN issue (ai/issue-N), so resolve N from the branch name.
issnum="$NUM"
case "$IS_PR" in ?*) issnum=$(printf '%s' "$branch_ref" | sed -nE 's,^ai/issue-([0-9]+).*,\1,p');; esac
[ -z "$issnum" ] && issnum="$NUM"
autopilot=false
if curl -sS -H "Authorization: token $GT" "$API/issues/$issnum/labels" 2>/dev/null \
| jq -e 'any(.[]?; .name=="autopilot")' >/dev/null 2>&1; then
autopilot=true
fi
echo "autopilot (autopilot label on #$issnum)=$autopilot"
{ echo "autopilot=$autopilot"; echo "issnum=$issnum"; } >> "$GITHUB_OUTPUT"
+36 -1
View File
@@ -3,8 +3,10 @@
# 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 AGENT_TOKEN NAME MODEL VISION MODE HAS_IMAGES BRANCH NUM TITLE IBODY CMT # ANTHROPIC_API_KEY SELF_TOKEN NAME MODEL VISION MODE HAS_IMAGES BRANCH AUTOPILOT NUM TITLE
# 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).
set -u set -u
[ -z "$CMT" ] && CMT="(a new issue was just opened — assess it)" [ -z "$CMT" ] && CMT="(a new issue was just opened — assess it)"
@@ -51,6 +53,39 @@ if [ "$MODE" = "comment" ]; then
END_SUBTASKS END_SUBTASKS
The automation creates the milestone + one sub-issue per line (each linked to this issue). It The automation creates the milestone + one sub-issue per line (each linked to this issue). It
does NOT auto-start any dev — the maintainer @mentions an agent on each sub-issue when ready." does NOT auto-start any dev — the maintainer @mentions an agent on each sub-issue when ready."
if [ "$AUTOPILOT" = "true" ]; then
ACTION="$ACTION
AUTOPILOT MODE IS ACTIVE (this issue carries the 'autopilot' label). This OVERRIDES the
two-phase approval gate above: do NOT ask '@ffaerber ready to start building?' and do NOT wait
for a 'yes'. When the task is clear, present your SHORT plan naming the best teammate to build it
AND end your reply with a 'DELEGATE: @<agent>' line in the SAME turn to hand off immediately.
Prefer @junior for small/low-risk (mostly YAML/compose/config), @senior/@lead for complex or
multi-file work. Only skip delegating (and instead ask @ffaerber) if the task is genuinely
ambiguous or unsafe — otherwise plan-and-delegate now."
fi
fi
if [ "$NAME" = "qa" ]; then
ACTION="$ACTION
As QA you verify a change works: read the PR/issue, drive the web app with your headless
browser if there is a URL, and report bugs or confirm behavior. You normally do NOT merge —
a human does that."
if [ "$AUTOPILOT" = "true" ]; then
ACTION="$ACTION
AUTOPILOT MODE IS ACTIVE (this issue/PR carries the 'autopilot' label). You are the quality gate.
You do NOT edit code or fix anything yourself — you either accept the PR or send it back to the dev
with precise instructions. After actually verifying, end your reply with EXACTLY one of:
- 'MERGE_PR' — the change is correct and any CI is green. The automation merges the PR and closes
the linked issue. Do NOT merge by any other means; only this marker triggers the merge.
- 'BOUNCE: @<dev>' — something needs changing. FIRST spell out, specifically and actionably, exactly
what the dev must change (name the file, label, value, hostname, etc.), THEN end with the BOUNCE
line naming who should fix it (@junior / @senior / @lead — usually whoever built it; @senior or
@lead for something harder). The automation sends the PR back to that dev and then re-verifies
with you. After 3 bounces it stops automatically and hands to @ffaerber — so make each round
count and list ALL problems at once, not one at a time.
Use BOUNCE for anything a dev can fix. Only use 'HALT_AUTOPILOT' when the problem is NOT fixable by
a dev — the request itself is ambiguous or needs a human decision — to hand back to @ffaerber.
Emit AT MOST one of MERGE_PR / BOUNCE / HALT_AUTOPILOT, and only after you have actually verified."
fi
fi fi
else else
ACTION="You start on git branch '${BRANCH}', with git and push credentials already configured. ACTION="You start on git branch '${BRANCH}', with git and push credentials already configured.
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
# Set up the `gitea-admin` skill — instance administration for the @ops agent ONLY.
# Emits an opencode Skill file under ~/.config/opencode/skills/ documenting how to create
# orgs/users/repos, manage labels & secrets, and mint scoped per-user tokens via the Gitea API.
#
# The credential is SELF_TOKEN (BOOTSTRAP: currently an admin PAT — temporary). This skill doc is
# written ONLY for @ops (gated on NAME) so the how-to never reaches other agents. NOTE: while
# SELF_TOKEN is admin, every agent's process technically holds an admin credential in its env —
# that is the bootstrap trade-off. Once @ops is minting scoped per-user tokens, SELF_TOKEN should be
# narrowed and a dedicated admin token injected only for @ops.
#
# Required env (provided by the workflow step): NAME SELF_TOKEN
set -eu
[ "${NAME:-}" = "ops" ] || { echo "not @ops — skipping gitea-admin skill"; exit 0; }
# The doc references $SELF_TOKEN (@ops's own admin token, present in the Run-agent step). This step
# only writes the doc for @ops; permission.skill also denies the skill to every other agent.
mkdir -p ~/.config/opencode/skills/gitea-admin && chmod 700 ~/.config/opencode/skills/gitea-admin
cat > ~/.config/opencode/skills/gitea-admin/SKILL.md <<'SKILLET'
---
name: gitea-admin
description: Administer this Gitea instance — create orgs, users, repos; manage labels & Actions secrets; mint scoped per-user access tokens; bootstrap a new repo with the agent caller workflow. Use for "create org X", "create repo Y", "add user Z", "give user W a token scoped to …", "set label set on …".
domains: [gitea, admin, orgs, users, repos, secrets, tokens]
tags: [gitea, admin, api, curl, bootstrap]
---
# `gitea-admin` Skill (operator / @ops only)
Administer the Gitea instance via its REST API at `${GITHUB_SERVER_URL}/api/v1`, authenticated with
`Authorization: token ${SELF_TOKEN}` (a site-admin token during bootstrap). Both env vars are
already set. Work from the issue instructions; report what you did.
## Golden rules
- **NEVER print, echo, or paste a token, password, or secret value** — not in comments, not in logs.
Capture into a shell variable and immediately store it as a secret; report only that it was stored.
- **ALWAYS confirm before anything destructive** (delete user/repo/org, remove a member). Post a
clear "reply `yes` to confirm deleting X" and stop; only act after the maintainer confirms.
- Prefer the **least privilege** that satisfies the request when minting tokens.
- Be idempotent where you can (check if the org/repo/label already exists before creating).
## Create an organisation
```
curl -sS -X POST -H "Authorization: token $SELF_TOKEN" -H "Content-Type: application/json" \
"$API/orgs" -d '{"username":"acme","visibility":"private"}'
```
## Create a user, then mint a TAILORED token for them (least privilege)
Admin creates the user with a password you generate; you then basic-auth AS that user (with the
password you just set) to mint a scoped token, and store the token straight into a secret.
```
API="${GITHUB_SERVER_URL}/api/v1"
PW=$(head -c 24 /dev/urandom | base64 | tr -d '/+=' | head -c 24) # generated, never printed
# 1) create the user
curl -sS -X POST -H "Authorization: token $SELF_TOKEN" -H "Content-Type: application/json" \
"$API/admin/users" -d "$(jq -nc --arg u inter --arg e inter@ffaerber.duckdns.org --arg p "$PW" \
'{username:$u,email:$e,password:$p,must_change_password:false,source_id:0,visibility:"private"}')"
# 2) mint a scoped token AS that user (pick the narrowest scopes needed)
tok=$(curl -sS -u "inter:$PW" -H "Content-Type: application/json" -X POST "$API/users/inter/tokens" \
-d '{"name":"inter","scopes":["read:repository","write:issue"]}' | jq -r '.sha1')
# 3) store the value in BOTH places (see "Secret storage" below) — never print $tok
curl -sS -X PUT -H "Authorization: token $SELF_TOKEN" -H "Content-Type: application/json" \
"$API/orgs/gitea/actions/secrets/TOKEN_INTER" -d "$(jq -nc --arg d "$tok" '{data:$d}')"
```
Token **scopes** are groups of `read:`/`write:` on: `repository`, `issue`, `organization`, `user`,
`package`, `notification`, `misc`, and (only for a privileged token) `admin`.
## Secret storage — `gitea/secrets/.env` is the SOURCE OF TRUTH
Every token/secret value MUST live in **`gitea/secrets/.env`** (private, readable only by @ffaerber and
@ops) as a `KEY=value` line. That file is the master; the workflows only get a secret because `.env` is
mirrored into the org Actions secrets. So whenever you mint, rotate, or re-scope a token you MUST do
BOTH, in sync:
1. **`.env`**: `GET /repos/gitea/secrets/contents/.env` for its `sha`, add or replace the `KEY=value`
line, then `PUT` the updated base64 content with that `sha`.
2. **Actions secret**: `PUT /orgs/gitea/actions/secrets/{KEY}` with the same value (what runs use).
When you DELETE a token, remove it from BOTH. Keep `gitea/secrets/README.md` (the table describing what
each KEY is) up to date. Do NOT use `tokens.md` — the values live in `.env`. NEVER paste a token value
into any issue/PR/comment/log; it only ever goes into `.env` and the Actions secret.
## Change a user's token scope (the "update my token" flow)
Tokens are immutable — you can't edit scopes. Re-mint: delete the old token and create a new one,
then overwrite the stored secret.
```
curl -sS -u "inter:$PW" -X DELETE "$API/users/inter/tokens/<name-or-id>" # needs the password again
tok=$(curl -sS -u "inter:$PW" -X POST "$API/users/inter/tokens" -d '{"name":"inter","scopes":[…new…]}' | jq -r '.sha1')
curl -sS -X PUT -H "Authorization: token $SELF_TOKEN" "$API/orgs/gitea/actions/secrets/TOKEN_INTER" -d "$(jq -nc --arg d "$tok" '{data:$d}')"
```
(If you no longer hold the user's password, reset it first via `PATCH /admin/users/{username}` with a
new generated password, then re-mint.)
## Actions secrets & variables
```
curl -sS -X PUT -H "Authorization: token $SELF_TOKEN" "$API/orgs/{org}/actions/secrets/{NAME}" -d '{"data":"<value>"}'
curl -sS -X PUT -H "Authorization: token $SELF_TOKEN" "$API/repos/{owner}/{repo}/actions/secrets/{NAME}" -d '{"data":"<value>"}'
curl -sS -X PUT -H "Authorization: token $SELF_TOKEN" "$API/user/actions/secrets/{NAME}" -d '{"data":"<value>"}' # user-level
```
## Labels (repo or org-wide). Scoped labels (name `scope/value`) are mutually exclusive if `exclusive:true`.
```
curl -sS -X POST -H "Authorization: token $SELF_TOKEN" "$API/repos/{owner}/{repo}/labels" \
-d '{"name":"status/review","color":"1d76db","description":"…","exclusive":true}'
curl -sS -X POST -H "Authorization: token $SELF_TOKEN" "$API/orgs/{org}/labels" -d '{…}'
```
## Bootstrap a new repo (create + wire it up for the agents)
1. Create: `POST /orgs/{org}/repos` or `POST /admin/users/{user}/repos` (e.g. `{"name":"homepage","auto_init":true,"private":true}`).
2. Add the standard label set (loop the labels above).
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).
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,
TOKEN_* , OLLAMA_URL, OLLAMA_CLOUD_API_KEY); set any missing via the secrets calls above.
## Admin user management
- Create: `POST /admin/users`. Edit: `PATCH /admin/users/{username}`. Delete: `DELETE /admin/users/{username}` (**confirm first**).
- List: `GET /admin/users`.
SKILLET
chmod -R o=rX ~/.config/opencode/skills/gitea-admin
echo "gitea-admin skill installed for @ops ($(wc -l < ~/.config/opencode/skills/gitea-admin/SKILL.md) lines)"
+19 -25
View File
@@ -1,19 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Set up `gitea-api` skill (let agents read/write issues, PRs, Actions across repos). # Set up `gitea-api` skill (let agents read/write issues, PRs, Actions across repos).
# Mirrors the node1-ssh pattern: emit an opencode Skill file under # Emits an opencode Skill file under ~/.config/opencode/skills/. The credential is SELF_TOKEN — the
# ~/.config/opencode/skills/ so any dev agent discovers the capability via OpenCode's # RUNNING agent's OWN token (e.g. TOKEN_PM for @pm), present in the Run-agent step's env. So each
# skill registry. The credential is the shared AGENT_TOKEN (a PAT whose scopes the # agent talks to Gitea as itself, with its own scopes. This step only writes the doc, so it always
# maintainer set at creation time — issue/repository/organization/misc read+write, cross-repo). # emits; permission.skill decides which agents may actually load it.
# Only emitted when AGENT_TOKEN is actually present, so repos without it don't get a
# broken skill. The token is passed via env and never inlined into shell.
# #
# Required env (provided by the workflow step): AGENT_TOKEN # Required env (provided by the workflow step): (none — the token is in the Run-agent step)
set -eu set -eu
if [ -z "$AGENT_TOKEN" ]; then
echo "AGENT_TOKEN not set — skipping gitea-api skill"
exit 0
fi
mkdir -p ~/.config/opencode/skills/gitea-api && chmod 700 ~/.config/opencode/skills/gitea-api mkdir -p ~/.config/opencode/skills/gitea-api && chmod 700 ~/.config/opencode/skills/gitea-api
cat > ~/.config/opencode/skills/gitea-api/SKILL.md <<'SKILLET' cat > ~/.config/opencode/skills/gitea-api/SKILL.md <<'SKILLET'
--- ---
@@ -33,14 +27,14 @@ Use this skill to talk to the **Gitea REST API** (`${GITHUB_SERVER_URL}/api/v1`)
## How it works ## How it works
Calls go via `curl` with the header `Authorization: token ${AGENT_TOKEN}`. Both Calls go via `curl` with the header `Authorization: token ${SELF_TOKEN}`. Both
`${GITHUB_SERVER_URL}` (the instance root, e.g. `https://git.example.com`) and `${GITHUB_SERVER_URL}` (the instance root, e.g. `https://git.example.com`) and
`${AGENT_TOKEN}` are present in your environment. The API root is `${SELF_TOKEN}` are present in your environment. The API root is
`${GITHUB_SERVER_URL}/api/v1`. `${GITHUB_SERVER_URL}/api/v1`.
## What you're actually allowed to do — the token's scopes are the source of truth ## What you're actually allowed to do — the token's scopes are the source of truth
The shared `AGENT_TOKEN` was granted **read and write** on the `issue`, The shared `SELF_TOKEN` was granted **read and write** on the `issue`,
`repository`, `organization`, and `misc` scope groups, **cross-repo** (any repo the `repository`, `organization`, and `misc` scope groups, **cross-repo** (any repo the
token's account can see). That covers: token's account can see). That covers:
- issues, PRs, comments, labels, milestones, reviewers (read + write) - issues, PRs, comments, labels, milestones, reviewers (read + write)
@@ -64,9 +58,9 @@ in `agent.yml` exists to enforce.
## Never echo the token ## Never echo the token
**Never print, log, or exfiltrate `AGENT_TOKEN`.** Do not pass it to `echo`, do not **Never print, log, or exfiltrate `SELF_TOKEN`.** Do not pass it to `echo`, do not
include it in a comment, do not write it to a file. If you need to show a curl command, include it in a comment, do not write it to a file. If you need to show a curl command,
redact the header as `Authorization: token $AGENT_TOKEN`. redact the header as `Authorization: token $SELF_TOKEN`.
## Examples ## Examples
@@ -77,9 +71,9 @@ All examples assume `API="${GITHUB_SERVER_URL}/api/v1"`.
```bash ```bash
API="${GITHUB_SERVER_URL}/api/v1" API="${GITHUB_SERVER_URL}/api/v1"
# Get issue/PR #12 on repo owner/repo (a PR if the number is a pull; issues/PRs share one number space) # Get issue/PR #12 on repo owner/repo (a PR if the number is a pull; issues/PRs share one number space)
curl -sS -H "Authorization: token $AGENT_TOKEN" "$API/repos/owner/repo/issues/12" | jq '{title,state,body,user:.user.login}' curl -sS -H "Authorization: token $SELF_TOKEN" "$API/repos/owner/repo/issues/12" | jq '{title,state,body,user:.user.login}'
# Its comment thread # Its comment thread
curl -sS -H "Authorization: token $AGENT_TOKEN" "$API/repos/owner/repo/issues/12/comments?limit=100" \ curl -sS -H "Authorization: token $SELF_TOKEN" "$API/repos/owner/repo/issues/12/comments?limit=100" \
| jq -r '.[] | "### @\(.user.login):\n\(.body)\n"' | jq -r '.[] | "### @\(.user.login):\n\(.body)\n"'
``` ```
@@ -91,27 +85,27 @@ find the owner/repo for a `#N` in *this* repo, just use `${GITHUB_REPOSITORY}`.
```bash ```bash
API="${GITHUB_SERVER_URL}/api/v1" API="${GITHUB_SERVER_URL}/api/v1"
# Recent runs on a repo # Recent runs on a repo
curl -sS -H "Authorization: token $AGENT_TOKEN" "$API/repos/owner/repo/actions/runs?limit=10" | jq '.[] | {id,status,conclusion,head_branch,event}' curl -sS -H "Authorization: token $SELF_TOKEN" "$API/repos/owner/repo/actions/runs?limit=10" | jq '.[] | {id,status,conclusion,head_branch,event}'
# Jobs for a run # Jobs for a run
curl -sS -H "Authorization: token $AGENT_TOKEN" "$API/repos/owner/repo/actions/runs/$RUN_ID/jobs" | jq '.[] | {name,status,conclusion}' curl -sS -H "Authorization: token $SELF_TOKEN" "$API/repos/owner/repo/actions/runs/$RUN_ID/jobs" | jq '.[] | {name,status,conclusion}'
# Logs for a job (returns a text/plain stream) # Logs for a job (returns a text/plain stream)
curl -sS -H "Authorization: token $AGENT_TOKEN" "$API/repos/owner/repo/actions/jobs/$JOB_ID/logs" curl -sS -H "Authorization: token $SELF_TOKEN" "$API/repos/owner/repo/actions/jobs/$JOB_ID/logs"
``` ```
### List repos across an org ### List repos across an org
```bash ```bash
curl -sS -H "Authorization: token $AGENT_TOKEN" "$API/orgs/$ORG/repos?limit=50" | jq '.[] | .full_name' curl -sS -H "Authorization: token $SELF_TOKEN" "$API/orgs/$ORG/repos?limit=50" | jq '.[] | .full_name'
``` ```
### Write: comment / label / close on another repo's issue (only when your task requires it) ### Write: comment / label / close on another repo's issue (only when your task requires it)
```bash ```bash
curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \ curl -sS -X POST -H "Authorization: token $SELF_TOKEN" -H "Content-Type: application/json" \
"$API/repos/owner/repo/issues/12/comments" -d '{"body":"related to #N"}' "$API/repos/owner/repo/issues/12/comments" -d '{"body":"related to #N"}'
curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \ curl -sS -X POST -H "Authorization: token $SELF_TOKEN" -H "Content-Type: application/json" \
"$API/repos/owner/repo/issues/12/labels" -d '{"labels":["related"]}' "$API/repos/owner/repo/issues/12/labels" -d '{"labels":["related"]}'
curl -sS -X PATCH -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \ curl -sS -X PATCH -H "Authorization: token $SELF_TOKEN" -H "Content-Type: application/json" \
"$API/repos/owner/repo/issues/12" -d '{"state":"closed"}' "$API/repos/owner/repo/issues/12" -d '{"state":"closed"}'
``` ```
+16
View File
@@ -11,10 +11,26 @@ the loop guards.
## Golden rules ## Golden rules
- You may edit **any file**. **NEVER push to `main`, NEVER merge a PR** — all work goes on a branch - You may edit **any file**. **NEVER push to `main`, NEVER merge a PR** — all work goes on a branch
and becomes a PR a human reviews and merges. and becomes a PR a human reviews and merges.
- **Narrow exception — `@qa` autopilot merge:** `@qa` (and only `@qa`) MAY merge a single PR **only**
when the linked issue carries the `autopilot` label, the PR is clearly correct, and any CI
checks are green. `@qa` triggers the merge by ending its reply with the `MERGE_PR` marker (the
workflow performs the merge + closes the issue). On **any** doubt or bug, `@qa` must NOT merge:
it ends with `HALT_AUTOPILOT` instead, which removes the `autopilot` label and returns the
issue to human control. No other agent may merge, and `@qa` may not merge without the label.
- **Never print, exfiltrate, or invent secret values.** - **Never print, exfiltrate, or invent secret values.**
- Keep changes **minimal** and match the conventions already in the file you're editing. - Keep changes **minimal** and match the conventions already in the file you're editing.
- Do the work on a **branch** — never paste code or diffs into the issue thread. - Do the work on a **branch** — never paste code or diffs into the issue thread.
## Autopilot (`autopilot` label)
An issue labeled **`autopilot`** runs without the usual human checkpoints:
- `@pm` plans **and** delegates in the same turn (skips the "ready to build? reply yes" gate).
- After the dev's PR is opened, `@qa` is auto-triggered to verify it, and merges + closes on success
(see the QA merge exception above).
- **Kill switch:** remove the `autopilot` label at any time. The label is re-read fresh at the
start of every run, so the next agent turn reverts to normal human-approval behavior. `@qa` also
removes the label itself whenever it halts on a bug or a failed merge.
No label (the default) = today's behavior, unchanged.
## Branches & pull requests ## Branches & pull requests
Start on `ai/issue-<N>`. Split independent changes into separate branches (one PR each). Commit and Start on `ai/issue-<N>`. Split independent changes into separate branches (one PR each). Commit and
push incrementally. Do NOT open PRs yourself (automated). End your reply with the PR description push incrementally. Do NOT open PRs yourself (automated). End your reply with the PR description
+11 -6
View File
@@ -12,6 +12,7 @@ Shared **AI dev-team** workflow for Gitea Actions, reusable across repos. It giv
| `@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). | | `@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. | | `@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. | | `@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 `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. or an agent's skills there, update this table too.
@@ -36,7 +37,7 @@ it is the source of truth, and `agents` itself uses the same file:
```yaml ```yaml
name: ai-agent name: ai-agent
run-name: "ai-agent · #${{ github.event.issue.number }}" # quotes required: bare # starts a YAML comment run-name: "ai-agent · #${{ github.event.issue.number }}" # quotes required: bare # starts a YAML comment
# Standard caller for the shared AI-agent workflow (ffaerber/agents). Copy this file VERBATIM into # 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 # 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 # 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. # required: a reusable (workflow_call) workflow can only be invoked from a caller job, not top-level.
@@ -48,7 +49,7 @@ on:
types: [opened] types: [opened]
jobs: jobs:
agent: agent:
uses: ffaerber/agents/.gitea/workflows/agent.yml@main uses: gitea/agents/.gitea/workflows/agent.yml@main
secrets: inherit secrets: inherit
``` ```
@@ -75,11 +76,15 @@ points `$SCRIPTS` at it. Keep the workflow and its scripts moving together on `m
|--------|-----| |--------|-----|
| `ANTHROPIC_API_KEY` | `@lead` (and `@pm`/`@senior`/`@qa` if on Claude) | | `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) |
| `AGENT_TOKEN` | PAT (issue/repository/organization/misc read+write, cross-repo) — posts the delegation comment that fires the next agent **and** powers the `gitea-api` skill (read/write issues, PRs, comments, labels, and Actions runs/logs across any repo). Do not re-narrow its scopes without also removing the `gitea-api` skill. | | `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_PM`,`TOKEN_SENIOR`,`TOKEN_JUNIOR`,`TOKEN_LEAD`,`TOKEN_QA` | optional — post/commit as each agent's own Gitea user (falls back to the bot) | | `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`. |
| `AGENT_TOKEN` | **fallback** — an admin PAT used only where per-agent `TOKEN_*` aren't configured (e.g. an un-migrated consuming repo). Covers the two things the built-in `GITEA_TOKEN` can't do: post the delegation/autopilot comment that *fires the next run*, and merge a PR so the push *triggers downstream deploys*. Where per-agent tokens exist, each agent uses its own instead. |
`GITEA_TOKEN` is auto-provided. Tip: set these once at the **org** level so every repo inherits Each agent authenticates as **itself**: the Run-agent step selects that agent's `TOKEN_*` into
them via `secrets: inherit`. `SELF_TOKEN` (never another agent's), falling back to `AGENT_TOKEN` only when its own token is unset.
`GITEA_TOKEN` is auto-provided (used for reads, and as the reply identity only when an agent has no
`TOKEN_*` of its own). Tip: set these once at the **org** level so every repo inherits them via
`secrets: inherit`.
## Also add to each consuming repo ## Also add to each consuming repo