Compare commits

...
Author SHA1 Message Date
lead ef06da3ffe Rename autopilot label 'auto' to 'autopilot' 2026-07-04 19:23:56 +00:00
lead 7278e06dff Rename autopilot label 'fully-automatic' to 'auto' 2026-07-04 19:23:44 +00:00
lead df0f6d6543 @lead: issue #16 — fully-automatic autopilot (label-gated @pm auto-delegate + @qa merge) 2026-07-04 19:23:20 +00:00
ffaerber 23cc3e192e Merge pull request 'fix(activity-log): tool calls only (drop prose summary)' (#50) from fix/activity-tool-calls-only into main 2026-07-04 13:41:02 +02:00
Felix FaerberandClaude Opus 4.8 0a89309ff1 fix(activity-log): tool calls only — drop the agent's prose summary
The activity <details> was labelled 'tool calls & reasoning' but included the agent's final prose
summary ('Added the whoami service... Exposed via Traefik... Pushed to branch...'), which is just a
restatement of the PR description and not a tool call. Emit only tool_use events; relabel to
'N tool calls'. The what-changed narrative lives in the PR description; this section is the record of
actions taken.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:40:59 +03:00
ffaerber cb11a6b2d1 Merge pull request 'fix(activity-log): strip PR-description block from reasoning trail' (#49) from fix/activity-log-strip-prdesc into main 2026-07-04 13:25:30 +02:00
Felix FaerberandClaude Opus 4.8 81882ee3ec fix(publish): de-dup agent comments (no repeated PR desc, no doubled header)
Batches the remaining comment-noise cleanups on top of the activity-log fix:
- Resume comment no longer re-posts the full PR description (## Summary/## Changes) — the PR body and
  diff already carry it; the comment just links the PR (the reasoning trail shows what the run did).
- Strip a leading self-header the model sometimes emits ("🤖 **@pm**" on its own line, sometimes
  twice) before we prepend our own, so headers aren't doubled.
- Also drop the BEGIN_PR_DESCRIPTION block from the plain reply text, and simplify the activity
  block's summary (it repeated "🤖 **@name** — activity log").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:24:37 +03:00
Felix FaerberandClaude Opus 4.8 1987ba792d fix(activity-log): strip the PR-description block from the reasoning trail
The 'tool calls & reasoning' activity log dumped each text part verbatim, including the agent's
BEGIN_PR_DESCRIPTION..END_PR_DESCRIPTION block — which is already published as the PR description, so
it appeared twice (e.g. homelab PR #117). Strip that block from text parts, and drop a text entry
that becomes empty after stripping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:12:32 +03:00
ffaerber a1ff1b9881 Merge pull request 'fix(route): resume existing ai/issue-N branch on re-trigger' (#47) from fix/route-resume-existing-branch into main 2026-07-04 12:21:07 +02:00
Felix FaerberandClaude Opus 4.8 d803ee7cf5 fix(route): resume an existing ai/issue-N branch instead of losing re-triggered work
When an agent was re-triggered on an issue that already had a branch/open PR, route.sh created a
fresh branch from main (git checkout -b), so the push back was rejected non-fast-forward and the new
commits were silently dropped (|| true) -- the run reported success and re-announced the stale PR,
but the work was gone (observed on issue #17 run #88). Resume the existing branch when it exists, so
new commits fast-forward onto it and update its PR; only branch from main when none exists.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 13:21:04 +03:00
ffaerber 1d248bc675 Merge pull request 'fix: quote run-name (bare # was a YAML comment)' (#45) from fix/run-name-quoting into main 2026-07-04 11:33:29 +02:00
Felix FaerberandClaude Opus 4.8 1fd32f0ff6 fix: quote run-name so the issue number is not eaten as a YAML comment
Bare '#' starts a YAML comment, so run-name resolved to 'ai-agent ·' with the number dropped.
Quote the value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 12:33:27 +03:00
ffaerber e9fad6bfad Merge pull request 'chore: title agent runs by issue number (run-name)' (#43) from chore/run-name into main 2026-07-04 11:29:54 +02:00
Felix FaerberandClaude Opus 4.8 729a14f8bd chore: title agent runs by issue number (run-name)
Without run-name, Gitea titles each ai-agent run by main's head commit, so runs aren't identifiable
in the Actions list. Add `run-name: ai-agent · #${{ github.event.issue.number }}` to the standard
caller (both the file and the README stub) so each run shows its issue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 12:29:09 +03:00
ffaerber d3c8f116f1 Merge pull request 'chore: standardize the ai-agent caller stub' (#41) from chore/standard-caller into main
Reviewed-on: ffaerber/agents#41
2026-07-04 11:24:49 +02:00
8 changed files with 171 additions and 18 deletions
+4
View File
@@ -162,6 +162,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 }}
@@ -192,6 +193,9 @@ jobs:
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
+1
View File
@@ -1,4 +1,5 @@
name: ai-agent 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 # 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 # 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 # agents/.gitea/workflows/; scripts are fetched from @main at run time. The `jobs.agent` wrapper is
+10 -12
View File
@@ -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,16 +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 (.part.tool // "?") as $t |
"💬 " + ((.part.text // "") | trunc(4000)) ((.part.state.title // (.part.state.input | tojson | trunc(160)) // "")) as $title |
else "🔧 **" + $t + "**: `" + ($title | trunc(240)) + "`"
(.part.tool // "?") as $t |
((.part.state.title // (.part.state.input | tojson | trunc(160)) // "")) as $title |
"🔧 **" + $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
+81 -4
View File
@@ -4,6 +4,7 @@
# 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
@@ -17,15 +18,42 @@ 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 / 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 ' reply=$(awk '
/^[[:space:]]*BEGIN_SUBTASKS/{s=1} /^[[:space:]]*BEGIN_SUBTASKS/{s=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}
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 }
{print} {print}
' /tmp/agent_out.md 2>/dev/null) ' /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.)_" [ -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. # 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) prdesc=$(awk '/BEGIN_PR_DESCRIPTION/{f=1;next} /END_PR_DESCRIPTION/{f=0} f' /tmp/agent_out.md)
@@ -80,6 +108,43 @@ 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})"
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 # 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.
@@ -133,7 +198,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---\n🤖 **@%s** — activity log (%s entries):\n<details>\n<summary>tool calls & reasoning</summary>\n\n%s\n\n</details>' "$NAME" "$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
@@ -174,7 +239,19 @@ 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 halt and remove the label if you find a problem." '{body:$b}')" \
-w '\ntrigger-qa -> HTTP %{http_code}\n' || true
fi
else else
# Resume (comment is on a PR thread): include the write-up here too. # Resume: just link the PR — its body and the diff already carry the description, so we don't
prpost "$prnum" "$(printf '🤖 **@%s** — updated branch/PR:\n- %s\n\n%s%s' "$NAME" "$url" "$prdesc" "$activity")" # 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 fi
+24 -1
View File
@@ -51,11 +51,19 @@ 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"
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" git checkout -b "ai/issue-$NUM"
{ echo "branch=ai/issue-$NUM"; echo "new=true"; } >> "$GITHUB_OUTPUT" { echo "branch=ai/issue-$NUM"; echo "new=true"; } >> "$GITHUB_OUTPUT"
# For dev agents, publish the branch immediately and tell the maintainer where to watch. # 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 -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"
+33 -1
View File
@@ -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,36 @@ 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). 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 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.
+16
View File
@@ -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
+2
View File
@@ -35,10 +35,12 @@ it is the source of truth, and `agents` itself uses the same file:
```yaml ```yaml
name: ai-agent 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 # 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 # 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 # 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. # 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: on:
issue_comment: issue_comment:
types: [created] types: [created]