Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6618de9c9f | ||
|
|
82c4b07fea | ||
|
|
2f1ae61b06 | ||
|
|
03c2bef880 | ||
|
|
6832d7ad6c | ||
|
|
74d3e1d229 | ||
|
|
4cbbc9b2d7 | ||
|
|
cf1e7178b5 | ||
|
|
5a9dba64fb | ||
|
|
9840c2a860 | ||
|
|
bb07558d70 | ||
|
|
ef06da3ffe | ||
|
|
7278e06dff | ||
|
|
df0f6d6543 | ||
|
|
23cc3e192e | ||
|
|
0a89309ff1 | ||
|
|
cb11a6b2d1 |
@@ -92,6 +92,7 @@ jobs:
|
|||||||
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
|
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
|
||||||
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 }}
|
||||||
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)
|
||||||
@@ -131,6 +132,17 @@ jobs:
|
|||||||
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }}
|
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }}
|
||||||
run: bash "$SCRIPTS/skill-gitea-api.sh"
|
run: bash "$SCRIPTS/skill-gitea-api.sh"
|
||||||
|
|
||||||
|
- name: Set up `gitea-admin` skill (@ops only — administer the Gitea instance)
|
||||||
|
# Instance administration (orgs/users/repos/labels/secrets/scoped tokens). The SKILL.md is
|
||||||
|
# written ONLY for @ops (skill-gitea-admin.sh gates on NAME), so the admin how-to never
|
||||||
|
# reaches other agents; permission.skill also denies it to everyone but @ops. Uses
|
||||||
|
# AGENT_TOKEN (an admin PAT during bootstrap) — see the script header for the token plan.
|
||||||
|
env:
|
||||||
|
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||||
|
NAME: ${{ steps.prep.outputs.name }}
|
||||||
|
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }}
|
||||||
|
run: bash "$SCRIPTS/skill-gitea-admin.sh"
|
||||||
|
|
||||||
- name: Inspect / fetch image attachments (download only for vision agents)
|
- name: Inspect / fetch image attachments (download only for vision agents)
|
||||||
id: imgs
|
id: imgs
|
||||||
env:
|
env:
|
||||||
@@ -162,6 +174,7 @@ jobs:
|
|||||||
MODE: ${{ steps.prep.outputs.mode }}
|
MODE: ${{ steps.prep.outputs.mode }}
|
||||||
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
|
||||||
NUM: ${{ github.event.issue.number }}
|
NUM: ${{ github.event.issue.number }}
|
||||||
TITLE: ${{ github.event.issue.title }}
|
TITLE: ${{ github.event.issue.title }}
|
||||||
IBODY: ${{ github.event.issue.body }}
|
IBODY: ${{ github.event.issue.body }}
|
||||||
@@ -186,12 +199,16 @@ jobs:
|
|||||||
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
|
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
|
||||||
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 }}
|
||||||
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 }}
|
||||||
TITLE: ${{ github.event.issue.title }}
|
TITLE: ${{ github.event.issue.title }}
|
||||||
BRANCH: ${{ steps.prep.outputs.branch }}
|
BRANCH: ${{ steps.prep.outputs.branch }}
|
||||||
NEW: ${{ steps.prep.outputs.new }}
|
NEW: ${{ steps.prep.outputs.new }}
|
||||||
|
IS_PR: ${{ github.event.issue.pull_request }} # set when this run is on a PR thread
|
||||||
|
AUTOPILOT: ${{ steps.prep.outputs.autopilot }} # 'true' when the origin issue carries `autopilot`
|
||||||
|
ISSNUM: ${{ steps.prep.outputs.issnum }} # origin issue number (resolved from branch on PR threads)
|
||||||
run: bash "$SCRIPTS/publish.sh"
|
run: bash "$SCRIPTS/publish.sh"
|
||||||
|
|
||||||
# Failure-safe: if any step above failed AFTER a dev agent already pushed commits, the normal
|
# Failure-safe: if any step above failed AFTER a dev agent already pushed commits, the normal
|
||||||
@@ -208,6 +225,7 @@ jobs:
|
|||||||
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
|
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
|
||||||
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 }}
|
||||||
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 }}
|
||||||
|
|||||||
@@ -3,5 +3,6 @@
|
|||||||
"junior": {"model":"ollama-cloud/kimi-k2.7-code:cloud","vision":false,"mode":"pr", "skills":[],"desc":"Junior dev — small, low-risk changes (mostly YAML/compose/config). Text-only, cannot read images. Defers complex or image tasks to @senior or @lead."},
|
"junior": {"model":"ollama-cloud/kimi-k2.7-code:cloud","vision":false,"mode":"pr", "skills":[],"desc":"Junior dev — small, low-risk changes (mostly YAML/compose/config). Text-only, cannot read images. Defers complex or image tasks to @senior or @lead."},
|
||||||
"senior": {"model":"ollama-cloud/glm-5.2:cloud","vision":false,"mode":"pr", "skills":["gitea-api","node1-ssh"],"desc":"Senior dev — complex, multi-file implementation (GLM-5.2 via Ollama Cloud, text-only)."},
|
"senior": {"model":"ollama-cloud/glm-5.2:cloud","vision":false,"mode":"pr", "skills":["gitea-api","node1-ssh"],"desc":"Senior dev — complex, multi-file implementation (GLM-5.2 via Ollama Cloud, text-only)."},
|
||||||
"lead": {"model":"anthropic/claude-opus-4-8","vision":true, "mode":"pr", "skills":["gitea-api","node1-ssh"],"desc":"Tech lead — the hardest problems, architecture, and final calls."},
|
"lead": {"model":"anthropic/claude-opus-4-8","vision":true, "mode":"pr", "skills":["gitea-api","node1-ssh"],"desc":"Tech lead — the hardest problems, architecture, and final calls."},
|
||||||
"qa": {"model":"ollama-cloud/minimax-m3:cloud","vision":true, "mode":"comment","skills":["gitea-api"],"desc":"QA — verifies things work. Drives a headless browser (Playwright) to open a URL/web app, click through it, screenshot, and report bugs or confirm behavior. Comments findings; opens no PRs."}
|
"qa": {"model":"ollama-cloud/minimax-m3:cloud","vision":true, "mode":"comment","skills":["gitea-api"],"desc":"QA — verifies things work. Drives a headless browser (Playwright) to open a URL/web app, click through it, screenshot, and report bugs or confirm behavior. Comments findings; opens no PRs."},
|
||||||
|
"ops": {"model":"anthropic/claude-opus-4-8","vision":false,"mode":"comment","skills":["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)."}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Build activity log (tool calls + reasoning) from the event stream into /tmp/activity_log.md.
|
# Build the activity log — the list of TOOL CALLS the agent made — into /tmp/activity_log.md.
|
||||||
# Only dev agents (mode=pr) get an activity-log comment — comment-only roles (pm/qa)
|
# Only dev agents (mode=pr) get an activity-log comment — comment-only roles (pm/qa) do no tool calls.
|
||||||
# do no tool calls, so a trail would be empty/noise.
|
# NOTE: we deliberately DO NOT include the agent's prose text parts. That final "here's what I did"
|
||||||
|
# text is just a restatement of the PR description (already published as the PR body), not a tool
|
||||||
|
# call — so it was noise in a section titled "tool calls". The log is the record of ACTIONS taken.
|
||||||
#
|
#
|
||||||
# Required env (provided by the workflow step): MODE
|
# Required env (provided by the workflow step): MODE
|
||||||
set -u
|
set -u
|
||||||
@@ -11,22 +13,12 @@ if [ "$MODE" != "pr" ]; then
|
|||||||
fi
|
fi
|
||||||
jq -r '
|
jq -r '
|
||||||
def trunc(n): if length > n then (.[0:n] + "…") else . end;
|
def trunc(n): if length > n then (.[0:n] + "…") else . end;
|
||||||
select(.type=="tool_use" or .type=="text") |
|
select(.type=="tool_use") |
|
||||||
if .type=="text" then
|
|
||||||
# Drop the PR-description block from the reasoning trail — it is already published verbatim as
|
|
||||||
# the PR description, so repeating it here is redundant noise. Skip a text part that is nothing
|
|
||||||
# but that block (would otherwise be an empty "💬 " entry).
|
|
||||||
((.part.text // "")
|
|
||||||
| gsub("BEGIN_PR_DESCRIPTION.*?END_PR_DESCRIPTION"; ""; "m")
|
|
||||||
| gsub("\\A[[:space:]]+|[[:space:]]+\\z"; "")) as $t |
|
|
||||||
if $t == "" then empty else "💬 " + ($t | trunc(4000)) end
|
|
||||||
else
|
|
||||||
(.part.tool // "?") as $t |
|
(.part.tool // "?") as $t |
|
||||||
((.part.state.title // (.part.state.input | tojson | trunc(160)) // "")) as $title |
|
((.part.state.title // (.part.state.input | tojson | trunc(160)) // "")) as $title |
|
||||||
"🔧 **" + $t + "**: `" + ($title | trunc(240)) + "`"
|
"🔧 **" + $t + "**: `" + ($title | trunc(240)) + "`"
|
||||||
end
|
|
||||||
' /tmp/events.jsonl > /tmp/activity_log.md 2>/dev/null || true
|
' /tmp/events.jsonl > /tmp/activity_log.md 2>/dev/null || true
|
||||||
n=$(wc -l < /tmp/activity_log.md 2>/dev/null || echo 0)
|
n=$(wc -l < /tmp/activity_log.md 2>/dev/null || echo 0)
|
||||||
echo "activity log: $n entries"
|
echo "activity log: $n tool calls"
|
||||||
[ "$n" -eq 0 ] && : > /tmp/activity_log.md
|
[ "$n" -eq 0 ] && : > /tmp/activity_log.md
|
||||||
head -3 /tmp/activity_log.md
|
head -3 /tmp/activity_log.md
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
# Required env (provided by the workflow step):
|
# Required env (provided by the workflow step):
|
||||||
# GT AGENT_TOKEN TOKEN_PM TOKEN_SENIOR TOKEN_JUNIOR TOKEN_LEAD TOKEN_QA
|
# GT AGENT_TOKEN TOKEN_PM TOKEN_SENIOR TOKEN_JUNIOR TOKEN_LEAD TOKEN_QA
|
||||||
# NAME MODE NUM TITLE BRANCH NEW GITHUB_SERVER_URL GITHUB_REPOSITORY
|
# NAME MODE NUM TITLE BRANCH NEW GITHUB_SERVER_URL GITHUB_REPOSITORY
|
||||||
|
# IS_PR AUTOPILOT ISSNUM (autopilot: @qa label-gated merge/halt + auto-trigger @qa on a fresh PR)
|
||||||
set +e # publish is best-effort: a grep-no-match / curl non-zero must NOT kill the step
|
set +e # publish is best-effort: a grep-no-match / curl non-zero must NOT kill the step
|
||||||
# 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";; *) TOK="";;
|
lead) TOK="$TOKEN_LEAD";; qa) TOK="$TOKEN_QA";; ops) TOK="$TOKEN_OPS";; *) TOK="";;
|
||||||
esac
|
esac
|
||||||
[ -z "$TOK" ] && TOK="$GT"
|
[ -z "$TOK" ] && TOK="$GT"
|
||||||
git config user.name "$NAME"
|
git config user.name "$NAME"
|
||||||
@@ -17,14 +18,31 @@ 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")
|
||||||
post() { curl -sS -w 'comment -> HTTP %{http_code}\n' -X POST "${hdr[@]}" \
|
post() { curl -sS -w 'comment -> HTTP %{http_code}\n' -X POST "${hdr[@]}" \
|
||||||
"$API/issues/$NUM/comments" -d "$(jq -nc --arg b "$1" '{body:$b}')"; }
|
"$API/issues/$NUM/comments" -d "$(jq -nc --arg b "$1" '{body:$b}')"; }
|
||||||
|
# Remove the 'autopilot' label from an issue by resolving its ID first (Gitea's DELETE label
|
||||||
|
# endpoint is by ID, not name). Arg $1 = issue number. Used as the autopilot kill switch.
|
||||||
|
del_autopilot_label() {
|
||||||
|
local iss="$1" lid
|
||||||
|
lid=$(curl -sS "${hdr[@]}" "$API/issues/$iss/labels" 2>/dev/null \
|
||||||
|
| jq -r 'if type=="array" then ([.[]|select(.name=="autopilot")][0].id // empty) else empty end')
|
||||||
|
if [ -n "$lid" ]; then
|
||||||
|
curl -sS -X DELETE "${hdr[@]}" "$API/issues/$iss/labels/$lid" \
|
||||||
|
-w '\nunlabel -> HTTP %{http_code}\n' || true
|
||||||
|
else
|
||||||
|
echo "no 'autopilot' label found on #$iss to remove"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# drop machine-readable markers: DELEGATE / CLOSE_ISSUE, and the BEGIN_SUBTASKS..END_SUBTASKS and
|
# drop machine-readable markers: DELEGATE / CLOSE_ISSUE / MERGE_PR / HALT_AUTOPILOT, and the
|
||||||
# BEGIN_PR_DESCRIPTION..END_PR_DESCRIPTION blocks (the PR description is published separately).
|
# BEGIN_SUBTASKS..END_SUBTASKS and BEGIN_PR_DESCRIPTION..END_PR_DESCRIPTION blocks (the PR
|
||||||
|
# description is published separately).
|
||||||
reply=$(awk '
|
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:]]*CLOSE_ISSUE[[:space:]]*$/{next}
|
/^[[:space:]]*CLOSE_ISSUE[[:space:]]*$/{next}
|
||||||
|
/^[[:space:]]*MERGE_PR[[:space:]]*$/{next}
|
||||||
|
/^[[:space:]]*HALT_AUTOPILOT[[:space:]]*$/{next}
|
||||||
|
/^[[:space:]]*BOUNCE:[[:space:]]*@/{next}
|
||||||
s{ if(/^[[:space:]]*END_SUBTASKS/){s=0}; next }
|
s{ if(/^[[:space:]]*END_SUBTASKS/){s=0}; next }
|
||||||
p{ if(/^[[:space:]]*END_PR_DESCRIPTION/){p=0}; next }
|
p{ if(/^[[:space:]]*END_PR_DESCRIPTION/){p=0}; next }
|
||||||
{print}
|
{print}
|
||||||
@@ -91,6 +109,75 @@ if [ "$MODE" != "pr" ]; then
|
|||||||
subtext=$(printf '\n\n---\n🤖 **@%s** — created sub-issues%s (mention an agent on each when ready):%b' "$NAME" "${ms:+ under milestone **$ms**}" "$links")
|
subtext=$(printf '\n\n---\n🤖 **@%s** — created sub-issues%s (mention an agent on each when ready):%b' "$NAME" "${ms:+ under milestone **$ms**}" "$links")
|
||||||
fi
|
fi
|
||||||
post "$(printf '🤖 **@%s**\n\n%s%s' "$NAME" "$msg" "$subtext")"
|
post "$(printf '🤖 **@%s**\n\n%s%s' "$NAME" "$msg" "$subtext")"
|
||||||
|
|
||||||
|
# --- AUTOPILOT: @qa's narrow, label-gated merge / halt authority ---
|
||||||
|
# Only @qa, only when 'autopilot' is set, and only on a PR thread. The MERGE_PR / HALT_AUTOPILOT
|
||||||
|
# markers come from the QA prompt. Merge + label ops use TOKEN_QA (the QA user's PAT, which the
|
||||||
|
# maintainer must grant write+merge scope). ISSNUM is the origin issue (resolved from the branch).
|
||||||
|
if [ "$NAME" = "qa" ] && [ "$AUTOPILOT" = "true" ]; then
|
||||||
|
if grep -qiE '^[[:space:]]*MERGE_PR[[:space:]]*$' /tmp/agent_out.md; then
|
||||||
|
if [ -z "$IS_PR" ]; then
|
||||||
|
echo "MERGE_PR marker but this run is not on a PR thread — skipping merge"
|
||||||
|
else
|
||||||
|
echo "@qa autopilot: merging PR #$NUM (origin issue #${ISSNUM:-$NUM})"
|
||||||
|
# Merge with AGENT_TOKEN (a PAT) — NOT the built-in Actions token — so the resulting push to
|
||||||
|
# main TRIGGERS downstream workflows (e.g. deploy). A merge made with the built-in GITEA_TOKEN
|
||||||
|
# does not fire new runs (loop-prevention), which silently skips the deploy. Fall back to the
|
||||||
|
# agent's own token only if AGENT_TOKEN isn't set (then the deploy would need a manual run).
|
||||||
|
mtok="${AGENT_TOKEN:-$TOK}"
|
||||||
|
mc=$(curl -sS -o /tmp/merge_resp.txt -w '%{http_code}' -X POST \
|
||||||
|
-H "Authorization: token $mtok" -H "Content-Type: application/json" \
|
||||||
|
"$API/pulls/$NUM/merge" -d '{"Do":"merge"}')
|
||||||
|
echo "merge -> HTTP $mc"; cat /tmp/merge_resp.txt 2>/dev/null || true
|
||||||
|
case "$mc" in
|
||||||
|
200|201|204)
|
||||||
|
echo "closing origin issue #${ISSNUM:-$NUM}"
|
||||||
|
curl -sS -X PATCH "${hdr[@]}" "$API/issues/${ISSNUM:-$NUM}" \
|
||||||
|
-d '{"state":"closed"}' -w '\nclose -> HTTP %{http_code}\n' || true
|
||||||
|
post "$(printf '🤖 **@qa** — ✅ verified & merged PR #%s (autopilot). Closed issue #%s.' "$NUM" "${ISSNUM:-$NUM}")"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Merge failed (checks not green, conflicts, or TOKEN_QA lacks merge scope) — do NOT
|
||||||
|
# silently proceed: drop the label so it reverts to human control and report.
|
||||||
|
del_autopilot_label "${ISSNUM:-$NUM}"
|
||||||
|
post "$(printf '🤖 **@qa** — ⚠️ tried to merge PR #%s but the API returned HTTP %s (checks not green, a conflict, or missing merge permission on TOKEN_QA). Removed the `autopilot` label — @ffaerber please take a look.' "$NUM" "$mc")"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
elif grep -qiE '^[[:space:]]*BOUNCE:[[:space:]]*@(junior|senior|lead)' /tmp/agent_out.md; then
|
||||||
|
# @qa wants the dev to fix something. Send it back — never fix it ourselves. After 3 bounces,
|
||||||
|
# stop and hand to the human. QA's feedback is already posted (the reply comment above).
|
||||||
|
if [ -z "$IS_PR" ]; then
|
||||||
|
echo "BOUNCE marker but this run is not on a PR thread — skipping"
|
||||||
|
else
|
||||||
|
target=$(grep -oiE 'BOUNCE:[[:space:]]*@(junior|senior|lead)' /tmp/agent_out.md | head -1 \
|
||||||
|
| grep -oiE '(junior|senior|lead)' | tr '[:upper:]' '[:lower:]')
|
||||||
|
[ -z "$target" ] && target=$(curl -sS "${hdr[@]}" "$API/pulls/$NUM" | jq -r '.user.login // "junior"')
|
||||||
|
# Count how many times this PR has already been bounced (marker in the trigger comment).
|
||||||
|
prior=$(curl -sS "${hdr[@]}" "$API/issues/$NUM/comments?limit=100" \
|
||||||
|
| jq -r 'if type=="array" then [.[]|select(.body|test("autopilot fix attempt"))]|length else 0 end' 2>/dev/null)
|
||||||
|
prior=${prior:-0}
|
||||||
|
if [ "$prior" -ge 3 ]; then
|
||||||
|
echo "@qa autopilot: 3 bounces already — halting"
|
||||||
|
del_autopilot_label "${ISSNUM:-$NUM}"
|
||||||
|
post "$(printf '🤖 **@qa** — 🛑 still not right after 3 fix attempts. Stopping autopilot (removed the `autopilot` label). @ffaerber please take over — details in the comments above.')"
|
||||||
|
else
|
||||||
|
n=$((prior + 1))
|
||||||
|
echo "@qa autopilot: bounce $n/3 -> @$target"
|
||||||
|
curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \
|
||||||
|
"$API/issues/$NUM/comments" \
|
||||||
|
-d "$(jq -nc --arg b "@$target please address @qa's feedback above and update this PR (autopilot fix attempt $n/3)." '{body:$b}')" \
|
||||||
|
-w '\nbounce -> HTTP %{http_code}\n' || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif grep -qiE '^[[:space:]]*HALT_AUTOPILOT[[:space:]]*$' /tmp/agent_out.md; then
|
||||||
|
echo "@qa autopilot: HALT — removing 'autopilot' label from #${ISSNUM:-$NUM}"
|
||||||
|
del_autopilot_label "${ISSNUM:-$NUM}"
|
||||||
|
post "$(printf '🤖 **@qa** — 🛑 this needs a human decision (not a dev fix). Removed the `autopilot` label (back to human control). @ffaerber please decide next steps (details above).')"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# Auto-delegate: if the plan names a teammate, trigger them via AGENT_TOKEN (a PAT, so it
|
# Auto-delegate: if the plan names a teammate, trigger them via AGENT_TOKEN (a PAT, so it
|
||||||
# fires a new workflow run — the built-in token cannot). Never targets @pm or self, so the
|
# fires a new workflow run — the built-in token cannot). Never targets @pm or self, so the
|
||||||
# chain always terminates at a dev. The '🤖' guard on the trigger stops status-comment loops.
|
# chain always terminates at a dev. The '🤖' guard on the trigger stops status-comment loops.
|
||||||
@@ -144,7 +231,7 @@ activity=""
|
|||||||
if [ -s /tmp/activity_log.md ]; then
|
if [ -s /tmp/activity_log.md ]; then
|
||||||
entries=$(wc -l < /tmp/activity_log.md 2>/dev/null || echo 0)
|
entries=$(wc -l < /tmp/activity_log.md 2>/dev/null || echo 0)
|
||||||
log=$(cat /tmp/activity_log.md)
|
log=$(cat /tmp/activity_log.md)
|
||||||
activity=$(printf '\n\n<details>\n<summary>🔧 activity — %s tool calls & reasoning</summary>\n\n%s\n\n</details>' "$entries" "$log")
|
activity=$(printf '\n\n<details>\n<summary>🔧 activity — %s tool calls</summary>\n\n%s\n\n</details>' "$entries" "$log")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# One PR per run: publish ONLY this run's own branch ($BRANCH), never sibling
|
# One PR per run: publish ONLY this run's own branch ($BRANCH), never sibling
|
||||||
@@ -185,8 +272,31 @@ prpost() {
|
|||||||
|
|
||||||
if [ "$NEW" = "true" ]; then
|
if [ "$NEW" = "true" ]; then
|
||||||
prpost "$prnum" "$(printf '🤖 **@%s** — ✅ PR ready for review — @ffaerber please review & merge:\n- %s%s' "$NAME" "$url" "$activity")"
|
prpost "$prnum" "$(printf '🤖 **@%s** — ✅ PR ready for review — @ffaerber please review & merge:\n- %s%s' "$NAME" "$url" "$activity")"
|
||||||
|
# AUTOPILOT: hand the fresh PR to @qa automatically (via AGENT_TOKEN, so it fires a new run).
|
||||||
|
# @qa then verifies and — if green — merges + closes via its MERGE_PR marker. The comment lands
|
||||||
|
# on the PR thread ($prnum) so the next run resolves the origin issue's label from the branch
|
||||||
|
# name. The '🤖' guard on the trigger gate stops status-comment loops.
|
||||||
|
if [ "$AUTOPILOT" = "true" ] && [ -n "$AGENT_TOKEN" ] && [ -n "$prnum" ]; then
|
||||||
|
echo "autopilot: auto-triggering @qa to review PR #$prnum"
|
||||||
|
curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \
|
||||||
|
"$API/issues/$prnum/comments" \
|
||||||
|
-d "$(jq -nc --arg b "@qa please verify this PR (autopilot: issue #$NUM is labeled autopilot). Merge it if correct, or bounce it back to the dev with exactly what needs fixing." '{body:$b}')" \
|
||||||
|
-w '\ntrigger-qa -> HTTP %{http_code}\n' || true
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
# Resume: just link the PR — its body and the diff already carry the description, so we don't
|
# Resume: just link the PR — its body and the diff already carry the description, so we don't
|
||||||
# repeat the full write-up in the comment (the reasoning trail below shows what this run did).
|
# repeat the full write-up in the comment (the reasoning trail below shows what this run did).
|
||||||
prpost "$prnum" "$(printf '🤖 **@%s** — pushed an update to the PR:\n- %s%s' "$NAME" "$url" "$activity")"
|
prpost "$prnum" "$(printf '🤖 **@%s** — pushed an update to the PR:\n- %s%s' "$NAME" "$url" "$activity")"
|
||||||
|
# AUTOPILOT: after a dev pushes a fix (e.g. following a @qa bounce), hand back to @qa to re-verify.
|
||||||
|
if [ "$AUTOPILOT" = "true" ] && [ -n "$AGENT_TOKEN" ] && [ -n "$prnum" ]; then
|
||||||
|
case "$NAME" in
|
||||||
|
junior|senior|lead)
|
||||||
|
echo "autopilot: dev pushed a fix — re-triggering @qa to re-verify PR #$prnum"
|
||||||
|
curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \
|
||||||
|
"$API/issues/$prnum/comments" \
|
||||||
|
-d "$(jq -nc --arg b "@qa please re-verify this PR (autopilot). Merge it if now correct, or bounce it back with exactly what still needs fixing." '{body:$b}')" \
|
||||||
|
-w '\ntrigger-qa -> HTTP %{http_code}\n' || true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ cp "$AGENTS_JSON" /tmp/agents.json
|
|||||||
# here — see agent.yml: this reusable workflow sees it as 'workflow_call'.)
|
# here — see agent.yml: this reusable workflow sees it as 'workflow_call'.)
|
||||||
if [ -n "$CID" ]; then scan="$BODY"; else scan="$IBODY"; fi
|
if [ -n "$CID" ]; then scan="$BODY"; else scan="$IBODY"; fi
|
||||||
name=""
|
name=""
|
||||||
for a in pm junior senior lead qa; do
|
for a in pm junior senior lead qa ops; 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
|
||||||
@@ -44,15 +44,17 @@ 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";; *) TOK="";;
|
lead) TOK="$TOKEN_LEAD";; qa) TOK="$TOKEN_QA";; ops) TOK="$TOKEN_OPS";; *) TOK="";;
|
||||||
esac
|
esac
|
||||||
[ -z "$TOK" ] && TOK="$GT"
|
[ -z "$TOK" ] && TOK="$GT"
|
||||||
git config user.name "$name"
|
git config user.name "$name"
|
||||||
git config user.email "$name@ffaerber.duckdns.org"
|
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=""
|
||||||
if [ -n "$IS_PR" ]; then # comment on a PR -> resume its branch
|
if [ -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"
|
||||||
git fetch origin "$ref" && git checkout "$ref"
|
git fetch origin "$ref" && git checkout "$ref"
|
||||||
{ echo "branch=$ref"; echo "new=false"; } >> "$GITHUB_OUTPUT"
|
{ echo "branch=$ref"; echo "new=false"; } >> "$GITHUB_OUTPUT"
|
||||||
elif git ls-remote --exit-code --heads origin "ai/issue-$NUM" >/dev/null 2>&1; then
|
elif git ls-remote --exit-code --heads origin "ai/issue-$NUM" >/dev/null 2>&1; then
|
||||||
@@ -72,3 +74,18 @@ else # comment on an issue, no branch ye
|
|||||||
-d "$(jq -nc --arg b "🔨 **@$name** is on it — building on branch [\`ai/issue-$NUM\`]($url). I'll open a PR when it's ready." '{body:$b}')" >/dev/null || true
|
-d "$(jq -nc --arg b "🔨 **@$name** is on it — building on branch [\`ai/issue-$NUM\`]($url). I'll open a PR when it's ready." '{body:$b}')" >/dev/null || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- Autopilot gate: read the `autopilot` label FRESH every run. ---
|
||||||
|
# Presence of this label is the opt-in switch (and the kill switch: remove it mid-flight and the
|
||||||
|
# next run reverts to normal human-approval behavior). When @qa is triggered on a PR thread, the
|
||||||
|
# label lives on the ORIGIN issue (ai/issue-N), so resolve N from the branch name.
|
||||||
|
issnum="$NUM"
|
||||||
|
case "$IS_PR" in ?*) issnum=$(printf '%s' "$branch_ref" | sed -nE 's,^ai/issue-([0-9]+).*,\1,p');; esac
|
||||||
|
[ -z "$issnum" ] && issnum="$NUM"
|
||||||
|
autopilot=false
|
||||||
|
if curl -sS -H "Authorization: token $GT" "$API/issues/$issnum/labels" 2>/dev/null \
|
||||||
|
| jq -e 'any(.[]?; .name=="autopilot")' >/dev/null 2>&1; then
|
||||||
|
autopilot=true
|
||||||
|
fi
|
||||||
|
echo "autopilot (autopilot label on #$issnum)=$autopilot"
|
||||||
|
{ echo "autopilot=$autopilot"; echo "issnum=$issnum"; } >> "$GITHUB_OUTPUT"
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
# plain-text reply (/tmp/agent_out.md) plus the raw event stream (/tmp/events.jsonl).
|
# plain-text reply (/tmp/agent_out.md) plus the raw event stream (/tmp/events.jsonl).
|
||||||
#
|
#
|
||||||
# Required env (provided by the workflow step):
|
# Required env (provided by the workflow step):
|
||||||
# ANTHROPIC_API_KEY AGENT_TOKEN NAME MODEL VISION MODE HAS_IMAGES BRANCH NUM TITLE IBODY CMT
|
# ANTHROPIC_API_KEY AGENT_TOKEN NAME MODEL VISION MODE HAS_IMAGES BRANCH AUTOPILOT NUM TITLE
|
||||||
|
# IBODY CMT
|
||||||
# FILES (the opencode -f image flags, from the imgs step output)
|
# FILES (the opencode -f image flags, from the imgs step output)
|
||||||
|
# AUTOPILOT is 'true' when the issue carries the `autopilot` label (label-gated autopilot mode).
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
[ -z "$CMT" ] && CMT="(a new issue was just opened — assess it)"
|
[ -z "$CMT" ] && CMT="(a new issue was just opened — assess it)"
|
||||||
@@ -51,6 +53,39 @@ if [ "$MODE" = "comment" ]; then
|
|||||||
END_SUBTASKS
|
END_SUBTASKS
|
||||||
The automation creates the milestone + one sub-issue per line (each linked to this issue). It
|
The automation creates the milestone + one sub-issue per line (each linked to this issue). It
|
||||||
does NOT auto-start any dev — the maintainer @mentions an agent on each sub-issue when ready."
|
does NOT auto-start any dev — the maintainer @mentions an agent on each sub-issue when ready."
|
||||||
|
if [ "$AUTOPILOT" = "true" ]; then
|
||||||
|
ACTION="$ACTION
|
||||||
|
AUTOPILOT MODE IS ACTIVE (this issue carries the 'autopilot' label). This OVERRIDES the
|
||||||
|
two-phase approval gate above: do NOT ask '@ffaerber ready to start building?' and do NOT wait
|
||||||
|
for a 'yes'. When the task is clear, present your SHORT plan naming the best teammate to build it
|
||||||
|
AND end your reply with a 'DELEGATE: @<agent>' line in the SAME turn to hand off immediately.
|
||||||
|
Prefer @junior for small/low-risk (mostly YAML/compose/config), @senior/@lead for complex or
|
||||||
|
multi-file work. Only skip delegating (and instead ask @ffaerber) if the task is genuinely
|
||||||
|
ambiguous or unsafe — otherwise plan-and-delegate now."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ "$NAME" = "qa" ]; then
|
||||||
|
ACTION="$ACTION
|
||||||
|
As QA you verify a change works: read the PR/issue, drive the web app with your headless
|
||||||
|
browser if there is a URL, and report bugs or confirm behavior. You normally do NOT merge —
|
||||||
|
a human does that."
|
||||||
|
if [ "$AUTOPILOT" = "true" ]; then
|
||||||
|
ACTION="$ACTION
|
||||||
|
AUTOPILOT MODE IS ACTIVE (this issue/PR carries the 'autopilot' label). You are the quality gate.
|
||||||
|
You do NOT edit code or fix anything yourself — you either accept the PR or send it back to the dev
|
||||||
|
with precise instructions. After actually verifying, end your reply with EXACTLY one of:
|
||||||
|
- 'MERGE_PR' — the change is correct and any CI is green. The automation merges the PR and closes
|
||||||
|
the linked issue. Do NOT merge by any other means; only this marker triggers the merge.
|
||||||
|
- 'BOUNCE: @<dev>' — something needs changing. FIRST spell out, specifically and actionably, exactly
|
||||||
|
what the dev must change (name the file, label, value, hostname, etc.), THEN end with the BOUNCE
|
||||||
|
line naming who should fix it (@junior / @senior / @lead — usually whoever built it; @senior or
|
||||||
|
@lead for something harder). The automation sends the PR back to that dev and then re-verifies
|
||||||
|
with you. After 3 bounces it stops automatically and hands to @ffaerber — so make each round
|
||||||
|
count and list ALL problems at once, not one at a time.
|
||||||
|
Use BOUNCE for anything a dev can fix. Only use 'HALT_AUTOPILOT' when the problem is NOT fixable by
|
||||||
|
a dev — the request itself is ambiguous or needs a human decision — to hand back to @ffaerber.
|
||||||
|
Emit AT MOST one of MERGE_PR / BOUNCE / HALT_AUTOPILOT, and only after you have actually verified."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
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.
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Set up the `gitea-admin` skill — instance administration for the @ops agent ONLY.
|
||||||
|
# Emits an opencode Skill file under ~/.config/opencode/skills/ documenting how to create
|
||||||
|
# orgs/users/repos, manage labels & secrets, and mint scoped per-user tokens via the Gitea API.
|
||||||
|
#
|
||||||
|
# The credential is AGENT_TOKEN (BOOTSTRAP: currently an admin PAT — temporary). This skill doc is
|
||||||
|
# written ONLY for @ops (gated on NAME) so the how-to never reaches other agents. NOTE: while
|
||||||
|
# AGENT_TOKEN is admin, every agent's process technically holds an admin credential in its env —
|
||||||
|
# that is the bootstrap trade-off. Once @ops is minting scoped per-user tokens, AGENT_TOKEN should be
|
||||||
|
# narrowed and a dedicated admin token injected only for @ops.
|
||||||
|
#
|
||||||
|
# Required env (provided by the workflow step): NAME AGENT_TOKEN
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
[ "${NAME:-}" = "ops" ] || { echo "not @ops — skipping gitea-admin skill"; exit 0; }
|
||||||
|
if [ -z "${AGENT_TOKEN:-}" ]; then
|
||||||
|
echo "AGENT_TOKEN not set — skipping gitea-admin skill"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
mkdir -p ~/.config/opencode/skills/gitea-admin && chmod 700 ~/.config/opencode/skills/gitea-admin
|
||||||
|
cat > ~/.config/opencode/skills/gitea-admin/SKILL.md <<'SKILLET'
|
||||||
|
---
|
||||||
|
name: gitea-admin
|
||||||
|
description: Administer this Gitea instance — create orgs, users, repos; manage labels & Actions secrets; mint scoped per-user access tokens; bootstrap a new repo with the agent caller workflow. Use for "create org X", "create repo Y", "add user Z", "give user W a token scoped to …", "set label set on …".
|
||||||
|
domains: [gitea, admin, orgs, users, repos, secrets, tokens]
|
||||||
|
tags: [gitea, admin, api, curl, bootstrap]
|
||||||
|
---
|
||||||
|
|
||||||
|
# `gitea-admin` Skill (operator / @ops only)
|
||||||
|
|
||||||
|
Administer the Gitea instance via its REST API at `${GITHUB_SERVER_URL}/api/v1`, authenticated with
|
||||||
|
`Authorization: token ${AGENT_TOKEN}` (a site-admin token during bootstrap). Both env vars are
|
||||||
|
already set. Work from the issue instructions; report what you did.
|
||||||
|
|
||||||
|
## Golden rules
|
||||||
|
- **NEVER print, echo, or paste a token, password, or secret value** — not in comments, not in logs.
|
||||||
|
Capture into a shell variable and immediately store it as a secret; report only that it was stored.
|
||||||
|
- **ALWAYS confirm before anything destructive** (delete user/repo/org, remove a member). Post a
|
||||||
|
clear "reply `yes` to confirm deleting X" and stop; only act after the maintainer confirms.
|
||||||
|
- Prefer the **least privilege** that satisfies the request when minting tokens.
|
||||||
|
- Be idempotent where you can (check if the org/repo/label already exists before creating).
|
||||||
|
|
||||||
|
## Create an organisation
|
||||||
|
```
|
||||||
|
curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \
|
||||||
|
"$API/orgs" -d '{"username":"acme","visibility":"private"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Create a user, then mint a TAILORED token for them (least privilege)
|
||||||
|
Admin creates the user with a password you generate; you then basic-auth AS that user (with the
|
||||||
|
password you just set) to mint a scoped token, and store the token straight into a secret.
|
||||||
|
```
|
||||||
|
API="${GITHUB_SERVER_URL}/api/v1"
|
||||||
|
PW=$(head -c 24 /dev/urandom | base64 | tr -d '/+=' | head -c 24) # generated, never printed
|
||||||
|
# 1) create the user
|
||||||
|
curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \
|
||||||
|
"$API/admin/users" -d "$(jq -nc --arg u inter --arg e inter@ffaerber.duckdns.org --arg p "$PW" \
|
||||||
|
'{username:$u,email:$e,password:$p,must_change_password:false,source_id:0,visibility:"private"}')"
|
||||||
|
# 2) mint a scoped token AS that user (pick the narrowest scopes needed)
|
||||||
|
tok=$(curl -sS -u "inter:$PW" -H "Content-Type: application/json" -X POST "$API/users/inter/tokens" \
|
||||||
|
-d '{"name":"inter","scopes":["read:repository","write:issue"]}' | jq -r '.sha1')
|
||||||
|
# 3) store the value in BOTH places (see "Secret storage" below) — never print $tok
|
||||||
|
curl -sS -X PUT -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \
|
||||||
|
"$API/orgs/gitea/actions/secrets/TOKEN_INTER" -d "$(jq -nc --arg d "$tok" '{data:$d}')"
|
||||||
|
```
|
||||||
|
Token **scopes** are groups of `read:`/`write:` on: `repository`, `issue`, `organization`, `user`,
|
||||||
|
`package`, `notification`, `misc`, and (only for a privileged token) `admin`.
|
||||||
|
|
||||||
|
## Secret storage — `gitea/secrets/.env` is the SOURCE OF TRUTH
|
||||||
|
Every token/secret value MUST live in **`gitea/secrets/.env`** (private, readable only by @ffaerber and
|
||||||
|
@ops) as a `KEY=value` line. That file is the master; the workflows only get a secret because `.env` is
|
||||||
|
mirrored into the org Actions secrets. So whenever you mint, rotate, or re-scope a token you MUST do
|
||||||
|
BOTH, in sync:
|
||||||
|
1. **`.env`**: `GET /repos/gitea/secrets/contents/.env` for its `sha`, add or replace the `KEY=value`
|
||||||
|
line, then `PUT` the updated base64 content with that `sha`.
|
||||||
|
2. **Actions secret**: `PUT /orgs/gitea/actions/secrets/{KEY}` with the same value (what runs use).
|
||||||
|
When you DELETE a token, remove it from BOTH. Keep `gitea/secrets/README.md` (the table describing what
|
||||||
|
each KEY is) up to date. Do NOT use `tokens.md` — the values live in `.env`. NEVER paste a token value
|
||||||
|
into any issue/PR/comment/log; it only ever goes into `.env` and the Actions secret.
|
||||||
|
|
||||||
|
## Change a user's token scope (the "update my token" flow)
|
||||||
|
Tokens are immutable — you can't edit scopes. Re-mint: delete the old token and create a new one,
|
||||||
|
then overwrite the stored secret.
|
||||||
|
```
|
||||||
|
curl -sS -u "inter:$PW" -X DELETE "$API/users/inter/tokens/<name-or-id>" # needs the password again
|
||||||
|
tok=$(curl -sS -u "inter:$PW" -X POST "$API/users/inter/tokens" -d '{"name":"inter","scopes":[…new…]}' | jq -r '.sha1')
|
||||||
|
curl -sS -X PUT -H "Authorization: token $AGENT_TOKEN" "$API/orgs/gitea/actions/secrets/TOKEN_INTER" -d "$(jq -nc --arg d "$tok" '{data:$d}')"
|
||||||
|
```
|
||||||
|
(If you no longer hold the user's password, reset it first via `PATCH /admin/users/{username}` with a
|
||||||
|
new generated password, then re-mint.)
|
||||||
|
|
||||||
|
## Actions secrets & variables
|
||||||
|
```
|
||||||
|
curl -sS -X PUT -H "Authorization: token $AGENT_TOKEN" "$API/orgs/{org}/actions/secrets/{NAME}" -d '{"data":"<value>"}'
|
||||||
|
curl -sS -X PUT -H "Authorization: token $AGENT_TOKEN" "$API/repos/{owner}/{repo}/actions/secrets/{NAME}" -d '{"data":"<value>"}'
|
||||||
|
curl -sS -X PUT -H "Authorization: token $AGENT_TOKEN" "$API/user/actions/secrets/{NAME}" -d '{"data":"<value>"}' # user-level
|
||||||
|
```
|
||||||
|
|
||||||
|
## Labels (repo or org-wide). Scoped labels (name `scope/value`) are mutually exclusive if `exclusive:true`.
|
||||||
|
```
|
||||||
|
curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" "$API/repos/{owner}/{repo}/labels" \
|
||||||
|
-d '{"name":"status/review","color":"1d76db","description":"…","exclusive":true}'
|
||||||
|
curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" "$API/orgs/{org}/labels" -d '{…}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bootstrap a new repo (create + wire it up for the agents)
|
||||||
|
1. Create: `POST /orgs/{org}/repos` or `POST /admin/users/{user}/repos` (e.g. `{"name":"homepage","auto_init":true,"private":true}`).
|
||||||
|
2. Add the standard label set (loop the labels above).
|
||||||
|
3. Commit the standard caller so it gets the agents — `PUT /repos/{owner}/{repo}/contents/.gitea/workflows/ai-agent.yml`
|
||||||
|
with base64 `content`, `message`, `branch:"main"` (copy the exact caller from the `agents` repo README).
|
||||||
|
4. Add the agent bot users as collaborators: `PUT /repos/{owner}/{repo}/collaborators/{username}` (`{"permission":"write"}`).
|
||||||
|
5. Ensure the repo can run agents — the org must hold the runtime secrets (ANTHROPIC_API_KEY, AGENT_TOKEN,
|
||||||
|
TOKEN_* , OLLAMA_URL, OLLAMA_CLOUD_API_KEY); set any missing via the secrets calls above.
|
||||||
|
|
||||||
|
## Admin user management
|
||||||
|
- Create: `POST /admin/users`. Edit: `PATCH /admin/users/{username}`. Delete: `DELETE /admin/users/{username}` (**confirm first**).
|
||||||
|
- List: `GET /admin/users`.
|
||||||
|
SKILLET
|
||||||
|
chmod -R o=rX ~/.config/opencode/skills/gitea-admin
|
||||||
|
echo "gitea-admin skill installed for @ops ($(wc -l < ~/.config/opencode/skills/gitea-admin/SKILL.md) lines)"
|
||||||
@@ -11,10 +11,26 @@ the loop guards.
|
|||||||
## Golden rules
|
## Golden rules
|
||||||
- You may edit **any file**. **NEVER push to `main`, NEVER merge a PR** — all work goes on a branch
|
- You may edit **any file**. **NEVER push to `main`, NEVER merge a PR** — all work goes on a branch
|
||||||
and becomes a PR a human reviews and merges.
|
and becomes a PR a human reviews and merges.
|
||||||
|
- **Narrow exception — `@qa` autopilot merge:** `@qa` (and only `@qa`) MAY merge a single PR **only**
|
||||||
|
when the linked issue carries the `autopilot` label, the PR is clearly correct, and any CI
|
||||||
|
checks are green. `@qa` triggers the merge by ending its reply with the `MERGE_PR` marker (the
|
||||||
|
workflow performs the merge + closes the issue). On **any** doubt or bug, `@qa` must NOT merge:
|
||||||
|
it ends with `HALT_AUTOPILOT` instead, which removes the `autopilot` label and returns the
|
||||||
|
issue to human control. No other agent may merge, and `@qa` may not merge without the label.
|
||||||
- **Never print, exfiltrate, or invent secret values.**
|
- **Never print, exfiltrate, or invent secret values.**
|
||||||
- Keep changes **minimal** and match the conventions already in the file you're editing.
|
- Keep changes **minimal** and match the conventions already in the file you're editing.
|
||||||
- Do the work on a **branch** — never paste code or diffs into the issue thread.
|
- Do the work on a **branch** — never paste code or diffs into the issue thread.
|
||||||
|
|
||||||
|
## Autopilot (`autopilot` label)
|
||||||
|
An issue labeled **`autopilot`** runs without the usual human checkpoints:
|
||||||
|
- `@pm` plans **and** delegates in the same turn (skips the "ready to build? reply yes" gate).
|
||||||
|
- After the dev's PR is opened, `@qa` is auto-triggered to verify it, and merges + closes on success
|
||||||
|
(see the QA merge exception above).
|
||||||
|
- **Kill switch:** remove the `autopilot` label at any time. The label is re-read fresh at the
|
||||||
|
start of every run, so the next agent turn reverts to normal human-approval behavior. `@qa` also
|
||||||
|
removes the label itself whenever it halts on a bug or a failed merge.
|
||||||
|
No label (the default) = today's behavior, unchanged.
|
||||||
|
|
||||||
## Branches & pull requests
|
## Branches & pull requests
|
||||||
Start on `ai/issue-<N>`. Split independent changes into separate branches (one PR each). Commit and
|
Start on `ai/issue-<N>`. Split independent changes into separate branches (one PR each). Commit and
|
||||||
push incrementally. Do NOT open PRs yourself (automated). End your reply with the PR description
|
push incrementally. Do NOT open PRs yourself (automated). End your reply with the PR description
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ Shared **AI dev-team** workflow for Gitea Actions, reusable across repos. It giv
|
|||||||
| `@senior` | `ollama-cloud/glm-5.2:cloud` | no | pr | `gitea-api`, `node1-ssh` | Senior dev — complex, multi-file implementation (GLM-5.2 via Ollama Cloud, text-only). |
|
| `@senior` | `ollama-cloud/glm-5.2:cloud` | no | pr | `gitea-api`, `node1-ssh` | Senior dev — complex, multi-file implementation (GLM-5.2 via Ollama Cloud, text-only). |
|
||||||
| `@lead` | `anthropic/claude-opus-4-8` | yes | pr | `gitea-api`, `node1-ssh` | Tech lead — the hardest problems, architecture, and final calls. |
|
| `@lead` | `anthropic/claude-opus-4-8` | yes | pr | `gitea-api`, `node1-ssh` | Tech lead — the hardest problems, architecture, and final calls. |
|
||||||
| `@qa` | `ollama-cloud/minimax-m3:cloud` | yes | comment | `gitea-api` | QA — verifies things work. Drives a headless browser (Playwright) to open a URL/web app, click through it, screenshot, and report bugs or confirm behavior. Comments findings; opens no PRs. |
|
| `@qa` | `ollama-cloud/minimax-m3:cloud` | yes | comment | `gitea-api` | QA — verifies things work. Drives a headless browser (Playwright) to open a URL/web app, click through it, screenshot, and report bugs or confirm behavior. Comments findings; opens no PRs. |
|
||||||
|
| `@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. |
|
||||||
|
|
||||||
`agent.yml`'s agent registry is the source of truth for this mapping — if you change a model
|
`agent.yml`'s agent registry is the source of truth for this mapping — if you change a model
|
||||||
or an agent's skills there, update this table too.
|
or an agent's skills there, update this table too.
|
||||||
|
|||||||
Reference in New Issue
Block a user