name: agent # 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). # The gate + steps run in the caller's event context (github.event.* / github.repository are the caller's). on: workflow_call: jobs: agent: # Trusted author only, and only when a known agent is mentioned. This gate is the main # defense against malicious-issue prompt injection — do not loosen it. if: > (github.event.comment == null && github.event.issue.user.login == 'ffaerber') || (github.event.comment != null && github.event.comment.user.login == 'ffaerber' && !contains(github.event.comment.body, '🤖') && (contains(github.event.comment.body, '@pm') || contains(github.event.comment.body, '@junior') || contains(github.event.comment.body, '@senior') || contains(github.event.comment.body, '@lead') || contains(github.event.comment.body, '@qa'))) runs-on: ci-runner 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 with: fetch-depth: 0 token: ${{ secrets.GITEA_TOKEN }} # This is a REUSABLE workflow (workflow_call): the checkout above clones the CALLER's repo, # not this `agents` repo — so the externalized step scripts (in THIS repo under # .gitea/workflows/scripts/) are NOT on disk yet. Check this repo out into a separate subdir # and run the scripts from $SCRIPTS. Pinned to @main to match the caller's # `uses: …/agent.yml@main`, so the scripts and the workflow always move together. - name: Fetch shared agent scripts (this repo) uses: actions/checkout@v4 with: repository: ffaerber/agents ref: main path: .agents-workflow token: ${{ secrets.GITEA_TOKEN }} - name: Route agent + prepare branch id: prep env: SCRIPTS: ${{ github.workspace }}/.agents-workflow/.gitea/workflows/scripts BODY: ${{ github.event.comment.body }} # event text via env, never inline in shell IBODY: ${{ github.event.issue.body }} EVENT: ${{ github.event_name }} IS_PR: ${{ github.event.issue.pull_request }} NUM: ${{ github.event.issue.number }} GT: ${{ secrets.GITEA_TOKEN }} TOKEN_PM: ${{ secrets.TOKEN_PM }} TOKEN_SENIOR: ${{ secrets.TOKEN_SENIOR }} TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }} TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }} TOKEN_QA: ${{ secrets.TOKEN_QA }} run: bash "$SCRIPTS/route.sh" - name: Install opencode + provider config (+ Playwright MCP for browser agents) env: SCRIPTS: ${{ github.workspace }}/.agents-workflow/.gitea/workflows/scripts OLLAMA_URL: ${{ secrets.OLLAMA_URL }} OLLAMA_CLOUD_API_KEY: ${{ secrets.OLLAMA_CLOUD_API_KEY }} NAME: ${{ steps.prep.outputs.name }} SKILLS: ${{ steps.prep.outputs.skills }} # JSON array of skills this agent may load run: bash "$SCRIPTS/install-opencode.sh" - name: Set up read-only SSH alias `node1` (+ opencode skill so the agent actually knows about it) # 1) Writes the deploy key + an SSH config alias so the agent can run # `ssh node1 ` (matches the homelab opencode.json allowlist). # 2) Emits a `node1-ssh` opencode Skill file under ~/.config/opencode/skills/ so any # downstream repo's dev agent discovers this capability via OpenCode's skill registry # rather than having to trial against the permission allowlist. Only emitted when the # swarm plumbing is actually wired for that caller (SWARM_HOST/SWARM_USER/SSH_PRIV_KEY). # All three secrets are passed via env and never inlined into shell — this shared workflow # runs in repos that don't have them and must not fail there. env: SCRIPTS: ${{ github.workspace }}/.agents-workflow/.gitea/workflows/scripts SWARM_HOST: ${{ secrets.SWARM_HOST }} SWARM_USER: ${{ secrets.SWARM_USER }} SSH_PRIV_KEY: ${{ secrets.SSH_PRIV_KEY }} run: bash "$SCRIPTS/skill-node1-ssh.sh" - 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 # ~/.config/opencode/skills/ so any dev agent discovers the capability via OpenCode's # skill registry. The credential is the shared AGENT_TOKEN (a PAT whose scopes the # 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: SCRIPTS: ${{ github.workspace }}/.agents-workflow/.gitea/workflows/scripts AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }} run: bash "$SCRIPTS/skill-gitea-api.sh" - name: Inspect / fetch image attachments (download only for vision agents) id: imgs env: SCRIPTS: ${{ github.workspace }}/.agents-workflow/.gitea/workflows/scripts GT: ${{ secrets.GITEA_TOKEN }} NUM: ${{ github.event.issue.number }} VISION: ${{ steps.prep.outputs.vision }} run: bash "$SCRIPTS/fetch-images.sh" - name: Fetch the full issue thread (shared memory) env: SCRIPTS: ${{ github.workspace }}/.agents-workflow/.gitea/workflows/scripts GT: ${{ secrets.GITEA_TOKEN }} NUM: ${{ github.event.issue.number }} run: bash "$SCRIPTS/fetch-thread.sh" - name: Run agent id: run env: SCRIPTS: ${{ github.workspace }}/.agents-workflow/.gitea/workflows/scripts ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} # AGENT_TOKEN powers the `gitea-api` skill (cross-repo issue/PR/Actions read+write). # It is already a required secret for the delegation step below; exposing it here too # lets the agent process itself call the Gitea API on demand. AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }} NAME: ${{ steps.prep.outputs.name }} MODEL: ${{ steps.prep.outputs.model }} VISION: ${{ steps.prep.outputs.vision }} MODE: ${{ steps.prep.outputs.mode }} HAS_IMAGES: ${{ steps.imgs.outputs.has_images }} BRANCH: ${{ steps.prep.outputs.branch }} NUM: ${{ github.event.issue.number }} TITLE: ${{ github.event.issue.title }} IBODY: ${{ github.event.issue.body }} CMT: ${{ github.event.comment.body }} FILES: ${{ steps.imgs.outputs.files }} # opencode -f image flags (vision agents only) run: bash "$SCRIPTS/run-agent.sh" - name: Build activity log (tool calls + reasoning) from the event stream id: log env: SCRIPTS: ${{ github.workspace }}/.agents-workflow/.gitea/workflows/scripts MODE: ${{ steps.prep.outputs.mode }} run: bash "$SCRIPTS/build-activity-log.sh" - name: Publish — PR (dev agents) or comment (pm), always reply in the issue env: SCRIPTS: ${{ github.workspace }}/.agents-workflow/.gitea/workflows/scripts GT: ${{ secrets.GITEA_TOKEN }} AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }} TOKEN_PM: ${{ secrets.TOKEN_PM }} TOKEN_SENIOR: ${{ secrets.TOKEN_SENIOR }} TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }} TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }} TOKEN_QA: ${{ secrets.TOKEN_QA }} NAME: ${{ steps.prep.outputs.name }} MODE: ${{ steps.prep.outputs.mode }} NUM: ${{ github.event.issue.number }} TITLE: ${{ github.event.issue.title }} BRANCH: ${{ steps.prep.outputs.branch }} NEW: ${{ steps.prep.outputs.new }} run: bash "$SCRIPTS/publish.sh" - name: Mark done with 🚀 (remove 👀) 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 DELETE -H "Authorization: token $GT" -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 - name: Mark failed with 😕 (remove 👀) if: failure() 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 DELETE -H "Authorization: token $GT" -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