agents: PM-orchestrated flow — @pm hub, @qa reviews, human/@pm merges

Rework the delegation/trigger choreography so @pm orchestrates end-to-end from
the issue thread while @qa reviews on the PR and never merges.

Flow: issue → @pm plans (asks creator; autopilot skips) → dev builds PR → dev
pings @pm on the issue → @pm hands to @qa → @qa reviews ON THE PR (recommends /
BOUNCE:@dev, direct qa↔dev loop, 3 max) → @qa APPROVEs → verdict on the ISSUE →
@pm → normal: tells creator "ready to merge" (human merges); autopilot: @pm
MERGE_PR. @pm never reads or comments on the PR (small context); @qa never merges.

- run-agent.sh: @pm prompt = 3 phases (plan / hand PR to @qa / finalize),
  autopilot only drops the approval gate + adds @pm MERGE_PR. @qa prompt =
  reviewer, ends with APPROVE / BOUNCE:@dev / HALT; identical in both flows.
- publish.sh: shared post_to()/trig()/resolve_pr() helpers + ISSN; @qa routes
  reply to PR (bounce) vs issue (approve) and fires @pm/@dev; @pm handles
  DELEGATE:@dev, DELEGATE:@qa (resolves the PR), and autopilot MERGE_PR; dev
  NEW=true fires @pm on the issue, NEW=false fires @qa on the PR. Strip
  APPROVE/HALT markers.

Tokens re-minted to match: TOKEN_PM gains write:repository (it merges now),
TOKEN_QA drops to read:repository (never merges).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Felix Faerber
2026-07-05 21:06:39 +03:00
co-authored by Claude Opus 4.8
parent 01497e9ebe
commit 84cec444ef
2 changed files with 141 additions and 148 deletions
+86 -98
View File
@@ -32,6 +32,20 @@ hdr=(-H "Authorization: token $TOK" -H "Content-Type: application/json")
MARK=$'\n\n<!-- 🤖 agent reply — do not trigger -->' MARK=$'\n\n<!-- 🤖 agent reply — do not trigger -->'
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$MARK" '{body:$b}')"; } "$API/issues/$NUM/comments" -d "$(jq -nc --arg b "$1$MARK" '{body:$b}')"; }
# Post a MARKED status/reply comment to an ARBITRARY thread (issue or PR) — never fires a run.
post_to() { curl -sS -w "comment(#$1) -> HTTP %{http_code}\n" -X POST "${hdr[@]}" \
"$API/issues/$1/comments" -d "$(jq -nc --arg b "$2$MARK" '{body:$b}')"; }
# Post an UNMARKED TRIGGER comment on a thread — fires the mentioned agent's next run. Must use a PAT
# (TTOK); the built-in GITEA_TOKEN cannot start new runs. No-op (logged) if this agent has no PAT.
trig() { if [ -z "$TTOK" ]; then echo "no trigger token — cannot fire on #$1"; return; fi
curl -sS -w "trigger(#$1) -> HTTP %{http_code}\n" -X POST \
-H "Authorization: token $TTOK" -H "Content-Type: application/json" \
"$API/issues/$1/comments" -d "$(jq -nc --arg b "$2" '{body:$b}')"; }
# Origin issue for this run (route.sh resolves it from the branch on PR threads), and a resolver for
# the open PR built from its branch (ai/issue-<issue>). Lets @pm/@qa cross between the issue and PR.
ISSN="${ISSNUM:-$NUM}"
resolve_pr() { curl -sS "${hdr[@]}" "$API/pulls?state=open&limit=50" \
| jq -r --arg br "ai/issue-$ISSN" 'if type=="array" then (map(select(.head.ref==$br))|.[0].number // empty) else empty end' 2>/dev/null; }
# Remove the 'autopilot' label from an issue by resolving its ID first (Gitea's DELETE label # 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. # endpoint is by ID, not name). Arg $1 = issue number. Used as the autopilot kill switch.
del_autopilot_label() { del_autopilot_label() {
@@ -46,7 +60,7 @@ del_autopilot_label() {
fi fi
} }
# drop machine-readable markers: DELEGATE / CLOSE_ISSUE / MERGE_PR / HALT_AUTOPILOT, and the # drop machine-readable markers: DELEGATE / CLOSE_ISSUE / MERGE_PR / APPROVE / HALT / BOUNCE, and the
# BEGIN_SUBTASKS..END_SUBTASKS and BEGIN_PR_DESCRIPTION..END_PR_DESCRIPTION blocks (the PR # BEGIN_SUBTASKS..END_SUBTASKS and BEGIN_PR_DESCRIPTION..END_PR_DESCRIPTION blocks (the PR
# description is published separately). # description is published separately).
reply=$(awk ' reply=$(awk '
@@ -55,7 +69,8 @@ reply=$(awk '
/^[[:space:]]*DELEGATE:[[:space:]]*@/{next} /^[[:space:]]*DELEGATE:[[:space:]]*@/{next}
/^[[:space:]]*CLOSE_ISSUE[[:space:]]*$/{next} /^[[:space:]]*CLOSE_ISSUE[[:space:]]*$/{next}
/^[[:space:]]*MERGE_PR[[:space:]]*$/{next} /^[[:space:]]*MERGE_PR[[:space:]]*$/{next}
/^[[:space:]]*HALT_AUTOPILOT[[:space:]]*$/{next} /^[[:space:]]*APPROVE[[:space:]]*$/{next}
/^[[:space:]]*HALT([_ ]AUTOPILOT)?[[:space:]]*$/{next}
/^[[:space:]]*BOUNCE:[[: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 }
@@ -81,6 +96,39 @@ prdesc=$(awk '/BEGIN_PR_DESCRIPTION/{f=1;next} /END_PR_DESCRIPTION/{f=0} f' /tmp
if [ "$MODE" != "pr" ]; then if [ "$MODE" != "pr" ]; then
git checkout -- . 2>/dev/null || true git checkout -- . 2>/dev/null || true
git clean -fd 2>/dev/null || true git clean -fd 2>/dev/null || true
# ---------- @qa: reviewer only — never edits, never merges ----------
# Recommendations land ON THE PR (onsite the diff); the pass/fail verdict lands ON THE ISSUE so
# @pm (who never reads the PR) can act on it. Ends its reply with APPROVE / BOUNCE: @dev / HALT.
if [ "$NAME" = "qa" ]; then
PRN=$(resolve_pr)
if grep -qiE '^[[:space:]]*APPROVE[[:space:]]*$' /tmp/agent_out.md; then
post_to "$ISSN" "$(printf '✅ Reviewed PR #%s — looks good.\n\n%s' "${PRN:-?}" "$reply")"
trig "$ISSN" "@pm — @qa approved PR #${PRN:-?} (issue #$ISSN). Over to you."
elif grep -qiE '^[[:space:]]*BOUNCE:[[:space:]]*@(junior|senior|lead)' /tmp/agent_out.md; then
dev=$(grep -oiE 'BOUNCE:[[:space:]]*@(junior|senior|lead)' /tmp/agent_out.md | head -1 | grep -oiE '(junior|senior|lead)' | tr '[:upper:]' '[:lower:]')
[ -z "$dev" ] && [ -n "$PRN" ] && dev=$(curl -sS "${hdr[@]}" "$API/pulls/$PRN" | jq -r '.user.login // "junior"')
dest="${PRN:-$NUM}"
post_to "$dest" "$reply" # recommendations, on the PR
# Bounce budget: count prior "fix attempt" markers on the PR thread; stop after 3.
prior=$(curl -sS "${hdr[@]}" "$API/issues/$dest/comments?limit=100" | jq -r 'if type=="array" then [.[]|select(.body|test("fix attempt"))]|length else 0 end' 2>/dev/null); prior=${prior:-0}
if [ "$prior" -ge 3 ]; then
[ "$AUTOPILOT" = "true" ] && del_autopilot_label "$ISSN"
post_to "$ISSN" "🛑 Still not right after 3 fix attempts on PR #${PRN:-?} — handing to @ffaerber (details on the PR)."
else
n=$((prior + 1))
trig "$dest" "@${dev:-junior} please address @qa's review above and update PR #${PRN:-?} (fix attempt $n/3)."
fi
elif grep -qiE '^[[:space:]]*HALT([_ ]AUTOPILOT)?[[:space:]]*$' /tmp/agent_out.md; then
[ "$AUTOPILOT" = "true" ] && del_autopilot_label "$ISSN"
post_to "$ISSN" "$(printf '🛑 This needs a human decision (not a dev fix) — @ffaerber please take a look.\n\n%s' "$reply")"
else
post_to "${PRN:-$NUM}" "$reply" # no verdict yet (a question) — post where qa works
fi
exit 0
fi
# ---------- @pm / @ops: issue-thread orchestration ----------
target=$(grep -oiE 'DELEGATE:[[:space:]]*@(junior|senior|lead|qa)' /tmp/agent_out.md 2>/dev/null | head -1 | grep -oiE '(junior|senior|lead|qa)' | tr '[:upper:]' '[:lower:]') target=$(grep -oiE 'DELEGATE:[[:space:]]*@(junior|senior|lead|qa)' /tmp/agent_out.md 2>/dev/null | head -1 | grep -oiE '(junior|senior|lead|qa)' | tr '[:upper:]' '[:lower:]')
# Visible comment: the reply text, or a sensible line if the agent only emitted a marker. # Visible comment: the reply text, or a sensible line if the agent only emitted a marker.
msg="$reply" msg="$reply"
@@ -127,92 +175,49 @@ if [ "$MODE" != "pr" ]; then
fi fi
post "$(printf '%s%s' "$msg" "$subtext")" post "$(printf '%s%s' "$msg" "$subtext")"
# --- AUTOPILOT: @qa's narrow, label-gated merge / halt authority --- # --- @pm autopilot merge: @pm is the ONLY agent that merges, and ONLY under the autopilot label ---
# Only @qa, only when 'autopilot' is set, and only on a PR thread. The MERGE_PR / HALT_AUTOPILOT # (@qa never merges — it approves and hands back here.) Merge with the PAT (TTOK), not the built-in
# markers come from the QA prompt. Merge + label ops use TOKEN_QA (the QA user's PAT, which the # token, so the push to main fires the deploy. TOKEN_PM must carry write:repository.
# maintainer must grant write+merge scope). ISSNUM is the origin issue (resolved from the branch). if [ "$NAME" = "pm" ] && [ "$AUTOPILOT" = "true" ] && grep -qiE '^[[:space:]]*MERGE_PR[[:space:]]*$' /tmp/agent_out.md; then
if [ "$NAME" = "qa" ] && [ "$AUTOPILOT" = "true" ]; then PRN=$(resolve_pr)
if grep -qiE '^[[:space:]]*MERGE_PR[[:space:]]*$' /tmp/agent_out.md; then if [ -z "$PRN" ]; then
if [ -z "$IS_PR" ]; then echo "MERGE_PR but no open PR found for issue #$ISSN"
echo "MERGE_PR marker but this run is not on a PR thread — skipping merge"
else else
echo "@qa autopilot: merging PR #$NUM (origin issue #${ISSNUM:-$NUM})" echo "@pm autopilot: merging PR #$PRN (issue #$ISSN)"
# Merge with TOK (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 TOK isn't set (then the deploy would need a manual run).
mtok="$TTOK"
mc=$(curl -sS -o /tmp/merge_resp.txt -w '%{http_code}' -X POST \ mc=$(curl -sS -o /tmp/merge_resp.txt -w '%{http_code}' -X POST \
-H "Authorization: token $mtok" -H "Content-Type: application/json" \ -H "Authorization: token $TTOK" -H "Content-Type: application/json" \
"$API/pulls/$NUM/merge" -d '{"Do":"merge"}') "$API/pulls/$PRN/merge" -d '{"Do":"merge"}')
echo "merge -> HTTP $mc"; cat /tmp/merge_resp.txt 2>/dev/null || true echo "merge -> HTTP $mc"; cat /tmp/merge_resp.txt 2>/dev/null || true
case "$mc" in case "$mc" in
200|201|204) 200|201|204)
echo "closing origin issue #${ISSNUM:-$NUM}" curl -sS -X PATCH "${hdr[@]}" "$API/issues/$ISSN" -d '{"state":"closed"}' -w '\nclose -> HTTP %{http_code}\n' || true
curl -sS -X PATCH "${hdr[@]}" "$API/issues/${ISSNUM:-$NUM}" \ post_to "$ISSN" "✅ Merged PR #$PRN (autopilot) and closed this issue." ;;
-d '{"state":"closed"}' -w '\nclose -> HTTP %{http_code}\n' || true
post "$(printf '✅ 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 del_autopilot_label "$ISSN"
# silently proceed: drop the label so it reverts to human control and report. post_to "$ISSN" "⚠️ Tried to merge PR #$PRN but the API returned HTTP $mc (checks not green, a conflict, or TOKEN_PM lacks merge scope). Removed the autopilot label — @ffaerber please take a look." ;;
del_autopilot_label "${ISSNUM:-$NUM}"
post "$(printf '⚠️ 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 esac
fi 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 '🛑 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 $TTOK" -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 '🛑 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 exit 0
fi fi
# Auto-delegate: if the plan names a teammate, trigger them via TOK (a PAT, so it # --- @pm delegation: hand the build to a dev, or hand the finished PR to @qa for review ---
# fires a new workflow run — the built-in token cannot). Never targets @pm or self, so the # Only an explicit 'DELEGATE: @<agent>' line acts (never a prose mention). Fires via the PAT (TTOK)
# chain always terminates at a dev. The '🤖' guard on the trigger stops status-comment loops. # so a new run starts; the built-in token cannot. Everything posts on the ISSUE — @pm never touches
if [ -n "$TTOK" ]; then # the PR. Chain terminates: normal → @pm tells the creator (no marker); autopilot → @pm merges above.
# Only delegate on an explicit "DELEGATE: @<agent>" line — never on a prose mention,
# so an agent that is asking the maintainer a question does not hand off prematurely.
target=$(grep -oiE 'DELEGATE:[[:space:]]*@(junior|senior|lead|qa)' /tmp/agent_out.md 2>/dev/null \
| head -1 | grep -oiE '(junior|senior|lead|qa)' | tr '[:upper:]' '[:lower:]')
if [ -n "$target" ] && [ "$target" != "$NAME" ]; then if [ -n "$target" ] && [ "$target" != "$NAME" ]; then
echo "auto-delegating to @$target" if [ "$target" = "qa" ]; then
curl -sS -X POST -H "Authorization: token $TTOK" -H "Content-Type: application/json" \ PRN=$(resolve_pr)
"$API/issues/$NUM/comments" \ if [ -n "$PRN" ]; then
-d "$(jq -nc --arg b "@$target please proceed with issue #$NUM per the plan above (delegated by $NAME)." '{body:$b}')" \ trig "$ISSN" "@qa please review PR #$PRN for issue #$ISSN — put your recommendations on the PR, or approve."
-w '\ndelegate -> HTTP %{http_code}\n' || true else
echo "DELEGATE:@qa but no open PR yet for issue #$ISSN — not firing"
fi
else
trig "$ISSN" "@$target please proceed with issue #$ISSN per the plan above (delegated by $NAME)."
fi
else else
echo "no DELEGATE marker — not delegating (agent is asking or finished)" echo "no DELEGATE marker — not delegating (agent is asking or finished)"
fi fi
fi
exit 0 exit 0
fi fi
@@ -288,32 +293,15 @@ prpost() {
} }
if [ "$NEW" = "true" ]; then if [ "$NEW" = "true" ]; then
prpost "$prnum" "$(printf '✅ PR ready for review — @ffaerber please review & merge:\n- %s%s' "$url" "$activity")" # First PR for this issue: record it on the PR thread, then notify @pm on the ISSUE. @pm never
# AUTOPILOT: hand the fresh PR to @qa automatically (via TOK, so it fires a new run). # reads the PR, so the issue gets only this one-line ping — @pm then routes it to @qa for review.
# @qa then verifies and — if green — merges + closes via its MERGE_PR marker. The comment lands prpost "$prnum" "$(printf 'Opened PR #%s for review.%s' "$prnum" "$activity")"
# on the PR thread ($prnum) so the next run resolves the origin issue's label from the branch trig "$ISSN" "@pm — PR #$prnum is ready for review (issue #$ISSN)."
# name. The '🤖' guard on the trigger gate stops status-comment loops.
if [ "$AUTOPILOT" = "true" ] && [ -n "$TTOK" ] && [ -n "$prnum" ]; then
echo "autopilot: auto-triggering @qa to review PR #$prnum"
curl -sS -X POST -H "Authorization: token $TTOK" -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 # A fix (usually after a @qa bounce): update the PR and hand straight back to @qa to re-verify,
# repeat the full write-up in the comment (the reasoning trail below shows what this run did). # on the PR thread. The qa↔dev loop is direct — it does NOT go back through @pm each round.
prpost "$prnum" "$(printf 'Pushed an update to the PR:\n- %s%s' "$url" "$activity")" prpost "$prnum" "$(printf 'Pushed an update to PR #%s.%s' "$prnum" "$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 "$TTOK" ] && [ -n "$prnum" ]; then
case "$NAME" in case "$NAME" in
junior|senior|lead) junior|senior|lead) trig "$prnum" "@qa please re-verify PR #$prnum — the dev has pushed an update." ;;
echo "autopilot: dev pushed a fix — re-triggering @qa to re-verify PR #$prnum"
curl -sS -X POST -H "Authorization: token $TTOK" -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 esac
fi fi
fi
+45 -40
View File
@@ -35,58 +35,63 @@ if [ "$MODE" = "comment" ]; then
obviously not needed; when in doubt, ask instead." obviously not needed; when in doubt, ask instead."
if [ "$NAME" = "pm" ]; then if [ "$NAME" = "pm" ]; then
ACTION="$ACTION ACTION="$ACTION
As PM you work in two phases and NEVER skip the approval gate: As PM you ORCHESTRATE this issue from the ISSUE THREAD ONLY — you never read or comment on the PR
PLAN — when the task is clear, present a SHORT plan naming which teammate should build it (keep your context on the issue). Read the thread and act for the CURRENT phase:
(@junior for small/low-risk, @senior/@lead for complex, @qa to verify), then END by asking
'@ffaerber ready to start building? reply yes to proceed.' Do NOT include a DELEGATE line yet. PHASE 1 — PLAN (a fresh request; no dev is building yet). Present a SHORT plan naming which
DELEGATE — ONLY after the maintainer has explicitly approved starting in the thread (a clear teammate should build it (@junior small/low-risk YAML/compose/config; @senior/@lead complex or
'yes' / 'go' / 'proceed' / 'start building' answering your ready-to-build question) do you end multi-file). Then END by asking '@ffaerber ready to start building? reply yes to proceed.' — do
your reply with a 'DELEGATE: @<agent>' line to hand off. NOT delegate yet. ONLY after an explicit 'yes'/'go'/'proceed' do you end a reply with a
Never present a plan and delegate on the same turn. If anything is unclear or needs a decision, 'DELEGATE: @<dev>' line to hand off. Never plan and delegate in the same reply.
START your reply with '@ffaerber', ask specific questions, and do NOT delegate.
BREAKDOWN (for a feature too big for one PR): first PLAN — propose a milestone name and the list PHASE 2 — REVIEW (a dev has reported 'PR #<n> is ready'). Do NOT re-plan. Briefly acknowledge and
of sub-tasks (title + one line each), then ask '@ffaerber create these N sub-issues? reply yes.' hand the PR to QA: end your reply with EXACTLY 'DELEGATE: @qa'. (The automation tells @qa which PR
Do NOT emit the block yet. ONLY after the maintainer approves, end your reply with EXACTLY: to review; @qa reviews it on the PR, not here — you never see the diff.)
PHASE 3 — FINALIZE (@qa has reported the PR is approved / 'code OK'). Tell the issue creator it is
ready: e.g. 'PR #<n> is reviewed and ready to merge, @ffaerber.' Do NOT delegate and do NOT merge —
the human merges.
If anything is unclear or needs a decision at any phase, START your reply with '@ffaerber', ask
specific questions, and do NOT emit a marker. Mentioning a teammate in prose does NOT act — only a
DELEGATE line does.
BREAKDOWN (a feature too big for one PR): in PHASE 1, propose a milestone name and the sub-task
list, then ask '@ffaerber create these N sub-issues? reply yes.' ONLY after approval, end with:
BEGIN_SUBTASKS BEGIN_SUBTASKS
milestone: <feature name> milestone: <feature name>
- <task title> :: <one-line description> - <task title> :: <one-line description>
- <task title> :: <one-line description> - <task title> :: <one-line description>
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 here); it does NOT
does NOT auto-start any dev — the maintainer @mentions an agent on each sub-issue when ready." auto-start any dev — the maintainer @mentions an agent on each sub-issue when ready."
if [ "$AUTOPILOT" = "true" ]; then if [ "$AUTOPILOT" = "true" ]; then
ACTION="$ACTION ACTION="$ACTION
AUTOPILOT MODE IS ACTIVE (this issue carries the 'autopilot' label). This OVERRIDES the AUTOPILOT MODE IS ACTIVE (this issue carries the 'autopilot' label) — it changes exactly TWO
two-phase approval gate above: do NOT ask '@ffaerber ready to start building?' and do NOT wait things for you; everything else above is unchanged:
for a 'yes'. When the task is clear, present your SHORT plan naming the best teammate to build it - PHASE 1: do NOT ask '@ffaerber ready to build?'. Present your SHORT plan AND end with a
AND end your reply with a 'DELEGATE: @<agent>' line in the SAME turn to hand off immediately. 'DELEGATE: @<dev>' line in the SAME reply. Only skip delegating (and ask @ffaerber) if the task
Prefer @junior for small/low-risk (mostly YAML/compose/config), @senior/@lead for complex or is genuinely ambiguous or unsafe.
multi-file work. Only skip delegating (and instead ask @ffaerber) if the task is genuinely - PHASE 3: do NOT ask the human to merge. When @qa has approved, end your reply with EXACTLY
ambiguous or unsafe — otherwise plan-and-delegate now." 'MERGE_PR' — the automation merges the PR and closes this issue. You are the ONLY agent that
merges, and only here."
fi fi
fi fi
if [ "$NAME" = "qa" ]; then if [ "$NAME" = "qa" ]; then
ACTION="$ACTION ACTION="$ACTION
As QA you verify a change works: read the PR/issue, drive the web app with your headless As QA you are the REVIEWER — you NEVER edit code and NEVER merge. @pm points you at a PR; review
browser if there is a URL, and report bugs or confirm behavior. You normally do NOT merge — it: read the diff, drive the web app with your headless browser if there is a URL, and put your
a human does that." detailed, specific recommendations ON THE PR (the automation posts your reply to the PR thread).
if [ "$AUTOPILOT" = "true" ]; then After actually verifying, end your reply with EXACTLY one of:
ACTION="$ACTION - 'APPROVE' — the change is correct and any CI is green. The automation records your verdict on the
AUTOPILOT MODE IS ACTIVE (this issue/PR carries the 'autopilot' label). You are the quality gate. issue and hands back to @pm (who tells the creator, or in autopilot merges). You do NOT merge.
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 - '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 what to change (file, label, value, hostname, ), THEN end with the BOUNCE line naming who fixes
line naming who should fix it (@junior / @senior / @lead — usually whoever built it; @senior or it (@junior / @senior / @lead — usually whoever built it). The automation sends the PR back and
@lead for something harder). The automation sends the PR back to that dev and then re-verifies re-verifies with you. After 3 rounds it stops and hands to @ffaerber — so list ALL problems at
with you. After 3 bounces it stops automatically and hands to @ffaerber — so make each round once, not one at a time.
count and list ALL problems at once, not one at a time. - 'HALT' — the problem is NOT something a dev can fix (the request is ambiguous / needs a human
Use BOUNCE for anything a dev can fix. Only use 'HALT_AUTOPILOT' when the problem is NOT fixable by decision). Hands back to @ffaerber.
a dev — the request itself is ambiguous or needs a human decision — to hand back to @ffaerber. Emit AT MOST one marker, and only after you have actually verified."
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.