agents: retro loop — @pm retro distills learnings into LEARNINGS.md #85

Merged
ffaerber merged 1 commits from feat/retro-learnings into main 2026-07-06 10:03:17 +02:00
3 changed files with 55 additions and 4 deletions
+31 -4
View File
@@ -60,8 +60,8 @@ del_autopilot_label() {
fi fi
} }
# drop machine-readable markers: DELEGATE / CLOSE_ISSUE / MERGE_PR / APPROVE / HALT / BOUNCE, and the # drop machine-readable markers: DELEGATE / CLOSE_ISSUE / MERGE_PR / RETRO / APPROVE / HALT / BOUNCE,
# BEGIN_SUBTASKS..END_SUBTASKS and BEGIN_PR_DESCRIPTION..END_PR_DESCRIPTION blocks (the PR # and the 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 '
/^[[:space:]]*BEGIN_SUBTASKS/{s=1} /^[[:space:]]*BEGIN_SUBTASKS/{s=1}
@@ -69,6 +69,7 @@ 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:]]*RETRO[[:space:]]*$/{next}
/^[[:space:]]*APPROVE[[:space:]]*$/{next} /^[[:space:]]*APPROVE[[:space:]]*$/{next}
/^[[:space:]]*HALT([_ ]AUTOPILOT)?[[:space:]]*$/{next} /^[[:space:]]*HALT([_ ]AUTOPILOT)?[[:space:]]*$/{next}
/^[[:space:]]*BOUNCE:[[: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"') [ -z "$dev" ] && [ -n "$PRN" ] && dev=$(curl -sS "${hdr[@]}" "$API/pulls/$PRN" | jq -r '.user.login // "junior"')
dest="${PRN:-$NUM}" dest="${PRN:-$NUM}"
post_to "$dest" "$reply$activity" # recommendations, on the PR post_to "$dest" "$reply$activity" # recommendations, on the PR
# Bounce budget: count prior "fix attempt" markers on the PR thread; stop after 3. # Bounce budget: count prior bounce TRIGGERS on the PR thread — only @qa-authored comments
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} # 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 if [ "$prior" -ge 3 ]; then
[ "$AUTOPILOT" = "true" ] && del_autopilot_label "$ISSN" [ "$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)." post_to "$ISSN" "🛑 Still not right after 3 fix attempts on PR #${PRN:-?} — handing to @ffaerber (details on the PR)."
else else
n=$((prior + 1)) 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)." trig "$dest" "@${dev:-junior} please address my review above and update PR #${PRN:-?} (fix attempt $n/3)."
fi fi
elif grep -qiE '^[[:space:]]*HALT([_ ]AUTOPILOT)?[[:space:]]*$' /tmp/agent_out.md; then elif grep -qiE '^[[:space:]]*HALT([_ ]AUTOPILOT)?[[:space:]]*$' /tmp/agent_out.md; then
@@ -203,6 +209,27 @@ if [ "$MODE" != "pr" ]; then
exit 0 exit 0
fi 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 --- # --- @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) # 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 # 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 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 specific questions, and do NOT emit a marker. Mentioning a teammate in prose does NOT act — only a
DELEGATE line does. 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 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: list, then ask '@ffaerber create these N sub-issues? reply yes.' ONLY after approval, end with:
BEGIN_SUBTASKS BEGIN_SUBTASKS
@@ -102,10 +106,21 @@ else
open pull requests yourself — that is automated for every branch you push. 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." If the task is genuinely unclear, make NO changes and reply with specific questions instead."
fi 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. PROMPT="You are @${NAME}, a member of an AI dev team working on this Gitea repository.
YOUR ROLE: ${DESC} YOUR ROLE: ${DESC}
YOUR CAPABILITIES: model ${MODEL}. ${CAP} YOUR CAPABILITIES: model ${MODEL}. ${CAP}
${NOTE} ${NOTE}
${LEARN}
Your reply is posted as a comment already attributed to you (@${NAME}) — your name and avatar are 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' shown by Gitea. Do NOT begin your reply with your own name, an '@${NAME}' header, or a '🤖/🔨 @you'
+9
View File
@@ -35,6 +35,15 @@ deploy-host SSH skill, live in the consuming repo under `.gitea/agent-skills/`
`@pm` is the only agent that ever merges, and only under the `autopilot` label (its kill switch: `@pm` is the only agent that ever merges, and only under the `autopilot` label (its kill switch:
remove the label mid-flight and the next step reverts to human control). remove the label mid-flight and the next step reverts to human control).
### Retros — the learning loop
Ask `@pm` for a retrospective on any issue (e.g. **"@pm run a retro"**, typically when merging). The
automation opens a `retro: issue #N` issue and assigns `@senior`, who reads the full issue + PR
threads (via the `gitea-api` skill), distills what went wrong or slow, and appends one-line
`symptom → rule` bullets to **`LEARNINGS.md`** at the repo root — through the normal PR choreography,
so the retro itself gets reviewed. `LEARNINGS.md` is injected into **every agent's prompt** on every
run, so the lessons actually change future behavior (better delegation, fewer repeated misses).
### Per-agent skill scoping ### Per-agent skill scoping
Skills load **on-demand**: only a skill's one-line `description` ever appears in an agent's Skills load **on-demand**: only a skill's one-line `description` ever appears in an agent's