diff --git a/.gitea/workflows/agent.yml b/.gitea/workflows/agent.yml index 939338c..3d41124 100644 --- a/.gitea/workflows/agent.yml +++ b/.gitea/workflows/agent.yml @@ -107,6 +107,7 @@ jobs: TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }} TOKEN_QA: ${{ secrets.TOKEN_QA }} TOKEN_OPS: ${{ secrets.TOKEN_OPS }} + TOKEN_INTERN: ${{ secrets.TOKEN_INTERN }} run: bash "$SCRIPTS/route.sh" - 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 # consuming repo now carries the per-agent TOKEN_* secrets (org-level for gitea/*, user-level # 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 }} MODEL: ${{ steps.prep.outputs.model }} VISION: ${{ steps.prep.outputs.vision }} @@ -209,6 +210,7 @@ jobs: TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }} TOKEN_QA: ${{ secrets.TOKEN_QA }} TOKEN_OPS: ${{ secrets.TOKEN_OPS }} + TOKEN_INTERN: ${{ secrets.TOKEN_INTERN }} NAME: ${{ steps.prep.outputs.name }} MODE: ${{ steps.prep.outputs.mode }} NUM: ${{ github.event.issue.number }} @@ -235,6 +237,7 @@ jobs: TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }} TOKEN_QA: ${{ secrets.TOKEN_QA }} TOKEN_OPS: ${{ secrets.TOKEN_OPS }} + TOKEN_INTERN: ${{ secrets.TOKEN_INTERN }} NAME: ${{ steps.prep.outputs.name }} MODE: ${{ steps.prep.outputs.mode }} NUM: ${{ github.event.issue.number }} diff --git a/.gitea/workflows/scripts/agents.json b/.gitea/workflows/scripts/agents.json index c1ce189..9cff416 100644 --- a/.gitea/workflows/scripts/agents.json +++ b/.gitea/workflows/scripts/agents.json @@ -50,5 +50,12 @@ "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)." + }, + "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." } } diff --git a/.gitea/workflows/scripts/install-opencode.sh b/.gitea/workflows/scripts/install-opencode.sh index 0707268..57ee2d2 100755 --- a/.gitea/workflows/scripts/install-opencode.sh +++ b/.gitea/workflows/scripts/install-opencode.sh @@ -32,15 +32,17 @@ SKILLS="${SKILLS:-[]}" PERM=$(jq -nc --argjson s "$SKILLS" ' {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). -# The ollama-cloud `models:` map is 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 -# `ollama-cloud/` provider prefix models participate — e.g. `anthropic/claude-opus-4-8` (@lead) is a -# built-in provider and `ornith:35b` is local-only, neither belongs here. See issue #31. +# 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. +# `ollama-cloud/` prefix models go to the cloud provider; `ollama/` prefix models go to the local +# 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" 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: { - 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} }, permission: $perm, diff --git a/.gitea/workflows/scripts/publish.sh b/.gitea/workflows/scripts/publish.sh index 5ce5f52..4e6b8e2 100755 --- a/.gitea/workflows/scripts/publish.sh +++ b/.gitea/workflows/scripts/publish.sh @@ -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. case "$NAME" in 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 [ -z "$TOK" ] && TOK="$GT" # 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 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." - elif grep -qiE '^[[:space:]]*BOUNCE:[[:space:]]*@(junior|senior|lead)' /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:]') + elif grep -qiE '^[[:space:]]*BOUNCE:[[:space:]]*@(junior|senior|lead|intern)' /tmp/agent_out.md; then + 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"') dest="${PRN:-$NUM}" post_to "$dest" "$reply$activity" # recommendations, on the PR @@ -121,6 +121,7 @@ if [ "$MODE" != "pr" ]; then else n=$((prior + 1)) trig "$dest" "@${dev:-junior} please address my review above and update PR #${PRN:-?} (fix attempt $n/3)." + fi elif grep -qiE '^[[:space:]]*HALT([_ ]AUTOPILOT)?[[:space:]]*$' /tmp/agent_out.md; then [ "$AUTOPILOT" = "true" ] && del_autopilot_label "$ISSN" @@ -132,7 +133,7 @@ if [ "$MODE" != "pr" ]; then fi # ---------- @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. msg="$reply" 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. prpost "$prnum" "$(printf 'Pushed an update to PR #%s.%s' "$prnum" "$activity")" 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 fi diff --git a/.gitea/workflows/scripts/rescue-pr.sh b/.gitea/workflows/scripts/rescue-pr.sh index 23c12a9..ae15802 100755 --- a/.gitea/workflows/scripts/rescue-pr.sh +++ b/.gitea/workflows/scripts/rescue-pr.sh @@ -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. case "$NAME" in 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 [ -z "$TOK" ] && TOK="$GT" # Trigger token: the @pm hand-back below must FIRE a new run, which the built-in token cannot. diff --git a/.gitea/workflows/scripts/route.sh b/.gitea/workflows/scripts/route.sh index b691290..f96cb5a 100755 --- a/.gitea/workflows/scripts/route.sh +++ b/.gitea/workflows/scripts/route.sh @@ -31,7 +31,7 @@ name="" # 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 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 done 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. case "$name" in 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 [ -z "$TOK" ] && TOK="$GT" git config user.name "$name" diff --git a/.gitea/workflows/scripts/run-agent.sh b/.gitea/workflows/scripts/run-agent.sh index 7f56b02..41a1417 100755 --- a/.gitea/workflows/scripts/run-agent.sh +++ b/.gitea/workflows/scripts/run-agent.sh @@ -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, 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: @' (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; 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. @@ -86,9 +86,9 @@ if [ "$MODE" = "comment" ]; then issue and hands back to @pm (who tells the creator, or in autopilot merges). You do NOT merge. - 'BOUNCE: @' — 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 - it (@junior / @senior / @lead — 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 - once, not one at a time. + 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 + once, not one at a time. - 'HALT' — the problem is NOT something a dev can fix (the request is ambiguous / needs a human decision). Hands back to @ffaerber. Emit AT MOST one marker, and only after you have actually verified." diff --git a/README.md b/README.md index 84aaa7c..754db06 100644 --- a/README.md +++ b/README.md @@ -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. | | `@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. | +| `@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