Compare commits

..
9 Commits
Author SHA1 Message Date
senior 55e00871f9 feat(agents): wire xAI provider, move @lead to xai/grok-4.5 (issue #12)
ci / lint (pull_request) Successful in 15s
Add an xai provider (OpenAI-compatible, https://api.x.ai/v1) to
install-opencode.sh, with its models map derived from agents.json
exactly like the ollama providers. Pass XAI_API_KEY into both the
Install-opencode and Run-agent steps of agent.yml. Switch @lead's
model from anthropic/claude-opus-4-8 to xai/grok-4.5 (vision-capable,
so @lead keeps image reading). All other agents unchanged;
ANTHROPIC_API_KEY is retained for @ops and not removed.
2026-07-17 09:03:03 +00:00
ffaerber eeae1fdaaa fix: deterministic dedup of agent double-posts (self-post + framework reply) (#109)
ci / lint (push) Successful in 10s
2026-07-08 09:35:19 +02:00
ffaerber 7abbbb1b8d feat(ci): re-enable dependency caching (runner cache server fixed) (#108)
ci / lint (push) Skipped
2026-07-07 17:57:33 +02:00
ffaerber 95bc254640 revert(ci): remove cache steps until runner cache server works (#107)
ci / lint (push) Skipped
2026-07-07 17:52:25 +02:00
ffaerber 75a2493dee feat(ci): dependency caching for agent runs (opencode + Playwright) (#106)
ci / lint (push) Skipped
2026-07-07 17:16:24 +02:00
ffaerber 53ac2b7ba8 fix(agent): reactions show the agent, not "Ghost" (#105)
ci / lint (push) Skipped
2026-07-07 15:20:11 +02:00
ffaerber 4b73d0b8e9 fix(run-agent): stop agents self-posting their reply as a duplicate comment (#104)
ci / lint (push) Skipped
2026-07-07 15:09:45 +02:00
ffaerber b587a7d9a7 Merge pull request 'agents: discussion mode — devs talk in threads, build only on explicit signal' (#102) from feat/discussion-mode into main
ci / lint (push) Skipped
2026-07-07 13:34:55 +02:00
Felix FaerberandClaude Opus 4.8 ef43d69309 agents: discussion mode — devs talk in threads; building starts only on explicit signal
ci / lint (push) Skipped
ci / lint (pull_request) Successful in 11s
Mentioning a dev agent no longer spawns a branch + PR machinery. It is a
CONVERSATION by default: the dev reads what it needs and replies in-thread.

- route.sh: workmode=build|discuss for mode=pr agents. Build ONLY on the
  explicit signals: PR-thread comments (resume), the pm delegation template
  ("please proceed with issue …" — a human can write it too), or the qa bounce
  ("please address my review …"). Everything else = discuss: no branch prep,
  no "🔨 Building" notice.
- run-agent.sh: discussion ACTION for devs (read-only consult, no edits/
  commits/markers). @pm gains 'ASK: @<dev> <question>' to consult devs before
  planning — gather feasibility/effort input, then DELEGATE when enough is
  known. One ASK per reply; never ASK+DELEGATE together.
- publish.sh: pm ASK handler posts the question as a discussion trigger; dev
  discuss path posts the reply (+run report) and skips ALL git/PR machinery
  (stray edits discarded). ASK marker stripped from visible replies.
- agent.yml: WORKMODE threaded to Run-agent + Publish. README documents it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 14:34:12 +03:00
8 changed files with 171 additions and 30 deletions
+49 -18
View File
@@ -5,6 +5,10 @@ name: agent
on: on:
workflow_call: workflow_call:
# Pinned opencode version — used to install it and to key the CI cache below.
env:
OPENCODE_VERSION: "1.17.13"
jobs: jobs:
agent: agent:
@@ -44,17 +48,6 @@ jobs:
# job must never hold the single runner slot for hours. # job must never hold the single runner slot for hours.
timeout-minutes: 45 timeout-minutes: 45
steps: steps:
- name: Acknowledge with 👀
env:
GT: ${{ secrets.GITEA_TOKEN }}
CID: ${{ github.event.comment.id }}
NUM: ${{ github.event.issue.number }}
run: |
B="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/issues"
if [ -n "$CID" ]; then R="$B/comments/$CID/reactions"; else R="$B/$NUM/reactions"; fi
curl -sS -X POST -H "Authorization: token $GT" -H "Content-Type: application/json" \
"$R" -d '{"content":"eyes"}' -w '\nreact -> HTTP %{http_code}\n' || true
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -119,12 +112,44 @@ jobs:
TOKEN_INTERN: ${{ secrets.TOKEN_INTERN }} TOKEN_INTERN: ${{ secrets.TOKEN_INTERN }}
run: bash "$SCRIPTS/route.sh" run: bash "$SCRIPTS/route.sh"
- name: Acknowledge with 👀 (as the routed agent)
if: steps.prep.outputs.mode != 'skip'
env:
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 || '' }}
CID: ${{ github.event.comment.id }}
NUM: ${{ github.event.issue.number }}
run: |
[ -n "$SELF_TOKEN" ] || { echo "no agent token — skipping 👀"; exit 0; }
B="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/issues"
if [ -n "$CID" ]; then R="$B/comments/$CID/reactions"; else R="$B/$NUM/reactions"; fi
curl -sS -X POST -H "Authorization: token $SELF_TOKEN" -H "Content-Type: application/json" \
"$R" -d '{"content":"eyes"}' -w '\nreact -> HTTP %{http_code}\n' || true
- name: Cache opencode CLI
if: steps.prep.outputs.mode != 'skip'
continue-on-error: true # a cache backend hiccup must never fail an agent run
uses: actions/cache@v4
with:
path: ~/.opencode
key: opencode-${{ runner.os }}-${{ env.OPENCODE_VERSION }}
- name: Cache Playwright browsers + npm (browser agents only)
if: steps.prep.outputs.mode != 'skip' && (steps.prep.outputs.name == 'senior' || steps.prep.outputs.name == 'lead' || steps.prep.outputs.name == 'qa')
continue-on-error: true
uses: actions/cache@v4
with:
path: |
~/.cache/ms-playwright
~/.npm
key: playwright-npm-${{ runner.os }}-v1
- name: Install opencode + provider config (+ Playwright MCP for browser agents) - name: Install opencode + provider config (+ Playwright MCP for browser agents)
if: steps.prep.outputs.mode != 'skip' if: steps.prep.outputs.mode != 'skip'
env: env:
SCRIPTS: ${{ runner.temp }}/agents-scripts SCRIPTS: ${{ runner.temp }}/agents-scripts
OLLAMA_URL: ${{ secrets.OLLAMA_URL }} OLLAMA_URL: ${{ secrets.OLLAMA_URL }}
OLLAMA_CLOUD_API_KEY: ${{ secrets.OLLAMA_CLOUD_API_KEY }} OLLAMA_CLOUD_API_KEY: ${{ secrets.OLLAMA_CLOUD_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
NAME: ${{ steps.prep.outputs.name }} NAME: ${{ steps.prep.outputs.name }}
SKILLS: ${{ steps.prep.outputs.skills }} # JSON array of skills this agent may load SKILLS: ${{ steps.prep.outputs.skills }} # JSON array of skills this agent may load
run: bash "$SCRIPTS/install-opencode.sh" run: bash "$SCRIPTS/install-opencode.sh"
@@ -188,6 +213,7 @@ 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 }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
# SELF_TOKEN = the RUNNING agent's OWN token (TOKEN_PM for @pm, TOKEN_OPS for @ops, …). # SELF_TOKEN = the RUNNING agent's OWN token (TOKEN_PM for @pm, TOKEN_OPS for @ops, …).
# Only this agent's token is placed in its process env, so no agent can act as another. # Only this agent's token is placed in its process env, so no agent can act as another.
# 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
@@ -198,6 +224,7 @@ jobs:
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 }}
WORKMODE: ${{ steps.prep.outputs.workmode }} # build | discuss (devs consulted in-thread)
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 AUTOPILOT: ${{ steps.prep.outputs.autopilot }} # 'true' when the issue carries the `autopilot` label
@@ -231,6 +258,7 @@ jobs:
TOKEN_INTERN: ${{ secrets.TOKEN_INTERN }} 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 }}
WORKMODE: ${{ steps.prep.outputs.workmode }}
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 }}
@@ -264,24 +292,27 @@ jobs:
run: bash "$SCRIPTS/rescue-pr.sh" || true run: bash "$SCRIPTS/rescue-pr.sh" || true
- name: Mark done with 🚀 (remove 👀) - name: Mark done with 🚀 (remove 👀)
if: steps.prep.outputs.mode != 'skip'
env: env:
GT: ${{ secrets.GITEA_TOKEN }} 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 || '' }}
CID: ${{ github.event.comment.id }} CID: ${{ github.event.comment.id }}
NUM: ${{ github.event.issue.number }} NUM: ${{ github.event.issue.number }}
run: | run: |
[ -n "$SELF_TOKEN" ] || exit 0
B="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/issues" B="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/issues"
if [ -n "$CID" ]; then R="$B/comments/$CID/reactions"; else R="$B/$NUM/reactions"; fi if [ -n "$CID" ]; then R="$B/comments/$CID/reactions"; else R="$B/$NUM/reactions"; fi
curl -sS -X DELETE -H "Authorization: token $GT" -H "Content-Type: application/json" "$R" -d '{"content":"eyes"}' || true curl -sS -X DELETE -H "Authorization: token $SELF_TOKEN" -H "Content-Type: application/json" "$R" -d '{"content":"eyes"}' || true
curl -sS -X POST -H "Authorization: token $GT" -H "Content-Type: application/json" "$R" -d '{"content":"rocket"}' -w '\nreact -> HTTP %{http_code}\n' || true curl -sS -X POST -H "Authorization: token $SELF_TOKEN" -H "Content-Type: application/json" "$R" -d '{"content":"rocket"}' -w '\nreact -> HTTP %{http_code}\n' || true
- name: Mark failed with 😕 (remove 👀) - name: Mark failed with 😕 (remove 👀)
if: failure() if: failure() && steps.prep.outputs.mode != 'skip'
env: env:
GT: ${{ secrets.GITEA_TOKEN }} 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 || '' }}
CID: ${{ github.event.comment.id }} CID: ${{ github.event.comment.id }}
NUM: ${{ github.event.issue.number }} NUM: ${{ github.event.issue.number }}
run: | run: |
[ -n "$SELF_TOKEN" ] || exit 0
B="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/issues" B="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/issues"
if [ -n "$CID" ]; then R="$B/comments/$CID/reactions"; else R="$B/$NUM/reactions"; fi if [ -n "$CID" ]; then R="$B/comments/$CID/reactions"; else R="$B/$NUM/reactions"; fi
curl -sS -X DELETE -H "Authorization: token $GT" -H "Content-Type: application/json" "$R" -d '{"content":"eyes"}' || true curl -sS -X DELETE -H "Authorization: token $SELF_TOKEN" -H "Content-Type: application/json" "$R" -d '{"content":"eyes"}' || true
curl -sS -X POST -H "Authorization: token $GT" -H "Content-Type: application/json" "$R" -d '{"content":"confused"}' -w '\nreact -> HTTP %{http_code}\n' || true curl -sS -X POST -H "Authorization: token $SELF_TOKEN" -H "Content-Type: application/json" "$R" -d '{"content":"confused"}' -w '\nreact -> HTTP %{http_code}\n' || true
+1 -1
View File
@@ -25,7 +25,7 @@
"desc": "Senior dev — complex, multi-file implementation (GLM-5.2 via Ollama Cloud, text-only)." "desc": "Senior dev — complex, multi-file implementation (GLM-5.2 via Ollama Cloud, text-only)."
}, },
"lead": { "lead": {
"model": "anthropic/claude-opus-4-8", "model": "xai/grok-4.5",
"vision": true, "vision": true,
"mode": "pr", "mode": "pr",
"skills": [ "skills": [
+6
View File
@@ -26,3 +26,9 @@ jq -rs '
"### comment by \($who):\n\(.body | gsub("\\s*<!-- 🤖 agent reply — do not trigger -->"; ""))\n"' \ "### comment by \($who):\n\(.body | gsub("\\s*<!-- 🤖 agent reply — do not trigger -->"; ""))\n"' \
/tmp/thread_pages.json > /tmp/thread.md 2>/dev/null || : > /tmp/thread.md /tmp/thread_pages.json > /tmp/thread.md 2>/dev/null || : > /tmp/thread.md
echo "thread comments fetched: $(grep -c '^### comment by ' /tmp/thread.md 2>/dev/null || echo 0) (newest 100 kept)" echo "thread comments fetched: $(grep -c '^### comment by ' /tmp/thread.md 2>/dev/null || echo 0) (newest 100 kept)"
# Record the newest comment id on the thread BEFORE the agent runs. publish.sh compares against
# it to detect an agent that self-posted its reply mid-run (via the gitea-api skill, despite the
# prompt telling it not to) and skips the duplicate framework reply. Ids are monotonic — no dates.
jq -rs '[ (add // [])[].id ] | max // 0' /tmp/thread_pages.json > /tmp/thread_max_cid 2>/dev/null || echo 0 > /tmp/thread_max_cid
echo "pre-run newest comment id: $(cat /tmp/thread_max_cid)"
+15 -5
View File
@@ -1,15 +1,22 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Install opencode + provider config (+ Playwright MCP for browser agents). # Install opencode + provider config (+ Playwright MCP for browser agents).
# #
# Required env (provided by the workflow step): OLLAMA_URL OLLAMA_CLOUD_API_KEY NAME SKILLS # Required env (provided by the workflow step): OLLAMA_URL OLLAMA_CLOUD_API_KEY XAI_API_KEY
# GITHUB_PATH HOME # NAME SKILLS GITHUB_PATH HOME
set -eu set -eu
# PIN the opencode version: an unpinned `latest` means a breaking release (CLI flags, or the # PIN the opencode version: an unpinned `latest` means a breaking release (CLI flags, or the
# --format json event schema that build-activity-log.sh parses) breaks every agent in every repo # --format json event schema that build-activity-log.sh parses) breaks every agent in every repo
# at once. Bump deliberately by changing this default (or set OPENCODE_VERSION in the step env). # at once. Bump deliberately by changing this default (or set OPENCODE_VERSION in the step env).
OPENCODE_VERSION="${OPENCODE_VERSION:-1.17.13}" OPENCODE_VERSION="${OPENCODE_VERSION:-1.17.13}"
# Skip the download when a cache hit already restored the pinned binary (see the Cache
# opencode CLI step in agent.yml). The installer always re-fetches otherwise.
OC_BIN="$HOME/.opencode/bin/opencode"
if [ -x "$OC_BIN" ] && "$OC_BIN" --version 2>/dev/null | grep -qF "$OPENCODE_VERSION"; then
echo "opencode $OPENCODE_VERSION already present (cache hit) — skipping install"
else
curl -fsSL https://opencode.ai/install | bash -s -- --version "$OPENCODE_VERSION" curl -fsSL https://opencode.ai/install | bash -s -- --version "$OPENCODE_VERSION"
fi
echo "$HOME/.opencode/bin" >> "$GITHUB_PATH" echo "$HOME/.opencode/bin" >> "$GITHUB_PATH"
mkdir -p ~/.config/opencode mkdir -p ~/.config/opencode
# Playwright browser MCP only for agents that need to drive a web app # Playwright browser MCP only for agents that need to drive a web app
@@ -35,15 +42,18 @@ PERM=$(jq -nc --argjson s "$SKILLS" '
# The provider `models:` maps are 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. # 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 # `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. # provider. `xai/` prefix models go to the xAI provider (OpenAI-compatible, https://api.x.ai/v1).
# Built-in providers (e.g. `anthropic/claude-opus-4-8` for @ops) are not derived here.
# See issue #31. # 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")
LOCAL_MODELS=$(jq -r '[.[] | .model | select(startswith("ollama/")) | sub("^ollama/";"")] | map({(.):{}}) | add // {"ornith:35b":{}}' "$AGENTS_JSON") 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" '{ XAI_MODELS=$(jq -r '[.[] | .model | select(startswith("xai/")) | sub("^xai/";"")] | map({(.):{}}) | add // {}' "$AGENTS_JSON")
jq -n --argjson mcp "$MCP" --argjson perm "$PERM" --argjson cloud "$CLOUD_MODELS" --argjson local "$LOCAL_MODELS" --argjson xai "$XAI_MODELS" --arg url "$OLLAMA_URL" --arg ckey "$OLLAMA_CLOUD_API_KEY" --arg xkey "$XAI_API_KEY" '{
provider: { provider: {
ollama: {npm:"@ai-sdk/openai-compatible", options:{baseURL:($url+"/v1")}, models:$local}, 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},
xai: {npm:"@ai-sdk/openai-compatible", options:{baseURL:"https://api.x.ai/v1", apiKey:$xkey}, models:$xai}
}, },
permission: $perm, permission: $perm,
mcp: $mcp mcp: $mcp
+46
View File
@@ -73,6 +73,7 @@ reply=$(awk '
/^[[:space:]]*BEGIN_SUBTASKS/{s=1} /^[[:space:]]*BEGIN_SUBTASKS/{s=1}
/^[[:space:]]*BEGIN_PR_DESCRIPTION/{p=1} /^[[:space:]]*BEGIN_PR_DESCRIPTION/{p=1}
/^[[:space:]]*DELEGATE:[[:space:]]*@/{next} /^[[:space:]]*DELEGATE:[[:space:]]*@/{next}
/^[[:space:]]*ASK:[[:space:]]*@/{next}
/^[[:space:]]*CLOSE_ISSUE[[:space:]]*$/{next} /^[[:space:]]*CLOSE_ISSUE[[:space:]]*$/{next}
/^[[:space:]]*MERGE_PR[[:space:]]*$/{next} /^[[:space:]]*MERGE_PR[[:space:]]*$/{next}
/^[[:space:]]*RETRO[[:space:]]*$/{next} /^[[:space:]]*RETRO[[:space:]]*$/{next}
@@ -194,7 +195,31 @@ if [ "$MODE" != "pr" ]; then
done < /tmp/subtasks.txt done < /tmp/subtasks.txt
subtext=$(printf '\n\n---\nCreated sub-issues%s (mention an agent on each when ready):%b' "${ms:+ under milestone **$ms**}" "$links") subtext=$(printf '\n\n---\nCreated sub-issues%s (mention an agent on each when ready):%b' "${ms:+ under milestone **$ms**}" "$links")
fi fi
# DEDUP GUARD (issue: @pm double-posts its report). Prompt-level "do not self-post" is ignored
# by some models, so enforce it here: if the agent ALREADY posted a comment on this thread
# during the run (any comment by $NAME newer than the pre-run newest id from fetch-thread.sh),
# its self-post IS the reply — skip the duplicate framework comment. Markers (CLOSE_ISSUE,
# DELEGATE, MERGE_PR, subtasks) were already processed above and are unaffected.
# FAIL OPEN: if the pre-run marker is missing (fetch-thread hiccup), pre_cid=0 would make the
# agent's comments from PREVIOUS runs count as self-posts and wrongly suppress the reply.
# Without the marker, skip the guard and post normally.
pre_cid=$(cat /tmp/thread_max_cid 2>/dev/null || echo "")
selfposts=0
if [ -n "$pre_cid" ]; then
: > /tmp/all_comments.json
for pg in $(seq 1 10); do
cpg=$(curl -sS "${hdr[@]}" "$API/issues/$NUM/comments?limit=50&page=$pg" 2>/dev/null) || cpg='[]'
cn=$(printf '%s' "$cpg" | jq 'if type=="array" then length else 0 end' 2>/dev/null || echo 0)
[ "${cn:-0}" -gt 0 ] && printf '%s\n' "$cpg" >> /tmp/all_comments.json
[ "${cn:-0}" -lt 50 ] && break
done
selfposts=$(jq -rs --arg n "$NAME" --argjson c "${pre_cid:-0}" '[ (add // [])[] | select(.user.login==$n) | select(.id > $c) ] | length' /tmp/all_comments.json 2>/dev/null || echo 0)
fi
if [ "${selfposts:-0}" -gt 0 ]; then
echo "agent @$NAME already posted ${selfposts} comment(s) on #$NUM during this run — skipping duplicate framework reply"
else
post "$(printf '%s%s%s' "$msg" "$subtext" "$activity")" post "$(printf '%s%s%s' "$msg" "$subtext" "$activity")"
fi
# --- @pm autopilot merge: @pm is the ONLY agent that merges, and ONLY under the autopilot label --- # --- @pm autopilot merge: @pm is the ONLY agent that merges, and ONLY under the autopilot label ---
# (@qa never merges — it approves and hands back here.) Merge with the PAT (TTOK), not the built-in # (@qa never merges — it approves and hands back here.) Merge with the PAT (TTOK), not the built-in
@@ -246,6 +271,18 @@ if [ "$MODE" != "pr" ]; then
exit 0 exit 0
fi fi
# --- @pm ASK: consult a dev in the thread WITHOUT starting a build ---
# 'ASK: @<dev> <question>' fires the dev in DISCUSSION mode (route.sh: the trigger below does not
# match any build phrase, so the dev replies in-thread — no branch, no PR). @pm gathers input this
# way, then DELEGATEs when enough is known.
ask_line=$(grep -oiE '^[[:space:]]*ASK:[[:space:]]*@(junior|senior|lead|intern)[[:space:]]+.*$' /tmp/agent_out.md 2>/dev/null | head -1)
if [ "$NAME" = "pm" ] && [ -n "$ask_line" ]; then
ask_dev=$(printf '%s' "$ask_line" | grep -oiE '@(junior|senior|lead|intern)' | head -1 | tr -d '@' | tr '[:upper:]' '[:lower:]')
ask_q=$(printf '%s' "$ask_line" | sed -E 's/^[[:space:]]*ASK:[[:space:]]*@[A-Za-z]+[[:space:]]+//')
trig "$ISSN" "@$ask_dev $ask_q — this is a discussion: reply in this thread with your assessment; do not start any work."
exit 0
fi
# --- @pm delegation: hand the build to a dev, or hand the finished PR to @qa for review --- # --- @pm delegation: hand the build to a dev, or hand the finished PR to @qa for review ---
# Only an explicit 'DELEGATE: @<agent>' line acts (never a prose mention). Fires via the PAT (TTOK) # Only an explicit 'DELEGATE: @<agent>' line acts (never a prose mention). Fires via the PAT (TTOK)
# so a new run starts; the built-in token cannot. Everything posts on the ISSUE — @pm never touches # so a new run starts; the built-in token cannot. Everything posts on the ISSUE — @pm never touches
@@ -269,6 +306,15 @@ if [ "$MODE" != "pr" ]; then
exit 0 exit 0
fi fi
# --- dev DISCUSSION mode: consulted for expertise, no build (route.sh workmode=discuss) ---
# The reply is a comment on the thread — discard any stray file edits, skip ALL git/PR machinery.
if [ "${WORKMODE:-build}" = "discuss" ]; then
git checkout -- . 2>/dev/null || true
git clean -fd 2>/dev/null || true
post "$(printf '%s%s' "$reply" "$activity")"
exit 0
fi
# Scrub the runtime scripts checkout (.agents-workflow) from the tree so it never lands in a # Scrub the runtime scripts checkout (.agents-workflow) from the tree so it never lands in a
# commit/PR and never confuses the git ops below (issue #33). The scripts we run live outside the # commit/PR and never confuses the git ops below (issue #33). The scripts we run live outside the
# workspace ($SCRIPTS -> runner.temp), so removing this in-tree copy is always safe. Handle every # workspace ($SCRIPTS -> runner.temp), so removing this in-tree copy is always safe. Handle every
+23 -3
View File
@@ -53,8 +53,25 @@ vision=$(jq -r --arg a "$name" '.[$a].vision' /tmp/agents.json)
mode=$(jq -r --arg a "$name" '.[$a].mode' /tmp/agents.json) mode=$(jq -r --arg a "$name" '.[$a].mode' /tmp/agents.json)
# Compact JSON array of the skills this agent may load (scopes permission.skill in install-opencode.sh). # Compact JSON array of the skills this agent may load (scopes permission.skill in install-opencode.sh).
skills=$(jq -c --arg a "$name" '.[$a].skills // []' /tmp/agents.json) skills=$(jq -c --arg a "$name" '.[$a].skills // []' /tmp/agents.json)
echo "Routing to @$name (model=$model vision=$vision mode=$mode skills=$skills)"
{ echo "name=$name"; echo "model=$model"; echo "vision=$vision"; echo "mode=$mode"; echo "skills=$skills"; } >> "$GITHUB_OUTPUT" # --- WORKMODE for dev (mode=pr) agents: build vs DISCUSS. ---
# Mentioning a dev is a CONVERSATION by default — it replies in the thread without creating a
# branch or PR. Actual building starts ONLY on the explicit signals:
# - a comment on a PR thread (resuming existing work), or
# - the pm delegation template ".. please proceed with issue .." (also usable by a human), or
# - the qa bounce template ".. please address my review ..".
# This lets @pm (via its ASK marker) and the maintainer consult devs to gather information first,
# and explicitly start the build later — see publish.sh / run-agent.sh.
workmode=build
if [ "$mode" = "pr" ] && [ -z "$IS_PR" ]; then
if printf '%s' "$scan" | grep -qiE 'please (proceed with issue|address my review)'; then
workmode=build
else
workmode=discuss
fi
fi
echo "Routing to @$name (model=$model vision=$vision mode=$mode workmode=$workmode skills=$skills)"
{ echo "name=$name"; echo "model=$model"; echo "vision=$vision"; echo "mode=$mode"; echo "workmode=$workmode"; echo "skills=$skills"; } >> "$GITHUB_OUTPUT"
# 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
@@ -67,7 +84,10 @@ 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="" branch_ref=""
if [ -n "$IS_PR" ]; then # comment on a PR -> resume its branch if [ "$workmode" = "discuss" ]; then # conversation only — no branch, no PR machinery
echo "discussion mode — staying on main, no branch prep"
{ echo "branch=main"; echo "new=false"; } >> "$GITHUB_OUTPUT"
elif [ -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" branch_ref="$ref"
git fetch origin "$ref" && git checkout "$ref" git fetch origin "$ref" && git checkout "$ref"
+20 -1
View File
@@ -54,7 +54,11 @@ if [ "$MODE" = "comment" ]; then
If anything is unclear or needs a decision at any phase, START your reply with '@ffaerber', ask If anything is unclear or needs a decision at any phase, START your reply with '@ffaerber', ask
specific questions, and do NOT emit a marker. Mentioning a teammate in prose does NOT act — only a specific questions, and do NOT emit a marker. Mentioning a teammate in prose does NOT act — only a
DELEGATE line does. marker line does.
ASK — to CONSULT a dev before (or instead of) planning, end your reply with EXACTLY one line:
'ASK: @<dev> <one concrete question>'. The dev replies in this thread WITHOUT starting any work —
use it to gather feasibility/effort/approach input, then present your plan (and later DELEGATE)
once you know enough. One ASK per reply; never ASK and DELEGATE in the same reply.
RETRO — when the maintainer asks for a retrospective on this issue (e.g. 'run a retro', RETRO — when the maintainer asks for a retrospective on this issue (e.g. 'run a retro',
'@pm retro'), briefly acknowledge and end your reply with EXACTLY one line: 'RETRO'. The '@pm retro'), briefly acknowledge and end your reply with EXACTLY one line: 'RETRO'. The
automation opens a retro issue (read this issue + its PR, distill learnings into LEARNINGS.md) automation opens a retro issue (read this issue + its PR, distill learnings into LEARNINGS.md)
@@ -97,6 +101,15 @@ if [ "$MODE" = "comment" ]; then
decision). Hands back to @ffaerber. decision). Hands back to @ffaerber.
Emit AT MOST one marker, and only after you have actually verified." Emit AT MOST one marker, and only after you have actually verified."
fi fi
elif [ "${WORKMODE:-build}" = "discuss" ]; then
# A dev agent consulted for its EXPERTISE — conversation only, no build. Building starts later,
# explicitly ('please proceed with issue …'). See route.sh workmode.
ACTION="You are being CONSULTED in this thread — this is a DISCUSSION, not a build task. Answer the
question you were asked: read whatever files/logs you need (read-only), give your assessment,
approach, effort estimate, risks, or answer — concise and concrete. Your reply becomes a comment.
Do NOT modify files, do NOT commit or push, do NOT create branches, do NOT open PRs. Do not
emit any marker. When the team has enough information, @pm (or the maintainer) will explicitly
tell a dev to start building."
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.
FIRST read AGENTS.md at the repo root and FOLLOW IT EXACTLY — it defines the golden rules, FIRST read AGENTS.md at the repo root and FOLLOW IT EXACTLY — it defines the golden rules,
@@ -136,6 +149,12 @@ ${LEARN}
shown by Gitea. Do NOT begin your reply with your own name, an '@${NAME}' header, or a '🤖/🔨 @you' shown by Gitea. Do NOT begin your reply with your own name, an '@${NAME}' header, or a '🤖/🔨 @you'
line; just write the content directly. line; just write the content directly.
Do NOT use the gitea-api skill to post your reply, report, or any comment on THIS thread
yourself. The automation already posts your reply exactly once — self-posting it too is what
creates the duplicate comments you must avoid. On this thread, use gitea-api only to READ, or to
take an explicit action you were asked for (add/remove a label, close the issue, merge the PR).
Your report or answer IS your reply text — write it as your reply; do not post it via the API.
TEAM ROSTER (who does what — hand off if a task isn't yours): TEAM ROSTER (who does what — hand off if a task isn't yours):
${ROSTER} ${ROSTER}
+9
View File
@@ -36,6 +36,15 @@ deploy-host SSH skill, live in the consuming repo under `.gitea/agent-skills/`
`@pm` is the only agent that ever merges, and only under the `autopilot` label (its kill switch: `@pm` is the only agent that ever merges, and only under the `autopilot` label (its kill switch:
remove the label mid-flight and the next step reverts to human control). remove the label mid-flight and the next step reverts to human control).
### Discussion vs building
Mentioning a dev agent is a **conversation by default**: it reads what it needs and replies in the
thread — no branch, no PR. `@pm` can consult devs the same way with an `ASK: @<dev> <question>`
marker (gather feasibility/effort input before planning). **Building starts only on the explicit
signals**: `@pm`'s delegation (*"please proceed with issue …"* — a human can write the same phrase
to start a build directly), a `@qa` bounce (*"please address my review …"*), or any comment on the
PR thread itself (resuming existing work).
### Retros — the learning loop ### Retros — the learning loop
Ask `@pm` for a retrospective on any issue (e.g. **"@pm run a retro"**, typically when merging). The Ask `@pm` for a retrospective on any issue (e.g. **"@pm run a retro"**, typically when merging). The