agents: retro loop — "@pm retro" distills learnings into LEARNINGS.md
ci / lint (push) Skipped
ci / lint (pull_request) Successful in 11s

New learning step: ask @pm for a retrospective on any issue and the system
turns what happened into prompt-visible rules for future runs.

- run-agent.sh: @pm gains a RETRO marker (emit only when the maintainer asks);
  LEARNINGS.md (caller repo root, capped at 4KB) is injected into EVERY agent's
  prompt as "TEAM LEARNINGS" — the feedback loop that makes delegation more
  robust over time.
- publish.sh: on @pm's RETRO marker, open a "retro: issue #N" issue pointing at
  the issue + its PR (state=all resolve, works after merge) and trigger @senior
  on it (has gitea-api to read both threads). The retro produces a LEARNINGS.md
  PR through the NORMAL choreography (senior → pm → qa), so retros are reviewed
  like any change. Strip the RETRO marker from visible replies.
- publish.sh: bounce counter now counts only @qa-authored comments matching the
  exact trigger template — on PR #84 it jumped 1/3 → 3/3 because a qa review
  QUOTED our own "(fix attempt …)" template from the diff, halving the fix
  budget. Template + regex pinned together with a sync note.
- README: document the retro loop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Felix Faerber
2026-07-06 11:02:19 +03:00
co-authored by Claude Opus 4.8
parent 950c639b0a
commit b40f547740
3 changed files with 55 additions and 4 deletions
+31 -4
View File
@@ -60,8 +60,8 @@ del_autopilot_label() {
fi
}
# 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
# drop machine-readable markers: DELEGATE / CLOSE_ISSUE / MERGE_PR / RETRO / APPROVE / HALT / BOUNCE,
# 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}
@@ -69,6 +69,7 @@ reply=$(awk '
/^[[:space:]]*DELEGATE:[[:space:]]*@/{next}
/^[[:space:]]*CLOSE_ISSUE[[:space:]]*$/{next}
/^[[:space:]]*MERGE_PR[[:space:]]*$/{next}
/^[[:space:]]*RETRO[[:space:]]*$/{next}
/^[[:space:]]*APPROVE[[:space:]]*$/{next}
/^[[:space:]]*HALT([_ ]AUTOPILOT)?[[:space:]]*$/{next}
/^[[:space:]]*BOUNCE:[[:space:]]*@/{next}
@@ -113,13 +114,18 @@ if [ "$MODE" != "pr" ]; then
[ -z "$dev" ] && [ -n "$PRN" ] && dev=$(curl -sS "${hdr[@]}" "$API/pulls/$PRN" | jq -r '.user.login // "junior"')
dest="${PRN:-$NUM}"
post_to "$dest" "$reply$activity" # 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}
# Bounce budget: count prior bounce TRIGGERS on the PR thread — only @qa-authored comments
# matching the exact "(fix attempt N/3)" template. A loose substring match would also count
# review text QUOTING our own templates (seen on PR #84: the counter jumped 1/3 → 3/3 because
# a qa review quoted publish.sh lines containing the phrase), halving the fix budget.
prior=$(curl -sS "${hdr[@]}" "$API/issues/$dest/comments?limit=100" | jq -r 'if type=="array" then [.[]|select(.user.login=="qa")|select(.body|test("^@[a-z]+ please address my review above and update PR #[0-9?]+ \\(fix attempt [0-9]+/3\\)\\.$"))]|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))
# NOTE: this template and the counter regex above MUST stay in sync — if you reword one,
# reword the other, or the count resets to 0 and the 3-round cap stops working.
trig "$dest" "@${dev:-junior} please address my review above and update PR #${PRN:-?} (fix attempt $n/3)."
fi
elif grep -qiE '^[[:space:]]*HALT([_ ]AUTOPILOT)?[[:space:]]*$' /tmp/agent_out.md; then
@@ -203,6 +209,27 @@ if [ "$MODE" != "pr" ]; then
exit 0
fi
# --- @pm RETRO: open a retrospective issue for this thread (maintainer asked for a retro) ---
# Creates a retro issue pointing at this issue + its PR and triggers @senior on it (has gitea-api
# to read both threads). The retro produces a LEARNINGS.md PR via the NORMAL choreography (senior →
# pm → qa → merge), and run-agent.sh injects LEARNINGS.md into every future prompt — closing the loop.
if [ "$NAME" = "pm" ] && grep -qiE '^[[:space:]]*RETRO[[:space:]]*$' /tmp/agent_out.md; then
PRN=$(curl -sS "${hdr[@]}" "$API/pulls?state=all&limit=50" \
| jq -r --arg br "ai/issue-$ISSN" 'if type=="array" then ([.[]|select(.head.ref==$br)] | sort_by(.number) | last | .number // empty) else empty end' 2>/dev/null)
rbody=$(printf 'Retrospective for issue #%s%s.\n\nRead the FULL issue thread%s using the gitea-api skill (issue comments%s and the PR diff). Identify what went wrong, slow, or needed human correction — missed wiring, review misses, bounced rounds, unclear delegation, missing context.\n\nThen APPEND the distilled learnings to `LEARNINGS.md` at the repo root (create it with a short header if missing). Rules for entries:\n- 3 to 6 bullets max, each ONE line: `symptom -> rule for next time`.\n- Concrete and checkable (name the file/step/marker), not generic advice.\n- Do not repeat an existing bullet; refine it instead.\n- Do not rewrite unrelated parts of the file.\n\nThese learnings are injected into every future agent prompt, so quality over quantity.' \
"$ISSN" "${PRN:+ / PR #$PRN}" "${PRN:+ and PR #$PRN thread}" "${PRN:+, PR comments}")
rnum=$(curl -sS -X POST "${hdr[@]}" "$API/issues" \
-d "$(jq -nc --arg t "retro: issue #$ISSN" --arg b "$rbody" '{title:$t,body:$b}')" | jq -r '.number // empty')
if [ -n "$rnum" ]; then
echo "opened retro issue #$rnum"
post_to "$ISSN" "📝 Opened retro issue #$rnum."
trig "$rnum" "@senior please run this retrospective per the issue body."
else
echo "retro issue creation failed"
fi
exit 0
fi
# --- @pm delegation: hand the build to a dev, or hand the finished PR to @qa for review ---
# Only an explicit 'DELEGATE: @<agent>' line acts (never a prose mention). Fires via the PAT (TTOK)
# so a new run starts; the built-in token cannot. Everything posts on the ISSUE — @pm never touches
+15
View File
@@ -55,6 +55,10 @@ if [ "$MODE" = "comment" ]; then
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.
RETRO — when the maintainer asks for a retrospective on this issue (e.g. 'run a retro',
'@pm retro'), briefly acknowledge and end your reply with EXACTLY one line: 'RETRO'. The
automation opens a retro issue (read this issue + its PR, distill learnings into LEARNINGS.md)
and assigns it. Emit RETRO only when explicitly asked.
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
@@ -102,10 +106,21 @@ else
open pull requests yourself — that is automated for every branch you push.
If the task is genuinely unclear, make NO changes and reply with specific questions instead."
fi
# TEAM LEARNINGS — distilled from past retros (see the RETRO flow in publish.sh). Lives at the
# CALLER repo root as LEARNINGS.md, maintained by retro PRs. Injected into EVERY agent's prompt
# (capped) so past mistakes actually change future behavior — this is the feedback loop.
LEARN=""
[ -s LEARNINGS.md ] && LEARN=$(head -c 4000 LEARNINGS.md)
[ -n "$LEARN" ] && LEARN="
TEAM LEARNINGS (distilled from past retros in this repo — APPLY them; they exist because a
previous task went wrong without them):
${LEARN}
"
PROMPT="You are @${NAME}, a member of an AI dev team working on this Gitea repository.
YOUR ROLE: ${DESC}
YOUR CAPABILITIES: model ${MODEL}. ${CAP}
${NOTE}
${LEARN}
Your reply is posted as a comment already attributed to you (@${NAME}) — your name and avatar are
shown by Gitea. Do NOT begin your reply with your own name, an '@${NAME}' header, or a '🤖/🔨 @you'