Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .github/workflows/issue-implement.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: issue-implement

# Dev agent **wiwi**. Fires only when label `agent:try` lands on an
# issue — typically added by the triage agent when verdict=do, or by
# a human bypassing triage. wiwi branches off main, implements the
# change, runs the build, and opens a DRAFT PR labelled `auto-agent`.
# Downstream auto-merge gating happens in pr-review.yml.

on:
issues:
types: [labeled]

permissions:
contents: write
issues: write
pull-requests: write

concurrency:
# Re-labelling `agent:try` on the same issue cancels any in-flight
# wiwi run and starts a fresh attempt. Useful when an operator
# spots a stuck run and wants to retry without manual cleanup.
group: implement-${{ github.event.issue.number }}
cancel-in-progress: true

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'

jobs:
implement:
if: ${{ github.event.label.name == 'agent:try' }}
runs-on: [self-hosted, tokenscope]
timeout-minutes: 45
env:
ANTHROPIC_BASE_URL: ${{ secrets.LITELLM_BASE_URL }}
ANTHROPIC_API_KEY: ${{ secrets.LITELLM_API_KEY }}
ANTHROPIC_MODEL: claude-3-5-sonnet-20241022
steps:
- name: Configure no_proxy for LiteLLM
run: |
{
echo "no_proxy=${{ secrets.LITELLM_NO_PROXY }}"
echo "NO_PROXY=${{ secrets.LITELLM_NO_PROXY }}"
} >> "$GITHUB_ENV"

- uses: actions/checkout@v4
with:
fetch-depth: 0
# AGENT_GH_TOKEN is a PAT (not GITHUB_TOKEN) so wiwi's
# `gh pr create` actually triggers the downstream `ci` and
# `pr-review` workflows. With the default GITHUB_TOKEN the
# spawned PR's check runs would be skipped.
token: ${{ secrets.AGENT_GH_TOKEN }}

- name: Implement issue
env:
ISSUE_NUMBER: ${{ github.event.issue.number }}
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
GH_TOKEN: ${{ secrets.AGENT_GH_TOKEN }}
run: bash scripts/agent-bot/run_wiwi.sh

- name: Cleanup transient files
if: always()
run: rm -f /tmp/wiwi-*.md /tmp/wiwi-*.log /tmp/wiwi-*.txt || true
55 changes: 55 additions & 0 deletions .github/workflows/issue-triage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: issue-triage

# Triage agent. Fires only when a human (or repo automation) adds the
# `agent:assess` label to an issue. The triage agent decides whether
# the issue is small/safe enough for the dev agent **wiwi** to attempt
# unattended. Strict 5-gate verdict — see scripts/agent-bot/run_triage.sh.

on:
issues:
types: [labeled]

permissions:
contents: read
issues: write

concurrency:
group: triage-${{ github.event.issue.number }}
cancel-in-progress: true

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'

jobs:
triage:
if: ${{ github.event.label.name == 'agent:assess' }}
runs-on: [self-hosted, tokenscope]
timeout-minutes: 10
env:
ANTHROPIC_BASE_URL: ${{ secrets.LITELLM_BASE_URL }}
ANTHROPIC_API_KEY: ${{ secrets.LITELLM_API_KEY }}
ANTHROPIC_MODEL: claude-3-5-sonnet-20241022
steps:
- name: Configure no_proxy for LiteLLM
run: |
{
echo "no_proxy=${{ secrets.LITELLM_NO_PROXY }}"
echo "NO_PROXY=${{ secrets.LITELLM_NO_PROXY }}"
} >> "$GITHUB_ENV"

- uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Triage issue
env:
ISSUE_NUMBER: ${{ github.event.issue.number }}
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bash scripts/agent-bot/run_triage.sh

- name: Cleanup transient files
if: always()
run: rm -f /tmp/triage-*.md /tmp/triage-*.log /tmp/triage-*.json || true
11 changes: 11 additions & 0 deletions .github/workflows/pr-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,17 @@ jobs:
AGENT_EXIT: ${{ steps.review.outcome }}
run: python3 scripts/pr-review/post_review.py "$PR_NUMBER"

- name: Auto-merge if eligible
# Only effective for wiwi-spawned PRs (label `auto-agent`) whose
# linked issue author is on the team. Idempotent + safe to call
# for every PR — bails out for human PRs. Needs AGENT_GH_TOKEN
# so the admin-merge call has admin bypass on branch protection.
if: ${{ steps.review.outcome == 'success' }}
env:
GH_TOKEN: ${{ secrets.AGENT_GH_TOKEN }}
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
run: bash scripts/agent-bot/auto_merge.sh

- name: Cleanup transient files
if: always()
run: rm -f /tmp/pr-review-*.md /tmp/pr-review-*.log /tmp/pr-review-*.json || true
5 changes: 5 additions & 0 deletions scripts/agent-bot/TEAM
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Team logins eligible for auto-merge on wiwi-spawned PRs.
# One GitHub login per line. Lines starting with `#` are ignored.
vaderyang
william
timmy
43 changes: 43 additions & 0 deletions scripts/agent-bot/auto_merge.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Called from the tail of pr-review.yml AFTER vivi posts her review.
# Auto-merges iff:
# - PR has label `auto-agent`
# - PR is not draft (wiwi may have flipped it; or the linked issue
# author was a team member and we promoted earlier — see below)
# - vivi's latest review state == APPROVED
# - the linked issue's author is in the TEAM file
set -euo pipefail

HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# TEAM file is the single source of truth, shared with run_wiwi.sh.
TEAM=$(grep -vE '^\s*(#|$)' "$HERE/TEAM" | tr '\n' ' ')

PR="${PR_NUMBER:?PR_NUMBER required}"

meta=$(gh pr view "$PR" --json isDraft,labels,body)
labels=$(echo "$meta" | jq -r '.labels[].name')
echo "$labels" | grep -qx auto-agent || { echo "not auto-agent PR; skip"; exit 0; }

# Latest review state. Some reviews have a null .body (e.g. quick
# APPROVE clicks without a comment); coerce to "" before contains()
# or jq aborts the whole pipeline.
state=$(gh pr view "$PR" --json reviews --jq '
[.reviews[] | select(.author.login=="vivi" or ((.body // "") | contains("vivi")))]
| last | .state // empty')
[ "$state" = "APPROVED" ] || { echo "vivi verdict=$state; skip"; exit 0; }

# Extract issue number from PR body `Closes #N`.
issue=$(echo "$meta" | jq -r '.body' | grep -oE 'Closes #[0-9]+' | head -1 | tr -dc 0-9)
[ -n "$issue" ] || { echo "no linked issue; skip"; exit 0; }

author=$(gh issue view "$issue" --json author --jq '.author.login')
for m in $TEAM; do
if [ "$m" = "$author" ]; then
echo "vivi APPROVED + author=$author ∈ TEAM → admin-merge"
# Lift draft (if still draft) and merge.
gh pr ready "$PR" >/dev/null 2>&1 || true
gh pr merge "$PR" --admin --squash --delete-branch
exit 0
fi
done
echo "author=$author not in TEAM; leaving PR for human review"
94 changes: 94 additions & 0 deletions scripts/agent-bot/run_triage.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env bash
# Triage agent: read issue, decide do/skip/needs_info under STRICT gates.
# verdict=do → add `agent:try` label (kicks off wiwi).
# else → post a comment explaining gate failure; human can manually
# add `agent:try` to force, or `agent:skip` to mute.
set -euo pipefail

PROMPT=$(mktemp)
OUT=$(mktemp)

cat > "$PROMPT" <<EOF
You are the **triage agent**. Decide if issue #${ISSUE_NUMBER} should be
auto-implemented by the dev agent **wiwi** running on this repo's
self-hosted runner with no human in the loop until PR review.

Verdict MUST be \`do\` only when ALL gates pass:

1. Issue has a concrete actionable description AND explicit acceptance
criteria (you can list 2+ checkable assertions).
2. Estimated diff < 300 LOC across < 10 files.
3. Change is contained: console/, docs/, one crate, or one workflow —
not cross-cutting architecture work.
4. No new runtime dependency, no new secret, no new external network
call required.
5. The fix has a deterministic test (unit/integration/cargo check) that
can be added in the same PR — not "needs manual QA".

If any gate fails, output verdict \`needs_info\` (gate 1 fails) or
\`skip\` (gates 2–5 fail). Be strict: when in doubt → \`needs_info\`.

Read the issue first (use \`gh issue view ${ISSUE_NUMBER}\`), inspect
referenced files, then emit exactly one JSON object on the last line of
your reply, no markdown fence:

{"verdict":"do|skip|needs_info","scope":"<short>","reason":"<≤200 chars>","files":["..."],"gates":{"1":true,"2":true,"3":true,"4":true,"5":true}}

Issue title: ${ISSUE_TITLE}
Author: ${ISSUE_AUTHOR}
EOF

# Run claude in print mode against our LiteLLM-style endpoint.
claude --print \
--allowed-tools Bash Read Grep Glob WebFetch \
--model "${ANTHROPIC_MODEL:-claude-3-5-sonnet-20241022}" \
< "$PROMPT" > "$OUT" 2> /tmp/triage-stderr.log || {
echo "triage agent failed (see workflow log)" >&2
cat /tmp/triage-stderr.log >&2
exit 1
}

# Strict JSON parse: scan every line, keep ones that are valid JSON AND
# carry a `verdict` field, take the last. This rejects lines inside
# code fences, partial fragments, and lines that merely *mention*
# "verdict" in prose — only a parsable JSON object survives.
LAST=$(jq -Rrc 'fromjson? | select(.verdict)' "$OUT" 2>/dev/null | tail -1)
if [ -z "$LAST" ]; then
echo "triage agent produced no parsable JSON verdict; aborting" >&2
cat "$OUT" >&2
exit 1
fi

VERDICT=$(echo "$LAST" | jq -r '.verdict')
REASON=$(echo "$LAST" | jq -r '.reason')
SCOPE=$(echo "$LAST" | jq -r '.scope')

# Defense-in-depth: require all 5 gates true for verdict=do.
if [ "$VERDICT" = "do" ]; then
ALLPASS=$(echo "$LAST" | jq -r '[.gates."1",.gates."2",.gates."3",.gates."4",.gates."5"] | all')
if [ "$ALLPASS" != "true" ]; then
echo "verdict=do but not all gates true; downgrading to needs_info" >&2
VERDICT=needs_info
REASON="triage gates incomplete: $REASON"
fi
fi

case "$VERDICT" in
do)
gh issue edit "$ISSUE_NUMBER" --add-label "agent:try"
gh issue comment "$ISSUE_NUMBER" --body "🤖 Triage: **${VERDICT}** — scope: ${SCOPE}

${REASON}

Auto-labeled \`agent:try\`. **wiwi** will pick this up shortly."
;;
needs_info|skip)
gh issue comment "$ISSUE_NUMBER" --body "🤖 Triage: **${VERDICT}**

${REASON}

Manually add the \`agent:try\` label to override this verdict and run **wiwi** anyway, or \`agent:skip\` to mute future re-triage."
;;
*)
echo "unknown verdict: $VERDICT" >&2; exit 1 ;;
esac
87 changes: 87 additions & 0 deletions scripts/agent-bot/run_wiwi.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# wiwi: dev agent. Branch off main, implement, ensure cargo build + tests
# green, open a DRAFT PR labelled `auto-agent`. Auto-merge gating happens
# downstream in pr-review.yml.
set -euo pipefail

HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
TEAM=$(grep -vE '^\s*(#|$)' "$HERE/TEAM" | tr '\n' ' ')
is_team_member() {
local who="$1"
for m in $TEAM; do [ "$m" = "$who" ] && return 0; done
return 1
}

# Branch name includes a short UTC timestamp so re-runs against the
# same issue don't collide with leftover branches from prior attempts.
STAMP=$(date -u +%Y%m%d-%H%M%S)
BRANCH="agent/wiwi/issue-${ISSUE_NUMBER}-${STAMP}"
git config user.email "wiwi-agent@noreply.local"
git config user.name "wiwi"
git fetch origin main
git checkout -B "$BRANCH" origin/main

PROMPT=$(mktemp)
cat > "$PROMPT" <<EOF
You are **wiwi**, the dev agent. Implement the change requested by issue
#${ISSUE_NUMBER}. Constraints:

- Stay within the scope the triage agent approved. If you discover the
task is larger than expected (>300 LOC or cross-cutting), STOP, leave
a note in /tmp/wiwi-abort.txt explaining why, and exit non-zero.
- Add a deterministic test for the change (unit / integration / a tiny
fixture). Don't claim done without one.
- After edits, run \`just build\` (or \`cargo check\` + \`bun run build\`
in console/) — must be green before you stop.
- Do NOT add new dependencies, new secrets, new network calls.
- Do NOT modify CI workflows, branch protection, or this script.
- Commit in logical chunks; sign-off line not required.

When done, write a brief summary to /tmp/wiwi-summary.md (Markdown) for
the PR body. End it with the literal line:

Closes #${ISSUE_NUMBER}

Issue title: ${ISSUE_TITLE}
EOF

claude --print \
--allowed-tools Bash Read Write Edit Grep Glob \
--model "${ANTHROPIC_MODEL:-claude-3-5-sonnet-20241022}" \
< "$PROMPT" > /tmp/wiwi-run.log 2>&1 || {
echo "wiwi run failed (see /tmp/wiwi-run.log)" >&2
gh issue comment "$ISSUE_NUMBER" --body "🤖 wiwi could not complete this task. See workflow log."
exit 1
}

if [ -f /tmp/wiwi-abort.txt ]; then
gh issue comment "$ISSUE_NUMBER" --body "🤖 wiwi aborted: $(cat /tmp/wiwi-abort.txt)"
exit 0
fi

# Sanity: must have produced commits.
if [ "$(git rev-list --count origin/main..HEAD)" = "0" ]; then
gh issue comment "$ISSUE_NUMBER" --body "🤖 wiwi finished without any commit; nothing to PR."
exit 0
fi

git push -u origin "$BRANCH"

BODY_FILE=$(mktemp)
{
cat /tmp/wiwi-summary.md 2>/dev/null || echo "(wiwi did not write a summary)"
echo
echo "---"
echo "🤖 Implemented by **wiwi** • issue author: @${ISSUE_AUTHOR}"
if is_team_member "$ISSUE_AUTHOR"; then
echo "Eligible for auto-merge on vivi APPROVE."
fi
} > "$BODY_FILE"

gh pr create \
--draft \
--base main \
--head "$BRANCH" \
--title "${ISSUE_TITLE}" \
--body-file "$BODY_FILE" \
--label auto-agent
Loading
Loading