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>
308 lines
18 KiB
Bash
Executable File
308 lines
18 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Publish — PR (dev agents) or comment (pm/qa), always reply in the issue.
|
|
#
|
|
# Required env (provided by the workflow step):
|
|
# GT TOKEN_PM TOKEN_SENIOR TOKEN_JUNIOR TOKEN_LEAD TOKEN_QA
|
|
# 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
|
|
# Post/PR as the agent's OWN Gitea user when its token is configured; else the built-in bot.
|
|
case "$NAME" in
|
|
pm) TOK="$TOKEN_PM";; senior) TOK="$TOKEN_SENIOR";; junior) TOK="$TOKEN_JUNIOR";;
|
|
lead) TOK="$TOKEN_LEAD";; qa) TOK="$TOKEN_QA";; ops) TOK="$TOKEN_OPS";; *) TOK="";;
|
|
esac
|
|
[ -z "$TOK" ] && TOK="$GT"
|
|
# Trigger token: comments that must FIRE the next workflow (delegation, autopilot) and PR merges
|
|
# cannot use the built-in GITEA_TOKEN (Gitea won't start new runs from it) — they need a real PAT.
|
|
# Every agent now has its own token, so TTOK is just the agent's token. If an agent somehow has none
|
|
# (TOK fell back to the built-in GT), TTOK is left empty so the trigger/merge is skipped rather than
|
|
# silently no-op'ing under the built-in token.
|
|
TTOK="$TOK"
|
|
[ "$TTOK" = "$GT" ] && TTOK=""
|
|
git config user.name "$NAME"
|
|
git config user.email "$NAME@ffaerber.duckdns.org"
|
|
API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
|
hdr=(-H "Authorization: token $TOK" -H "Content-Type: application/json")
|
|
# Hidden loop-prevention marker appended to every agent REPLY/STATUS comment. Gitea already shows
|
|
# who authored a comment, so we don't repeat the agent's name in the body; but the trigger gate keys
|
|
# on the '🤖' character to know "this is an agent's own comment, don't fire a new run". An HTML
|
|
# comment renders as nothing, so the marker is invisible while still tripping the gate's guard.
|
|
# NOTE: trigger comments (delegation / autopilot / bounce) are posted with inline curl, NOT post()/
|
|
# prpost(), so they never get this marker and therefore DO fire the next run — that is intended.
|
|
MARK=$'\n\n<!-- 🤖 agent reply — do not trigger -->'
|
|
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}')"; }
|
|
# 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
|
|
# 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 / MERGE_PR / 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}
|
|
/^[[:space:]]*BEGIN_PR_DESCRIPTION/{p=1}
|
|
/^[[:space:]]*DELEGATE:[[:space:]]*@/{next}
|
|
/^[[:space:]]*CLOSE_ISSUE[[:space:]]*$/{next}
|
|
/^[[:space:]]*MERGE_PR[[:space:]]*$/{next}
|
|
/^[[:space:]]*APPROVE[[:space:]]*$/{next}
|
|
/^[[:space:]]*HALT([_ ]AUTOPILOT)?[[:space:]]*$/{next}
|
|
/^[[:space:]]*BOUNCE:[[:space:]]*@/{next}
|
|
s{ if(/^[[:space:]]*END_SUBTASKS/){s=0}; next }
|
|
p{ if(/^[[:space:]]*END_PR_DESCRIPTION/){p=0}; next }
|
|
{print}
|
|
' /tmp/agent_out.md 2>/dev/null)
|
|
# Strip a leading self-identification header the model sometimes emits, e.g. "🤖 **@pm**",
|
|
# "🔨 **@senior**", or a heading like "## 🔨 @senior — <title>". Gitea already attributes the comment
|
|
# to its author, so we drop any leading line that references the agent's OWN @handle — or a bare
|
|
# "**@name**" line — together with surrounding blank lines, up to the first real content line.
|
|
reply=$(printf '%s' "$reply" | awk -v me="@$NAME" '
|
|
BEGIN{s=1}
|
|
s && /^[[:space:]]*$/ {next}
|
|
s && index($0, me) {next}
|
|
s && /^[^A-Za-z0-9]*\*\*@[A-Za-z]+\*\*[[:space:]]*$/ {next}
|
|
{s=0; print}
|
|
')
|
|
[ -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.
|
|
prdesc=$(awk '/BEGIN_PR_DESCRIPTION/{f=1;next} /END_PR_DESCRIPTION/{f=0} f' /tmp/agent_out.md)
|
|
[ -z "$prdesc" ] && prdesc="$reply"
|
|
|
|
# comment-only roles (pm/qa): never change files
|
|
if [ "$MODE" != "pr" ]; then
|
|
git checkout -- . 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:]')
|
|
# Visible comment: the reply text, or a sensible line if the agent only emitted a marker.
|
|
msg="$reply"
|
|
case "$msg" in ""|"_(Made changes"*) msg=$([ -n "$target" ] && echo "Handing off to @$target." || echo "_(no further comment)_") ;; esac
|
|
# Close the issue if the agent flagged it (maintainer said it's not needed / duplicate).
|
|
if grep -qiE '^[[:space:]]*CLOSE_ISSUE[[:space:]]*$' /tmp/agent_out.md; then
|
|
echo "closing issue #$NUM"
|
|
curl -sS -X PATCH "${hdr[@]}" "$API/issues/$NUM" \
|
|
-d '{"state":"closed"}' -w '\nclose -> HTTP %{http_code}\n' || true
|
|
fi
|
|
# BREAKDOWN: from a BEGIN_SUBTASKS block, create a milestone + one sub-issue per line
|
|
# (linked to this issue). Sub-issues are NOT auto-started — maintainer mentions agents later.
|
|
# Process subtasks first so we can append the created-issues list to the SAME comment as
|
|
# the reply (issue #38 — one comment per run).
|
|
subtext=""
|
|
if grep -qiE '^[[:space:]]*BEGIN_SUBTASKS' /tmp/agent_out.md; then
|
|
block=$(awk '/^[[:space:]]*BEGIN_SUBTASKS/{f=1;next} /^[[:space:]]*END_SUBTASKS/{f=0} f' /tmp/agent_out.md)
|
|
ms=$(printf '%s\n' "$block" | sed -nE 's/^[[:space:]]*milestone:[[:space:]]*//Ip' | head -1)
|
|
msid=""
|
|
if [ -n "$ms" ]; then
|
|
msid=$(curl -sS "${hdr[@]}" "$API/milestones?state=open&limit=100" | jq -r --arg t "$ms" 'if type=="array" then ([.[]|select(.title==$t)][0].id // empty) else empty end')
|
|
[ -z "$msid" ] && msid=$(curl -sS -X POST "${hdr[@]}" "$API/milestones" -d "$(jq -nc --arg t "$ms" '{title:$t}')" | jq -r '.id // empty')
|
|
echo "milestone '$ms' -> id ${msid:-?}"
|
|
fi
|
|
printf '%s\n' "$block" | grep -E '^[[:space:]]*-[[:space:]]' > /tmp/subtasks.txt || true
|
|
links=""
|
|
while IFS= read -r line; do
|
|
item=$(printf '%s' "$line" | sed -E 's/^[[:space:]]*-[[:space:]]*//')
|
|
title=${item%%::*}; body=${item#*::}; [ "$body" = "$item" ] && body=""
|
|
title=$(printf '%s' "$title" | sed -E 's/[[:space:]]*$//')
|
|
body=$(printf '%s' "$body" | sed -E 's/^[[:space:]]*//')
|
|
[ -z "$title" ] && continue
|
|
ibody=$(printf 'Part of #%s\n\n%s' "$NUM" "$body")
|
|
if [ -n "$msid" ]; then
|
|
payload=$(jq -nc --arg t "$title" --arg b "$ibody" --argjson m "$msid" '{title:$t,body:$b,milestone:$m}')
|
|
else
|
|
payload=$(jq -nc --arg t "$title" --arg b "$ibody" '{title:$t,body:$b}')
|
|
fi
|
|
n=$(curl -sS -X POST "${hdr[@]}" "$API/issues" -d "$payload" | jq -r '.number // empty')
|
|
echo "created sub-issue #${n:-?}: $title"
|
|
[ -n "$n" ] && links="$links\n- #$n — $title"
|
|
done < /tmp/subtasks.txt
|
|
subtext=$(printf '\n\n---\nCreated sub-issues%s (mention an agent on each when ready):%b' "${ms:+ under milestone **$ms**}" "$links")
|
|
fi
|
|
post "$(printf '%s%s' "$msg" "$subtext")"
|
|
|
|
# --- @pm autopilot merge: @pm is the ONLY agent that merges, and ONLY under the autopilot label ---
|
|
# (@qa never merges — it approves and hands back here.) Merge with the PAT (TTOK), not the built-in
|
|
# token, so the push to main fires the deploy. TOKEN_PM must carry write:repository.
|
|
if [ "$NAME" = "pm" ] && [ "$AUTOPILOT" = "true" ] && grep -qiE '^[[:space:]]*MERGE_PR[[:space:]]*$' /tmp/agent_out.md; then
|
|
PRN=$(resolve_pr)
|
|
if [ -z "$PRN" ]; then
|
|
echo "MERGE_PR but no open PR found for issue #$ISSN"
|
|
else
|
|
echo "@pm autopilot: merging PR #$PRN (issue #$ISSN)"
|
|
mc=$(curl -sS -o /tmp/merge_resp.txt -w '%{http_code}' -X POST \
|
|
-H "Authorization: token $TTOK" -H "Content-Type: application/json" \
|
|
"$API/pulls/$PRN/merge" -d '{"Do":"merge"}')
|
|
echo "merge -> HTTP $mc"; cat /tmp/merge_resp.txt 2>/dev/null || true
|
|
case "$mc" in
|
|
200|201|204)
|
|
curl -sS -X PATCH "${hdr[@]}" "$API/issues/$ISSN" -d '{"state":"closed"}' -w '\nclose -> HTTP %{http_code}\n' || true
|
|
post_to "$ISSN" "✅ Merged PR #$PRN (autopilot) and closed this issue." ;;
|
|
*)
|
|
del_autopilot_label "$ISSN"
|
|
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." ;;
|
|
esac
|
|
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
|
|
# the PR. Chain terminates: normal → @pm tells the creator (no marker); autopilot → @pm merges above.
|
|
if [ -n "$target" ] && [ "$target" != "$NAME" ]; then
|
|
if [ "$target" = "qa" ]; then
|
|
PRN=$(resolve_pr)
|
|
if [ -n "$PRN" ]; then
|
|
trig "$ISSN" "@qa please review PR #$PRN for issue #$ISSN — put your recommendations on the PR, or approve."
|
|
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
|
|
echo "no DELEGATE marker — not delegating (agent is asking or finished)"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# Scrub the runtime scripts checkout (.agents-workflow) from the tree so it never lands in a
|
|
# commit/PR and never confuses the git ops below (issue #33). The scripts we run live outside the
|
|
# workspace ($SCRIPTS -> runner.temp), so removing this in-tree copy is always safe. Handle every
|
|
# way an agent might have left it: untracked dir, tracked files, or a committed gitlink/submodule.
|
|
if git ls-files --error-unmatch .agents-workflow >/dev/null 2>&1 || \
|
|
[ -n "$(git ls-files .agents-workflow 2>/dev/null)" ]; then
|
|
git rm -r --cached --quiet --ignore-unmatch .agents-workflow 2>/dev/null || true
|
|
fi
|
|
git config -f .gitmodules --remove-section submodule..agents-workflow 2>/dev/null || true
|
|
[ -s .gitmodules ] || rm -f .gitmodules 2>/dev/null || true
|
|
rm -rf .agents-workflow 2>/dev/null || true
|
|
|
|
# The agent may have committed on the starting branch AND/OR created extra
|
|
# ai/issue-N-<slug> branches. Commit any leftover on the current branch, push it, then
|
|
# open a PR for EVERY ai/issue-N* branch that has commits beyond main.
|
|
if [ -n "$(git status --porcelain)" ]; then
|
|
git add -A
|
|
git commit -m "@$NAME: issue #$NUM"
|
|
fi
|
|
git push origin "HEAD:$BRANCH" || true
|
|
git fetch -q origin 2>/dev/null || true
|
|
|
|
prbody=$(printf '%s\n\n---\nResolves #%s' "$prdesc" "$NUM")
|
|
owner=${GITHUB_REPOSITORY%%/*}
|
|
|
|
# Post the agent's activity trail (tool calls + reasoning) inline in the same comment so
|
|
# each run produces exactly ONE comment (issue #38). Computed once here so every dev-agent
|
|
# exit path (no-changes, PR-open-failed, normal) appends it to the single reply comment.
|
|
activity=""
|
|
if [ -s /tmp/activity_log.md ]; then
|
|
entries=$(wc -l < /tmp/activity_log.md 2>/dev/null || echo 0)
|
|
log=$(cat /tmp/activity_log.md)
|
|
activity=$(printf '\n\n<details>\n<summary>🔧 activity — %s tool calls</summary>\n\n%s\n\n</details>' "$entries" "$log")
|
|
fi
|
|
|
|
# One PR per run: publish ONLY this run's own branch ($BRANCH), never sibling
|
|
# ai/issue-N-* branches. This removes the multi-PR ambiguity that left the
|
|
# activity log stranded on the triggering issue instead of the PR thread.
|
|
br="$BRANCH"
|
|
ahead=$(git rev-list --count "origin/main..origin/$br" 2>/dev/null || echo 0)
|
|
if [ "${ahead:-0}" -eq 0 ]; then
|
|
# No changes on this branch — a plan / questions / analysis only.
|
|
post "$(printf '%s%s' "$reply" "$activity")"
|
|
exit 0
|
|
fi
|
|
|
|
# NOTE: Gitea ignores the ?head= filter, so match the head branch client-side.
|
|
resp=$(curl -sS "${hdr[@]}" "$API/pulls?state=open&limit=50" \
|
|
| jq -r --arg br "$br" 'if type=="array" then (map(select(.head.ref==$br)) | .[0] // empty) else empty end' 2>/dev/null)
|
|
url=$(printf '%s' "$resp" | jq -r '.html_url // empty' 2>/dev/null)
|
|
prnum=$(printf '%s' "$resp" | jq -r '.number // empty' 2>/dev/null)
|
|
if [ -z "$url" ]; then
|
|
title="@$NAME: $TITLE"
|
|
resp=$(curl -sS -X POST "${hdr[@]}" "$API/pulls" \
|
|
-d "$(jq -nc --arg t "$title" --arg h "$br" --arg b "$prbody" \
|
|
'{title:$t, head:$h, base:"main", body:$b}')")
|
|
echo "PR create ($br): $resp"
|
|
url=$(printf '%s' "$resp" | jq -r '.html_url // empty' 2>/dev/null)
|
|
prnum=$(printf '%s' "$resp" | jq -r '.number // empty' 2>/dev/null)
|
|
fi
|
|
[ -z "$url" ] && { echo "PR open/lookup failed for $br — posting reply on issue instead"; post "$(printf '%s%s' "$reply" "$activity")"; exit 0; }
|
|
|
|
# Posts to the PR thread when we have a PR number, else to the origin issue ($NUM).
|
|
prpost() {
|
|
local n="$1"; shift; local t="$NUM"
|
|
[ -n "$n" ] && [ "$n" != "$NUM" ] && t="$n"
|
|
echo "posting to #$t"
|
|
curl -sS -w 'comment -> HTTP %{http_code}\n' -X POST "${hdr[@]}" \
|
|
"$API/issues/$t/comments" -d "$(jq -nc --arg b "$1$MARK" '{body:$b}')"
|
|
}
|
|
|
|
if [ "$NEW" = "true" ]; then
|
|
# First PR for this issue: record it on the PR thread, then notify @pm on the ISSUE. @pm never
|
|
# reads the PR, so the issue gets only this one-line ping — @pm then routes it to @qa for review.
|
|
prpost "$prnum" "$(printf 'Opened PR #%s for review.%s' "$prnum" "$activity")"
|
|
trig "$ISSN" "@pm — PR #$prnum is ready for review (issue #$ISSN)."
|
|
else
|
|
# A fix (usually after a @qa bounce): update the PR and hand straight back to @qa to re-verify,
|
|
# 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 PR #%s.%s' "$prnum" "$activity")"
|
|
case "$NAME" in
|
|
junior|senior|lead) trig "$prnum" "@qa please re-verify PR #$prnum — the dev has pushed an update." ;;
|
|
esac
|
|
fi
|