From cbe3e3cbc0cd74cec0a1ae11fe946dd480b432cd Mon Sep 17 00:00:00 2001 From: Felix Faerber Date: Wed, 16 Sep 2026 17:08:27 +0300 Subject: [PATCH] Add a human-merge label that keeps autopilot from merging autopilot skips both human checkpoints: approval before the build and the merge. Scheduled update issues in homelab want the hands-off build but a human merge, since merging there deploys. With human-merge also on the issue, @pm is told to hand back at phase 3 and publish.sh refuses MERGE_PR. Co-Authored-By: Claude Opus 5 --- .gitea/workflows/agent.yml | 2 ++ .gitea/workflows/scripts/publish.sh | 5 +++-- .gitea/workflows/scripts/route.sh | 14 ++++++++------ .gitea/workflows/scripts/run-agent.sh | 15 +++++++++++---- AGENTS.md | 2 ++ README.md | 3 ++- 6 files changed, 28 insertions(+), 13 deletions(-) diff --git a/.gitea/workflows/agent.yml b/.gitea/workflows/agent.yml index 0437236..105e419 100644 --- a/.gitea/workflows/agent.yml +++ b/.gitea/workflows/agent.yml @@ -268,6 +268,7 @@ jobs: HAS_IMAGES: ${{ steps.imgs.outputs.has_images }} BRANCH: ${{ steps.prep.outputs.branch }} AUTOPILOT: ${{ steps.prep.outputs.autopilot }} # 'true' when the issue carries the `autopilot` label + AUTOMERGE: ${{ steps.prep.outputs.automerge }} # 'false' when `human-merge` is also set NUM: ${{ github.event.issue.number }} TITLE: ${{ github.event.issue.title }} IBODY: ${{ github.event.issue.body }} @@ -305,6 +306,7 @@ jobs: NEW: ${{ steps.prep.outputs.new }} IS_PR: ${{ github.event.issue.pull_request }} # set when this run is on a PR thread AUTOPILOT: ${{ steps.prep.outputs.autopilot }} # 'true' when the origin issue carries `autopilot` + AUTOMERGE: ${{ steps.prep.outputs.automerge }} # 'false' when `human-merge` is also set ISSNUM: ${{ steps.prep.outputs.issnum }} # origin issue number (resolved from branch on PR threads) run: bash "$SCRIPTS/publish.sh" diff --git a/.gitea/workflows/scripts/publish.sh b/.gitea/workflows/scripts/publish.sh index 708a567..2f7b2a7 100755 --- a/.gitea/workflows/scripts/publish.sh +++ b/.gitea/workflows/scripts/publish.sh @@ -4,7 +4,7 @@ # 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) +# IS_PR AUTOPILOT AUTOMERGE 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 @@ -222,9 +222,10 @@ if [ "$MODE" != "pr" ]; then fi # --- @pm autopilot merge: @pm is the ONLY agent that merges, and ONLY under the autopilot label --- + # (and never when `human-merge` is also set — AUTOMERGE is false then). # (@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 + if [ "$NAME" = "pm" ] && [ "$AUTOMERGE" = "true" ] && grep -qiE '^[[:space:]]*MERGE_PR[[:space:]]*$' /tmp/agent_out.md; then PRS=$(resolve_prs); PRN=${PRS##* }; CNT=$(echo "$PRS" | wc -w) if [ -z "$PRN" ]; then echo "MERGE_PR but no open PR found for issue #$ISSN" diff --git a/.gitea/workflows/scripts/route.sh b/.gitea/workflows/scripts/route.sh index d15ead3..119a3f0 100755 --- a/.gitea/workflows/scripts/route.sh +++ b/.gitea/workflows/scripts/route.sh @@ -44,7 +44,7 @@ if [ -z "$name" ]; then # mode=skip so every later step no-ops — a red run for a non-agent comment is just noise. echo "no known agent mentioned (word-boundary) — skipping run" { echo "name=none"; echo "model=none"; echo "fallback="; echo "vision=false"; echo "mode=skip"; echo "skills=[]"; - echo "branch=main"; echo "new=false"; echo "autopilot=false"; echo "issnum=$NUM"; } >> "$GITHUB_OUTPUT" + echo "branch=main"; echo "new=false"; echo "autopilot=false"; echo "automerge=false"; echo "issnum=$NUM"; } >> "$GITHUB_OUTPUT" exit 0 fi fi @@ -121,10 +121,12 @@ fi issnum="$NUM" case "$IS_PR" in ?*) issnum=$(printf '%s' "$branch_ref" | sed -nE 's,^ai/issue-([0-9]+).*,\1,p');; esac [ -z "$issnum" ] && issnum="$NUM" -autopilot=false -if curl -sS -H "Authorization: token $GT" "$API/issues/$issnum/labels" 2>/dev/null \ - | jq -e 'any(.[]?; .name=="autopilot")' >/dev/null 2>&1; then +# `human-merge` alongside it keeps the hands-off build but leaves the merge to a human. +labels=$(curl -sS -H "Authorization: token $GT" "$API/issues/$issnum/labels" 2>/dev/null) +autopilot=false; automerge=false +if printf '%s' "$labels" | jq -e 'any(.[]?; .name=="autopilot")' >/dev/null 2>&1; then autopilot=true + printf '%s' "$labels" | jq -e 'any(.[]?; .name=="human-merge")' >/dev/null 2>&1 || automerge=true fi -echo "autopilot (autopilot label on #$issnum)=$autopilot" -{ echo "autopilot=$autopilot"; echo "issnum=$issnum"; } >> "$GITHUB_OUTPUT" +echo "autopilot (autopilot label on #$issnum)=$autopilot automerge=$automerge" +{ echo "autopilot=$autopilot"; echo "automerge=$automerge"; echo "issnum=$issnum"; } >> "$GITHUB_OUTPUT" diff --git a/.gitea/workflows/scripts/run-agent.sh b/.gitea/workflows/scripts/run-agent.sh index 1c4eb24..f18d0aa 100755 --- a/.gitea/workflows/scripts/run-agent.sh +++ b/.gitea/workflows/scripts/run-agent.sh @@ -3,7 +3,7 @@ # plain-text reply (/tmp/agent_out.md) plus the raw event stream (/tmp/events.jsonl). # # Required env (provided by the workflow step): -# XAI_API_KEY SELF_TOKEN NAME MODEL VISION MODE HAS_IMAGES BRANCH AUTOPILOT NUM TITLE +# XAI_API_KEY SELF_TOKEN NAME MODEL VISION MODE HAS_IMAGES BRANCH AUTOPILOT AUTOMERGE NUM TITLE # IBODY CMT # FILES (the opencode -f image flags, from the imgs step output) # AUTOPILOT is 'true' when the issue carries the `autopilot` label (label-gated autopilot mode). @@ -74,14 +74,21 @@ if [ "$MODE" = "comment" ]; then auto-start any dev — the maintainer @mentions an agent on each sub-issue when ready." if [ "$AUTOPILOT" = "true" ]; then ACTION="$ACTION - AUTOPILOT MODE IS ACTIVE (this issue carries the 'autopilot' label) — it changes exactly TWO - things for you; everything else above is unchanged: + AUTOPILOT MODE IS ACTIVE (this issue carries the 'autopilot' label) — it changes only the + phases below; everything else above is unchanged: - PHASE 1: do NOT ask '@ffaerber ready to build?'. Present your SHORT plan AND end with a 'DELEGATE: @' line in the SAME reply. Only skip delegating (and ask @ffaerber) if the task - is genuinely ambiguous or unsafe. + is genuinely ambiguous or unsafe." + if [ "$AUTOMERGE" = "true" ]; then + ACTION="$ACTION - PHASE 3: do NOT ask the human to merge. When @qa has approved, end your reply with EXACTLY 'MERGE_PR' — the automation merges the PR and closes this issue. You are the ONLY agent that merges, and only here." + else + ACTION="$ACTION + - PHASE 3 is UNCHANGED: this issue also carries 'human-merge', so when @qa has approved, tell + @ffaerber the PR is ready to merge. Do NOT emit 'MERGE_PR'." + fi fi fi if [ "$NAME" = "qa" ]; then diff --git a/AGENTS.md b/AGENTS.md index cab0f69..42b3dee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,8 @@ An issue labeled **`autopilot`** runs without the usual human checkpoints: - **Kill switch:** remove the `autopilot` label at any time. The label is re-read fresh at the start of every run, so the next agent turn reverts to normal human-approval behavior. `@qa` also removes the label itself whenever it halts on a bug or a failed merge. +- **`human-merge` label** (together with `autopilot`): everything above except the merge — after + `@qa` approves, `@pm` tells the creator the PR is ready and a human merges. No label (the default) = today's behavior, unchanged. ## Branches & pull requests diff --git a/README.md b/README.md index 9d754ee..389ef51 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ deploy-host SSH skill, live in the consuming repo under `.gitea/agent-skills/` 4. `@qa` reviews **on the PR** — either recommendations + `BOUNCE: @dev` (dev fixes → `@qa` re-verifies, direct loop, max 3 rounds) or `APPROVE`. 5. On approval `@qa` posts the verdict **on the issue** → `@pm` tells the creator *"ready to merge"* - and a **human merges** — or, with the `autopilot` label, `@pm` merges and closes the issue itself. + and a **human merges** — or, with the `autopilot` label, `@pm` merges and closes the issue itself + (unless the issue also carries `human-merge`, which keeps the merge with a human). `@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).