agents: add @intern routed to local ollama/ornith:35b
ci / lint (pull_request) Successful in 12s

This commit is contained in:
2026-07-06 07:30:30 +00:00
parent 950c639b0a
commit bf1256ecec
8 changed files with 33 additions and 19 deletions
+4 -1
View File
@@ -107,6 +107,7 @@ jobs:
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 }} TOKEN_OPS: ${{ secrets.TOKEN_OPS }}
TOKEN_INTERN: ${{ secrets.TOKEN_INTERN }}
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)
@@ -176,7 +177,7 @@ jobs:
# 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
# consuming repo now carries the per-agent TOKEN_* secrets (org-level for gitea/*, user-level # consuming repo now carries the per-agent TOKEN_* secrets (org-level for gitea/*, user-level
# for ffaerber/*), so there is no shared-token fallback. # for ffaerber/*), so there is no shared-token fallback.
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 || '' }} 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 }}
VISION: ${{ steps.prep.outputs.vision }} VISION: ${{ steps.prep.outputs.vision }}
@@ -209,6 +210,7 @@ jobs:
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 }} TOKEN_OPS: ${{ secrets.TOKEN_OPS }}
TOKEN_INTERN: ${{ secrets.TOKEN_INTERN }}
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 }}
@@ -235,6 +237,7 @@ jobs:
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 }} TOKEN_OPS: ${{ secrets.TOKEN_OPS }}
TOKEN_INTERN: ${{ secrets.TOKEN_INTERN }}
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 }}
+7
View File
@@ -50,5 +50,12 @@
"gitea-admin" "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)." "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)."
},
"intern": {
"model": "ollama/ornith:35b",
"vision": false,
"mode": "pr",
"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."
} }
} }
+8 -6
View File
@@ -32,15 +32,17 @@ SKILLS="${SKILLS:-[]}"
PERM=$(jq -nc --argjson s "$SKILLS" ' PERM=$(jq -nc --argjson s "$SKILLS" '
{skill: ( {"*":"deny"} + (reduce $s[] as $k ({}; . + {($k):"allow"})) )}') {skill: ( {"*":"deny"} + (reduce $s[] as $k ({}; . + {($k):"allow"})) )}')
# Two ollama providers: local self-hosted (ornith) + Ollama Cloud (gemma4/kimi-k2.7-code/glm-5.2/minimax-m3). # Two ollama providers: local self-hosted (ornith) + Ollama Cloud (gemma4/kimi-k2.7-code/glm-5.2/minimax-m3).
# The ollama-cloud `models:` map is DERIVED from agents.json (the single source of truth, shared with # The provider `models:` maps are DERIVED from agents.json (the single source of truth, shared with
# route.sh) so every model an agent is routed to is always declared in the provider config. Only the # route.sh) so every model an agent is routed to is always declared in the provider config.
# `ollama-cloud/` provider prefix models participate — e.g. `anthropic/claude-opus-4-8` (@lead) is a # `ollama-cloud/` prefix models go to the cloud provider; `ollama/` prefix models go to the local
# built-in provider and `ornith:35b` is local-only, neither belongs here. See issue #31. # provider. Built-in providers (e.g. `anthropic/claude-opus-4-8` for @lead) are not derived here.
# See issue #31.
AGENTS_JSON="${SCRIPTS:-$(dirname -- "$0")}/agents.json" AGENTS_JSON="${SCRIPTS:-$(dirname -- "$0")}/agents.json"
CLOUD_MODELS=$(jq -r '[.[] | .model | select(startswith("ollama-cloud/")) | sub("^ollama-cloud/";"")] | map({(.):{}}) | add // {}' "$AGENTS_JSON") CLOUD_MODELS=$(jq -r '[.[] | .model | select(startswith("ollama-cloud/")) | sub("^ollama-cloud/";"")] | map({(.):{}}) | add // {}' "$AGENTS_JSON")
jq -n --argjson mcp "$MCP" --argjson perm "$PERM" --argjson cloud "$CLOUD_MODELS" --arg url "$OLLAMA_URL" --arg ckey "$OLLAMA_CLOUD_API_KEY" '{ LOCAL_MODELS=$(jq -r '[.[] | .model | select(startswith("ollama/")) | sub("^ollama/";"")] | map({(.):{}}) | add // {"ornith:35b":{}}' "$AGENTS_JSON")
jq -n --argjson mcp "$MCP" --argjson perm "$PERM" --argjson cloud "$CLOUD_MODELS" --argjson local "$LOCAL_MODELS" --arg url "$OLLAMA_URL" --arg ckey "$OLLAMA_CLOUD_API_KEY" '{
provider: { provider: {
ollama: {npm:"@ai-sdk/openai-compatible", options:{baseURL:($url+"/v1")}, models:{"ornith:35b":{}}}, 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}
}, },
permission: $perm, permission: $perm,
+6 -5
View File
@@ -9,7 +9,7 @@ set +e # publish is best-effort: a grep-no-match / curl non-zero must NOT kill
# 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";; ops) TOK="$TOKEN_OPS";; *) TOK="";; lead) TOK="$TOKEN_LEAD";; qa) TOK="$TOKEN_QA";; ops) TOK="$TOKEN_OPS";; intern) TOK="$TOKEN_INTERN";; *) 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 # Trigger token: comments that must FIRE the next workflow (delegation, autopilot) and PR merges
@@ -108,8 +108,8 @@ if [ "$MODE" != "pr" ]; then
if grep -qiE '^[[:space:]]*APPROVE[[:space:]]*$' /tmp/agent_out.md; then if grep -qiE '^[[:space:]]*APPROVE[[:space:]]*$' /tmp/agent_out.md; then
post_to "$ISSN" "$(printf '✅ Reviewed PR #%s — looks good.\n\n%s%s' "${PRN:-?}" "$reply" "$activity")" post_to "$ISSN" "$(printf '✅ Reviewed PR #%s — looks good.\n\n%s%s' "${PRN:-?}" "$reply" "$activity")"
trig "$ISSN" "@pm — I have reviewed and approved PR #${PRN:-?} (issue #$ISSN). Over to you." trig "$ISSN" "@pm — I have reviewed and approved PR #${PRN:-?} (issue #$ISSN). Over to you."
elif grep -qiE '^[[:space:]]*BOUNCE:[[:space:]]*@(junior|senior|lead)' /tmp/agent_out.md; then elif grep -qiE '^[[:space:]]*BOUNCE:[[:space:]]*@(junior|senior|lead|intern)' /tmp/agent_out.md; then
dev=$(grep -oiE 'BOUNCE:[[:space:]]*@(junior|senior|lead)' /tmp/agent_out.md | head -1 | grep -oiE '(junior|senior|lead)' | tr '[:upper:]' '[:lower:]') dev=$(grep -oiE 'BOUNCE:[[:space:]]*@(junior|senior|lead|intern)' /tmp/agent_out.md | head -1 | grep -oiE '(junior|senior|lead|intern)' | tr '[:upper:]' '[:lower:]')
[ -z "$dev" ] && [ -n "$PRN" ] && dev=$(curl -sS "${hdr[@]}" "$API/pulls/$PRN" | jq -r '.user.login // "junior"') [ -z "$dev" ] && [ -n "$PRN" ] && dev=$(curl -sS "${hdr[@]}" "$API/pulls/$PRN" | jq -r '.user.login // "junior"')
dest="${PRN:-$NUM}" dest="${PRN:-$NUM}"
post_to "$dest" "$reply$activity" # recommendations, on the PR post_to "$dest" "$reply$activity" # recommendations, on the PR
@@ -121,6 +121,7 @@ if [ "$MODE" != "pr" ]; then
else else
n=$((prior + 1)) n=$((prior + 1))
trig "$dest" "@${dev:-junior} please address my review above and update PR #${PRN:-?} (fix attempt $n/3)." trig "$dest" "@${dev:-junior} please address my review above and update PR #${PRN:-?} (fix attempt $n/3)."
fi fi
elif grep -qiE '^[[:space:]]*HALT([_ ]AUTOPILOT)?[[:space:]]*$' /tmp/agent_out.md; then elif grep -qiE '^[[:space:]]*HALT([_ ]AUTOPILOT)?[[:space:]]*$' /tmp/agent_out.md; then
[ "$AUTOPILOT" = "true" ] && del_autopilot_label "$ISSN" [ "$AUTOPILOT" = "true" ] && del_autopilot_label "$ISSN"
@@ -132,7 +133,7 @@ if [ "$MODE" != "pr" ]; then
fi fi
# ---------- @pm / @ops: issue-thread orchestration ---------- # ---------- @pm / @ops: issue-thread orchestration ----------
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:]') target=$(grep -oiE 'DELEGATE:[[:space:]]*@(junior|senior|lead|qa|intern)' /tmp/agent_out.md 2>/dev/null | head -1 | grep -oiE '(junior|senior|lead|qa|intern)' | tr '[:upper:]' '[:lower:]')
# Visible comment: the reply text, or a sensible line if the agent only emitted a marker. # Visible comment: the reply text, or a sensible line if the agent only emitted a marker.
msg="$reply" msg="$reply"
case "$msg" in ""|"_(Made changes"*) msg=$([ -n "$target" ] && echo "Handing off to @$target." || echo "_(no further comment)_") ;; esac case "$msg" in ""|"_(Made changes"*) msg=$([ -n "$target" ] && echo "Handing off to @$target." || echo "_(no further comment)_") ;; esac
@@ -297,6 +298,6 @@ else
# on the PR thread. The qa↔dev loop is direct — it does NOT go back through @pm each round. # on the PR thread. The qa↔dev loop is direct — it does NOT go back through @pm each round.
prpost "$prnum" "$(printf 'Pushed an update to PR #%s.%s' "$prnum" "$activity")" prpost "$prnum" "$(printf 'Pushed an update to PR #%s.%s' "$prnum" "$activity")"
case "$NAME" in case "$NAME" in
junior|senior|lead) trig "$prnum" "@qa please re-verify PR #$prnum — I have pushed an update." ;; junior|senior|lead|intern) trig "$prnum" "@qa please re-verify PR #$prnum — I have pushed an update." ;;
esac esac
fi fi
+1 -1
View File
@@ -17,7 +17,7 @@ set +e
# 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";; ops) TOK="$TOKEN_OPS";; *) TOK="";; lead) TOK="$TOKEN_LEAD";; qa) TOK="$TOKEN_QA";; ops) TOK="$TOKEN_OPS";; intern) TOK="$TOKEN_INTERN";; *) TOK="";;
esac esac
[ -z "$TOK" ] && TOK="$GT" [ -z "$TOK" ] && TOK="$GT"
# Trigger token: the @pm hand-back below must FIRE a new run, which the built-in token cannot. # Trigger token: the @pm hand-back below must FIRE a new run, which the built-in token cannot.
+2 -2
View File
@@ -31,7 +31,7 @@ name=""
# load-bearing for the flow's trigger comments: "@pm — @qa approved …" must route to @pm (pm is # load-bearing for the flow's trigger comments: "@pm — @qa approved …" must route to @pm (pm is
# checked first), while "@junior please address @qa's review …" must route to the dev (devs are # checked first), while "@junior please address @qa's review …" must route to the dev (devs are
# checked before qa). If you add an agent or reword a trigger in publish.sh, re-check this order. # checked before qa). If you add an agent or reword a trigger in publish.sh, re-check this order.
for a in pm junior senior lead qa ops; do for a in pm junior senior lead qa ops intern; 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
@@ -48,7 +48,7 @@ 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";; ops) TOK="$TOKEN_OPS";; *) TOK="";; lead) TOK="$TOKEN_LEAD";; qa) TOK="$TOKEN_QA";; ops) TOK="$TOKEN_OPS";; intern) TOK="$TOKEN_INTERN";; *) TOK="";;
esac esac
[ -z "$TOK" ] && TOK="$GT" [ -z "$TOK" ] && TOK="$GT"
git config user.name "$name" git config user.name "$name"
+2 -2
View File
@@ -26,7 +26,7 @@ if [ "$MODE" = "comment" ]; then
ACTION="You do NOT edit files, create branches, or write a PR description. Respond with your analysis, ACTION="You do NOT edit files, create branches, or write a PR description. Respond with your analysis,
plan, research, or clarifying questions — your reply becomes a comment on the issue. plan, research, or clarifying questions — your reply becomes a comment on the issue.
To hand work to a teammate, end your reply with EXACTLY one line: 'DELEGATE: @<agent>' (one of To hand work to a teammate, end your reply with EXACTLY one line: 'DELEGATE: @<agent>' (one of
@junior @senior @lead @qa) — but ONLY when you are ready to hand off AND need nothing further from the @junior @senior @lead @qa @intern) — but ONLY when you are ready to hand off AND need nothing further from the
maintainer. If you are asking @ffaerber to confirm or decide ANYTHING, do NOT include a DELEGATE line; maintainer. If you are asking @ffaerber to confirm or decide ANYTHING, do NOT include a DELEGATE line;
just ask and wait. Never ask for confirmation and delegate in the same reply. Mentioning a teammate in just ask and wait. Never ask for confirmation and delegate in the same reply. Mentioning a teammate in
prose does NOT delegate — only the DELEGATE line does. prose does NOT delegate — only the DELEGATE line does.
@@ -86,7 +86,7 @@ if [ "$MODE" = "comment" ]; then
issue and hands back to @pm (who tells the creator, or in autopilot merges). You do NOT merge. issue and hands back to @pm (who tells the creator, or in autopilot merges). You do NOT merge.
- 'BOUNCE: @<dev>' — something needs changing. FIRST spell out, specifically and actionably, exactly - 'BOUNCE: @<dev>' — something needs changing. FIRST spell out, specifically and actionably, exactly
what to change (file, label, value, hostname, …), THEN end with the BOUNCE line naming who fixes what to change (file, label, value, hostname, …), THEN end with the BOUNCE line naming who fixes
it (@junior / @senior / @lead — usually whoever built it). The automation sends the PR back and it (@junior / @senior / @lead / @intern — usually whoever built it). The automation sends the PR back and
re-verifies with you. After 3 rounds it stops and hands to @ffaerber — so list ALL problems at re-verifies with you. After 3 rounds it stops and hands to @ffaerber — so list ALL problems at
once, not one at a time. once, not one at a time.
- 'HALT' — the problem is NOT something a dev can fix (the request is ambiguous / needs a human - 'HALT' — the problem is NOT something a dev can fix (the request is ambiguous / needs a human
+1
View File
@@ -13,6 +13,7 @@ Shared **AI dev-team** workflow for Gitea Actions, reusable across repos. It giv
| `@lead` | `anthropic/claude-opus-4-8` | 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` | `anthropic/claude-opus-4-8` | no | comment | `gitea-admin` | Gitea operator — administers the instance itself (create orgs/users/repos, labels, secrets, scoped per-user tokens, bootstrap repos). Comments only; never edits code. Confirms before destructive actions. | | `@ops` | `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`. |
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
change a model or an agent's skills there, update this table too. (Repo-specific skills, e.g. a change a model or an agent's skills there, update this table too. (Repo-specific skills, e.g. a