Conversation
The auto-ticket fleet auto-starts bug tickets but still stalled on a human reviewing/merging each PR and then closing each ticket (a closed ticket is what auto-stops the session and frees the slot). For orgs that opt in, the hub now takes both steps for the class of tickets it already auto-starts. Hub (turma/server.js): - New per-org toggle `autoMergeOrgs` (sibling of autoStartOrgs): file-backed, SSE + payload, POST /api/jira/<site>/automerge. OFF by default — it merges to the default branch unreviewed, a deliberate per-project trust decision. - `autoMergeSweep`: for a running, idle, auto-start-ELIGIBLE ticket session in an opted-in org, queue a `mergePr` command for each PR that reads merge-ready (green CI + MERGEABLE + no conflict). Backoff per PR; a gh refusal (branch protection, review required, fresh conflict) gives up rather than spinning. - `autoCloseSweep`: once every PR of the session has landed (>=1 actually merged), move the ticket to Done (XERK-138 write-back, routed to a board-cred host) AND kill the session directly, freeing the slot without waiting out the ~10-min Jira poll. - Eligibility reuses the same content gate auto-start applies (`autoStartContentGate`), so it acts on exactly the bug class the org auto-starts and never on a human-started feature session; a cross-check test pins agreement with autoStartSweep. Agent (agent/hub-agent.py): - `mergePr` command -> `_merge_pr_async` runs `gh pr merge --squash --delete-branch` (env-overridable) on a WORKER THREAD (off the beat, XERK-395), as the manager (not a guarded session). Outcome staged on `mergePrResults`, lock-guarded + cleared by identity like spawn_failures. GitHub only for now; a GitLab/ADO url stages a refusal so the hub gives up. UI (turma/public/org.js): a second org-menu switch beside auto-start. Android switch deferred with a PARITY.md line (the new payload key is unknown-key-safe; the hub route works from web). Tests: 18 hub cases (routes, gate cross-check, both sweeps, backoff/give-up, ingest, close+kill), 10 agent cases (TestMergePr), 1 org UI case. Docs in turma-board.md, agent-prs.md, android/PARITY.md.
QA (adversarial) found three reachable holes; all fixed hub-side: 1. A ticket a human moved to Done still had its open PR auto-merged. autoStopSweep only QUEUES the kill, so the session reads running for a beat and the column-agnostic gate still merged it — landing unreviewed code on a ticket the human was abandoning/rejecting. Fix: autoMergeSession excludes `statusCategory === "done"` (the column, not the run state, stands it down). 2. auto-close killed the session even when the Done write couldn't be dispatched (no board-cred host / agent too old for setTicketStatus), orphaning the ticket In Progress with a merged PR and no session forever. Fix: the kill is now gated on the Done write actually being queued — else stand down and retry next beat, never kill. 3. A DRAFT PR was treated as merge-ready (_merge_ready returns "ready" for a green+MERGEABLE draft), dispatched, refused by `gh`, then marked gaveUp FOREVER — silently excluding any session that opened a draft. Fix: the sweep requires `state === "OPEN"`. Also corrected the overclaim "never merges a human-started feature": the gate is content-only (not provenance), so features are kept out only by the org's triage policy — the same thing that limits auto-start to bugs. Docs (turma-board.md) now say so plainly. Tests: 3 new regression cases (Done not merged, DRAFT not merged, no-kill when Done can't dispatch). afterEach now releases the auto-merge opt-in so server.js's live sweep interval idles outside each test (hygiene; unrelated to the pre-existing control-WS/drain timing flakes that also occur on origin/main).
… policy Auto-merge lands unreviewed code on the default branch, so restrict it to a hard floor of triage type "bug" in autoMergeSession — on TOP of the shared content gate. The content gate matches whatever the auto-START stream would start, which an operator can widen past bugs via the org's triage policy (or leave wide with no excludeTypes). Now, even an org that auto-starts other types only ever has its BUG PRs merged + closed hands-off; every non-bug (task/feature/chore/…) PR still waits for a human. The policy can narrow auto-merge but never widen it past bugs. Test: a task/feature/chore/improvement/other ticket that is otherwise fully eligible (actionable, no excludeTypes) gets neither mergePr nor Done+kill, while the same setup with type "bug" does. Docs (turma-board.md) + the org switch tooltip say "bugs only".
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
XERK-550 — PR & ticket close bottleneck
The auto-ticket fleet auto-starts bug tickets, but throughput still stalled on a human reviewing/merging each PR and then manually closing each ticket (closing a ticket is what auto-stops the session and frees the slot). For orgs that opt in, the hub now takes both steps for the class of tickets it already auto-starts.
What changed
Hub (
turma/server.js)autoMergeOrgstoggle — the sibling of the existing auto-start switch: file-backed (/data/automerge-orgs.json), on the/api/agentspayload + its own SSE,POST /api/jira/<site>/automerge. OFF by default — it merges to the default branch with no human review, so it's a deliberate per-project trust decision, independent of the auto-start switch.autoMergeSweep(15s timer, no-op unless opted in) — for a running, idle ticket session that is auto-start-eligible (autoStartContentGate, the same content gate the auto stream uses), queues amergePrcommand to the session's host for each PR readingready === "ready"(green CI +MERGEABLE+ no conflict — the flag the agent already computes) andstate === "OPEN". Per-PR backoff (autoMergeState); aghrefusal (mergePrResultsok:false— branch protection, review-required, a fresh conflict) marks the PRgaveUpand stops retrying; a silent failure retries to a cap then gives up.autoCloseSweep— once every PR a session opened has landed and ≥1 actually MERGED, moves the ticket to Done (the XERK-138 status write-back, routed to a board-cred host of the org) and kills the session, freeing the slot without waiting out the ~10-min Jira poll. The kill is gated on the Done write being queued (never orphans the ticket).autoStartContentGate, so it acts on exactly the class the auto stream would start; a cross-check test pins agreement withautoStartSweep. A Done ticket is excluded (moving to Done is the human's abandon/stop gesture).Agent (
agent/hub-agent.py)mergePrcommand →_merge_pr_asyncrunsgh pr merge --squash --delete-branch <url>(method/branch-delete env-overridable) on a worker thread — off the heartbeat loop (XERK-395), as the manager, not inside a guarded session (the session guard forbids self-merge; the operator's per-org opt-in is the deliberate override). Outcome staged onmerge_pr_results, lock-guarded and cleared by identity likespawn_failures. GitHub only for now; a GitLab MR / ADO PR stages a refusal so the hub gives up.UI (
turma/public/org.js) — a second "merge" switch in the header org menu, beside auto-start.Safety gate — the hub merges only when ALL hold
org opted in · session running, not working, not blocked on a question · PR
ready(green CI, mergeable, no conflict) and OPEN · ticket is an actionable, non-held/rejected/duplicate bug in a non-ignored repo, per the org's triage policy · not Done. Sessions run their own QA before opening the PR (repo policy), and green CI is required.Note: the "bug class" limitation comes from the org's triage policy (
excludeTypes) — the same thing that limits auto-start to bugs — not from the ticket type by itself. Enabling auto-merge on an org without a bug-limiting triage policy will auto-merge whatever auto-start would.How it was verified
qaagent (built/drove the real exported sweeps + agentmerge_pr) returned FAIL with 3 findings; all fixed and re-verified:statusCategory === "done";gaveUpforever → requirestate === "OPEN".A
qa-deltare-run confirms the fixes hold.XERK-550:/automerge route:inserver.test.js) incl. the gate cross-check and the 3 regressions;TestMergePr(10) intest_hub_agent.py; the auto-merge switch inorg.test.js. Full turma JS suite (1779) green; agent suite green (bar the known environmentalTestDshWebreal-port test). Two suite flakes (control WS ... pongs,drain ...) are pre-existing timing tests that flake on cleanorigin/maintoo.Web ⇄ Android parity
The org-menu auto-merge switch shipped on web only; Android ignores the new
autoMergeOrgspayload key (unknown-key-safe) and has no switch yet. Tracked inandroid/PARITY.md; the hub route works from web regardless.Docs
.claude/rules/turma-board.md(the two sweeps + gate),.claude/rules/agent-prs.md(themergePrcommand),android/PARITY.md.