Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef06da3ffe | ||
|
|
7278e06dff | ||
|
|
df0f6d6543 | ||
|
|
23cc3e192e | ||
|
|
0a89309ff1 | ||
|
|
cb11a6b2d1 | ||
|
|
81882ee3ec | ||
|
|
1987ba792d | ||
|
|
a1ff1b9881 | ||
|
|
d803ee7cf5 | ||
|
|
1d248bc675 | ||
|
|
1fd32f0ff6 | ||
|
|
e9fad6bfad | ||
|
|
729a14f8bd | ||
|
|
d3c8f116f1 | ||
|
|
658c10fc35 | ||
|
|
649cd4ea9b | ||
|
|
4ed96dcdf2 | ||
|
|
767a3341fd |
@@ -162,6 +162,7 @@ jobs:
|
||||
MODE: ${{ steps.prep.outputs.mode }}
|
||||
HAS_IMAGES: ${{ steps.imgs.outputs.has_images }}
|
||||
BRANCH: ${{ steps.prep.outputs.branch }}
|
||||
AUTOPILOT: ${{ steps.prep.outputs.autopilot }} # 'true' when the issue carries the `autopilot` label
|
||||
NUM: ${{ github.event.issue.number }}
|
||||
TITLE: ${{ github.event.issue.title }}
|
||||
IBODY: ${{ github.event.issue.body }}
|
||||
@@ -192,6 +193,9 @@ jobs:
|
||||
TITLE: ${{ github.event.issue.title }}
|
||||
BRANCH: ${{ steps.prep.outputs.branch }}
|
||||
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"
|
||||
|
||||
# Failure-safe: if any step above failed AFTER a dev agent already pushed commits, the normal
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
name: ai-agent
|
||||
# Thin caller so the agents work on THIS repo too (their own workflow). Same shared logic.
|
||||
# New issues opened by ffaerber auto-start @pm; @mention an agent in a comment to route on replies.
|
||||
run-name: "ai-agent · #${{ github.event.issue.number }}" # quotes required: bare # starts a YAML comment
|
||||
# Standard caller for the shared AI-agent workflow (ffaerber/agents). Copy this file VERBATIM into
|
||||
# any repo that should get the agents — it is identical in every repo. All logic + scripts live in
|
||||
# agents/.gitea/workflows/; scripts are fetched from @main at run time. The `jobs.agent` wrapper is
|
||||
# required: a reusable (workflow_call) workflow can only be invoked from a caller job, not top-level.
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build activity log (tool calls + reasoning) from the event stream into /tmp/activity_log.md.
|
||||
# Only dev agents (mode=pr) get an activity-log comment — comment-only roles (pm/qa)
|
||||
# do no tool calls, so a trail would be empty/noise.
|
||||
# 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) do no tool calls.
|
||||
# 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
|
||||
set -u
|
||||
@@ -11,16 +13,12 @@ if [ "$MODE" != "pr" ]; then
|
||||
fi
|
||||
jq -r '
|
||||
def trunc(n): if length > n then (.[0:n] + "…") else . end;
|
||||
select(.type=="tool_use" or .type=="text") |
|
||||
if .type=="text" then
|
||||
"💬 " + ((.part.text // "") | trunc(4000))
|
||||
else
|
||||
(.part.tool // "?") as $t |
|
||||
((.part.state.title // (.part.state.input | tojson | trunc(160)) // "")) as $title |
|
||||
"🔧 **" + $t + "**: `" + ($title | trunc(240)) + "`"
|
||||
end
|
||||
select(.type=="tool_use") |
|
||||
(.part.tool // "?") as $t |
|
||||
((.part.state.title // (.part.state.input | tojson | trunc(160)) // "")) as $title |
|
||||
"🔧 **" + $t + "**: `" + ($title | trunc(240)) + "`"
|
||||
' /tmp/events.jsonl > /tmp/activity_log.md 2>/dev/null || true
|
||||
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
|
||||
head -3 /tmp/activity_log.md
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# Required env (provided by the workflow step):
|
||||
# GT AGENT_TOKEN TOKEN_PM TOKEN_SENIOR TOKEN_JUNIOR TOKEN_LEAD TOKEN_QA
|
||||
# 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
|
||||
# Post/PR as the agent's OWN Gitea user when its token is configured; else the built-in bot.
|
||||
case "$NAME" in
|
||||
@@ -17,15 +18,42 @@ API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||
hdr=(-H "Authorization: token $TOK" -H "Content-Type: application/json")
|
||||
post() { curl -sS -w 'comment -> HTTP %{http_code}\n' -X POST "${hdr[@]}" \
|
||||
"$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 / the BEGIN_SUBTASKS..END_SUBTASKS block)
|
||||
# drop machine-readable markers: DELEGATE / CLOSE_ISSUE / MERGE_PR / HALT_AUTOPILOT, and the
|
||||
# BEGIN_SUBTASKS..END_SUBTASKS and BEGIN_PR_DESCRIPTION..END_PR_DESCRIPTION blocks (the PR
|
||||
# description is published separately).
|
||||
reply=$(awk '
|
||||
/^[[:space:]]*BEGIN_SUBTASKS/{s=1}
|
||||
/^[[:space:]]*BEGIN_PR_DESCRIPTION/{p=1}
|
||||
/^[[:space:]]*DELEGATE:[[:space:]]*@/{next}
|
||||
/^[[:space:]]*CLOSE_ISSUE[[:space:]]*$/{next}
|
||||
/^[[:space:]]*MERGE_PR[[:space:]]*$/{next}
|
||||
/^[[:space:]]*HALT_AUTOPILOT[[:space:]]*$/{next}
|
||||
s{ if(/^[[:space:]]*END_SUBTASKS/){s=0}; next }
|
||||
p{ if(/^[[:space:]]*END_PR_DESCRIPTION/){p=0}; next }
|
||||
{print}
|
||||
' /tmp/agent_out.md 2>/dev/null)
|
||||
# Strip a leading self-header the model sometimes emits ("🤖 **@pm**" on its own line) so we don't
|
||||
# double it when we prepend our own. Removes a leading run of such header lines and blank lines.
|
||||
reply=$(printf '%s' "$reply" | awk '
|
||||
BEGIN{s=1}
|
||||
s && /^[^A-Za-z0-9]*\*\*@[A-Za-z]+\*\*[[:space:]]*$/ {next}
|
||||
s && /^[[:space:]]*$/ {next}
|
||||
{s=0; print}
|
||||
')
|
||||
[ -z "$reply" ] && reply="_(Made changes without a text summary — see the diff below.)_"
|
||||
# Prefer the agent's clean delimited PR description; fall back to the whole reply.
|
||||
prdesc=$(awk '/BEGIN_PR_DESCRIPTION/{f=1;next} /END_PR_DESCRIPTION/{f=0} f' /tmp/agent_out.md)
|
||||
@@ -39,7 +67,6 @@ if [ "$MODE" != "pr" ]; then
|
||||
# Visible comment: the reply text, or a sensible line if the agent only emitted a marker.
|
||||
msg="$reply"
|
||||
case "$msg" in ""|"_(Made changes"*) msg=$([ -n "$target" ] && echo "Handing off to @$target." || echo "_(no further comment)_") ;; esac
|
||||
post "$(printf '🤖 **@%s**\n\n%s' "$NAME" "$msg")"
|
||||
# Close the issue if the agent flagged it (maintainer said it's not needed / duplicate).
|
||||
if grep -qiE '^[[:space:]]*CLOSE_ISSUE[[:space:]]*$' /tmp/agent_out.md; then
|
||||
echo "closing issue #$NUM"
|
||||
@@ -48,6 +75,9 @@ if [ "$MODE" != "pr" ]; then
|
||||
fi
|
||||
# BREAKDOWN: from a BEGIN_SUBTASKS block, create a milestone + one sub-issue per line
|
||||
# (linked to this issue). Sub-issues are NOT auto-started — maintainer mentions agents later.
|
||||
# Process subtasks first so we can append the created-issues list to the SAME comment as
|
||||
# the reply (issue #38 — one comment per run).
|
||||
subtext=""
|
||||
if grep -qiE '^[[:space:]]*BEGIN_SUBTASKS' /tmp/agent_out.md; then
|
||||
block=$(awk '/^[[:space:]]*BEGIN_SUBTASKS/{f=1;next} /^[[:space:]]*END_SUBTASKS/{f=0} f' /tmp/agent_out.md)
|
||||
ms=$(printf '%s\n' "$block" | sed -nE 's/^[[:space:]]*milestone:[[:space:]]*//Ip' | head -1)
|
||||
@@ -75,8 +105,46 @@ if [ "$MODE" != "pr" ]; then
|
||||
echo "created sub-issue #${n:-?}: $title"
|
||||
[ -n "$n" ] && links="$links\n- #$n — $title"
|
||||
done < /tmp/subtasks.txt
|
||||
post "$(printf '🤖 **@%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
|
||||
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})"
|
||||
mc=$(curl -sS -o /tmp/merge_resp.txt -w '%{http_code}' -X POST "${hdr[@]}" \
|
||||
"$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:]]*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** — 🛑 found a problem, so I did NOT merge. 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
|
||||
# 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.
|
||||
@@ -123,6 +191,16 @@ git fetch -q origin 2>/dev/null || true
|
||||
prbody=$(printf '%s\n\n---\nResolves #%s · 🤖 @%s' "$prdesc" "$NUM" "$NAME")
|
||||
owner=${GITHUB_REPOSITORY%%/*}
|
||||
|
||||
# Post the agent's activity trail (tool calls + reasoning) inline in the same comment so
|
||||
# each run produces exactly ONE comment (issue #38). Computed once here so every dev-agent
|
||||
# exit path (no-changes, PR-open-failed, normal) appends it to the single reply comment.
|
||||
activity=""
|
||||
if [ -s /tmp/activity_log.md ]; then
|
||||
entries=$(wc -l < /tmp/activity_log.md 2>/dev/null || echo 0)
|
||||
log=$(cat /tmp/activity_log.md)
|
||||
activity=$(printf '\n\n<details>\n<summary>🔧 activity — %s tool calls</summary>\n\n%s\n\n</details>' "$entries" "$log")
|
||||
fi
|
||||
|
||||
# One PR per run: publish ONLY this run's own branch ($BRANCH), never sibling
|
||||
# ai/issue-N-* branches. This removes the multi-PR ambiguity that left the
|
||||
# activity log stranded on the triggering issue instead of the PR thread.
|
||||
@@ -130,7 +208,7 @@ br="$BRANCH"
|
||||
ahead=$(git rev-list --count "origin/main..origin/$br" 2>/dev/null || echo 0)
|
||||
if [ "${ahead:-0}" -eq 0 ]; then
|
||||
# No changes on this branch — a plan / questions / analysis only.
|
||||
post "$(printf '🤖 **@%s**\n\n%s' "$NAME" "$reply")"
|
||||
post "$(printf '🤖 **@%s**\n\n%s%s' "$NAME" "$reply" "$activity")"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -148,7 +226,7 @@ if [ -z "$url" ]; then
|
||||
url=$(printf '%s' "$resp" | jq -r '.html_url // empty' 2>/dev/null)
|
||||
prnum=$(printf '%s' "$resp" | jq -r '.number // empty' 2>/dev/null)
|
||||
fi
|
||||
[ -z "$url" ] && { echo "PR open/lookup failed for $br — posting reply on issue instead"; post "$(printf '🤖 **@%s**\n\n%s' "$NAME" "$reply")"; exit 0; }
|
||||
[ -z "$url" ] && { echo "PR open/lookup failed for $br — posting reply on issue instead"; post "$(printf '🤖 **@%s**\n\n%s%s' "$NAME" "$reply" "$activity")"; exit 0; }
|
||||
|
||||
# Posts to the PR thread when we have a PR number, else to the origin issue ($NUM).
|
||||
prpost() {
|
||||
@@ -160,17 +238,20 @@ prpost() {
|
||||
}
|
||||
|
||||
if [ "$NEW" = "true" ]; then
|
||||
prpost "$prnum" "$(printf '🤖 **@%s** — ✅ PR ready for review — @ffaerber please review & merge:\n- %s' "$NAME" "$url")"
|
||||
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 halt and remove the label if you find a problem." '{body:$b}')" \
|
||||
-w '\ntrigger-qa -> HTTP %{http_code}\n' || true
|
||||
fi
|
||||
else
|
||||
# Resume (comment is on a PR thread): include the write-up here too.
|
||||
prpost "$prnum" "$(printf '🤖 **@%s** — updated branch/PR:\n- %s\n\n%s' "$NAME" "$url" "$prdesc")"
|
||||
fi
|
||||
|
||||
# Post the agent's activity trail (tool calls + reasoning) as a separate comment so
|
||||
# it is visible on the PR thread. Additive — kept here even when nothing changed, so a
|
||||
# follow-up run (re-trigger) can see what this run did via the fetched issue thread.
|
||||
if [ -s /tmp/activity_log.md ]; then
|
||||
entries=$(wc -l < /tmp/activity_log.md 2>/dev/null || echo 0)
|
||||
log=$(cat /tmp/activity_log.md)
|
||||
prpost "$prnum" "$(printf '🤖 **@%s** — activity log (%s entries):\n<details>\n<summary>tool calls & reasoning</summary>\n\n%s\n\n</details>' "$NAME" "$entries" "$log")"
|
||||
# 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).
|
||||
prpost "$prnum" "$(printf '🤖 **@%s** — pushed an update to the PR:\n- %s%s' "$NAME" "$url" "$activity")"
|
||||
fi
|
||||
|
||||
@@ -51,11 +51,19 @@ git config user.name "$name"
|
||||
git config user.email "$name@ffaerber.duckdns.org"
|
||||
API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||
hdr=(-H "Authorization: token $TOK" -H "Content-Type: application/json")
|
||||
branch_ref=""
|
||||
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)
|
||||
branch_ref="$ref"
|
||||
git fetch origin "$ref" && git checkout "$ref"
|
||||
{ echo "branch=$ref"; echo "new=false"; } >> "$GITHUB_OUTPUT"
|
||||
else # comment on an issue -> new branch
|
||||
elif git ls-remote --exit-code --heads origin "ai/issue-$NUM" >/dev/null 2>&1; then
|
||||
# comment on an issue whose branch ALREADY exists (a prior run / open PR) -> RESUME it, so new
|
||||
# commits fast-forward onto the same branch and update its PR. Branching fresh from main here would
|
||||
# be rejected on push as non-fast-forward and the new work would be silently lost (see issue #17).
|
||||
git fetch origin "ai/issue-$NUM" && git checkout "ai/issue-$NUM"
|
||||
{ echo "branch=ai/issue-$NUM"; echo "new=false"; } >> "$GITHUB_OUTPUT"
|
||||
else # comment on an issue, no branch yet -> new branch
|
||||
git checkout -b "ai/issue-$NUM"
|
||||
{ echo "branch=ai/issue-$NUM"; echo "new=true"; } >> "$GITHUB_OUTPUT"
|
||||
# For dev agents, publish the branch immediately and tell the maintainer where to watch.
|
||||
@@ -66,3 +74,18 @@ else # comment on an issue -> new branch
|
||||
-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
|
||||
|
||||
# --- 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).
|
||||
#
|
||||
# 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)
|
||||
# AUTOPILOT is 'true' when the issue carries the `autopilot` label (label-gated autopilot mode).
|
||||
set -u
|
||||
|
||||
[ -z "$CMT" ] && CMT="(a new issue was just opened — assess it)"
|
||||
@@ -51,6 +53,36 @@ if [ "$MODE" = "comment" ]; then
|
||||
END_SUBTASKS
|
||||
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."
|
||||
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). This grants you a
|
||||
NARROW, one-time merge authority for THIS PR only:
|
||||
- If, after verifying, the PR is correct and any CI checks are green, end your reply with EXACTLY
|
||||
one line: 'MERGE_PR'. The automation will then merge the PR and close the linked issue for you.
|
||||
Do NOT merge via any other means; only the MERGE_PR marker triggers the merge.
|
||||
- If you find ANY bug, doubt, or the change is not clearly correct, do NOT merge. Instead describe
|
||||
the problem clearly and end your reply with EXACTLY one line: 'HALT_AUTOPILOT'. The automation
|
||||
removes the 'autopilot' label (returning this issue to normal human control) and leaves
|
||||
it for @ffaerber to decide next steps. Never auto-bounce back to a dev.
|
||||
Emit AT MOST one of MERGE_PR or HALT_AUTOPILOT, and only after you have actually verified. When in
|
||||
doubt, prefer HALT_AUTOPILOT."
|
||||
fi
|
||||
fi
|
||||
else
|
||||
ACTION="You start on git branch '${BRANCH}', with git and push credentials already configured.
|
||||
|
||||
@@ -11,10 +11,26 @@ the loop guards.
|
||||
## Golden rules
|
||||
- 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.
|
||||
- **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.**
|
||||
- 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.
|
||||
|
||||
## 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
|
||||
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
|
||||
|
||||
@@ -29,10 +29,18 @@ detail out of agents that shouldn't act on it while still letting them know the
|
||||
|
||||
## Use it in a repo
|
||||
|
||||
Add `.gitea/workflows/ai-agent.yml` to the consuming repo:
|
||||
**The standard caller is one file, identical in every repo.** Copy this repo's own
|
||||
[`.gitea/workflows/ai-agent.yml`](.gitea/workflows/ai-agent.yml) verbatim into the consuming repo —
|
||||
it is the source of truth, and `agents` itself uses the same file:
|
||||
|
||||
```yaml
|
||||
name: ai-agent
|
||||
run-name: "ai-agent · #${{ github.event.issue.number }}" # quotes required: bare # starts a YAML comment
|
||||
# Standard caller for the shared AI-agent workflow (ffaerber/agents). Copy this file VERBATIM into
|
||||
# any repo that should get the agents — it is identical in every repo. All logic + scripts live in
|
||||
# agents/.gitea/workflows/; scripts are fetched from @main at run time. The `jobs.agent` wrapper is
|
||||
# required: a reusable (workflow_call) workflow can only be invoked from a caller job, not top-level.
|
||||
# `run-name` titles each run by the triggering issue (e.g. "ai-agent · #42") in the Actions list.
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
@@ -44,7 +52,9 @@ jobs:
|
||||
secrets: inherit
|
||||
```
|
||||
|
||||
That's the whole per-repo footprint. All the logic (agent registry, routing, delegation,
|
||||
That's the whole per-repo footprint, and it's the minimum a caller can be: the `on:` triggers must
|
||||
live in each repo (a reusable workflow can't declare its callers' triggers) and the `jobs.agent`
|
||||
wrapper is mandatory for `workflow_call`. Everything else (agent registry, routing, delegation,
|
||||
reactions, PR/issue plumbing) lives here in `agent.yml`.
|
||||
|
||||
## Repo layout
|
||||
|
||||
Reference in New Issue
Block a user