- Product status: Confirmed
- Implementation status: Complete on feature branch
- Pull request status: In progress
- Rollout status: Discord preparation, review, merge, deployment, and smoke test pending
- Source of truth: This document
- Delivery: Feature branch and pull request only; do not push implementation directly to
main
Redesign Hermit's Shell Society nomination workflow so nominations originate in
#shell-society, Community Team voting happens privately in #ct-general, and
Hermit grants the Shell Society role after three approval votes.
The review board must support both approval and decline votes. The first side to reach three votes ends the review. Hermit must not publish a welcome or announcement; Barnacle Sapphire may handle that separately outside this project.
The current workflow:
- Allows
/nominatein both#shell-societyand the existing#nominate-users. - Posts the review card into the channel where the command was invoked.
- Supports only positive approvals.
- Requires three distinct Community Team approvals.
- Expires open nominations after 48 hours.
- Uses a durable
grantingstate to retry failed Discord role assignments. - Replaces the review card with public-facing welcome copy after approval.
The redesigned workflow separates nomination entry, private review, role assignment, public documentation, and any external announcement behavior.
- Make
#shell-societythe only channel where/nominatemay be used. - Send every review card to the private Community Team channel.
- Support distinct thumbs-up and thumbs-down votes.
- Finish a review when either side reaches three votes.
- Preserve durable and idempotent role assignment.
- Keep the review card private and retain its nomination details after completion.
- Keep Hermit completely out of public welcome and announcement delivery.
- Replace the old nomination channel with a read-only information channel managed through Discord.
- Posting public welcomes or announcements from Hermit.
- Integrating Hermit with Barnacle Sapphire through events, webhooks, APIs, or shared state.
- Adding nomination cooldowns.
- Building a backoffice or web administration interface.
- Changing general Shell Society access policy beyond this voting workflow.
- Automating Discord channel creation, archival, permissions, or informational message publication.
| Purpose | Channel | ID |
|---|---|---|
| Nomination command source | #shell-society |
1471742293055635536 |
| Community Team review | #ct-general |
1519064274561929328 |
| Existing nomination channel to archive | #nominate-users |
1471743636592001024 |
| New read-only information channel | #nominate-users |
Assigned when created |
Existing guild, Community Team role, and Shell Society role IDs remain unchanged.
The new information channel is documentation-only. Members must not be directed
to run /nominate there. Its ID remains intentionally unassigned until the
Discord administrator creates it during rollout preparation.
- A Shell Society member runs
/nominatein#shell-society. - The member selects a nominee and supplies a required reason.
- Hermit performs the existing validation:
- The nominee is not the nominator.
- The nominee is not a bot.
- The nominee is present in the guild.
- The nominee does not already have the Shell Society role.
- The nominee has no active nomination for the target role.
- Hermit stores the nomination and posts its review card in
#ct-general. - Hermit returns an ephemeral confirmation to the nominator.
No nomination review card or status message is posted publicly.
- The review card displays separate thumbs-up and thumbs-down controls.
- Only a member currently holding the configured Community Team approver role may vote.
- Each reviewer has one active vote per nomination:
- Selecting the same vote again is an idempotent no-op.
- Selecting the opposite vote replaces the reviewer's prior vote.
- The card displays both totals, for example
Approvals: 2/3andDeclines: 1/3. - Vote mutations and threshold evaluation are atomic so concurrent clicks cannot produce two terminal outcomes.
- The first valid transaction that reaches three approval votes moves the nomination into the role-granting state.
- Voting becomes terminal and both controls are disabled.
- Hermit grants the configured Shell Society role using the existing idempotent
Discord role
PUT. - After the role grant succeeds, Hermit marks the nomination approved.
- The private card is updated to show:
- Nominee, nominator, and reason.
- Final approval and decline totals.
- Status:
Approved. - Confirmation that the Shell Society role was granted.
Hermit does not post a welcome or announcement anywhere.
- The first valid transaction that reaches three decline votes marks the nomination declined.
- Voting becomes terminal and both controls are disabled.
- The private card retains the nomination details and final vote totals.
- Hermit performs no role or public-message action.
- A nomination still open after 48 hours is marked expired.
- Both voting controls are disabled.
- The private card retains the nomination details and final vote totals.
- A nomination that has already entered role granting does not expire.
| State | Card behavior |
|---|---|
| Open | Shows nomination details, both vote totals, and enabled voting controls |
| Role grant pending | Shows final totals and disabled controls while role assignment retries |
| Approved | Shows details, final totals, role-granted confirmation, and disabled controls |
| Declined | Shows details, final totals, and disabled controls |
| Expired | Shows details, final totals, and disabled controls |
The current embedded welcome copy must be removed from Hermit's nomination card and must not be relocated to another Hermit message.
Shell Society nominations
To nominate someone, use
/nominatein <#1471742293055635536> and include a brief reason.The Community Team reviews nominations privately. Three thumbs-up votes approve a nomination; three thumbs-down votes decline it. Nominations expire after 48 hours if neither side reaches three votes. Approved nominees receive the Shell Society role.
- Add a new migration after the current
0008nomination migration. - Keep the migration additive and compatible with the currently deployed code
because
deploy:cfapplies remote migrations before deploying the new Worker. New required fields must have old-code-compatible defaults, and the migration must not rename or remove fields used by the existing implementation. - Store one vote choice per nomination and reviewer.
- Migrate existing approval rows to the positive vote choice. The vote-choice field must default to the positive choice so old-code inserts remain valid during the migration-to-deploy window.
- Enforce uniqueness for
(nomination_id, reviewer_id). - Add the terminal
declinednomination status. - Preserve the active-nomination uniqueness rule for
submittedandgrantingnominations. - Preserve existing nomination history.
- Persist a monotonic desired review-card revision and the last successfully synchronized revision.
- Persist or derive the start time and consecutive failure count for pending role grants and review-card synchronization.
- Threshold evaluation must use current persisted votes, not client-provided counts.
- Only one transition may claim a submitted nomination as terminal or role-granting.
- A vote arriving after approval, decline, or expiry must not change stored state.
- Expiry and voting races must resolve to exactly one valid result.
- Component interactions must match the stored guild, review channel, message, and nomination identifiers.
- Repeated interactions must remain idempotent.
- Preserve the current durable
grantingstate. - A failed role grant remains pending and is retried by the scheduled worker.
- A role failure must never automatically decline or expire the nomination.
- The nomination becomes approved only after Discord confirms the role operation.
- Render every card update from fresh persisted nomination and vote state.
- Retry transient failures when updating terminal or role-pending cards.
- Every state or vote change increments the desired card revision.
- A worker marks a revision synchronized only when that same revision is still current after the Discord edit completes.
- If an older render reaches Discord after a newer state exists, the newer revision remains pending and must be rendered again. A stale worker must never clear the pending synchronization marker for a newer revision.
- Do not recreate a deleted review card automatically.
- Record and surface missing-card or repeated synchronization failures for manual repair.
- A card update failure must not reverse an approved, declined, or expired result.
/nominateremains guild-only and guild-install only./nominateis accepted only from#shell-society.- Voting is accepted only from current members of the configured Community Team role.
- Review messages are posted only in
#ct-general. - Nomination reasons must not be written to operational logs.
- Hermit sends no nomination-related public message beyond ephemeral command or component responses to the acting user.
The nomination recovery service owns observability. Use the existing Cloudflare Worker structured console logs; adding a separate alerting platform is out of scope.
Log these fields for every vote transition, role attempt, and card-sync attempt:
- Nomination ID.
- State transition.
- Approval and decline totals.
- Consecutive retry count.
- Pending duration in minutes.
- Discord response status.
- Desired and synchronized card revisions.
Emit warning or error logs when:
- A role grant or card synchronization fails.
- A nomination remains
grantingfor at least 30 minutes. - A card revision remains unsynchronized for at least 30 minutes.
- Either operation fails three consecutive times.
- Discord returns
404for the stored review card.
Do not log nomination reasons.
These decisions are confirmed for implementation:
| ID | Status | Proposed decision |
|---|---|---|
| DEC-1 | confirmed | Any current Community Team member may vote, including the nominator or nominee when that person holds the Community Team role |
| DEC-2 | confirmed | A declined nomination may be submitted again immediately; no cooldown is added |
| DEC-3 | confirmed | Rollout uses a command freeze and zero-active-nomination gate immediately before merge, followed by a named smoke-only command exception while normal access remains frozen |
| DEC-4 | confirmed | The information-channel copy in this document is the initial production copy |
For DEC-3, a Discord administrator disables application-command use in both current
nomination source channels before merge. An authorized release owner then verifies
the remote database has zero submitted or granting nominations and records that
result in the pull request. The command remains frozen for normal users until the new
deployment passes its smoke test. For that test only, the administrator grants one
named nominator a temporary explicit command permission in #shell-society, either
directly or through a dedicated one-member smoke-test role. A separate consenting
nominee must not already hold the Shell Society role, and three named Community Team
members must be available to vote. The release owner records those five user IDs plus
the resulting nomination and review-message IDs in the pull request, then removes the
nominee's temporary Shell Society role and the smoke-only command permission before
normal command access is enabled.
| ID | Workstream | Owner | Status | Dependencies | Acceptance criteria |
|---|---|---|---|---|---|
| PRD-1 | Channel configuration | Bot implementation owner | complete | DEC-1 through DEC-4 | /nominate accepts only #shell-society; new review cards always post to #ct-general; the old nomination channel ID is removed from the command allowlist |
| PRD-2 | Schema and migration | Data implementation owner | complete | DEC-1 through DEC-4, PRD-1 | Add an online-compatible 0009 migration for vote choice, declined state, existing-approval conversion, uniqueness, measurable retry state, and monotonic card revisions without losing history |
| PRD-3 | Atomic voting | Data implementation owner | complete | PRD-2 | Same-vote clicks are idempotent, opposite votes switch, concurrent votes produce one first-to-three outcome, and late votes cannot mutate terminal nominations |
| PRD-4 | Carbon review UI | Discord UX implementation owner | complete | PRD-1, PRD-3 | Carbon v2 card shows both counts and two thumb controls across open, pending, approved, declined, and expired states; no welcome copy remains |
| PRD-5 | Role, card recovery, and logs | Nomination service owner | complete | PRD-2 through PRD-4 | Existing durable role retry remains intact; stale card writes cannot clear newer pending revisions; measurable Worker logs cover pending and failure thresholds |
| PRD-6 | Tests | Test owner | complete | PRD-1 through PRD-5 | Automated coverage proves routing, permissions, interaction binding, vote switching, threshold races, expiry races, 0009 migration, all card states, recovery, and absence of public Hermit messages |
| PRD-7 | Discord rollout preparation | Discord administrator | pending | DEC-3, DEC-4, PRD-6 | Create and permission the read-only information channel without publishing it; prepare its guide; document command-freeze, unfreeze, publication, and old-channel archival steps |
| PRD-8 | Pull request and production rollout | Release owner | in progress; rollout pending | DEC-1 through DEC-4, PRD-1 through PRD-7 | Implement on a feature branch, push that branch, open a PR, pass review and checks, freeze normal command access, prove zero active nominations, merge to main, let Cloudflare Builds deploy, run a controlled smoke test through one temporary named command exception, record the test identities and Discord evidence, remove the test role and command exception, then activate normal Discord access; never push implementation directly to main or manually deploy production |
bun test: 98 passed, 0 failed.bun run typecheck: passed.bun run deploy:dry-run: passed.git diff --check: passed.bunx drizzle-kit check: passed.- Fresh local D1 migration application through
0009: passed.
Automated coverage must include:
- Command accepted in
#shell-society. - Command rejected from every other channel, including both old and new
#nominate-users. - Review card posted to
#ct-general. - Self, bot, missing-member, existing-role, missing-reason, long-reason, and duplicate-active-nomination validation.
- Community Team authorization for both voting controls.
- One vote per reviewer.
- Same-vote idempotency.
- Vote switching in both directions.
- Three-approval and three-decline outcomes.
- Mixed votes that remain open.
- Concurrent opposing threshold attempts.
- Vote-versus-expiry races.
- No votes accepted after terminal or granting states.
- Role failure and scheduled recovery.
- Card synchronization failure and retry.
- Approved, declined, expired, and role-pending card rendering.
- Existing approval-row migration.
- Migration discovery updated so the nomination harness includes
0009rather than stopping at0008. - Active-nomination uniqueness after migration.
- Forged or replayed component interactions from the wrong guild, channel, message, or nomination are rejected without changing votes.
- Stale card synchronization cannot mark a newer revision synchronized.
- No nomination-related public Discord message calls from Hermit.
Validation commands:
bun test
bun run typecheck
bun run deploy:dry-run
git diff --check- Confirm the Product Decisions section.
- Implement the PRD on a feature branch.
- Apply and validate the migration against a representative local database,
including a simulation where old code runs after
0009is applied. - Run all required tests and type checks.
- Push only the feature branch and open a pull request.
- Review the migration, race handling, Discord permissions, and rendered Carbon components.
- Create and permission the new read-only information channel, but do not publish the guide or archive the old channel yet.
- Immediately before merge, a Discord administrator disables application-command
use in
#shell-societyand the old#nominate-users. - With commands frozen, an authorized release owner verifies the remote database has
zero
submittedorgrantingnominations and records the result in the PR. - Merge the approved PR to
main; allow Cloudflare Builds to apply the online-compatible migration and deploy the Worker. - Confirm the configured Cloudflare production build succeeds.
- While normal command access remains frozen, designate and record:
- One named nominator who receives a temporary explicit
/nominatepermission in#shell-society, either directly or through a dedicated one-member smoke-test role. - A separate consenting nominee who does not hold the Shell Society role.
- Three named voters who currently hold the Community Team role.
- One named nominator who receives a temporary explicit
- Using only that temporary exception, submit a clearly labeled test nomination, complete the three-approval path, and verify the private terminal card and role assignment. Record the five user IDs plus the nomination and review-message IDs in the pull request. Retain the terminal test record for audit rather than deleting it.
- Remove the Shell Society role granted to the test nominee. Remove the nominator's temporary command permission, dedicated smoke-test role, or role membership.
- If the smoke test passes and cleanup is confirmed, enable normal
application-command use in
#shell-societyonly. - Publish the information-channel guide and archive the old nomination channel.
- If the smoke test fails, remove the temporary role and command exception, keep normal commands frozen, and use a reviewed roll-forward fix.
- Keep
0009additive and compatible with the old code during the migration-to-deploy window. - Before the first new-model vote is accepted, application code may be reverted through a reviewed PR because the additive migration defaults remain readable by old code.
- After any approve or decline vote is stored under the new model, rollback to the
previous approval-only application is prohibited. The previous code does not
understand decline votes, the
declinedstate, or card revisions. - After new-model voting begins, freeze commands and roll forward through a reviewed repair PR. Do not attempt destructive data conversion during an incident.
- Do not manually deploy a rollback or repair; use the repository's reviewed
mainworkflow.
The redesign is complete only when:
- Every delivery item is verified complete.
- All automated validation passes without weakened tests.
- The migration preserves existing nomination history.
- Concurrency tests prove one terminal outcome per nomination.
- Hermit grants approved roles durably and sends no public announcement.
- The CT card remains the complete private audit record after every terminal outcome.
- Discord operations are complete and documented.
- The implementation is merged through a reviewed pull request and the Cloudflare production build succeeds.