From df0f6d65434db90986cb9ad2271643b5e7e867f2 Mon Sep 17 00:00:00 2001 From: lead Date: Fri, 3 Jul 2026 12:33:35 +0000 Subject: [PATCH 1/3] =?UTF-8?q?@lead:=20issue=20#16=20=E2=80=94=20fully-au?= =?UTF-8?q?tomatic=20autopilot=20(label-gated=20@pm=20auto-delegate=20+=20?= =?UTF-8?q?@qa=20merge)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/agent.yml | 4 ++ .gitea/workflows/scripts/publish.sh | 69 ++++++++++++++++++++++++++- .gitea/workflows/scripts/route.sh | 17 +++++++ .gitea/workflows/scripts/run-agent.sh | 34 ++++++++++++- AGENTS.md | 16 +++++++ 5 files changed, 137 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/agent.yml b/.gitea/workflows/agent.yml index c210a1a..a233df2 100644 --- a/.gitea/workflows/agent.yml +++ b/.gitea/workflows/agent.yml @@ -162,6 +162,7 @@ jobs: MODE: ${{ steps.prep.outputs.mode }} 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 NUM: ${{ github.event.issue.number }} TITLE: ${{ github.event.issue.title }} IBODY: ${{ github.event.issue.body }} @@ -192,6 +193,9 @@ jobs: TITLE: ${{ github.event.issue.title }} BRANCH: ${{ steps.prep.outputs.branch }} 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` + ISSNUM: ${{ steps.prep.outputs.issnum }} # origin issue number (resolved from branch on PR threads) run: bash "$SCRIPTS/publish.sh" # Failure-safe: if any step above failed AFTER a dev agent already pushed commits, the normal diff --git a/.gitea/workflows/scripts/publish.sh b/.gitea/workflows/scripts/publish.sh index a0d93f5..2fc4db7 100755 --- a/.gitea/workflows/scripts/publish.sh +++ b/.gitea/workflows/scripts/publish.sh @@ -4,6 +4,7 @@ # Required env (provided by the workflow step): # GT AGENT_TOKEN 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 @@ -17,14 +18,30 @@ API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" hdr=(-H "Authorization: token $TOK" -H "Content-Type: application/json") post() { curl -sS -w 'comment -> HTTP %{http_code}\n' -X POST "${hdr[@]}" \ "$API/issues/$NUM/comments" -d "$(jq -nc --arg b "$1" '{body:$b}')"; } +# 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, and the BEGIN_SUBTASKS..END_SUBTASKS and -# BEGIN_PR_DESCRIPTION..END_PR_DESCRIPTION blocks (the PR description is published separately). +# drop machine-readable markers: DELEGATE / CLOSE_ISSUE / MERGE_PR / HALT_AUTOPILOT, 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:]]*HALT_AUTOPILOT[[:space:]]*$/{next} s{ if(/^[[:space:]]*END_SUBTASKS/){s=0}; next } p{ if(/^[[:space:]]*END_PR_DESCRIPTION/){p=0}; next } {print} @@ -91,6 +108,43 @@ if [ "$MODE" != "pr" ]; then subtext=$(printf '\n\n---\nšŸ¤– **@%s** — created sub-issues%s (mention an agent on each when ready):%b' "$NAME" "${ms:+ under milestone **$ms**}" "$links") fi post "$(printf 'šŸ¤– **@%s**\n\n%s%s' "$NAME" "$msg" "$subtext")" + + # --- AUTOPILOT: @qa's narrow, label-gated merge / halt authority --- + # Only @qa, only when 'autopilot' is set, and only on a PR thread. The MERGE_PR / HALT_AUTOPILOT + # markers come from the QA prompt. Merge + label ops use TOKEN_QA (the QA user's PAT, which the + # maintainer must grant write+merge scope). ISSNUM is the origin issue (resolved from the branch). + if [ "$NAME" = "qa" ] && [ "$AUTOPILOT" = "true" ]; then + if grep -qiE '^[[:space:]]*MERGE_PR[[:space:]]*$' /tmp/agent_out.md; then + if [ -z "$IS_PR" ]; then + echo "MERGE_PR marker but this run is not on a PR thread — skipping merge" + else + echo "@qa autopilot: merging PR #$NUM (origin issue #${ISSNUM:-$NUM})" + mc=$(curl -sS -o /tmp/merge_resp.txt -w '%{http_code}' -X POST "${hdr[@]}" \ + "$API/pulls/$NUM/merge" -d '{"Do":"merge"}') + echo "merge -> HTTP $mc"; cat /tmp/merge_resp.txt 2>/dev/null || true + case "$mc" in + 200|201|204) + echo "closing origin issue #${ISSNUM:-$NUM}" + curl -sS -X PATCH "${hdr[@]}" "$API/issues/${ISSNUM:-$NUM}" \ + -d '{"state":"closed"}' -w '\nclose -> HTTP %{http_code}\n' || true + post "$(printf 'šŸ¤– **@qa** — āœ… verified & merged PR #%s (autopilot). Closed issue #%s.' "$NUM" "${ISSNUM:-$NUM}")" + ;; + *) + # Merge failed (checks not green, conflicts, or TOKEN_QA lacks merge scope) — do NOT + # silently proceed: drop the label so it reverts to human control and report. + del_autopilot_label "${ISSNUM:-$NUM}" + post "$(printf 'šŸ¤– **@qa** — āš ļø tried to merge PR #%s but the API returned HTTP %s (checks not green, a conflict, or missing merge permission on TOKEN_QA). Removed the `autopilot` label — @ffaerber please take a look.' "$NUM" "$mc")" + ;; + esac + fi + elif grep -qiE '^[[:space:]]*HALT_AUTOPILOT[[:space:]]*$' /tmp/agent_out.md; then + echo "@qa autopilot: HALT — removing 'autopilot' label from #${ISSNUM:-$NUM}" + del_autopilot_label "${ISSNUM:-$NUM}" + post "$(printf 'šŸ¤– **@qa** — šŸ›‘ found a problem, so I did NOT merge. Removed the `autopilot` label (back to human control). @ffaerber please decide next steps (details above).')" + fi + exit 0 + fi + # Auto-delegate: if the plan names a teammate, trigger them via AGENT_TOKEN (a PAT, so it # fires a new workflow run — the built-in token cannot). Never targets @pm or self, so the # chain always terminates at a dev. The 'šŸ¤–' guard on the trigger stops status-comment loops. @@ -185,6 +239,17 @@ prpost() { if [ "$NEW" = "true" ]; then prpost "$prnum" "$(printf 'šŸ¤– **@%s** — āœ… PR ready for review — @ffaerber please review & merge:\n- %s%s' "$NAME" "$url" "$activity")" + # AUTOPILOT: hand the fresh PR to @qa automatically (via AGENT_TOKEN, so it fires a new run). + # @qa then verifies and — if green — merges + closes via its MERGE_PR marker. The comment lands + # on the PR thread ($prnum) so the next run resolves the origin issue's label from the branch + # name. The 'šŸ¤–' guard on the trigger gate stops status-comment loops. + if [ "$AUTOPILOT" = "true" ] && [ -n "$AGENT_TOKEN" ] && [ -n "$prnum" ]; then + echo "autopilot: auto-triggering @qa to review PR #$prnum" + curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \ + "$API/issues/$prnum/comments" \ + -d "$(jq -nc --arg b "@qa please verify this PR (autopilot: issue #$NUM is labeled autopilot). Merge it if correct, or halt and remove the label if you find a problem." '{body:$b}')" \ + -w '\ntrigger-qa -> HTTP %{http_code}\n' || true + fi else # Resume: just link the PR — its body and the diff already carry the description, so we don't # repeat the full write-up in the comment (the reasoning trail below shows what this run did). diff --git a/.gitea/workflows/scripts/route.sh b/.gitea/workflows/scripts/route.sh index f4452f9..32328dd 100755 --- a/.gitea/workflows/scripts/route.sh +++ b/.gitea/workflows/scripts/route.sh @@ -51,8 +51,10 @@ 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") +branch_ref="" if [ -n "$IS_PR" ]; then # comment on a PR -> resume its branch ref=$(curl -s -H "Authorization: token $GT" "$API/pulls/$NUM" | jq -r .head.ref) + branch_ref="$ref" git fetch origin "$ref" && git checkout "$ref" { echo "branch=$ref"; echo "new=false"; } >> "$GITHUB_OUTPUT" elif git ls-remote --exit-code --heads origin "ai/issue-$NUM" >/dev/null 2>&1; then @@ -72,3 +74,18 @@ else # comment on an issue, no branch ye -d "$(jq -nc --arg b "šŸ”Ø **@$name** is on it — building on branch [\`ai/issue-$NUM\`]($url). I'll open a PR when it's ready." '{body:$b}')" >/dev/null || true fi fi + +# --- Autopilot gate: read the `autopilot` label FRESH every run. --- +# Presence of this label is the opt-in switch (and the kill switch: remove it mid-flight and the +# next run reverts to normal human-approval behavior). When @qa is triggered on a PR thread, the +# label lives on the ORIGIN issue (ai/issue-N), so resolve N from the branch name. +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 + autopilot=true +fi +echo "autopilot (autopilot label on #$issnum)=$autopilot" +{ echo "autopilot=$autopilot"; echo "issnum=$issnum"; } >> "$GITHUB_OUTPUT" diff --git a/.gitea/workflows/scripts/run-agent.sh b/.gitea/workflows/scripts/run-agent.sh index d913160..3903df4 100755 --- a/.gitea/workflows/scripts/run-agent.sh +++ b/.gitea/workflows/scripts/run-agent.sh @@ -3,8 +3,10 @@ # plain-text reply (/tmp/agent_out.md) plus the raw event stream (/tmp/events.jsonl). # # Required env (provided by the workflow step): -# ANTHROPIC_API_KEY AGENT_TOKEN NAME MODEL VISION MODE HAS_IMAGES BRANCH NUM TITLE IBODY CMT +# ANTHROPIC_API_KEY AGENT_TOKEN NAME MODEL VISION MODE HAS_IMAGES BRANCH AUTOPILOT 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). set -u [ -z "$CMT" ] && CMT="(a new issue was just opened — assess it)" @@ -51,6 +53,36 @@ if [ "$MODE" = "comment" ]; then END_SUBTASKS The automation creates the milestone + one sub-issue per line (each linked to this issue). It does NOT 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). This OVERRIDES the + two-phase approval gate above: do NOT ask '@ffaerber ready to start building?' and do NOT wait + for a 'yes'. When the task is clear, present your SHORT plan naming the best teammate to build it + AND end your reply with a 'DELEGATE: @' line in the SAME turn to hand off immediately. + Prefer @junior for small/low-risk (mostly YAML/compose/config), @senior/@lead for complex or + multi-file work. Only skip delegating (and instead ask @ffaerber) if the task is genuinely + ambiguous or unsafe — otherwise plan-and-delegate now." + fi + fi + if [ "$NAME" = "qa" ]; then + ACTION="$ACTION + As QA you verify a change works: read the PR/issue, drive the web app with your headless + browser if there is a URL, and report bugs or confirm behavior. You normally do NOT merge — + a human does that." + if [ "$AUTOPILOT" = "true" ]; then + ACTION="$ACTION + AUTOPILOT MODE IS ACTIVE (this issue/PR carries the 'autopilot' label). This grants you a + NARROW, one-time merge authority for THIS PR only: + - If, after verifying, the PR is correct and any CI checks are green, end your reply with EXACTLY + one line: 'MERGE_PR'. The automation will then merge the PR and close the linked issue for you. + Do NOT merge via any other means; only the MERGE_PR marker triggers the merge. + - If you find ANY bug, doubt, or the change is not clearly correct, do NOT merge. Instead describe + the problem clearly and end your reply with EXACTLY one line: 'HALT_AUTOPILOT'. The automation + removes the 'autopilot' label (returning this issue to normal human control) and leaves + it for @ffaerber to decide next steps. Never auto-bounce back to a dev. + Emit AT MOST one of MERGE_PR or HALT_AUTOPILOT, and only after you have actually verified. When in + doubt, prefer HALT_AUTOPILOT." + fi fi else ACTION="You start on git branch '${BRANCH}', with git and push credentials already configured. diff --git a/AGENTS.md b/AGENTS.md index d6c8856..1a37452 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,10 +11,26 @@ the loop guards. ## Golden rules - You may edit **any file**. **NEVER push to `main`, NEVER merge a PR** — all work goes on a branch and becomes a PR a human reviews and merges. + - **Narrow exception — `@qa` autopilot merge:** `@qa` (and only `@qa`) MAY merge a single PR **only** + when the linked issue carries the `fully-automatic` label, the PR is clearly correct, and any CI + checks are green. `@qa` triggers the merge by ending its reply with the `MERGE_PR` marker (the + workflow performs the merge + closes the issue). On **any** doubt or bug, `@qa` must NOT merge: + it ends with `HALT_AUTOPILOT` instead, which removes the `fully-automatic` label and returns the + issue to human control. No other agent may merge, and `@qa` may not merge without the label. - **Never print, exfiltrate, or invent secret values.** - Keep changes **minimal** and match the conventions already in the file you're editing. - Do the work on a **branch** — never paste code or diffs into the issue thread. +## Autopilot (`fully-automatic` label) +An issue labeled **`fully-automatic`** runs without the usual human checkpoints: +- `@pm` plans **and** delegates in the same turn (skips the "ready to build? reply yes" gate). +- After the dev's PR is opened, `@qa` is auto-triggered to verify it, and merges + closes on success + (see the QA merge exception above). +- **Kill switch:** remove the `fully-automatic` 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. +No label (the default) = today's behavior, unchanged. + ## Branches & pull requests Start on `ai/issue-`. Split independent changes into separate branches (one PR each). Commit and push incrementally. Do NOT open PRs yourself (automated). End your reply with the PR description From 7278e06dffd1740446fc54637095ebad4810e6d9 Mon Sep 17 00:00:00 2001 From: lead Date: Fri, 3 Jul 2026 13:08:16 +0000 Subject: [PATCH 2/3] Rename autopilot label 'fully-automatic' to 'auto' --- AGENTS.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1a37452..2fd54af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,21 +12,21 @@ the loop guards. - You may edit **any file**. **NEVER push to `main`, NEVER merge a PR** — all work goes on a branch and becomes a PR a human reviews and merges. - **Narrow exception — `@qa` autopilot merge:** `@qa` (and only `@qa`) MAY merge a single PR **only** - when the linked issue carries the `fully-automatic` label, the PR is clearly correct, and any CI + when the linked issue carries the `auto` label, the PR is clearly correct, and any CI checks are green. `@qa` triggers the merge by ending its reply with the `MERGE_PR` marker (the workflow performs the merge + closes the issue). On **any** doubt or bug, `@qa` must NOT merge: - it ends with `HALT_AUTOPILOT` instead, which removes the `fully-automatic` label and returns the + it ends with `HALT_AUTOPILOT` instead, which removes the `auto` label and returns the issue to human control. No other agent may merge, and `@qa` may not merge without the label. - **Never print, exfiltrate, or invent secret values.** - Keep changes **minimal** and match the conventions already in the file you're editing. - Do the work on a **branch** — never paste code or diffs into the issue thread. -## Autopilot (`fully-automatic` label) -An issue labeled **`fully-automatic`** runs without the usual human checkpoints: +## Autopilot (`auto` label) +An issue labeled **`auto`** runs without the usual human checkpoints: - `@pm` plans **and** delegates in the same turn (skips the "ready to build? reply yes" gate). - After the dev's PR is opened, `@qa` is auto-triggered to verify it, and merges + closes on success (see the QA merge exception above). -- **Kill switch:** remove the `fully-automatic` label at any time. The label is re-read fresh at the +- **Kill switch:** remove the `auto` 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. No label (the default) = today's behavior, unchanged. From ef06da3ffed28e9c84a484c38285a1511862bef4 Mon Sep 17 00:00:00 2001 From: lead Date: Sat, 4 Jul 2026 19:16:47 +0000 Subject: [PATCH 3/3] Rename autopilot label 'auto' to 'autopilot' --- AGENTS.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2fd54af..cab0f69 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,21 +12,21 @@ the loop guards. - You may edit **any file**. **NEVER push to `main`, NEVER merge a PR** — all work goes on a branch and becomes a PR a human reviews and merges. - **Narrow exception — `@qa` autopilot merge:** `@qa` (and only `@qa`) MAY merge a single PR **only** - when the linked issue carries the `auto` label, the PR is clearly correct, and any CI + when the linked issue carries the `autopilot` label, the PR is clearly correct, and any CI checks are green. `@qa` triggers the merge by ending its reply with the `MERGE_PR` marker (the workflow performs the merge + closes the issue). On **any** doubt or bug, `@qa` must NOT merge: - it ends with `HALT_AUTOPILOT` instead, which removes the `auto` label and returns the + it ends with `HALT_AUTOPILOT` instead, which removes the `autopilot` label and returns the issue to human control. No other agent may merge, and `@qa` may not merge without the label. - **Never print, exfiltrate, or invent secret values.** - Keep changes **minimal** and match the conventions already in the file you're editing. - Do the work on a **branch** — never paste code or diffs into the issue thread. -## Autopilot (`auto` label) -An issue labeled **`auto`** runs without the usual human checkpoints: +## Autopilot (`autopilot` label) +An issue labeled **`autopilot`** runs without the usual human checkpoints: - `@pm` plans **and** delegates in the same turn (skips the "ready to build? reply yes" gate). - After the dev's PR is opened, `@qa` is auto-triggered to verify it, and merges + closes on success (see the QA merge exception above). -- **Kill switch:** remove the `auto` label at any time. The label is re-read fresh at the +- **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. No label (the default) = today's behavior, unchanged.