Skip to content

Harden Party queue UX for auto-publish, named queues, and multi-queue servers - #64

Merged
diese-tech merged 8 commits into
mainfrom
claude/issue-63-party-queue-irh0qv
Aug 21, 2026
Merged

Harden Party queue UX for auto-publish, named queues, and multi-queue servers#64
diese-tech merged 8 commits into
mainfrom
claude/issue-63-party-queue-irh0qv

Conversation

@diese-tech

@diese-tech diese-tech commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Closes #63.

Summary

Fixes the join/public-card desync bug first, then refactors the Discord Party UX around the queue-first flow defined in #63 and its follow-up comments: Start Queue → auto-publish → Join Queue → quick SMITE 2 roles → Ready Check → automatic handoff → private match workspace. Reuses the existing durable lobby/queue/waitlist/ready-check/room/formation/draft/continuity services throughout — no rewrite of those services. This description reflects the final state after two owner-requested follow-up cleanup passes (see commit history) — it's current, not the original submission.

The bug fixed first

join_lobby_from_preferences() edited interaction.message as if it were the public queue card. When the join came from an ephemeral wizard (the normal path), that edited the wizard message instead — durable state updated correctly, but the real public card went stale until an unrelated organizer action (e.g. Cancel) happened to redraw it. Every mutation now refreshes the queue's card exclusively via its durable lobby.delivery.panel_channel_id / panel_message_id reference (refresh_public_lobby_card), never via whichever interaction triggered it.

Player-facing UX changes

  • Start Queue asks only for an optional queue name (a single-field modal), then creates immediately. Mode/region/format/capacity/voice/skill default sensibly (Conquest / 5v5 / 10 players / no voice requirement / open skill) and are only ever customized afterward via Queue Settings → Edit Details.
  • Because the organizer is seated as the queue's first participant, a first-time organizer (no saved Primary role) sees the same lightweight role picker a joining player gets before the queue is created; a returning organizer with saved roles goes straight from the name modal to a live queue.
  • Join Queue on a specific queue card always joins that queue.
  • Find a Queue (Play panel) — the old "Join Queue" silently picked the first open lobby with space. It now: joins immediately if exactly one queue is open, otherwise presents an explicit Select of every open queue (name, code, mode, roster count). No path ever silently picks "the first one." The redundant Browse Queues button was removed — the public queue card is the one canonical place to see a queue.
  • First-time join only asks Primary role (required), Secondary role (optional — defaults to None, no explicit "None" selection needed), and Fill (required). Captain willingness is not collected here; it stays editable from My Roles (renamed from "My Preferences"), and only matters again if an organizer later picks Captain Teams formation.
  • Returning players with a saved primary role join in one click, with a Change Roles button on the confirmation if the saved roles are stale.
  • One active queue per player per guild — joining a second queue while already committed elsewhere is rejected with the other queue's name/code and a Leave That Queue recovery button.
  • Ready Check shows an explicit Waiting on list (@user, with "· needs 5 minutes" where relevant) instead of only a count. Starting a ready check does not itself send a notification ping — outstanding players are shown as mentions only inside the embed, which doesn't notify.
  • Match-ready handoff is fully automatic and is the only roster ping in the whole flow: the final Ready response provisions the private room, posts the formation card, and sends one in-server ping with a real <#channel> mention — no DMs, no extra organizer step. The public card also updates to show "Match forming · Continue in #match-...".
  • Public queue cards are small: Join / Leave / Queue Settings / Cancel only. Formation, ready, and room controls live in their existing dedicated surfaces (private match workspace, ready-check card).
  • The public card shows compact per-player role context, e.g. Dustin · Support / Solo (Fill), capped at 6 rows with +N others beyond that — informational only, no team assignment happens here.

Organizer-facing UX changes

  • Share → Repost Queue, moved into an ephemeral Queue Settings panel (organizer-only) alongside Rename, Edit Details (the full mode/region/format/capacity/voice/skill/notes customization, moved out of the default creation path), Transfer Organizer (a Select limited to the queue's own roster), and a manual Start Ready Check fallback. share is kept as a recoverable alias custom_id for cards posted before this change.
  • Rename is a modal (the only remaining free-text input besides notes), sanitizes mentions/length, and never changes the lobby's internal ID or its stable queue code.
  • Leaving as organizer auto-transfers to the longest-tenured remaining participant, or cancels the queue if it's now empty.

Lifecycle / state changes

  • PartyLobby gains queue_code (stable 4-character code, deterministically derived from lobby_id, collision-checked per guild) and display_name (optional, sanitized, renameable while recruiting — never used as identity).
  • DiscordDelivery gains ready_channel_id/ready_message_id (durable ready-check card, mirroring the existing formation-card pattern) and match_ready_notified (idempotency flag for the one-time roster ping).
  • Recruiting queues now expire from 60 minutes of inactivity (was a flat 120-minute timer from creation) — touch_recruiting_activity() extends the clock on join/leave/rename/edit/waitlist-promotion, reusing the existing expires_at/recover_active() expiry machinery rather than a new timer system.
  • The final "everyone is ready" handoff is serialized per lobby (asyncio.Lock) so a double-click or retried interaction can't provision two rooms or send two roster pings; the roster ping is additionally gated on the durable match_ready_notified flag so a retry after a failed send can still deliver it.
  • Restart recovery (recover_match_controls) reconciles every active lifecycle stage — OPEN/FULL (repost card if missing), READY_CHECK (repost/refresh ready-check card), FORMING/ACTIVE (existing formation-card + re-sends a missed roster ping) — not just FORMING/ACTIVE as before.
  • One-active-queue-per-player is enforced at both join time and queue-creation time, and correctly catches waitlisted players (who aren't written to the durable participants table).
  • Leaving a queue mid-ready-check (including via the cross-queue "Leave That Queue" recovery action) reopens it to OPEN and resets the roster's ready state, mirroring the Ready Check Drop button.

Schema / migration changes

Additive only, SQLitePartyRepository auto-migrates on start (existing _add_columns pattern):

  • party_lobbies.queue_code TEXT NOT NULL DEFAULT '', party_lobbies.display_name TEXT NOT NULL DEFAULT ''. Pre-existing rows with an empty queue_code get one derived from lobby_id on read (no backfill script needed).
  • DiscordDelivery's new fields live inside the existing delivery_json blob column — no schema change there.

Tests

pytest tests/644 passed (up from 611 on main), across the original characterization suite plus tests/unit/test_party_queue_first_ux.py (35 tests) covering every #63 acceptance criterion: the desync bug fix, saved-preference fast join vs. first-time wizard, waitlist join/promotion, auto-publish on creation, rename identity-preservation, organizer transfer (manual + automatic), one-queue-per-player enforcement (including waitlisted players and at creation time), recruiting expiry + activity extending the clock, multi-queue isolation and explicit-choice routing, idempotent final-Ready handoff under concurrent retries, restart recovery across every lifecycle stage, the streamlined Start Queue path for returning/first-time organizers, no-ping ready-check start, and compact roster role context.

Implementation interpretations made

  • /party join <code> fallback command mentioned in Refactor and harden queue UX for auto-publish, named queues, and multi-queue servers #63 as "may be added... if it fits existing command patterns" was not added — the Find-a-Queue button flow already satisfies the explicit-choice requirement, and the issue marks this optional.
  • "End Queue" post-match continuity control: the existing MatchContinuityService/MatchContinuityView (Run It Back / Shuffle Teams / Return to Queue / Invite Substitutes / Continue Series) was reused as-is; there's no literal "End Queue" button distinct from simply not choosing a continuity action.
  • "Advanced Settings" during creation: does not exist as a separate in-line step. Full customization is reachable only after creation via Edit Details, matching the owner's literal target flow (Start Queue → optional queue name → Start) rather than adding a second, redundant configuration surface before the queue exists.
  • Fixed one pre-existing one-line latent bug encountered while touching launch_party_draft's error handler (except Exception: referenced an undefined exc — added as exc).

Known limitations

  • Recruiting-inactivity expiry is enforced correctly in durable state (verified by test) and rejects stale interactions, but the public card's visual "expired" state is pushed to Discord by the periodic ready-check-expiry cleanup pass rather than a dedicated per-guild timer — smallest safe interpretation reusing existing infrastructure.
  • /party room and the broader room/formation/draft pipeline were intentionally left untouched.

Manual Discord test checklist

Setup

  1. /party setup in a fresh test server — confirm #godforge-play and the Play panel are created/refreshed with exactly three buttons: Start Queue / Find a Queue / My Roles.

Creation & auto-publish
2. Click Start Queue as a player with no saved role prefs — confirm a modal asking only for an optional queue name. Submit it (with or without a name).
3. Confirm you're then shown the lightweight Primary/Secondary/Fill picker (no mode/region/capacity/etc. questions) — submit it.
4. Confirm: an ephemeral confirmation names the queue + its code and says it's live; a public card appears in #godforge-play immediately with no Share click needed; it shows queue name (or <you>'s Queue fallback), code, mode/format defaults (Conquest/5v5), roster 1/10 with your name and role, and only Join / Leave / Queue Settings / Cancel buttons.
5. Repeat Start Queue as an account that now has saved roles — confirm it skips straight from the name modal to a live queue with no role picker.

Join flow
6. As a second account with no saved role prefs, click Join Queue on the card → confirm the wizard only asks Primary (required) / Secondary (optional, pre-shows "None") / Fill (required) → submit without touching Secondary → confirm it still succeeds → confirm an ephemeral "Joined ..." ack, and the public card updates to show the new roster count and that player's role.
7. As that same account, leave and re-join with saved prefs now present → confirm it joins instantly with a "Change Roles" button on the ack, no wizard.
8. Fill the queue to one below capacity, then have a saved-prefs player join without a wizard, and confirm the queue auto-transitions to a Ready Check the moment it's full — a ready-check message posts automatically, no organizer action needed, and no one gets pinged by that message appearing.

Multi-queue
9. Start a second queue with different settings (use Queue Settings → Edit Details to change mode/region/etc. after creating it). Confirm both public cards coexist in #godforge-play independently.
10. From the Play panel, click Find a Queue → confirm you get an explicit picker listing both queues (name/code/roster) — never an automatic join. With only one open, confirm it shortcuts straight to joining/wizard.
11. Fill/ready-check one queue to completion; confirm the other queue's card and roster are completely unaffected.

Ready check & handoff
12. With a queue full, have players click Ready one at a time; confirm the ready-check message updates its Waiting on list after each click, without re-pinging anyone.
13. Have the last player click Ready: confirm — a private match channel is created; a single message pings the full roster with a clickable #match-... mention; the public card updates to "Match forming · Continue in #match-..."; the private channel has a formation-control message with Role Fit / Balanced / Captain Teams buttons.
14. Double-click Ready rapidly as the last player (or have two people click within the same second): confirm only one private room and one roster ping are created.

Organizer actions
15. As organizer, open Queue Settings on your card → try Rename (include an @mention in the name — confirm it's stripped), Edit Details, Transfer Organizer (pick another participant from the Select), and Repost Queue (delete the public card manually in Discord first, then Repost — confirm it reappears and delivery updates).
16. Leave a queue as organizer with other players still in it — confirm ownership silently transfers to the earliest joiner and the public card's Organizer field updates. Leave as the last remaining player — confirm the queue cancels.

Guardrails
17. Try to join a second queue while already active in one — confirm you're rejected with the existing queue's name/code and a Leave That Queue button that works, including while the other queue is mid-ready-check (confirm that queue reopens to OPEN afterward).
18. Try to Start Queue while already active in another one — confirm the same rejection instead of a second queue being created.
19. Force-expire a queue (or wait out the inactivity window in test mode) — confirm it transitions to expired and stale Join/Leave clicks are rejected with a clear message.

Restart recovery
20. Restart the bot with an OPEN queue, a READY_CHECK queue, and a FORMING match all in flight. Confirm on restart: the OPEN queue's card is still live (reposted if it had been deleted), the READY_CHECK card is still clickable, and the FORMING match's formation card + roster ping (if it hadn't gone out yet) are present.

🤖 Generated with Claude Code

https://claude.ai/code/session_014RF4jAhbRnZzcgG11SXarx


Generated by Claude Code

… servers (#63)

Fixes the join/public-card desync where join_lobby_from_preferences edited
interaction.message (often an ephemeral wizard) instead of the durable
public queue card. Refactors the Discord UX around a queue-first flow:
queue creation now auto-publishes to the configured Play channel, queues
get a stable code and optional rename-able display name, the global "Join
Queue" entry point requires an explicit choice once multiple queues are
open, join collects only Primary/Secondary/Fill (captain preference stays
in My Preferences), returning players join instantly from saved
preferences, ready checks show who is still outstanding and hand off to a
private match workspace automatically with a one-time roster ping and
clickable channel mention, and organizer transfer / one-active-queue-per-
player / recruiting inactivity expiry are enforced. Restart recovery now
restores controls for every active queue stage, and the final Ready
handoff is guarded against duplicate rooms/pings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RF4jAhbRnZzcgG11SXarx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

queue = await deps.party_queue_service.start_ready_check(lobby_id)

P2 Badge Reject unsupported rosters before starting ready checks

When the organizer manually starts a ready check with one player or any odd roster, this call succeeds and the lobby moves out of recruiting, which removes Join controls from the public card. Once everyone responds Ready, the handler says to wait for another player, but no player can join that ready check; the organizer must drop someone—potentially cancelling an empty queue—to recover. Validate the current roster as even and at least two before starting, or keep recruitment available during the check.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread utils/party_lobby.py
Comment thread utils/party_lobby.py
Comment thread utils/party_store.py
Comment thread utils/party_lobby.py
Comment thread utils/party_lobby.py
Comment thread utils/party_lobby.py
…us 5 P2s

- Leaving a queue via the cross-queue "Leave That Queue" recovery action
  while that queue is mid-ready-check now reopens it to OPEN and resets
  ready state (mirrors the Ready Check Drop button), instead of leaving the
  queue stuck in READY_CHECK with a roster that can never all become ready.
- Manual "Start Ready Check" now rejects an odd/undersized roster instead
  of stranding the queue outside recruiting with no way to complete.
- The periodic inactivity sweep now refreshes/disables the public card for
  queues it just expired, instead of only transitioning them in the DB.
- The one-active-queue-per-player guard now also applies to Start Queue
  (creating seats the organizer as a participant) and correctly catches
  waitlisted players, who aren't written to party_participants.
- The queue-name field added to the creation modal is now actually wired
  into the wizard's submitted payload.
- Renaming a queue now extends its recruiting-inactivity clock like other
  meaningful activity.

6 new regression tests added; full suite at 637 passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RF4jAhbRnZzcgG11SXarx

Copy link
Copy Markdown
Owner Author

Final UX cleanup pass before merge/testing

The current implementation is substantially aligned with #63 and the earlier review findings appear addressed. Before we freeze this for Discord testing, I want one last narrowly scoped cleanup pass focused only on preserving the NeatQueue-style simplicity we designed.

Please do not expand scope beyond these items.

1. Simplify organizer queue creation only — do not change player role selection

This applies only to the organizer-facing Start Queue flow.

New players joining a queue must still complete the existing lightweight SMITE role picker:

  • Primary Role: required
  • Secondary Role: optional
  • Fill: Yes/No

Returning players with a valid saved Primary Role may continue using the existing one-click fast path with Change Roles available afterward.

Target organizer path:

Start Queue
→ optional queue name
→ Start

Queue configuration that has sensible defaults should move behind Advanced Settings / Edit Details rather than being required every time. Do not remove the ability to customize queue settings; move complexity out of the default creation path.

Because the organizer is automatically seated as the queue's first participant, an organizer without a saved Primary Role must complete the same lightweight role selection before the queue is created. Organizers with saved roles should use those automatically.

So for a first-time organizer the effective flow is:

Start Queue
→ optional queue name
→ lightweight role selection
→ Start
→ organizer becomes player #1

For a returning organizer with saved roles:

Start Queue
→ optional queue name
→ Start
→ organizer becomes player #1 using saved roles

Do not remove, bypass, or weaken the first-time role-selection requirement. The goal is specifically to make queue configuration lightweight, not to remove SMITE role context.

2. Remove the Browse Queues / Find a Queue duplication

The Play panel should not expose both Browse Queues and Find a Queue.

Keep the primary panel minimal:

  • Start Queue
  • Find a Queue
  • My Roles

Remove Browse Queues from the normal player-facing surface.

Also verify there is no remaining path where browsing an open queue renders the legacy LobbyCardView with the larger/older action set. Open recruiting queues should consistently use the new simplified queue UX.

The public queue card remains the canonical queue surface.

3. Only ping the roster when the match is ready

We previously locked the notification contract as:

  • no required DMs
  • one in-server roster ping when the private match workspace is ready
  • clickable channel mention in that handoff
  • public queue card also links to the match channel

The ready-check card should not create an additional notification ping just because the ready check started.

It can still display the outstanding users as mentions in the embed/UI for clarity, but avoid a separate message-content roster ping that produces another notification.

Expected notification behavior:

queue fills
→ ready-check surface appears without notification-pinging the roster
→ players respond
→ final Ready completes
→ ONE roster ping:
  @players
  Match ready! Continue in <#match-channel>

Please verify retries/recovery still cannot duplicate that handoff ping.

4. Surface compact SMITE role context on the public queue card

The public card currently shows the roster/count but should retain the SMITE-specific value of GodForge by giving players quick role context before joining.

Keep this compact. Example:

Roster · 6/10

Dustin · Support / Solo
Debo · Jungle / Mid
Alex · ADC
Chris · Fill
+2 others

Exact formatting is flexible, but:

  • show primary/secondary role when available
  • clearly represent Fill
  • do not turn the card into a large analytics panel
  • preserve the existing small action set

This is informational only. Do not assign/finalize team roles on the recruiting card.

Scope freeze

After these four items:

  • run the relevant/full tests
  • add or update regression coverage where behavior changed
  • review for any remaining legacy Browse/Lobby surface
  • verify ready-check start does not notification-ping the roster
  • verify the only required roster ping is the match-ready handoff
  • verify Start Queue uses the streamlined default path
  • verify first-time players and first-time organizers still provide a required Primary Role
  • verify returning players/organizers correctly reuse saved roles
  • verify public queue cards show compact role context

Do not add new Party features or redesign downstream formation/draft/continuity behavior in this pass.

Once these are clean, the PR should be considered feature-frozen and ready for extensive manual Discord validation.

…eady-check ping, add role context

- Start Queue now asks only for an optional queue name (a one-field modal),
  using sensible defaults for mode/region/format/capacity/voice/skill;
  full customization stays available afterward via Queue Settings -> Edit
  Details. A first-time organizer (no saved primary role) still completes
  the same lightweight role picker a joining player would, since they're
  seated as the queue's first participant; a returning organizer with
  saved roles skips straight to Start.
- Removed "Browse Queues" from the Play panel (duplicated "Find a Queue");
  renamed "My Preferences" to "My Roles". Panel is now Start Queue / Find a
  Queue / My Roles.
- The ready-check card no longer sends a message-content roster ping when
  it's first created or refreshed — outstanding players are still shown as
  mentions, but inside the embed's "Waiting on" field, which renders
  without notifying anyone. The one-time match-ready handoff remains the
  only roster ping in the flow.
- The public queue card's roster field now shows compact per-player role
  context (name/mention · role, or Fill), capped at 6 rows with a "+N
  others" summary beyond that, instead of a bare list of mentions.

10 new regression tests added; full suite at 640 passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RF4jAhbRnZzcgG11SXarx

Copy link
Copy Markdown
Owner Author

Done in 6c38798 — all four items, tests added, full suite at 640 passing.

  1. Start Queue simplified. It's now a single-field modal (optional queue name only) — mode/region/format/capacity/voice/skill default (Conquest / 5v5 / 10 players / no voice requirement / open skill) and stay fully customizable afterward via Queue Settings → Edit Details. A first-time organizer (no saved Primary role) sees the same lightweight Primary/Secondary/Fill picker a joining player gets, since they're seated as participant Integrate Activity drafting backend via HTTP + WebSocket #1; a returning organizer with saved roles goes straight from the name modal to a live queue. handle_create_lobby_submission (the actual creation mechanics — validation, one-queue guard, auto-publish) was untouched; only how it's reached and what payload feeds it changed, so all its existing tests still pass unmodified.

  2. Browse Queues removed. Play panel is now Start Queue / Find a Queue / My Roles (renamed from My Preferences to match your list). Deleted the browse handler branch entirely rather than leaving it dead — confirmed the only other reference to the full LobbyCardView action set is the private match-formation-card fallback, unrelated to the public recruiting surface.

  3. Ready-check start no longer pings. Removed the message-content mention string from ensure_ready_check_card (both the initial post and every refresh) — outstanding players still show as mentions, but only inside the embed's "Waiting on" field, which Discord doesn't notify on. The one-time match-ready roster ping (_send_match_ready_handoff) is untouched and remains the only ping in the flow; its retry/dedup guarantees weren't touched by this change (still verified by the existing asyncio.gather duplicate-Ready test).

  4. Compact role context on the public card. Roster field is now Roster · 6/10 with per-player lines like Dustin · Support / Solo (Fill) / Debo · Fill, using guild display names where resolvable and falling back to a mention otherwise. Capped at 6 rows with +N others beyond that so it can't grow into an analytics panel.

One interpretation to flag: "Advanced Settings" during creation itself doesn't exist as a separate step — full customization is reachable only after creation via the existing Edit Details, not as an optional expand-in-place during Start Queue. That matched your literal target flow (Start Queue → optional queue name → Start) with nothing else in between, and avoided a second, redundant configuration surface. Happy to add a pre-creation "Advanced" branch if you'd rather have it available before the queue exists.

CI is green on the new commit; PR is feature-frozen from my side pending your read of the above.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Final micro-cleanup before feature freeze

One small UX mismatch remains in the first-time role picker.

We describe Secondary Role as optional, but JoinPreferencesView still requires the user to explicitly make a Secondary selection (including choosing None) before Join succeeds.

Please make Secondary Role default to None so the true minimum first-time flow is:

Primary Role [required]
Fill [Yes/No]
Join Queue

Secondary Role should remain available to change, but it should not require an explicit interaction when the player does not want one.

This applies equally to first-time organizers using the same lightweight role picker before queue creation.

Please add/update regression coverage to prove:

  • Primary Role is still required
  • Fill is still required
  • Secondary defaults to None
  • Join/Create succeeds without touching the Secondary select
  • selecting a Secondary role still works normally

After this, no more Party UX changes in this PR unless Discord testing reveals an actual defect. Consider the feature frozen for manual validation.

JoinPreferencesView required an explicit Secondary role choice (even
"None") before Join/Create would succeed, contradicting its own "optional"
label. Secondary role now defaults to None in view state, so Join succeeds
with only Primary role and Fill answered; it remains fully selectable.
Applies to both the joining-player wizard and the first-time-organizer
Start Queue path, which reuses the same view.

4 new regression tests covering: Primary still required, Fill still
required, Secondary defaults to None without interaction, Join succeeds
without touching Secondary, and selecting a Secondary role still works.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RF4jAhbRnZzcgG11SXarx

Copy link
Copy Markdown
Owner Author

Done in 630a784.

JoinPreferencesView.state["secondary_role"] now defaults to None in __init__ rather than only being set once the player interacts with that select — so it's always present by the time act() validates, and the dropdown itself pre-shows "None" as the default chip. Required-field validation now only checks Primary role and Fill.

Since the first-time-organizer Start Queue path reuses this exact same view, the fix applies there too automatically — no separate change needed.

Added 5 regression tests:

  • test_join_wizard_secondary_role_defaults_to_none_without_selection — state defaults correctly, dropdown pre-selects "None"
  • test_join_wizard_requires_only_primary_role_and_fill — Primary missing → rejected; Fill missing → rejected; both present with Secondary untouched → succeeds with secondary_role: None
  • test_join_wizard_secondary_role_still_selectable — explicitly picking a Secondary role still works
  • test_first_time_join_succeeds_without_touching_secondary_role — end-to-end: a real Join through handle_lobby_card_action with Secondary never touched persists correctly
  • (existing duplicate-role test still passes unmodified, confirming the primary==secondary rejection still works when both are explicitly set)

Full suite: 644 passing. Feature-frozen from my side per your note — ready for manual Discord validation.


Generated by Claude Code

Docs describing the Play panel, lobby creation, and join flow were stale
against the Issue #63/#64 queue-first hardening (auto-publish, named/multi
queues, no-Share, no-captain-in-join, single match-ready ping, etc.):

- README.md's "Zero-Config Guild Setup" section described the old 4-button
  panel and full-config creation wizard.
- docs/STANDALONE_PRODUCT_PLAN.md's Phase 1 spec (button list, creation
  modal fields, Share button) predates the rework; marked superseded with a
  pointer rather than rewritten, to preserve it as historical design intent.
- VERSION_HISTORY.md's "Guided lobby cards" bullet still described captain
  willingness as part of lobby creation/joining.

Added docs/PARTY_QUEUE_UX.md as the canonical current-behavior reference
(workflow, lifecycle rules, durable storage), indexed in docs/README.md.

Bumped GODFORGE_VERSION to 2.3.0-rc.3 (formatter.py, VERSION_HISTORY.md,
RELEASE_PROCESS.md, README.md, web/README.md, web/DATA_CONTRACT.md, and the
corresponding test) per RELEASE_PROCESS.md's own convention: this PR is
behavior change beyond rc.2, and the release gate blocks tagging stable
v2.3.0 until the live Discord smoke test this PR is meant for actually
happens.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RF4jAhbRnZzcgG11SXarx

Copy link
Copy Markdown
Owner Author

Added a documentation pass in 709f0f1 (unprompted follow-up, at the user's request in this session — not part of the earlier scoped items).

  • New docs/PARTY_QUEUE_UX.md: canonical reference for the current Play panel workflow, lifecycle rules (one-queue-per-player, 60-min expiry, organizer succession, idempotent handoff, restart recovery), and durable storage — indexed in docs/README.md.
  • Fixed README.md's "Zero-Config Guild Setup" section, which still described the old 4-button panel and full-config creation wizard.
  • docs/STANDALONE_PRODUCT_PLAN.md's Phase 1 spec (old button list, creation fields, Share button) predates this rework — marked superseded with a pointer to the new doc rather than rewritten, to keep it intact as historical design intent.
  • Fixed a stale VERSION_HISTORY.md bullet still describing captain willingness as part of creation/joining, and added a new bullet for this PR's hardening pass.
  • Bumped GODFORGE_VERSION to 2.3.0-rc.3 (formatter.py + the 6 doc/test locations RELEASE_PROCESS.md says to keep in sync). Reasoning: this PR is real behavior change beyond what rc.2's version string represents, and per RELEASE_PROCESS.md's own gate, stable v2.3.0 can't be tagged until the live Discord smoke test — which is exactly what this PR is queued up for — actually happens. No git tag was created; that's still gated on your manual validation.

Full suite: 644 passing, unchanged.


Generated by Claude Code

…est review

A dry-run of the Half-Shell adversarial review pipeline against this PR
surfaced one real, narrow gap: if match-room provisioning succeeds but
posting the formation card then fails right before a crash, the lobby is
left in READY_CHECK with rooms already provisioned, and restart recovery
does not retry completing that handoff.

The pipeline's own Shredder pass argued against building auto-resume for
this: it's self-healing today via one ordinary Ready re-click, and having
recovery author a Discord response with no live interaction behind it adds
more risk than the gap itself. Went with the smaller fix instead: log a
warning when recovery finds a lobby in this exact state, so it's observable
rather than silent, without changing its behavior.

Two tests pin this down: one proves the stuck state now logs a warning
(and confirms recovery still does NOT auto-transition it, documenting the
limitation as intentional rather than accidental), the other proves an
ordinary in-progress ready check with no rooms yet — the common case —
never triggers a false warning.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RF4jAhbRnZzcgG11SXarx

Copy link
Copy Markdown
Owner Author

Small addition in 233583a, unrelated to your last two rounds of feedback — surfaced by running an adversarial multi-agent review pipeline (a separate tool, half-shell) against this PR as a one-off test.

It found one real but narrow gap: if match-room provisioning succeeds but posting the formation card then fails right before a crash, the lobby is left in READY_CHECK with rooms already live, and restart recovery doesn't retry that handoff. It's self-healing today (a player re-clicking Ready re-triggers completion normally), so I didn't build auto-resume for it — that would mean recovery authoring a Discord response with no live interaction behind it, which is more risk than the gap itself. Instead: recover_match_controls now logs a warning when it finds a lobby stuck in exactly this state, so it's observable rather than silent. Two tests pin this down (the warning fires for the stuck case, and does not fire for an ordinary in-progress ready check).

Full suite: 646 passing.


Generated by Claude Code

Prompted by a real incident: an admin ran /party setup, then moved
#godforge-play into a different category, and it silently became
inaccessible (Discord's audit log showed it as "No Access") with no way to
recover other than manual server surgery.

- create_play_channel() now gives the bot an explicit channel-level
  permission overwrite (View Channel, Send Messages, Embed Links, Read
  Message History) at creation time. A plain drag-to-a-new-category move
  doesn't touch a channel's own overwrites unless someone explicitly syncs
  permissions, so this survives the exact scenario that broke it — it's
  not bulletproof against a deliberate permission sync, but it closes the
  common accidental case.
- New /party reset command: deletes every Discord resource /party setup is
  currently tracking (Play channel, room category, managed cosmetic roles)
  and clears the stored configuration, so a follow-up /party setup starts
  clean instead of hitting "channel already exists" conflicts from
  orphaned resources. Defaults to a dry-run preview; only deletes anything
  when called with confirm:True. Requires Manage Server, same as setup.
  Partial deletion failures (e.g. the exact permission loss that caused
  this) are reported but don't block clearing the stored config, so the
  admin isn't left stuck a second time.

12 new/updated tests. Full suite: 653 passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RF4jAhbRnZzcgG11SXarx

Copy link
Copy Markdown
Owner Author

Moved the setup/permissions fix (/party reset + the Play channel bot-overwrite hardening) out to its own PR — #65 — since it's unrelated to this PR's queue-UX scope, which you'd already marked frozen. Reverted it here in ff34a33; this branch is back to exactly what it was at 233583a.


Generated by Claude Code

@diese-tech
diese-tech merged commit 59e4dc8 into main Aug 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor and harden queue UX for auto-publish, named queues, and multi-queue servers

2 participants