Merge pull request 'agents: retro loop — @pm retro distills learnings into LEARNINGS.md' (#85) from feat/retro-learnings into main
ci / lint (push) Successful in 11s
ci / lint (push) Successful in 11s
This commit was merged in pull request #85.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user