From 9aa6a53518189f2d60f4afc8fb723d3209218825 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sun, 1 Nov 2026 00:23:40 +0100 Subject: [PATCH 01/30] chore: spec for onboarding process --- GLOSSARY.md | 54 +++++++ ...onboarding-without-guild-members-intent.md | 137 ++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 GLOSSARY.md create mode 100644 docs/adr/0001-onboarding-without-guild-members-intent.md diff --git a/GLOSSARY.md b/GLOSSARY.md new file mode 100644 index 0000000..40035f4 --- /dev/null +++ b/GLOSSARY.md @@ -0,0 +1,54 @@ +# Web Dev & Design Bot + +A Discord bot for the Web Dev & Design server. This glossary covers the language of the new-member onboarding flow. + +## Onboarding + +**Onboarding**: +The bot's own, custom flow that moves a new member from limited access to full access through a series of roles. +_Avoid_: Discord onboarding (that is a different thing), welcome flow, verification + +**Discord onboarding**: +Discord's built-in question flow shown when someone joins, in which the member picks a role such as Developer or Designer. +_Avoid_: Onboarding (reserved for the bot's flow) + +**Everyone**: +A member holding no onboarding role, for example because they skipped Discord onboarding. They can only write in lounge and can read a few channels, including message mods. +_Avoid_: Roleless member, guest + +**Start role** (`onboarding:start`): +The role a member receives from Discord onboarding. It unlocks write access to the first set of channels and is the entry point of Onboarding. + +**7-day role** (`onboarding:7day`): +Granted once a member has been in the server for 7 days. + +**Messages role** (`onboarding:messages`): +Granted once a member's counted messages reach 400 characters in total. + +**Finished role** (`onboarding:finished`): +Granted when a member holds both the 7-day role and the messages role. It unlocks the rest of the server. The 7-day and messages roles are removed when it is granted. + +**Dormant role** (`onboarding:dormant`): +Granted to a member who has not reached the finished role 3 months after joining. It strips their onboarding progress and signals that they must take action to return. + +**Rectification**: +Leaving dormancy, either by the member pressing the button in the dormant channel or by a moderator using a command. It removes the dormant role and restarts the 7-day clock, the message count and the 3-month deadline from that moment. + +**Restart anchor**: +The moment from which a member's Onboarding clocks run: their join date, or their latest rectification if that is later. + +**Dormant channel**: +The one channel a dormant member can read besides message mods. It explains that their account is dormant and holds the button for rectification. + +**Counted message**: +A message that contributes characters toward the messages role. Messages sent before the start role count; messages deleted or caught by spam detection do not. Only the text counts, stripped of whitespace, markdown formatting and links; attachments add nothing. + +**Exempt member**: +A bot or moderator. They never receive onboarding roles and never go dormant. + +**Legacy member**: +A member who joined before Onboarding launched. They skip the flow and receive the start, finished and legacy roles the first time the bot sees them post. +_Avoid_: Grandfathered member (gendered term) + +**Legacy role** (`onboarding:legacy`): +A permanent marker that tells moderators a member was given the finished role by being a legacy member and did not earn it. diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.md b/docs/adr/0001-onboarding-without-guild-members-intent.md new file mode 100644 index 0000000..2ed710f --- /dev/null +++ b/docs/adr/0001-onboarding-without-guild-members-intent.md @@ -0,0 +1,137 @@ +--- +status: accepted +--- + +# Onboarding: a role-based member journey, built without the GuildMembers intent + +Terms in **bold** are defined in [GLOSSARY.md](../../GLOSSARY.md). + +## 1. What Onboarding is + +New members of the Web Dev & Design server start with very little access and earn the rest of the server over time. **Onboarding** is the bot's own flow for this. It is separate from **Discord onboarding**, the question Discord shows at join, where the member picks a role such as Developer or Designer. + +The purpose is to let newcomers take part in a few friendly channels straight away, while the rest of the server is held back until they have shown they are present and engaged. Members who never come back lose access again. + +### The journey + +| Stage | How a member gets there | What they can do | +| ---------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Everyone** | Joined, but has no onboarding role (for example skipped Discord onboarding) | Write in lounge, read a few channels, read message mods to open a ticket | +| **Start role** | Granted by Discord onboarding | Also write in help forum, ai, gaming, real life, frontend, design, backend and showcase | +| **7-day role** | 7 days after the **restart anchor** | Milestone only | +| **Messages role** | 400 **counted message** characters since the restart anchor | Milestone only | +| **Finished role** | Holds start, 7-day and messages | Full server access. Permanent. The 7-day and messages roles are removed | +| **Dormant role** | 3 months after the restart anchor without the finished role | Loses access to everything except message mods and the **dormant channel**. The 7-day and messages roles are removed; the start role stays | +| **Legacy member** | Joined before launch, first seen posting | Receives start, finished and **legacy role** at once | + +### Rules + +- The restart anchor is the later of the member's join date and their latest rectification. The 7-day clock, the message count and the 3-month dormancy deadline all run from it. +- Dormancy applies to anyone without the finished role, including members who never held the start role. +- Finishing needs the start role. A member could meet the 7-day and message conditions without it, but the finished role waits until Discord onboarding has been completed. +- Counted messages are text only, with whitespace, markdown syntax and links removed. Attachments add nothing. Messages written before the start role count. Messages that are deleted, or removed by spam detection, do not. Edits recount the message. +- **Rectification** leaves dormancy. A moderator runs `/onboarding reactivate @member`, or the member presses the button in the dormant channel. Both remove the dormant role, set a new restart anchor and discard the old message count. +- A member who leaves and rejoins starts from nothing, even if they had finished before. +- Bots and moderators (**exempt members**) are never touched. +- Which channels each role can see or write in is configured by moderators in Discord. The bot only adds and removes roles. It never edits channel permission overwrites. + +## 2. The decision + +Onboarding is implemented as a role reconciler that works **without the GuildMembers privileged intent**. It relies on a small tracking table, the join system message, per-message events and a daily job that fetches members one by one. + +### Why not use the intent + +The intent is the obvious route. It delivers join, leave and role-update events and lets the bot list every member. It is privileged. Discord's [Gateway documentation](https://docs.discord.com/developers/events/gateway) says apps with fewer than 10,000 users can switch privileged intents on in the Developer Portal, and approval is needed above that. This app already needed approval for the Message Content intent, so the same process would apply to GuildMembers. We chose not to depend on an approval that Discord controls and could refuse or revoke. + +### What this costs + +- Roles granted outside the bot (Discord onboarding giving the start role) are noticed late: when the member next posts, or at the next daily run. +- Leaves are noticed late, at the next daily run. +- Time-based roles (7-day, dormant) are applied once a day, so they can arrive up to a day late. +- Existing members can't be listed, so they become **legacy members** one at a time, as they post. +- A silent member whose join message was missed is invisible to the daily run and will never go dormant. + +## 3. Technical design + +### Signals the bot uses + +| Need | Source | Needs privileged intent | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | +| A member joined | System join message (`MessageType.UserJoin`) in the server's system channel. `message.author` is the joiner | No (GuildMessages) | +| Join date, roles | `message.member` on every guild message (`joinedAt`, `roles`) | No | +| Message counted | `MessageCreate` | No | +| Message edited | `MessageUpdate` | No | +| Message uncounted | `MessageDelete` and `MessageDeleteBulk` | No | +| Member left | Daily run fetches each tracked member by ID and treats Discord's "Unknown Member" error (code 10007) as a leave | No | +| Start role granted | Seen on the member's next message, or when the daily run fetches them | No | + +Spam detection removes messages with `bulkDelete`. discord.js reports that with a separate bulk-delete event, and the bot has no delete listeners today. Both events must be handled, otherwise spam would still count toward the 400 characters. + +### Data + +Two new Prisma models in [schema.prisma](../../prisma/schema.prisma): + +- **Onboarding**: one row per member who is in progress or dormant. Columns are the user ID (primary key), join date and restart anchor. The row is deleted when the member finishes or leaves. +- **OnboardingMessage**: one row per counted message. Columns are the message ID (primary key), the user ID (a foreign key to `Onboarding` that cascades on delete) and the counted length. A member's total is the sum of their rows. A message's creation time is not stored: messages before the anchor are filtered when they arrive, and the time is encoded in the message ID if it is ever needed. Rectification deletes the member's rows. + +Roles are not mirrored in the database. Discord's role list is the source of truth for the current stage, and the tables hold only what Discord cannot tell us (the anchor and the message lengths). + +### Why message records get their own table + +Counting a message, uncounting a deleted one and recounting an edited one are each a single statement keyed by message ID, so concurrent events for the same member cannot overwrite each other. A delete event for a message that is not in the bot's cache carries only the message ID and no author, and the primary key finds the owner directly. Replays (startup catch-up, duplicate events) become upserts on the primary key and cannot double-count. The cascading foreign key removes a member's messages when their row goes. + +Storing the messages as a JSON column on `Onboarding` was considered and rejected; see section 4. + +### One decision function + +All entry points call the same two pieces: + +1. A pure function takes the member's roles, restart anchor, counted characters, join date and the current time, and returns the roles to add and remove. It has no Discord calls, so it can be unit-tested. +2. An applier makes the role changes and writes a line to the onboarding log channel. + +Evaluation order inside the pure function: + +1. **Exempt member**: nothing. +2. **Finished role present**: remove any leftover 7-day and messages roles, stop tracking. +3. **Joined before the launch date**: add start, finished and legacy. +4. **Dormant role present**: nothing until rectification. +5. **3 months past the anchor**: remove 7-day and messages, add dormant. +6. **7 days past the anchor**: add 7-day. +7. **400 counted characters**: add messages. +8. **Start, 7-day and messages all held**: add finished, remove 7-day and messages. + +Role changes run one at a time, as the existing [addRoleToUser](../../src/util/add-role-to-user.ts) does, so a large first run stays inside Discord's rate limits. Functions throw when they cannot complete their job and the caller handles it. + +### Entry points + +- **Join message**: create the tracking row, or reset it if the member is already tracked with an earlier join date. Handling is idempotent so replays are safe. +- **Any guild message from a member who is not exempt and not finished**: if there is no tracking row, create one from `message.member.joinedAt`. Record the counted length (messages before the restart anchor are ignored), then evaluate. +- **Message edit and delete events**: recompute or remove the message record, then evaluate. +- **Daily job**: scheduled with `node-cron` at a fixed UTC time, like the showcase digest. For each tracked member it fetches the member by ID. If the member is gone, their data is deleted. Otherwise it re-evaluates them. +- **Startup catch-up**: fetch recent history of the system channel and replay join messages the bot missed while offline. +- **Rectification**: a shared function checks the member holds the dormant role (throws if not), removes it, sets the anchor to now, deletes the message records and evaluates. The `/onboarding reactivate` slash command and the dormant-channel button both call it. + +### Dormant channel + +At startup the bot makes sure one fixed message with a "Reactivate" button exists in the dormant channel, and posts it if missing. The placeholder text says the account has been dormant and the member should press the button to restart onboarding. The final wording is a constant that will be replaced later. The button is handled by the persistent interaction handlers so it survives restarts, and it rejects members without the dormant role. + +### Configuration + +- **Constants** in one onboarding config file: counted-character threshold (400), days until the 7-day role (7) and dormancy period (3 months), daily-job time, launch-date constant. +- **Required env vars** in [env.ts](../../src/env.ts): the five onboarding role IDs plus the legacy role ID, the dormant channel ID and the onboarding log channel ID, and the launch date. The optional `ONBOARDING_ROLE_ID` and `ONBOARDING_CHANNEL_ID` and the unused `/onboarding` stub are removed. +- **Permissions**: the bot's own role must sit above all onboarding roles and have Manage Roles, and it needs View Channel on the system channel. +- The GuildMembers intent stays off. No change to the intents in [index.ts](../../src/index.ts). + +## 4. Considered options + +- **Apply for the GuildMembers intent.** Simplest design. It would also give events for role changes and leaves, and a list of all members for a clean backfill. Rejected because it depends on Discord's approval, which has been needed before and is not ours to decide. +- **Keep a running character total per member instead of per-message records.** Less storage. Rejected because deleted and spam-removed messages must stop counting, and a total alone cannot tell which characters to subtract. +- **One table, with the per-message lengths in a JSON column on `Onboarding`** (message ID mapped to counted length). Simpler schema, one read for a member's whole state, and one delete for cleanup. Rejected for three reasons. Every update is a read-modify-write of the whole column, so two events for the same member, such as a new message during a spam bulk-delete, can silently overwrite each other unless updates are serialised per member. A delete event for an uncached message gives only the message ID, so finding the member who owns it means scanning every member's JSON with raw SQL, since Prisma cannot filter inside JSON on SQLite as far as we know. And the total would be computed in code, with the whole blob rewritten on every message. Fixing the first two with a per-member lock and a message-to-member lookup would rebuild the second table in a worse form. +- **Hourly job instead of daily.** Faster role delivery. Not chosen: a daily job was the requirement and the lag is acceptable. +- **Bot manages channel permission overwrites.** Would let the bot enforce what "dormant" means. Rejected because channel permissions belong to moderators and the onboarding flow is meant to be roles only. + +## 5. To verify before building + +- That the bot can see and read the system join messages in the real server, and that welcome messages are enabled there. +- That fetching a single member by ID works with the intent off. Discord's docs say only List Guild Members requires the intent. The Get Guild Member page does not mention one, and the existing stub in [features/onboarding](../../src/features/onboarding/index.ts) already calls it. +- Whether the `messages` role should be revoked if later deletions drop a member below the threshold before they finish. The current design reconciles in that direction, but it needs a decision. From 64ca11df584df1a429f08c55e87dafd55eb019d1 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sun, 1 Nov 2026 01:11:46 +0100 Subject: [PATCH 02/30] chore: update row deletion and user leaves --- ...1-onboarding-without-guild-members-intent.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.md b/docs/adr/0001-onboarding-without-guild-members-intent.md index 2ed710f..ca4b2da 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.md @@ -30,8 +30,8 @@ The purpose is to let newcomers take part in a few friendly channels straight aw - Dormancy applies to anyone without the finished role, including members who never held the start role. - Finishing needs the start role. A member could meet the 7-day and message conditions without it, but the finished role waits until Discord onboarding has been completed. - Counted messages are text only, with whitespace, markdown syntax and links removed. Attachments add nothing. Messages written before the start role count. Messages that are deleted, or removed by spam detection, do not. Edits recount the message. -- **Rectification** leaves dormancy. A moderator runs `/onboarding reactivate @member`, or the member presses the button in the dormant channel. Both remove the dormant role, set a new restart anchor and discard the old message count. -- A member who leaves and rejoins starts from nothing, even if they had finished before. +- **Rectification** leaves dormancy. A moderator runs `/onboarding reactivate @member`, or the member presses the button in the dormant channel. Both remove the dormant role and set a new restart anchor. The message count is already empty, because it is cleared when the member becomes dormant. +- A member who leaves loses their Onboarding progress the next time the daily job runs. If they rejoin before that run, nothing changes: their restart anchor and counted messages stay as they were. If they rejoin after it, they start Onboarding again, even if they had finished before. Counted messages are only deleted in two cases: their onboarding record is deleted (they finished, or the daily job found they had left), or they become dormant. - Bots and moderators (**exempt members**) are never touched. - Which channels each role can see or write in is configured by moderators in Discord. The bot only adds and removes roles. It never edits channel permission overwrites. @@ -50,6 +50,7 @@ The intent is the obvious route. It delivers join, leave and role-update events - Time-based roles (7-day, dormant) are applied once a day, so they can arrive up to a day late. - Existing members can't be listed, so they become **legacy members** one at a time, as they post. - A silent member whose join message was missed is invisible to the daily run and will never go dormant. +- Whether a leave resets progress depends on timing. A member who leaves and rejoins before the daily job runs keeps their anchor and counted messages, while one who rejoins after the job has deleted their row starts from zero. That follows from detecting leaves only in the daily job. ## 3. Technical design @@ -72,13 +73,15 @@ Spam detection removes messages with `bulkDelete`. discord.js reports that with Two new Prisma models in [schema.prisma](../../prisma/schema.prisma): - **Onboarding**: one row per member who is in progress or dormant. Columns are the user ID (primary key), join date and restart anchor. The row is deleted when the member finishes or leaves. -- **OnboardingMessage**: one row per counted message. Columns are the message ID (primary key), the user ID (a foreign key to `Onboarding` that cascades on delete) and the counted length. A member's total is the sum of their rows. A message's creation time is not stored: messages before the anchor are filtered when they arrive, and the time is encoded in the message ID if it is ever needed. Rectification deletes the member's rows. +- **OnboardingMessage**: one row per counted message. Columns are the message ID (primary key), the user ID (a foreign key to `Onboarding` that cascades on delete) and the counted length. A member's total is the sum of their rows. A message's creation time is not stored: messages before the anchor are filtered when they arrive, and the time is encoded in the message ID if it is ever needed. A member's rows are deleted in exactly two cases: their `Onboarding` row is deleted, or they become dormant. Roles are not mirrored in the database. Discord's role list is the source of truth for the current stage, and the tables hold only what Discord cannot tell us (the anchor and the message lengths). ### Why message records get their own table -Counting a message, uncounting a deleted one and recounting an edited one are each a single statement keyed by message ID, so concurrent events for the same member cannot overwrite each other. A delete event for a message that is not in the bot's cache carries only the message ID and no author, and the primary key finds the owner directly. Replays (startup catch-up, duplicate events) become upserts on the primary key and cannot double-count. The cascading foreign key removes a member's messages when their row goes. +Counting a message, uncounting a deleted one and recounting an edited one are each a single statement keyed by message ID, so concurrent events for the same member cannot overwrite each other. A delete event for a message that is not in the bot's cache carries only the message ID and no author, and the primary key finds the owner directly. Replays (startup catch-up, duplicate events) become upserts on the primary key and cannot double-count. Message rows are removed by a cascade, not by application code. `OnboardingMessage` declares its relation to `Onboarding` with `onDelete: Cascade`, so deleting a member's `Onboarding` row deletes all of their `OnboardingMessage` rows in the same operation. The code that finishes a member or handles a leave only deletes the `Onboarding` row and cannot forget the messages. Becoming dormant is the exception: the `Onboarding` row stays, so the dormancy step deletes that member's message rows explicitly. Rectification and rejoining never delete message rows. + +SQLite only enforces foreign keys when they are switched on for the connection. A test must confirm the cascade really fires with the better-sqlite3 adapter in use, and the schema must not rely on it silently doing nothing. Storing the messages as a JSON column on `Onboarding` was considered and rejected; see section 4. @@ -95,7 +98,7 @@ Evaluation order inside the pure function: 2. **Finished role present**: remove any leftover 7-day and messages roles, stop tracking. 3. **Joined before the launch date**: add start, finished and legacy. 4. **Dormant role present**: nothing until rectification. -5. **3 months past the anchor**: remove 7-day and messages, add dormant. +5. **3 months past the anchor**: remove 7-day and messages, add dormant, delete the member's message rows. 6. **7 days past the anchor**: add 7-day. 7. **400 counted characters**: add messages. 8. **Start, 7-day and messages all held**: add finished, remove 7-day and messages. @@ -104,12 +107,12 @@ Role changes run one at a time, as the existing [addRoleToUser](../../src/util/a ### Entry points -- **Join message**: create the tracking row, or reset it if the member is already tracked with an earlier join date. Handling is idempotent so replays are safe. +- **Join message**: create the member's `Onboarding` row. If a row already exists, nothing happens, whether the join message is a replay or the member left and rejoined before the daily job noticed (for example after a technical glitch). The row keeps its original join date and restart anchor, so a member who had been in the server for 6 days does not restart their 7-day clock, and the message rows are untouched. The roles lost while away are re-granted by the next evaluation. - **Any guild message from a member who is not exempt and not finished**: if there is no tracking row, create one from `message.member.joinedAt`. Record the counted length (messages before the restart anchor are ignored), then evaluate. - **Message edit and delete events**: recompute or remove the message record, then evaluate. - **Daily job**: scheduled with `node-cron` at a fixed UTC time, like the showcase digest. For each tracked member it fetches the member by ID. If the member is gone, their data is deleted. Otherwise it re-evaluates them. - **Startup catch-up**: fetch recent history of the system channel and replay join messages the bot missed while offline. -- **Rectification**: a shared function checks the member holds the dormant role (throws if not), removes it, sets the anchor to now, deletes the message records and evaluates. The `/onboarding reactivate` slash command and the dormant-channel button both call it. +- **Rectification**: a shared function checks the member holds the dormant role (throws if not), removes it, sets the anchor to now and evaluates. It does not touch message rows. The `/onboarding reactivate` slash command and the dormant-channel button both call it. ### Dormant channel From b92f9b99d06ab3b15c7ae7564ca03262a08617d4 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sun, 1 Nov 2026 02:26:34 +0100 Subject: [PATCH 03/30] chore: state problem and goal mor eexplicitly --- ...onboarding-without-guild-members-intent.md | 109 +++++++++++++----- 1 file changed, 80 insertions(+), 29 deletions(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.md b/docs/adr/0001-onboarding-without-guild-members-intent.md index ca4b2da..2393d7e 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.md @@ -6,23 +6,44 @@ status: accepted Terms in **bold** are defined in [GLOSSARY.md](../../GLOSSARY.md). +## Problem and goals + +Regulars have said they are tired of dealing with low-quality content from new joiners every day: badly formed help requests, and "look at my site" posts for projects that are fully vibe coded. They would prefer a calmer environment, with newcomers kept out of general chat and given the lounge instead. The trigger for this work is low-effort help requests, not a specific spam incident. + +Onboarding exists to serve these goals, in this order of weight: + +1. **Protect regulars' attention.** Keep low-effort questions and self-promotion out of the busiest channels until a newcomer has had time to settle in. +2. **Server comprehension.** Newcomers should have seen the rules and learned how to ask a good question before they reach the whole server. +3. **Spam and raids.** Fresh accounts have limited reach while trust is unknown. +4. **Drive-by accounts.** People who join, post once and leave should not cost moderators time. +5. **Limit hijacked dormant accounts.** An account that has never taken part for months can later be compromised and used to post scam messages. Dormancy removes the ability to write from accounts that have shown no sign of life. +6. **Encourage participation.** Newcomers get a friendly place to take part straight away (the lounge and a few topic channels) and a path to the rest. + +Reading is not restricted by this flow. Members who read but rarely post keep their access to read the server. Onboarding gates where a member can write, not whether they may be here. A dormant member can read but cannot write until they reactivate. + +### What the gates do and do not do + +The 7-day wait and the 400-character threshold are deliberate friction. They keep newcomers out of general chat and the rest of the server while they settle in, and they cost a drive-by account some effort. They do not judge quality. A member can pass them with filler, and a newcomer can still post a low-effort question or project in the channels the start role opens, including help forum and showcase. Moderation and the community's own norms remain responsible for quality. Onboarding only limits how much of the server a newcomer can reach while that happens. + +Every gate in section 1 must trace back to one of these goals. A gate that serves none of them should be removed. + ## 1. What Onboarding is New members of the Web Dev & Design server start with very little access and earn the rest of the server over time. **Onboarding** is the bot's own flow for this. It is separate from **Discord onboarding**, the question Discord shows at join, where the member picks a role such as Developer or Designer. -The purpose is to let newcomers take part in a few friendly channels straight away, while the rest of the server is held back until they have shown they are present and engaged. Members who never come back lose access again. +The purpose is to let newcomers take part in a few friendly channels straight away, while the rest of the server is held back until they have shown they are present and engaged. Members who never finish lose the ability to write again, though they can still read. ### The journey -| Stage | How a member gets there | What they can do | -| ---------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Everyone** | Joined, but has no onboarding role (for example skipped Discord onboarding) | Write in lounge, read a few channels, read message mods to open a ticket | -| **Start role** | Granted by Discord onboarding | Also write in help forum, ai, gaming, real life, frontend, design, backend and showcase | -| **7-day role** | 7 days after the **restart anchor** | Milestone only | -| **Messages role** | 400 **counted message** characters since the restart anchor | Milestone only | -| **Finished role** | Holds start, 7-day and messages | Full server access. Permanent. The 7-day and messages roles are removed | -| **Dormant role** | 3 months after the restart anchor without the finished role | Loses access to everything except message mods and the **dormant channel**. The 7-day and messages roles are removed; the start role stays | -| **Legacy member** | Joined before launch, first seen posting | Receives start, finished and **legacy role** at once | +| Stage | How a member gets there | What they can do | +| ----------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| **Everyone** | Joined, but has no onboarding role (for example skipped Discord onboarding) | Write in lounge, read a few channels, read message mods to open a ticket | +| **Start role** | Granted by Discord onboarding | Also write in help forum, ai, gaming, real life, frontend, design, backend and showcase | +| **7-day role** | 7 days after the **restart anchor** | Milestone only | +| **Messages role** | 400 **counted message** characters since the restart anchor | Milestone only | +| **Finished role** | Holds start, 7-day and messages | Full server access. Permanent. The 7-day and messages roles are removed | +| **Dormant role** | 3 months after the restart anchor without the finished role | Loses access to everything except message mods and the **dormant channel**. The 7-day and messages roles are removed; the start role stays | +| **Legacy member** | Joined before launch, first seen posting | Receives start, finished and **legacy role** at once | ### Rules @@ -30,8 +51,9 @@ The purpose is to let newcomers take part in a few friendly channels straight aw - Dormancy applies to anyone without the finished role, including members who never held the start role. - Finishing needs the start role. A member could meet the 7-day and message conditions without it, but the finished role waits until Discord onboarding has been completed. - Counted messages are text only, with whitespace, markdown syntax and links removed. Attachments add nothing. Messages written before the start role count. Messages that are deleted, or removed by spam detection, do not. Edits recount the message. -- **Rectification** leaves dormancy. A moderator runs `/onboarding reactivate @member`, or the member presses the button in the dormant channel. Both remove the dormant role and set a new restart anchor. The message count is already empty, because it is cleared when the member becomes dormant. -- A member who leaves loses their Onboarding progress the next time the daily job runs. If they rejoin before that run, nothing changes: their restart anchor and counted messages stay as they were. If they rejoin after it, they start Onboarding again, even if they had finished before. Counted messages are only deleted in two cases: their onboarding record is deleted (they finished, or the daily job found they had left), or they become dormant. +- **Rectification** leaves dormancy. A moderator runs `/onboarding reactivate @member`, or the member presses the button in the dormant channel. Both remove the dormant role and set a new restart anchor. The message count is already empty, because it is cleared when the member becomes dormant. A returning member starts over on purpose, with another 7 days and 400 characters: the point is to show they are present again. +- The messages role is held only while the counted characters stay at or above the threshold. If deletions or spam removals drop a member below it before they finish, the role is removed, so posting 400 characters and deleting them does not count. +- A member who leaves keeps their Onboarding progress for a grace period of 7 days, counted from when the daily job first notices they are gone. If they rejoin within it, their restart anchor and counted messages are as they were, however soon after leaving they return. The time away counts toward the 7 days and the dormancy deadline, because the anchor does not move. After the grace period the progress is deleted and they start Onboarding again. Members who had already finished always start again, because their record is gone and Discord removes their roles when they leave. Counted messages are only deleted in two cases: their onboarding record is deleted (they finished, or the grace period after leaving ran out), or they become dormant. - Bots and moderators (**exempt members**) are never touched. - Which channels each role can see or write in is configured by moderators in Discord. The bot only adds and removes roles. It never edits channel permission overwrites. @@ -49,22 +71,22 @@ The intent is the obvious route. It delivers join, leave and role-update events - Leaves are noticed late, at the next daily run. - Time-based roles (7-day, dormant) are applied once a day, so they can arrive up to a day late. - Existing members can't be listed, so they become **legacy members** one at a time, as they post. -- A silent member whose join message was missed is invisible to the daily run and will never go dormant. -- Whether a leave resets progress depends on timing. A member who leaves and rejoins before the daily job runs keeps their anchor and counted messages, while one who rejoins after the job has deleted their row starts from zero. That follows from detecting leaves only in the daily job. +- Dormancy is best-effort. It only covers members the bot saw join or post. A silent member whose join message was missed is invisible to the daily run, and a legacy member who never posts is never evaluated, so neither goes dormant and both stay exposed to the hijacked-account risk in the goals. This is accepted. +- The leave grace period starts when the daily job notices the leave, not when the member left, so it can run up to a day longer than 7 days. ## 3. Technical design ### Signals the bot uses -| Need | Source | Needs privileged intent | -| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -| A member joined | System join message (`MessageType.UserJoin`) in the server's system channel. `message.author` is the joiner | No (GuildMessages) | -| Join date, roles | `message.member` on every guild message (`joinedAt`, `roles`) | No | -| Message counted | `MessageCreate` | No | -| Message edited | `MessageUpdate` | No | -| Message uncounted | `MessageDelete` and `MessageDeleteBulk` | No | -| Member left | Daily run fetches each tracked member by ID and treats Discord's "Unknown Member" error (code 10007) as a leave | No | -| Start role granted | Seen on the member's next message, or when the daily run fetches them | No | +| Need | Source | Needs privileged intent | +| ------------------ | --------------------------------------------------------------------------------------------------------------- | ----------------------- | +| A member joined | System join message (`MessageType.UserJoin`) in the server's system channel. `message.author` is the joiner | No (GuildMessages) | +| Join date, roles | `message.member` on every guild message (`joinedAt`, `roles`) | No | +| Message counted | `MessageCreate` | No | +| Message edited | `MessageUpdate` | No | +| Message uncounted | `MessageDelete` and `MessageDeleteBulk` | No | +| Member left | Daily run fetches each tracked member by ID and treats Discord's "Unknown Member" error (code 10007) as a leave | No | +| Start role granted | Seen on the member's next message, or when the daily run fetches them | No | Spam detection removes messages with `bulkDelete`. discord.js reports that with a separate bulk-delete event, and the bot has no delete listeners today. Both events must be handled, otherwise spam would still count toward the 400 characters. @@ -72,7 +94,7 @@ Spam detection removes messages with `bulkDelete`. discord.js reports that with Two new Prisma models in [schema.prisma](../../prisma/schema.prisma): -- **Onboarding**: one row per member who is in progress or dormant. Columns are the user ID (primary key), join date and restart anchor. The row is deleted when the member finishes or leaves. +- **Onboarding**: one row per member who is in progress or dormant. Columns are the user ID (primary key), join date, restart anchor, and the time the bot first noticed the member had left (empty while they are in the server). The row is deleted when the member finishes, or when the grace period after leaving has ended. - **OnboardingMessage**: one row per counted message. Columns are the message ID (primary key), the user ID (a foreign key to `Onboarding` that cascades on delete) and the counted length. A member's total is the sum of their rows. A message's creation time is not stored: messages before the anchor are filtered when they arrive, and the time is encoded in the message ID if it is ever needed. A member's rows are deleted in exactly two cases: their `Onboarding` row is deleted, or they become dormant. Roles are not mirrored in the database. Discord's role list is the source of truth for the current stage, and the tables hold only what Discord cannot tell us (the anchor and the message lengths). @@ -100,17 +122,17 @@ Evaluation order inside the pure function: 4. **Dormant role present**: nothing until rectification. 5. **3 months past the anchor**: remove 7-day and messages, add dormant, delete the member's message rows. 6. **7 days past the anchor**: add 7-day. -7. **400 counted characters**: add messages. +7. **400 counted characters**: add messages. Below 400 while holding the messages role: remove it. 8. **Start, 7-day and messages all held**: add finished, remove 7-day and messages. Role changes run one at a time, as the existing [addRoleToUser](../../src/util/add-role-to-user.ts) does, so a large first run stays inside Discord's rate limits. Functions throw when they cannot complete their job and the caller handles it. ### Entry points -- **Join message**: create the member's `Onboarding` row. If a row already exists, nothing happens, whether the join message is a replay or the member left and rejoined before the daily job noticed (for example after a technical glitch). The row keeps its original join date and restart anchor, so a member who had been in the server for 6 days does not restart their 7-day clock, and the message rows are untouched. The roles lost while away are re-granted by the next evaluation. +- **Join message**: create the member's `Onboarding` row. If a row already exists, the bot clears its left time and changes nothing else, whether the join message is a replay or the member is returning within the grace period. The row keeps its original join date and restart anchor, so a member who had been in the server for 6 days does not restart their 7-day clock, and the message rows are untouched. The roles lost while away are re-granted by the next evaluation. - **Any guild message from a member who is not exempt and not finished**: if there is no tracking row, create one from `message.member.joinedAt`. Record the counted length (messages before the restart anchor are ignored), then evaluate. - **Message edit and delete events**: recompute or remove the message record, then evaluate. -- **Daily job**: scheduled with `node-cron` at a fixed UTC time, like the showcase digest. For each tracked member it fetches the member by ID. If the member is gone, their data is deleted. Otherwise it re-evaluates them. +- **Daily job**: scheduled with `node-cron` at a fixed UTC time, like the showcase digest. For each tracked member it fetches the member by ID. If the member is gone, it records when it first noticed, and deletes their data once the grace period has passed; members marked as left are not evaluated. If the member is present, it clears any recorded left time and re-evaluates them. - **Startup catch-up**: fetch recent history of the system channel and replay join messages the bot missed while offline. - **Rectification**: a shared function checks the member holds the dormant role (throws if not), removes it, sets the anchor to now and evaluates. It does not touch message rows. The `/onboarding reactivate` slash command and the dormant-channel button both call it. @@ -120,7 +142,7 @@ At startup the bot makes sure one fixed message with a "Reactivate" button exist ### Configuration -- **Constants** in one onboarding config file: counted-character threshold (400), days until the 7-day role (7) and dormancy period (3 months), daily-job time, launch-date constant. +- **Constants** in one onboarding config file: counted-character threshold (400), days until the 7-day role (7) and dormancy period (3 months), leave grace period (7 days), daily-job time, launch-date constant. - **Required env vars** in [env.ts](../../src/env.ts): the five onboarding role IDs plus the legacy role ID, the dormant channel ID and the onboarding log channel ID, and the launch date. The optional `ONBOARDING_ROLE_ID` and `ONBOARDING_CHANNEL_ID` and the unused `/onboarding` stub are removed. - **Permissions**: the bot's own role must sit above all onboarding roles and have Manage Roles, and it needs View Channel on the system channel. - The GuildMembers intent stays off. No change to the intents in [index.ts](../../src/index.ts). @@ -130,6 +152,7 @@ At startup the bot makes sure one fixed message with a "Reactivate" button exist - **Apply for the GuildMembers intent.** Simplest design. It would also give events for role changes and leaves, and a list of all members for a clean backfill. Rejected because it depends on Discord's approval, which has been needed before and is not ours to decide. - **Keep a running character total per member instead of per-message records.** Less storage. Rejected because deleted and spam-removed messages must stop counting, and a total alone cannot tell which characters to subtract. - **One table, with the per-message lengths in a JSON column on `Onboarding`** (message ID mapped to counted length). Simpler schema, one read for a member's whole state, and one delete for cleanup. Rejected for three reasons. Every update is a read-modify-write of the whole column, so two events for the same member, such as a new message during a spam bulk-delete, can silently overwrite each other unless updates are serialised per member. A delete event for an uncached message gives only the message ID, so finding the member who owns it means scanning every member's JSON with raw SQL, since Prisma cannot filter inside JSON on SQLite as far as we know. And the total would be computed in code, with the whole blob rewritten on every message. Fixing the first two with a per-member lock and a message-to-member lookup would rebuild the second table in a worse form. +- **Restore finished members who leave and rejoin.** Would need a permanent record of every finished member. Without the intent the bot only learns of a rejoin, never of the leave, so the 7-day grace period given to in-progress members cannot be applied: the choices are restoring access whenever they return, or guessing from the finish time. Rejected to keep the design small and not grant full access automatically; a finished member who returns starts again. - **Hourly job instead of daily.** Faster role delivery. Not chosen: a daily job was the requirement and the lag is acceptable. - **Bot manages channel permission overwrites.** Would let the bot enforce what "dormant" means. Rejected because channel permissions belong to moderators and the onboarding flow is meant to be roles only. @@ -137,4 +160,32 @@ At startup the bot makes sure one fixed message with a "Reactivate" button exist - That the bot can see and read the system join messages in the real server, and that welcome messages are enabled there. - That fetching a single member by ID works with the intent off. Discord's docs say only List Guild Members requires the intent. The Get Guild Member page does not mention one, and the existing stub in [features/onboarding](../../src/features/onboarding/index.ts) already calls it. -- Whether the `messages` role should be revoked if later deletions drop a member below the threshold before they finish. The current design reconciles in that direction, but it needs a decision. +- How a dormant member is made read-only. Decision pending; the two options are described below. + +### Open decision: making dormant members read-only + +A dormant member must be able to read the server but not write anywhere, including the lounge. The lounge stays open to every other member. Two ways to get there. + +**Option A: channel permissions.** Dormancy removes the start role. Moderators configure the dormant role with read access everywhere and a Send Messages deny on every writable channel, the lounge included. Discord applies `@everyone` overwrites first and role overwrites after, so the dormant role's deny beats the lounge's `@everyone` allow. Another role's explicit allow would beat the deny, because Discord combines role overwrites so that an allow from any role wins over a deny from another. The start role's write allows are such an allow, which is why the start role has to be removed. Reactivation re-adds it. + +- Discord enforces it. Nothing is posted, and it works when the bot is down. +- The bot still only moves roles and never edits overwrites. +- The member cannot write anywhere and gets no prompt on their own. They may not know why, or how to come back, unless they are told (see "Telling the member" below). +- Reactivation skips Discord onboarding the second time. + +**Option B: delete and notify.** Dormant members keep their channel permissions. When one writes a message, the bot deletes the message and tells them to press a reactivate button. This gives the member a clear, immediate task without searching the server. + +- Discord only allows an ephemeral message as the response to an interaction (a slash command or button press). The bot cannot send one in reply to a normal message. The message that carries the button is therefore a DM, which fails if the member has DMs closed, a visible reply that deletes itself, or a private thread (see "Telling the member" below). Pressing the button can then respond ephemerally. +- The offending message is posted before it is deleted. It can ping people and unfurl links for the moment it exists, and it stays if the bot is down or rate-limited. This weakens the hijacked-account goal, since a compromised account can post a scam before the delete lands. +- The bot needs the Manage Messages permission and must act on every guild message from a dormant member. +- Every dormant member who writes is deleted and notified, so a member with a legitimate message sees it removed. + +### Telling the member + +A dormant member should be told what happened and given one action to take, without searching the server. A private thread does this better than a DM: it does not fail when DMs are closed, the message stays inside the server, and the reactivate button sits next to the explanation. When the member becomes dormant (or, in option B, when they first write), the bot creates a private thread, adds the member, and posts the explanation and the reactivate button. The member is notified because they were added to the thread. + +This works with either option and replaces the single shared button message in the dormant channel, or sits beside it. To verify before relying on it: + +- A member added to a private thread can see and use it when the parent channel is otherwise hidden from them. +- The bot has permission to create private threads and add members. +- The number of threads stays manageable, and an archived thread can still be used. Threads should be deleted or reused on reactivation. From f0908d3183073b17b55f2ab16b0e9a354bedd7db Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Mon, 2 Nov 2026 14:48:05 +0100 Subject: [PATCH 04/30] chore: capture review findings for ADR 0001 in scratchpad Record blockers, logic bugs, technical gaps and structure concerns from the harsh review of the onboarding ADR as open items; no decisions made yet and the main document is untouched. Co-Authored-By: Claude Sonnet 5.5 --- ...ing-without-guild-members-intent.claude.md | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/adr/0001-onboarding-without-guild-members-intent.claude.md diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md new file mode 100644 index 0000000..a599068 --- /dev/null +++ b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md @@ -0,0 +1,47 @@ +# Scratchpad: ADR 0001 (onboarding without GuildMembers intent) + +Main doc: `0001-onboarding-without-guild-members-intent.md`. Status in main doc is `accepted`; review says it should be `proposed`. + +## Session 1 state + +Harsh review done (in chat). Nothing in the main doc edited yet. All findings below are OPEN until the human decides. + +## Open findings (need human decisions) + +### Blockers +- B1 Intent rationale self-contradicts: bot already depends on MessageContent (privileged, `src/index.ts:17`); the 400-char mechanic needs it. "Approval could be refused/revoked" applies equally. Also no evidence anyone applied for GuildMembers and was refused. 10,000-user claim unverified. Q: has GuildMembers been applied for? What is the guild/bot size? What is the real reason to avoid the intent? +- B2 Goal 1 (protect regulars) is defeated: start role (granted at join by Discord onboarding) opens help forum + showcase, the exact channels that caused the problem. Q: move those behind finished role, or reword goal 1? +- B3 Status `accepted` but section 5 has open decision + unverified assumptions. Should be `proposed`. +- B4 Contradictions: line 22/34 say dormant can read; journey table line 45 says dormant loses access to everything except message mods + dormant channel. Table says "start role stays"; Option A removes start role. Rectification text and eval-order step 5 silent on start role. + +### Logic bugs +- L5 Messages posted while dormant (dormant channel, Option B) are after the old anchor so get counted; rectification does not delete rows, contradicting "count is already empty". +- L6 Dormancy rule (3 months after anchor w/o finished) does not match goal 5 (never-taken-part accounts). Hits slow active members and active lounge chatters who skipped Discord onboarding (cannot finish without start role). Never touches finished members (real hijack targets). Legacy non-posters invisible at launch. +- L7 Leave grace loophole: kick + rejoin within 7 days keeps progress; time away counts toward the 7 days. +- L8 Launch date listed as both constant (line 145) and env var (line 146). + +### Technical gaps +- T9 No `Partials` in `src/index.ts`; discord.js will not emit messageDelete/messageUpdate for uncached messages, and messageDeleteBulk drops uncached ones. ADR relies on "ID-only delete event". +- T10 Deletes/edits while bot offline are lost; counts drift forever; startup catch-up only replays join messages. +- T11 No estimate of tracked-member count N; daily job does N fetches; dormant rows never expire. +- T12 Read-only needs more than Send Messages deny (threads, forum posts, reactions, voice, attachments); new channels do not inherit. +- T13 Concurrency argument against JSON column applied selectively; evaluate() is also read-modify-write and step 5 deletes rows while inserts may race. + +### Structure +- S14 ADR contains a full spec (journey table, rules, schema, eval order, env vars). Q: split spec out of the ADR into a separate doc? +- S15 Option B is a strawman (defeats goal 5 by its own text); recommend closing on Option A. "Telling the member" private-thread section is scope creep; candidate for follow-up issue. + +### Minor +- Who accepted dormancy gap (line 74) and under what conditions it becomes unacceptable? +- No stated path for "Everyone" members to get start role. +- "a daily job was the requirement": cite source. + +## Proposed section order (not agreed yet) +1. Status + Problem/goals (B2, B3, L6 goal 5 wording) +2. Decision / why not the intent (B1) +3. Journey table + rules (B4, L5, L7, L8) +4. Technical design (T9-T13) +5. Open decision: dormant read-only (S15, T12) + +## Decisions made +None yet. From 7979489e28f9d4ce6ab1613703099c4c057d30eb Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Mon, 2 Nov 2026 14:51:40 +0100 Subject: [PATCH 05/30] chore: record decision to drop ADR 0001 status Human decided the ADR carries no status or front matter; B3 marked resolved and Q2-Q4 (goal 1 channels, goal 5 fate, section order/spec split) still pending. Co-Authored-By: Claude Sonnet 5.5 --- ...-onboarding-without-guild-members-intent.claude.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md index a599068..8ee812a 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md @@ -1,6 +1,6 @@ # Scratchpad: ADR 0001 (onboarding without GuildMembers intent) -Main doc: `0001-onboarding-without-guild-members-intent.md`. Status in main doc is `accepted`; review says it should be `proposed`. +Main doc: `0001-onboarding-without-guild-members-intent.md`. Front matter (status) removed from main doc at the human's direction: the ADR carries no status at all. ## Session 1 state @@ -11,7 +11,7 @@ Harsh review done (in chat). Nothing in the main doc edited yet. All findings be ### Blockers - B1 Intent rationale self-contradicts: bot already depends on MessageContent (privileged, `src/index.ts:17`); the 400-char mechanic needs it. "Approval could be refused/revoked" applies equally. Also no evidence anyone applied for GuildMembers and was refused. 10,000-user claim unverified. Q: has GuildMembers been applied for? What is the guild/bot size? What is the real reason to avoid the intent? - B2 Goal 1 (protect regulars) is defeated: start role (granted at join by Discord onboarding) opens help forum + showcase, the exact channels that caused the problem. Q: move those behind finished role, or reword goal 1? -- B3 Status `accepted` but section 5 has open decision + unverified assumptions. Should be `proposed`. +- B3 RESOLVED: status/front matter removed entirely (human: "it should have no status at all"). Section 5 open items still stand on their own. - B4 Contradictions: line 22/34 say dormant can read; journey table line 45 says dormant loses access to everything except message mods + dormant channel. Table says "start role stays"; Option A removes start role. Rectification text and eval-order step 5 silent on start role. ### Logic bugs @@ -44,4 +44,9 @@ Harsh review done (in chat). Nothing in the main doc edited yet. All findings be 5. Open decision: dormant read-only (S15, T12) ## Decisions made -None yet. +- ADR has no status and no YAML front matter. Done in main doc. + +## Still waiting on human (asked end of session 1) +- Q2 goal 1 vs start role channels (a/b/c) +- Q3 goal 5 fate (a/b/c) +- Q4 section order; and whether to split the spec out of the ADR From 3c83437b2f0fff7bf69225a783d5627841214dfe Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Mon, 2 Nov 2026 14:57:49 +0100 Subject: [PATCH 06/30] chore: capture Q2 showcase facts and Q3 activity-based dormancy decision Showcase creation is bot-button gated so start role only comments; dormancy moves to a lastActivity column (messages, forum posts, voice join). Pending: finished-member scope, voice semantics, help forum posting rights. Co-Authored-By: Claude Sonnet 5.5 --- ...rding-without-guild-members-intent.claude.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md index 8ee812a..90a478a 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md @@ -15,6 +15,7 @@ Harsh review done (in chat). Nothing in the main doc edited yet. All findings be - B4 Contradictions: line 22/34 say dormant can read; journey table line 45 says dormant loses access to everything except message mods + dormant channel. Table says "start role stays"; Option A removes start role. Rectification text and eval-order step 5 silent on start role. ### Logic bugs +- (Q3 decided: dormancy now activity-based, see Decisions; L6 largely addressed except finished-member scope and legacy non-posters) - L5 Messages posted while dormant (dormant channel, Option B) are after the old anchor so get counted; rectification does not delete rows, contradicting "count is already empty". - L6 Dormancy rule (3 months after anchor w/o finished) does not match goal 5 (never-taken-part accounts). Hits slow active members and active lounge chatters who skipped Discord onboarding (cannot finish without start role). Never touches finished members (real hijack targets). Legacy non-posters invisible at launch. - L7 Leave grace loophole: kick + rejoin within 7 days keeps progress; time away counts toward the 7 days. @@ -46,7 +47,17 @@ Harsh review done (in chat). Nothing in the main doc edited yet. All findings be ## Decisions made - ADR has no status and no YAML front matter. Done in main doc. -## Still waiting on human (asked end of session 1) -- Q2 goal 1 vs start role channels (a/b/c) -- Q3 goal 5 fate (a/b/c) +- Q2 (goal 1 vs start role channels), human's facts: new joiners with the start role can COMMENT on showcases in the showcase forum but cannot CREATE showcases. Creation goes through the bot via a button in showcase-guidelines, which the start role cannot reach. Start role can "participate" in the help forum. So the journey table line "write in ... showcase" is inaccurate; must say comment-only. Reasoning: showcase spam is already blocked structurally; the remaining question is help forum posting. +- Q3 (goal 5): dormancy becomes ACTIVITY-based. Store `lastActivity` datetime in DB. Activity = wrote a message anywhere, created a forum post, joined and talked in voice. Each updates lastActivity. Dormant after 3 months with no activity. Replaces "3 months after restart anchor without finished". Reasoning: dormancy should target accounts with no sign of life, not slow members. + +## Implications of Q3 to resolve (asked via AskUserQuestion) +- Does activity dormancy apply to finished members too? Today: finished => row deleted, permanent. Covering finished means keeping a row (lastActivity) for every finished member, which reverses "row deleted on finish" and affects the leave/rejoin design and the no-intent tracking (finished members only seen when they post/join voice). +- Voice: bot cannot detect "talking", only joining (VoiceStateUpdate, GuildVoiceStates already enabled). +- Write volume: updating lastActivity on every message; probably throttle (only write if older than N hours). +- Initial lastActivity for a new row = join date; rectification sets lastActivity = now. +- Interaction with message-count bug L5: dormant-period messages are activity AND get counted; still needs fixing. +- Eval order step 5 and journey table dormant row need rewording once decided. + +## Still waiting on human - Q4 section order; and whether to split the spec out of the ADR +- Help forum: can start role CREATE posts or only reply? (matters for goal 1) From 9225547afcbc2c8970a76e5939cf00fb8fd90ecd Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Mon, 2 Nov 2026 15:00:19 +0100 Subject: [PATCH 07/30] chore: record help forum, dormancy scope, voice and throttle decisions Start role can create help posts (trigger not addressed by gates); dormancy covers only unfinished members; voice join counts; lastActivity throttled. Problem-and-goals draft is in the main doc awaiting review. Co-Authored-By: Claude Sonnet 5.5 --- ...onboarding-without-guild-members-intent.claude.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md index 90a478a..15384a1 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md @@ -58,6 +58,16 @@ Harsh review done (in chat). Nothing in the main doc edited yet. All findings be - Interaction with message-count bug L5: dormant-period messages are activity AND get counted; still needs fixing. - Eval order step 5 and journey table dormant row need rewording once decided. +## Decisions from AskUserQuestion round (session 1) +- Help forum: start role can CREATE and reply. So goal 1 cannot claim to stop low-effort help requests; draft rewords it and the "do not do" paragraph says so. Flag to human: this means the original trigger (low-effort help requests) is NOT addressed by this ADR. Ask whether that is intended. +- Dormancy scope: ONLY unfinished members. Finished stay permanent and row is still deleted on finish. Consequence: hijack goal covers only never-finished accounts (stated in goal 5 draft). Human chose this over recommended "everyone". +- Voice activity = joining a voice channel (VoiceStateUpdate). +- lastActivity writes throttled (only update when stored value older than an interval, e.g. 1 day). Exact interval to settle in technical design. + +## Drafted in main doc (awaiting human review): Problem and goals +- Goal 1 reworded, goal 5 reworded, "what the gates do" paragraph shortened to help forum only. +- Not yet touched: journey table (still says showcase write; must become comment-only), dormant row, eval order step 5, Data section (lastActivity column), rectification (sets lastActivity), L5 etc. + ## Still waiting on human - Q4 section order; and whether to split the spec out of the ADR -- Help forum: can start role CREATE posts or only reply? (matters for goal 1) +- Review of the drafted Problem and goals section From 90147a2babeea5fec5aa4c9864e8e81bcd1f3fe9 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Mon, 2 Nov 2026 15:10:31 +0100 Subject: [PATCH 08/30] chore: capture journey-section decisions and drafted rule changes Dormant uses delete-and-notify (Option B, chosen over recommendation A); dormant messages not counted; time away pauses anchor and lastActivity on rejoin. Lists untouched downstream sections and the unformatted-table caveat. Co-Authored-By: Claude Sonnet 5.5 --- ...ing-without-guild-members-intent.claude.md | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md index 15384a1..567caca 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md @@ -64,10 +64,29 @@ Harsh review done (in chat). Nothing in the main doc edited yet. All findings be - Voice activity = joining a voice channel (VoiceStateUpdate). - lastActivity writes throttled (only update when stored value older than an interval, e.g. 1 day). Exact interval to settle in technical design. -## Drafted in main doc (awaiting human review): Problem and goals -- Goal 1 reworded, goal 5 reworded, "what the gates do" paragraph shortened to help forum only. -- Not yet touched: journey table (still says showcase write; must become comment-only), dormant row, eval order step 5, Data section (lastActivity column), rectification (sets lastActivity), L5 etc. +## FINALIZED by human: Problem and goals (goals 1 and 5, gates paragraph) +- Human on the trigger gap: "low effort or low quality messages should not pollute the main area where people hang out, notably general chat. this is why all new comers only have access to the lounge." Intended scope of goal 1 = protect GENERAL CHAT. Trigger paragraph left as is. +- Human on stages: "people without any roles only have access to lounge, then the start role gives access to more channels, and after finishing the onboarding they gain access to the full server." So the journey table channel list is right; just showcase is comment-only. + +## Decisions (journey section round) +- Dormant = Option B (delete and notify). Human chose it over my recommendation A. Known cost, recorded in ADR already: message is visible briefly, weakens goal 5, bot needs Manage Messages and must handle every message from dormant members. Option A text and S15/T12 now partly moot; Option A can be kept in "considered options" instead of "open decision". Start role stays on dormancy (matches table). +- Messages from dormant members are NOT counted (skip recording while dormant role held). Resolves L5. +- Time away does NOT count on rejoin within grace: anchor AND lastActivity shift forward by away time. Human chose this. Away time measured from when the daily job noticed the leave (imprecise by up to a day, consistent with grace period). Resolves L7. Needs stored state: Onboarding row already has "left noticed" time; shift on rejoin. + +## Drafted in main doc (awaiting human review): Journey table and rules +- Start-role row: showcase comment-only, help forum create+reply. +- Dormant row: 3 months without activity; keeps channel access, messages deleted + notified. +- Rules: activity definition (message anywhere incl. forum post, voice join; throttled; initial = join date), dormancy runs from lastActivity, finished never dormant, rectification sets lastActivity=now and dormant messages not counted, rejoin shifts anchor+lastActivity. +- Formatting NOT run: `pnpm exec oxfmt` fails on Node v20.17 (needs newer; .nvmrc). Table padding is unformatted; run fmt before commit of main doc. + +## Not yet touched (later sections) +- Section 2/5: Option A/B open-decision section needs rewriting to reflect B chosen. "Telling the member" thread section now relevant to B (DM vs ephemeral vs private thread: still undecided). +- Section 3: Data (add lastActivity column on Onboarding), evaluation order step 5 (dormancy by lastActivity, not anchor), join-message entry point ("changes nothing else" now shifts anchor/lastActivity), message entry point (skip recording when dormant, update lastActivity incl. voice and forum posts), new VoiceStateUpdate entry point, Option B handler (delete messages from dormant members, Manage Messages permission), daily job wording. +- Verify: forum post starter message arrives as MessageCreate; VoiceStateUpdate available. +- L8 launch date in both constants and env vars: still open, decide which. +- B1 (intent argument), T9-T13, S14 (split spec) still open. ## Still waiting on human -- Q4 section order; and whether to split the spec out of the ADR -- Review of the drafted Problem and goals section +- Review of drafted Journey table and rules +- Launch date: constant or env var (L8) +- Q4: whether to split spec out of ADR (deferred; human chose to edit in place first) From f892371f56b6e4b7fe79fafe3d0855192cc3d077 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Mon, 2 Nov 2026 15:26:45 +0100 Subject: [PATCH 09/30] chore: scope work to business definition; park technical sections Human wants Problem/goals and the journey+rules settled before any technical design edits. Listed open business questions and parked the technical items. Co-Authored-By: Claude Sonnet 5.5 --- ...onboarding-without-guild-members-intent.claude.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md index 567caca..bbc2285 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md @@ -79,7 +79,17 @@ Harsh review done (in chat). Nothing in the main doc edited yet. All findings be - Rules: activity definition (message anywhere incl. forum post, voice join; throttled; initial = join date), dormancy runs from lastActivity, finished never dormant, rectification sets lastActivity=now and dormant messages not counted, rejoin shifts anchor+lastActivity. - Formatting NOT run: `pnpm exec oxfmt` fails on Node v20.17 (needs newer; .nvmrc). Table padding is unformatted; run fmt before commit of main doc. -## Not yet touched (later sections) +## SCOPE DECISION (human): business definition first, technical details later +Work only on "Problem and goals" and "1. What Onboarding is" (journey + rules) until the business definition is settled. Do NOT edit sections 2-5 (decision, technical design, considered options, open decision) yet. Technical items below are parked, not dropped. Business items still open are listed under "Business questions open". + +## Business questions open +- Review of drafted journey table and rules +- How is a dormant member told what happened (what the member experiences)? DM / reply / private thread: business-visible, decide wording only, mechanism later +- Path for "Everyone" (no start role) members to get the start role / are they nudged +- Why 7 days and 400 chars (trace each gate to a goal) +- Legacy member rule is business-visible: confirm + +## Not yet touched (later sections; PARKED until business definition done) - Section 2/5: Option A/B open-decision section needs rewriting to reflect B chosen. "Telling the member" thread section now relevant to B (DM vs ephemeral vs private thread: still undecided). - Section 3: Data (add lastActivity column on Onboarding), evaluation order step 5 (dormancy by lastActivity, not anchor), join-message entry point ("changes nothing else" now shifts anchor/lastActivity), message entry point (skip recording when dormant, update lastActivity incl. voice and forum posts), new VoiceStateUpdate entry point, Option B handler (delete messages from dormant members, Manage Messages permission), daily job wording. - Verify: forum post starter message arrives as MessageCreate; VoiceStateUpdate available. From b706ff8935a7eceaa79865ee47b4098f75f6e8c1 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Mon, 2 Nov 2026 15:31:30 +0100 Subject: [PATCH 10/30] chore: record dormant notice, no-nudge and gate-reason decisions Journey and rules finalized. Dormant members get a private notice (private thread vs public reply left for the team). Goal 2 has no gate behind it; asked human. Co-Authored-By: Claude Sonnet 5.5 --- ...oarding-without-guild-members-intent.claude.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md index bbc2285..a2ba757 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md @@ -82,12 +82,17 @@ Harsh review done (in chat). Nothing in the main doc edited yet. All findings be ## SCOPE DECISION (human): business definition first, technical details later Work only on "Problem and goals" and "1. What Onboarding is" (journey + rules) until the business definition is settled. Do NOT edit sections 2-5 (decision, technical design, considered options, open decision) yet. Technical items below are parked, not dropped. Business items still open are listed under "Business questions open". +## FINALIZED by human: journey table and rules (as drafted in session 1) + +## Business decisions (latest round) +- Dormant notice: member is told in PRIVATE (private thread leaning) immediately when their message is deleted, with a way to reactivate. Reasoning (human): a user who cannot write somewhere will more likely churn than investigate; informing them keeps them in the server and makes reactivating easy; a bot is less likely to react to such a message. OPEN FOR THE TEAM (not the human alone): private thread (member, bot, mods only) vs reply in the channel where everyone sees it. Written into ADR rules as an open point. +- No start role: nothing, their choice, no nudge. Written into rules. +- Gates: 7 days = friction for drive-bys (goals 3, 4). 400 chars = rough engagement sign (goal 6) AND friction; no quality judgement. Human did NOT pick "7 days: time to read rules", so goal 2 (server comprehension) is backed by no gate; only by Discord onboarding presumably. Check with human. + ## Business questions open -- Review of drafted journey table and rules -- How is a dormant member told what happened (what the member experiences)? DM / reply / private thread: business-visible, decide wording only, mechanism later -- Path for "Everyone" (no start role) members to get the start role / are they nudged -- Why 7 days and 400 chars (trace each gate to a goal) -- Legacy member rule is business-visible: confirm +- Goal 2 (comprehension) has no gate behind it: drop, reword, or attribute to Discord onboarding? +- Legacy member rule: confirm (joined before launch, receives start+finished+legacy on first post; never evaluated if they never post) +- Launch date: constant vs env var (L8), probably technical; park ## Not yet touched (later sections; PARKED until business definition done) - Section 2/5: Option A/B open-decision section needs rewriting to reflect B chosen. "Telling the member" thread section now relevant to B (DM vs ephemeral vs private thread: still undecided). From 17e95a4f6a236759a9a0008816ab8c1803797136 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Mon, 2 Nov 2026 15:34:33 +0100 Subject: [PATCH 11/30] chore: clarify goal dormancy --- ...onboarding-without-guild-members-intent.md | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.md b/docs/adr/0001-onboarding-without-guild-members-intent.md index 2393d7e..7781b80 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.md @@ -1,7 +1,3 @@ ---- -status: accepted ---- - # Onboarding: a role-based member journey, built without the GuildMembers intent Terms in **bold** are defined in [GLOSSARY.md](../../GLOSSARY.md). @@ -12,18 +8,18 @@ Regulars have said they are tired of dealing with low-quality content from new j Onboarding exists to serve these goals, in this order of weight: -1. **Protect regulars' attention.** Keep low-effort questions and self-promotion out of the busiest channels until a newcomer has had time to settle in. +1. **Protect regulars' attention.** Keep newcomers out of general chat and the rest of the server until they have had time to settle in. Newcomers cannot create showcases, because that goes through a bot button in a channel they cannot reach. They can comment on showcases and take part in the help forum from the start. 2. **Server comprehension.** Newcomers should have seen the rules and learned how to ask a good question before they reach the whole server. 3. **Spam and raids.** Fresh accounts have limited reach while trust is unknown. 4. **Drive-by accounts.** People who join, post once and leave should not cost moderators time. -5. **Limit hijacked dormant accounts.** An account that has never taken part for months can later be compromised and used to post scam messages. Dormancy removes the ability to write from accounts that have shown no sign of life. +5. **Limit hijacked dormant accounts.** An account that has shown no activity for months can later be compromised and used to post scam messages. A member who has not finished Onboarding and has no activity for 3 months loses the ability to write. Activity means writing a message anywhere, creating a forum post, or joining a voice channel. Finished members are never made dormant, so this goal does not cover them. 6. **Encourage participation.** Newcomers get a friendly place to take part straight away (the lounge and a few topic channels) and a path to the rest. Reading is not restricted by this flow. Members who read but rarely post keep their access to read the server. Onboarding gates where a member can write, not whether they may be here. A dormant member can read but cannot write until they reactivate. ### What the gates do and do not do -The 7-day wait and the 400-character threshold are deliberate friction. They keep newcomers out of general chat and the rest of the server while they settle in, and they cost a drive-by account some effort. They do not judge quality. A member can pass them with filler, and a newcomer can still post a low-effort question or project in the channels the start role opens, including help forum and showcase. Moderation and the community's own norms remain responsible for quality. Onboarding only limits how much of the server a newcomer can reach while that happens. +The 7-day wait and the 400-character threshold are deliberate friction. They keep newcomers out of general chat and the rest of the server while they settle in, and they cost a drive-by account some effort. The 7 days make a throwaway account wait before it reaches everything. The 400 characters are a rough sign that the member has taken part, and also cost effort. They do not judge quality. A member can pass them with filler, and a newcomer can still create a low-effort help request in the help forum, which the start role opens. Moderation and the community's own norms remain responsible for quality. Onboarding only limits how much of the server a newcomer can reach while that happens. Every gate in section 1 must trace back to one of these goals. A gate that serves none of them should be removed. @@ -38,22 +34,25 @@ The purpose is to let newcomers take part in a few friendly channels straight aw | Stage | How a member gets there | What they can do | | ----------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | **Everyone** | Joined, but has no onboarding role (for example skipped Discord onboarding) | Write in lounge, read a few channels, read message mods to open a ticket | -| **Start role** | Granted by Discord onboarding | Also write in help forum, ai, gaming, real life, frontend, design, backend and showcase | +| **Start role** | Granted by Discord onboarding | Also write in help forum (create and reply), ai, gaming, real life, frontend, design and backend, and comment on showcases. Showcases are created through a bot button in a channel this role cannot reach | | **7-day role** | 7 days after the **restart anchor** | Milestone only | | **Messages role** | 400 **counted message** characters since the restart anchor | Milestone only | | **Finished role** | Holds start, 7-day and messages | Full server access. Permanent. The 7-day and messages roles are removed | -| **Dormant role** | 3 months after the restart anchor without the finished role | Loses access to everything except message mods and the **dormant channel**. The 7-day and messages roles are removed; the start role stays | +| **Dormant role** | 3 months without **activity**, without the finished role | Keeps its channel access, but every message they write is deleted and they are told how to reactivate. The 7-day and messages roles are removed; the start role stays | | **Legacy member** | Joined before launch, first seen posting | Receives start, finished and **legacy role** at once | ### Rules -- The restart anchor is the later of the member's join date and their latest rectification. The 7-day clock, the message count and the 3-month dormancy deadline all run from it. -- Dormancy applies to anyone without the finished role, including members who never held the start role. +- The restart anchor is the later of the member's join date and their latest rectification. The 7-day clock and the message count run from it. +- **Activity** is writing a message anywhere (a forum post counts), or joining a voice channel. The bot cannot tell whether someone speaks, only that they joined. It stores the time of the member's last activity, and updates it only when the stored time is older than a set interval, because dormancy is measured in months. A new member's last activity starts as their join date. +- Dormancy runs from last activity: 3 months with none, for anyone without the finished role, including members who never held the start role. Finished members are never made dormant. +- When a dormant member writes, their message is deleted and the bot immediately tells them why, privately, and how to reactivate. A member who simply cannot write somewhere is more likely to leave than to investigate, and this keeps them in the server and makes reactivating easy. Open for the team: whether this happens in a private thread (only the member, the bot and moderators can see it) or as a reply in the channel itself, where everyone sees it. +- A member who has no start role (for example, skipped Discord onboarding) stays limited to the lounge until they complete Discord onboarding themselves. They are not nudged. - Finishing needs the start role. A member could meet the 7-day and message conditions without it, but the finished role waits until Discord onboarding has been completed. - Counted messages are text only, with whitespace, markdown syntax and links removed. Attachments add nothing. Messages written before the start role count. Messages that are deleted, or removed by spam detection, do not. Edits recount the message. -- **Rectification** leaves dormancy. A moderator runs `/onboarding reactivate @member`, or the member presses the button in the dormant channel. Both remove the dormant role and set a new restart anchor. The message count is already empty, because it is cleared when the member becomes dormant. A returning member starts over on purpose, with another 7 days and 400 characters: the point is to show they are present again. +- **Rectification** leaves dormancy. A moderator runs `/onboarding reactivate @member`, or the member presses the button in the dormant channel. Both remove the dormant role, set a new restart anchor and set last activity to now. The message count is already empty, because it is cleared when the member becomes dormant, and messages from a member holding the dormant role are not counted. A returning member starts over on purpose, with another 7 days and 400 characters: the point is to show they are present again. - The messages role is held only while the counted characters stay at or above the threshold. If deletions or spam removals drop a member below it before they finish, the role is removed, so posting 400 characters and deleting them does not count. -- A member who leaves keeps their Onboarding progress for a grace period of 7 days, counted from when the daily job first notices they are gone. If they rejoin within it, their restart anchor and counted messages are as they were, however soon after leaving they return. The time away counts toward the 7 days and the dormancy deadline, because the anchor does not move. After the grace period the progress is deleted and they start Onboarding again. Members who had already finished always start again, because their record is gone and Discord removes their roles when they leave. Counted messages are only deleted in two cases: their onboarding record is deleted (they finished, or the grace period after leaving ran out), or they become dormant. +- A member who leaves keeps their Onboarding progress for a grace period of 7 days, counted from when the daily job first notices they are gone. If they rejoin within it, their counted messages are as they were, however soon after leaving they return. The time away does not count toward the 7 days or the dormancy deadline: the restart anchor and last activity move forward by the time away, measured from when the daily job noticed the leave until the rejoin. After the grace period the progress is deleted and they start Onboarding again. Members who had already finished always start again, because their record is gone and Discord removes their roles when they leave. Counted messages are only deleted in two cases: their onboarding record is deleted (they finished, or the grace period after leaving ran out), or they become dormant. - Bots and moderators (**exempt members**) are never touched. - Which channels each role can see or write in is configured by moderators in Discord. The bot only adds and removes roles. It never edits channel permission overwrites. From 3fa91b000f275337630ae6a1d0c7f0aa4ced9298 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Mon, 2 Nov 2026 15:35:56 +0100 Subject: [PATCH 12/30] chore: reword goal 2, confirm legacy rule; business definition near done Goal 2 now says the 7-day wait gives reading time without enforcement. Legacy rule confirmed. Remaining: sign-off on business half, then technical sections starting with the intent argument. Co-Authored-By: Claude Sonnet 5.5 --- ...1-onboarding-without-guild-members-intent.claude.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md index a2ba757..fa0c02f 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md @@ -89,10 +89,14 @@ Work only on "Problem and goals" and "1. What Onboarding is" (journey + rules) u - No start role: nothing, their choice, no nudge. Written into rules. - Gates: 7 days = friction for drive-bys (goals 3, 4). 400 chars = rough engagement sign (goal 6) AND friction; no quality judgement. Human did NOT pick "7 days: time to read rules", so goal 2 (server comprehension) is backed by no gate; only by Discord onboarding presumably. Check with human. +## Business definition status: essentially complete +- Goal 2 reworded (human chose): the 7-day wait gives time to read rules and see good questions; nothing checks it. Drafted in main doc, awaiting a final look. +- Legacy rule confirmed as written (start+finished+legacy on first post). + ## Business questions open -- Goal 2 (comprehension) has no gate behind it: drop, reword, or attribute to Discord onboarding? -- Legacy member rule: confirm (joined before launch, receives start+finished+legacy on first post; never evaluated if they never post) -- Launch date: constant vs env var (L8), probably technical; park +- Team decision (not ours): private thread vs in-channel reply for dormant notice. Stays as open point in ADR rules. +- Launch date: constant vs env var (L8), technical; park +- Next: either human reads the whole business half once more and signs off, or we move to technical sections (B1 intent argument first, then rewrite open decision section for Option B, T9-T13, S14) ## Not yet touched (later sections; PARKED until business definition done) - Section 2/5: Option A/B open-decision section needs rewriting to reflect B chosen. "Telling the member" thread section now relevant to B (DM vs ephemeral vs private thread: still undecided). From 06d67040a8d5b3c112921545d98f0d49a94aa05e Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Mon, 2 Nov 2026 15:37:08 +0100 Subject: [PATCH 13/30] chore: add that the 7 day gives users also enough time to get a read on the server and its community --- docs/adr/0001-onboarding-without-guild-members-intent.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.md b/docs/adr/0001-onboarding-without-guild-members-intent.md index 7781b80..8ee55e4 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.md @@ -9,7 +9,7 @@ Regulars have said they are tired of dealing with low-quality content from new j Onboarding exists to serve these goals, in this order of weight: 1. **Protect regulars' attention.** Keep newcomers out of general chat and the rest of the server until they have had time to settle in. Newcomers cannot create showcases, because that goes through a bot button in a channel they cannot reach. They can comment on showcases and take part in the help forum from the start. -2. **Server comprehension.** Newcomers should have seen the rules and learned how to ask a good question before they reach the whole server. +2. **Server comprehension.** The 7-day wait gives newcomers time to read the rules and see how good questions are asked before they reach the whole server. Nothing checks that they did. 3. **Spam and raids.** Fresh accounts have limited reach while trust is unknown. 4. **Drive-by accounts.** People who join, post once and leave should not cost moderators time. 5. **Limit hijacked dormant accounts.** An account that has shown no activity for months can later be compromised and used to post scam messages. A member who has not finished Onboarding and has no activity for 3 months loses the ability to write. Activity means writing a message anywhere, creating a forum post, or joining a voice channel. Finished members are never made dormant, so this goal does not cover them. From 463aaef190ba896f8521b44423df05186ea0835a Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Mon, 2 Nov 2026 15:39:33 +0100 Subject: [PATCH 14/30] chore: rewrite ADR 0001 scratchpad as a resume point Consolidate decisions with reasoning, mark business half finalized and technical sections 2-5 stale and parked, order the parked technical items (intent argument first), and note main doc is uncommitted and unformatted. Co-Authored-By: Claude Sonnet 5.5 --- ...ing-without-guild-members-intent.claude.md | 157 ++++++------------ 1 file changed, 48 insertions(+), 109 deletions(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md index fa0c02f..5d54626 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md @@ -1,111 +1,50 @@ # Scratchpad: ADR 0001 (onboarding without GuildMembers intent) -Main doc: `0001-onboarding-without-guild-members-intent.md`. Front matter (status) removed from main doc at the human's direction: the ADR carries no status at all. - -## Session 1 state - -Harsh review done (in chat). Nothing in the main doc edited yet. All findings below are OPEN until the human decides. - -## Open findings (need human decisions) - -### Blockers -- B1 Intent rationale self-contradicts: bot already depends on MessageContent (privileged, `src/index.ts:17`); the 400-char mechanic needs it. "Approval could be refused/revoked" applies equally. Also no evidence anyone applied for GuildMembers and was refused. 10,000-user claim unverified. Q: has GuildMembers been applied for? What is the guild/bot size? What is the real reason to avoid the intent? -- B2 Goal 1 (protect regulars) is defeated: start role (granted at join by Discord onboarding) opens help forum + showcase, the exact channels that caused the problem. Q: move those behind finished role, or reword goal 1? -- B3 RESOLVED: status/front matter removed entirely (human: "it should have no status at all"). Section 5 open items still stand on their own. -- B4 Contradictions: line 22/34 say dormant can read; journey table line 45 says dormant loses access to everything except message mods + dormant channel. Table says "start role stays"; Option A removes start role. Rectification text and eval-order step 5 silent on start role. - -### Logic bugs -- (Q3 decided: dormancy now activity-based, see Decisions; L6 largely addressed except finished-member scope and legacy non-posters) -- L5 Messages posted while dormant (dormant channel, Option B) are after the old anchor so get counted; rectification does not delete rows, contradicting "count is already empty". -- L6 Dormancy rule (3 months after anchor w/o finished) does not match goal 5 (never-taken-part accounts). Hits slow active members and active lounge chatters who skipped Discord onboarding (cannot finish without start role). Never touches finished members (real hijack targets). Legacy non-posters invisible at launch. -- L7 Leave grace loophole: kick + rejoin within 7 days keeps progress; time away counts toward the 7 days. -- L8 Launch date listed as both constant (line 145) and env var (line 146). - -### Technical gaps -- T9 No `Partials` in `src/index.ts`; discord.js will not emit messageDelete/messageUpdate for uncached messages, and messageDeleteBulk drops uncached ones. ADR relies on "ID-only delete event". -- T10 Deletes/edits while bot offline are lost; counts drift forever; startup catch-up only replays join messages. -- T11 No estimate of tracked-member count N; daily job does N fetches; dormant rows never expire. -- T12 Read-only needs more than Send Messages deny (threads, forum posts, reactions, voice, attachments); new channels do not inherit. -- T13 Concurrency argument against JSON column applied selectively; evaluate() is also read-modify-write and step 5 deletes rows while inserts may race. - -### Structure -- S14 ADR contains a full spec (journey table, rules, schema, eval order, env vars). Q: split spec out of the ADR into a separate doc? -- S15 Option B is a strawman (defeats goal 5 by its own text); recommend closing on Option A. "Telling the member" private-thread section is scope creep; candidate for follow-up issue. - -### Minor -- Who accepted dormancy gap (line 74) and under what conditions it becomes unacceptable? -- No stated path for "Everyone" members to get start role. -- "a daily job was the requirement": cite source. - -## Proposed section order (not agreed yet) -1. Status + Problem/goals (B2, B3, L6 goal 5 wording) -2. Decision / why not the intent (B1) -3. Journey table + rules (B4, L5, L7, L8) -4. Technical design (T9-T13) -5. Open decision: dormant read-only (S15, T12) - -## Decisions made -- ADR has no status and no YAML front matter. Done in main doc. - -- Q2 (goal 1 vs start role channels), human's facts: new joiners with the start role can COMMENT on showcases in the showcase forum but cannot CREATE showcases. Creation goes through the bot via a button in showcase-guidelines, which the start role cannot reach. Start role can "participate" in the help forum. So the journey table line "write in ... showcase" is inaccurate; must say comment-only. Reasoning: showcase spam is already blocked structurally; the remaining question is help forum posting. -- Q3 (goal 5): dormancy becomes ACTIVITY-based. Store `lastActivity` datetime in DB. Activity = wrote a message anywhere, created a forum post, joined and talked in voice. Each updates lastActivity. Dormant after 3 months with no activity. Replaces "3 months after restart anchor without finished". Reasoning: dormancy should target accounts with no sign of life, not slow members. - -## Implications of Q3 to resolve (asked via AskUserQuestion) -- Does activity dormancy apply to finished members too? Today: finished => row deleted, permanent. Covering finished means keeping a row (lastActivity) for every finished member, which reverses "row deleted on finish" and affects the leave/rejoin design and the no-intent tracking (finished members only seen when they post/join voice). -- Voice: bot cannot detect "talking", only joining (VoiceStateUpdate, GuildVoiceStates already enabled). -- Write volume: updating lastActivity on every message; probably throttle (only write if older than N hours). -- Initial lastActivity for a new row = join date; rectification sets lastActivity = now. -- Interaction with message-count bug L5: dormant-period messages are activity AND get counted; still needs fixing. -- Eval order step 5 and journey table dormant row need rewording once decided. - -## Decisions from AskUserQuestion round (session 1) -- Help forum: start role can CREATE and reply. So goal 1 cannot claim to stop low-effort help requests; draft rewords it and the "do not do" paragraph says so. Flag to human: this means the original trigger (low-effort help requests) is NOT addressed by this ADR. Ask whether that is intended. -- Dormancy scope: ONLY unfinished members. Finished stay permanent and row is still deleted on finish. Consequence: hijack goal covers only never-finished accounts (stated in goal 5 draft). Human chose this over recommended "everyone". -- Voice activity = joining a voice channel (VoiceStateUpdate). -- lastActivity writes throttled (only update when stored value older than an interval, e.g. 1 day). Exact interval to settle in technical design. - -## FINALIZED by human: Problem and goals (goals 1 and 5, gates paragraph) -- Human on the trigger gap: "low effort or low quality messages should not pollute the main area where people hang out, notably general chat. this is why all new comers only have access to the lounge." Intended scope of goal 1 = protect GENERAL CHAT. Trigger paragraph left as is. -- Human on stages: "people without any roles only have access to lounge, then the start role gives access to more channels, and after finishing the onboarding they gain access to the full server." So the journey table channel list is right; just showcase is comment-only. - -## Decisions (journey section round) -- Dormant = Option B (delete and notify). Human chose it over my recommendation A. Known cost, recorded in ADR already: message is visible briefly, weakens goal 5, bot needs Manage Messages and must handle every message from dormant members. Option A text and S15/T12 now partly moot; Option A can be kept in "considered options" instead of "open decision". Start role stays on dormancy (matches table). -- Messages from dormant members are NOT counted (skip recording while dormant role held). Resolves L5. -- Time away does NOT count on rejoin within grace: anchor AND lastActivity shift forward by away time. Human chose this. Away time measured from when the daily job noticed the leave (imprecise by up to a day, consistent with grace period). Resolves L7. Needs stored state: Onboarding row already has "left noticed" time; shift on rejoin. - -## Drafted in main doc (awaiting human review): Journey table and rules -- Start-role row: showcase comment-only, help forum create+reply. -- Dormant row: 3 months without activity; keeps channel access, messages deleted + notified. -- Rules: activity definition (message anywhere incl. forum post, voice join; throttled; initial = join date), dormancy runs from lastActivity, finished never dormant, rectification sets lastActivity=now and dormant messages not counted, rejoin shifts anchor+lastActivity. -- Formatting NOT run: `pnpm exec oxfmt` fails on Node v20.17 (needs newer; .nvmrc). Table padding is unformatted; run fmt before commit of main doc. - -## SCOPE DECISION (human): business definition first, technical details later -Work only on "Problem and goals" and "1. What Onboarding is" (journey + rules) until the business definition is settled. Do NOT edit sections 2-5 (decision, technical design, considered options, open decision) yet. Technical items below are parked, not dropped. Business items still open are listed under "Business questions open". - -## FINALIZED by human: journey table and rules (as drafted in session 1) - -## Business decisions (latest round) -- Dormant notice: member is told in PRIVATE (private thread leaning) immediately when their message is deleted, with a way to reactivate. Reasoning (human): a user who cannot write somewhere will more likely churn than investigate; informing them keeps them in the server and makes reactivating easy; a bot is less likely to react to such a message. OPEN FOR THE TEAM (not the human alone): private thread (member, bot, mods only) vs reply in the channel where everyone sees it. Written into ADR rules as an open point. -- No start role: nothing, their choice, no nudge. Written into rules. -- Gates: 7 days = friction for drive-bys (goals 3, 4). 400 chars = rough engagement sign (goal 6) AND friction; no quality judgement. Human did NOT pick "7 days: time to read rules", so goal 2 (server comprehension) is backed by no gate; only by Discord onboarding presumably. Check with human. - -## Business definition status: essentially complete -- Goal 2 reworded (human chose): the 7-day wait gives time to read rules and see good questions; nothing checks it. Drafted in main doc, awaiting a final look. -- Legacy rule confirmed as written (start+finished+legacy on first post). - -## Business questions open -- Team decision (not ours): private thread vs in-channel reply for dormant notice. Stays as open point in ADR rules. -- Launch date: constant vs env var (L8), technical; park -- Next: either human reads the whole business half once more and signs off, or we move to technical sections (B1 intent argument first, then rewrite open decision section for Option B, T9-T13, S14) - -## Not yet touched (later sections; PARKED until business definition done) -- Section 2/5: Option A/B open-decision section needs rewriting to reflect B chosen. "Telling the member" thread section now relevant to B (DM vs ephemeral vs private thread: still undecided). -- Section 3: Data (add lastActivity column on Onboarding), evaluation order step 5 (dormancy by lastActivity, not anchor), join-message entry point ("changes nothing else" now shifts anchor/lastActivity), message entry point (skip recording when dormant, update lastActivity incl. voice and forum posts), new VoiceStateUpdate entry point, Option B handler (delete messages from dormant members, Manage Messages permission), daily job wording. -- Verify: forum post starter message arrives as MessageCreate; VoiceStateUpdate available. -- L8 launch date in both constants and env vars: still open, decide which. -- B1 (intent argument), T9-T13, S14 (split spec) still open. - -## Still waiting on human -- Review of drafted Journey table and rules -- Launch date: constant or env var (L8) -- Q4: whether to split spec out of ADR (deferred; human chose to edit in place first) +Main doc: `0001-onboarding-without-guild-members-intent.md` (human controls it; Claude never commits it). +Workflow: iterative authoring skill. Ask, don't guess. Prefer AskUserQuestion (human asked for this). Commit only this scratchpad, with descriptive messages. + +## Where we are (end of session 1) + +Business half is DRAFTED in the main doc and the human finalized it, except one last read-through they have not explicitly done: +- "Problem and goals" (goals 1, 2, 5 and the gates paragraph reworded) and "1. What Onboarding is" (journey table + rules). +- Sections 2-5 (decision, technical design, considered options, to-verify/open decision) are UNTOUCHED and now STALE: they still describe anchor-based dormancy, Option A/B as undecided, no lastActivity. +- The main doc has UNCOMMITTED edits (status front matter removed, business half). Human decides when to commit it. `pnpm exec oxfmt` fails on Node v20.17 (needs newer, see .nvmrc), so the widened table rows are unformatted; run fmt on a supported Node before committing. +- Note: AGENTS.md and docs/agents/ are also uncommitted in the working tree and are not ours. + +## Decisions made (business, reasoning kept for context) + +- No status/front matter in the ADR. +- Goal 1 = protect GENERAL CHAT. Human: low-effort or low-quality messages should not pollute the main area where people hang out; newcomers (no roles) only have the lounge. Start role adds help forum (create+reply), ai, gaming, real life, frontend, design, backend, and showcase COMMENTS only. Showcase creation goes through a bot button in a channel the start role cannot reach. Finished = full server. +- Dormancy is ACTIVITY-based: 3 months without activity, unfinished members only (finished are permanent and never dormant; human chose this over covering everyone, so goal 5 does not cover finished accounts). Activity = message anywhere (forum post counts) or joining voice (bot cannot detect talking). lastActivity stored, writes throttled, starts at join date, rectification sets it to now. +- Dormant behaviour = Option B (delete and notify), chosen over my recommendation of Option A (permission deny). Known cost: message briefly visible, weakens goal 5, bot needs Manage Messages. Start role stays on dormancy. +- Messages from members holding the dormant role are not counted toward the 400. +- Rejoin within the 7-day grace: time away does NOT count; anchor and lastActivity shift forward by away time (measured from when the daily job noticed the leave). +- Dormant notice: member told privately and immediately, with a way to reactivate. Reasoning (human): a user who cannot write somewhere churns rather than investigates; this keeps them in the server and makes reactivating easy. +- No start role (skipped Discord onboarding): stays lounge-only, no nudge. +- Gates: 7 days = friction for drive-bys. 400 chars = rough engagement sign and friction; no quality judgement. Goal 2 reworded: the 7 days give time to read, nothing checks it. +- Legacy rule confirmed: start + finished + legacy role on first post. + +## Open items + +Business, for the TEAM to decide (not the human alone): dormant notice in a private thread (member, bot, mods only) vs a reply in the channel itself. Written into the ADR rules as an open point. + +Business, optional: human reads the business half once more and signs off. + +Technical (PARKED by human: "business definition first, technical details later"). Suggested order when resumed: +1. B1 Intent rationale. The bot already depends on MessageContent (privileged, `src/index.ts:17`) and the 400-char mechanic needs it, so "approval could be refused/revoked" applies to it equally. No evidence anyone applied for GuildMembers and was refused. The "10,000 users" claim is unverified. Ask: has GuildMembers been applied for, how big is the server, what is the real reason to avoid it? Revisit "Why not the intent" and "Considered options". +2. Rewrite the "open decision: making dormant members read-only" section: Option B chosen; keep A in Considered options. "Telling the member" (DM vs ephemeral vs private thread) ties to the team decision above. +3. Update section 3 for the new business rules: Data (lastActivity column; maybe left-time already exists), evaluation order step 5 (dormancy by lastActivity, skip recording while dormant), join-message entry point ("changes nothing else" now shifts anchor + lastActivity), message entry point (update lastActivity, throttle), new VoiceStateUpdate entry point, Option B handler (delete dormant members' messages, Manage Messages, exclude the dormant channel), daily job wording, entry-point step 3 legacy check. +4. Technical gaps from the review: + - T9 No `Partials` in `src/index.ts`: discord.js does not emit messageDelete/messageUpdate for uncached messages, and messageDeleteBulk drops uncached ones. The ADR relies on an ID-only delete event. + - T10 Deletes/edits while the bot is offline are lost, counts drift forever; startup catch-up replays only join messages. + - T11 No estimate of tracked-member count N; daily job does N fetches; dormant rows never expire. + - T13 Concurrency argument against the JSON column is applied selectively; evaluate() is also read-modify-write, and step 5 deletes rows while inserts may race. + - T12 (mostly moot under Option B) read-only would need more than Send Messages deny. +5. L8 Launch date is listed as both a config constant and an env var. Decide which. +6. S14 Split the spec (journey, rules, schema, eval order, env vars) out of the ADR? Human deferred; edit in place first. +7. Minor: who accepted the dormancy best-effort gap (invisible members) and when does it become unacceptable; cite the source of "a daily job was the requirement". +8. Verify before building: forum post starter message arrives as MessageCreate; VoiceStateUpdate available (GuildVoiceStates already on); join messages visible in the system channel; single-member fetch works without the intent. + +## Next session: how to start + +Read the main doc and this file, then ask (AskUserQuestion) which to do: (a) business sign-off read-through, or (b) start the technical sections at item 1 (B1). Do not edit sections 2-5 without confirming scope. From a5853b62e0dc22950ecfec65bbe5eb8aebb86e02 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Mon, 2 Nov 2026 15:39:53 +0100 Subject: [PATCH 15/30] chore: note human already committed the ADR edits; re-read before resuming Main doc business-half edits landed in 17e95a4 and 06d6704 (possibly with human tweaks), so the next session must treat the main doc as source of truth. Co-Authored-By: Claude Sonnet 5.5 --- docs/adr/0001-onboarding-without-guild-members-intent.claude.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md index 5d54626..0b301f1 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md @@ -8,7 +8,7 @@ Workflow: iterative authoring skill. Ask, don't guess. Prefer AskUserQuestion (h Business half is DRAFTED in the main doc and the human finalized it, except one last read-through they have not explicitly done: - "Problem and goals" (goals 1, 2, 5 and the gates paragraph reworded) and "1. What Onboarding is" (journey table + rules). - Sections 2-5 (decision, technical design, considered options, to-verify/open decision) are UNTOUCHED and now STALE: they still describe anchor-based dormancy, Option A/B as undecided, no lastActivity. -- The main doc has UNCOMMITTED edits (status front matter removed, business half). Human decides when to commit it. `pnpm exec oxfmt` fails on Node v20.17 (needs newer, see .nvmrc), so the widened table rows are unformatted; run fmt on a supported Node before committing. +- The human has already committed the main doc edits themselves (17e95a4 "clarify goal dormancy", 06d6704 "7 day gives users also enough time to get a read on the server and its community"). The working tree is clean for that file. They may have tweaked wording, so RE-READ the main doc at the start of the next session and treat it as the source of truth over this file. `pnpm exec oxfmt` fails on Node v20.17 (needs newer, see .nvmrc), so the widened table rows may still be unformatted; check `pnpm fmt:check` on a supported Node. - Note: AGENTS.md and docs/agents/ are also uncommitted in the working tree and are not ours. ## Decisions made (business, reasoning kept for context) From 68be35ea198d73a6198e4063b271a52c477b1887 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Mon, 2 Nov 2026 15:47:30 +0100 Subject: [PATCH 16/30] chore: queue two new suggestions for next session Capture S1 (block media attachments until Onboarding finished) and S2 (replace Discord rules quiz with writing the rules in a channel) with impact notes and questions to ask; S2 likely reshapes the journey so do it before the technical sections. Co-Authored-By: Claude Sonnet 5.5 --- ...nboarding-without-guild-members-intent.claude.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md index 0b301f1..4c2b5f9 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md @@ -45,6 +45,17 @@ Technical (PARKED by human: "business definition first, technical details later" 7. Minor: who accepted the dormancy best-effort gap (invisible members) and when does it become unacceptable; cite the source of "a daily job was the requirement". 8. Verify before building: forum post starter message arrives as MessageCreate; VoiceStateUpdate available (GuildVoiceStates already on); join messages visible in the system channel; single-member fetch works without the intent. +## New suggestions from the human (end of session 1), NOT yet discussed or scoped + +S1. Prevent media attachments while Onboarding is not finished. +- My notes (unconfirmed): could be a Discord permission (deny Attach Files / Embed Links on the unfinished roles, set by moderators, which fits the rule "the bot only moves roles") or the bot deleting such messages (Manage Messages, briefly visible, like Option B). Journey rules currently say "Attachments add nothing" to the count, so wording changes too. +- Questions to ask: which stage (Everyone, start role, 7-day/messages milestone holders, dormant)? Attachments only, or also embeds/links? All channels or only some? Which goal does it serve (spam/raids, goal 3, and general-chat quality, goal 1)? Does a finished member keep full access? Does dormant need it? + +S2. Replace the Discord onboarding rules quiz (rules shown with 4 answers, 3 wrong and 1 right) with the newcomer writing the rules out in their own words in a channel. +- My notes (unconfirmed): this is a bigger change than it looks. Today Discord onboarding grants the start role and is the only gate for goal 2. Writing in a channel means someone or something must grant the start role afterwards, so the bot (or a moderator) would take over granting it. That would also remove a technical cost in the ADR ("roles granted outside the bot are noticed late"). It touches the "Everyone" stage, "Discord onboarding" in the glossary, goal 2 (the "nothing checks it" wording could become false), the "No start role" rule and the technical sections. +- Questions to ask: what exactly do they write (rules in own words, an introduction, both)? Who validates it: moderators, the bot (length/keywords), or nobody? Which channel, and can the Everyone stage write there (it is currently lounge-only)? Does Discord onboarding still exist for picking the Developer/Designer role? What is the grant mechanism: moderator button/command or automatic? Is spam in that channel a risk (goal 3)? +- Do this as its own business-level discussion BEFORE the technical sections, because it changes the journey. + ## Next session: how to start -Read the main doc and this file, then ask (AskUserQuestion) which to do: (a) business sign-off read-through, or (b) start the technical sections at item 1 (B1). Do not edit sections 2-5 without confirming scope. +Read the main doc and this file, then ask (AskUserQuestion) which to do: (a) business sign-off read-through, (b) the two new suggestions S1 and S2 (business level, S2 may reshape the journey, so probably first), or (c) start the technical sections at item 1 (B1). Do not edit sections 2-5 without confirming scope. From 9cb503b92685f0599e2dae4cec72f2e213085db3 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Thu, 5 Nov 2026 21:36:44 +0100 Subject: [PATCH 17/30] feat: update dormancy and swithc out 400 char count to 3 day activity --- ...onboarding-without-guild-members-intent.md | 216 ++++++++++++------ 1 file changed, 141 insertions(+), 75 deletions(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.md b/docs/adr/0001-onboarding-without-guild-members-intent.md index 8ee55e4..501d8bd 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.md @@ -1,4 +1,4 @@ -# Onboarding: a role-based member journey, built without the GuildMembers intent +# Onboarding: a role-based member journey Terms in **bold** are defined in [GLOSSARY.md](../../GLOSSARY.md). @@ -19,7 +19,7 @@ Reading is not restricted by this flow. Members who read but rarely post keep th ### What the gates do and do not do -The 7-day wait and the 400-character threshold are deliberate friction. They keep newcomers out of general chat and the rest of the server while they settle in, and they cost a drive-by account some effort. The 7 days make a throwaway account wait before it reaches everything. The 400 characters are a rough sign that the member has taken part, and also cost effort. They do not judge quality. A member can pass them with filler, and a newcomer can still create a low-effort help request in the help forum, which the start role opens. Moderation and the community's own norms remain responsible for quality. Onboarding only limits how much of the server a newcomer can reach while that happens. +The 7-day wait and the 3-days-of-activity requirement are deliberate friction. They keep newcomers out of general chat and the rest of the server while they settle in, and they cost a drive-by account some effort. The 7 days make a throwaway account wait before it reaches everything. Requiring activity on 3 different days shows the member returns to chat and join in, which is a more meaningful sign of trust than an arbitrary character count. They do not judge quality. A member can still create a low-effort help request in the help forum, which the start role opens. Moderation and the community's own norms remain responsible for quality. Onboarding only limits how much of the server a newcomer can reach while that happens. Every gate in section 1 must trace back to one of these goals. A gate that serves none of them should be removed. @@ -31,42 +31,55 @@ The purpose is to let newcomers take part in a few friendly channels straight aw ### The journey -| Stage | How a member gets there | What they can do | -| ----------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -| **Everyone** | Joined, but has no onboarding role (for example skipped Discord onboarding) | Write in lounge, read a few channels, read message mods to open a ticket | -| **Start role** | Granted by Discord onboarding | Also write in help forum (create and reply), ai, gaming, real life, frontend, design and backend, and comment on showcases. Showcases are created through a bot button in a channel this role cannot reach | -| **7-day role** | 7 days after the **restart anchor** | Milestone only | -| **Messages role** | 400 **counted message** characters since the restart anchor | Milestone only | -| **Finished role** | Holds start, 7-day and messages | Full server access. Permanent. The 7-day and messages roles are removed | -| **Dormant role** | 3 months without **activity**, without the finished role | Keeps its channel access, but every message they write is deleted and they are told how to reactivate. The 7-day and messages roles are removed; the start role stays | -| **Legacy member** | Joined before launch, first seen posting | Receives start, finished and **legacy role** at once | +| Stage | How a member gets there | What they can do | +| ----------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Everyone** | Joined, but has no onboarding role (for example skipped Discord onboarding) | Write in lounge, read a few channels (announcement, rules, etc.), read message mods to open a ticket | +| **Start role** | Granted by Discord onboarding | Also write in help forum (create and reply), ai, gaming, real-life, frontend, design-ui-ux and backend-and-cloud, and comment on showcases. Showcases are created through a bot button in a channel this role cannot reach | +| **7-day role** | 7 days after the **restart anchor** | Milestone only | +| **3-days role** | Activity recorded on 3 different days since the restart anchor | Milestone only | +| **Finished role** | Holds start, 7-day and 3-days | Full server access. Permanent. The 7-day and 3-days roles are removed | +| **Dormant role** | 3 months without **activity**, without the finished role | Keeps its channel access, but every message they write is deleted and they are told how to reactivate. The 7-day and 3-days roles are removed; the start role stays | +| **Legacy member** | Joined before launch, first seen posting | Receives start, finished and **legacy role** at once | ### Rules -- The restart anchor is the later of the member's join date and their latest rectification. The 7-day clock and the message count run from it. +- The restart anchor is the later of the member's join date and their latest rectification. The 7-day clock and the activity-day count run from it. - **Activity** is writing a message anywhere (a forum post counts), or joining a voice channel. The bot cannot tell whether someone speaks, only that they joined. It stores the time of the member's last activity, and updates it only when the stored time is older than a set interval, because dormancy is measured in months. A new member's last activity starts as their join date. - Dormancy runs from last activity: 3 months with none, for anyone without the finished role, including members who never held the start role. Finished members are never made dormant. -- When a dormant member writes, their message is deleted and the bot immediately tells them why, privately, and how to reactivate. A member who simply cannot write somewhere is more likely to leave than to investigate, and this keeps them in the server and makes reactivating easy. Open for the team: whether this happens in a private thread (only the member, the bot and moderators can see it) or as a reply in the channel itself, where everyone sees it. +- When a dormant member writes, their message is deleted immediately, they receive a 10-minute timeout, and the bot creates a private thread in that channel, adds the member, and posts an explanation with a reactivate button. This **soft block** approach ensures the user learns about their dormant status at the moment of attempt and is given a clear, actionable path forward without having to search the server. The timeout prevents spam and is removed when the member reactivates. - A member who has no start role (for example, skipped Discord onboarding) stays limited to the lounge until they complete Discord onboarding themselves. They are not nudged. -- Finishing needs the start role. A member could meet the 7-day and message conditions without it, but the finished role waits until Discord onboarding has been completed. -- Counted messages are text only, with whitespace, markdown syntax and links removed. Attachments add nothing. Messages written before the start role count. Messages that are deleted, or removed by spam detection, do not. Edits recount the message. -- **Rectification** leaves dormancy. A moderator runs `/onboarding reactivate @member`, or the member presses the button in the dormant channel. Both remove the dormant role, set a new restart anchor and set last activity to now. The message count is already empty, because it is cleared when the member becomes dormant, and messages from a member holding the dormant role are not counted. A returning member starts over on purpose, with another 7 days and 400 characters: the point is to show they are present again. -- The messages role is held only while the counted characters stay at or above the threshold. If deletions or spam removals drop a member below it before they finish, the role is removed, so posting 400 characters and deleting them does not count. -- A member who leaves keeps their Onboarding progress for a grace period of 7 days, counted from when the daily job first notices they are gone. If they rejoin within it, their counted messages are as they were, however soon after leaving they return. The time away does not count toward the 7 days or the dormancy deadline: the restart anchor and last activity move forward by the time away, measured from when the daily job noticed the leave until the rejoin. After the grace period the progress is deleted and they start Onboarding again. Members who had already finished always start again, because their record is gone and Discord removes their roles when they leave. Counted messages are only deleted in two cases: their onboarding record is deleted (they finished, or the grace period after leaving ran out), or they become dormant. +- Finishing needs the start role. A member could meet the 7-day and 3-days conditions without it, but the finished role waits until Discord onboarding has been completed. +- Activity on a day is recorded once per calendar day, regardless of how many messages are sent. Activity means writing a message anywhere (a forum post counts), or joining a voice channel. A member must record activity on 3 distinct days to earn the 3-days role. +- **Rectification** leaves dormancy. A moderator runs `/onboarding reactivate @member`, or the member presses the button in the dormant channel. Both remove the dormant role, set a new restart anchor and set last activity to now. The day count is already empty, because it is cleared when the member becomes dormant, and activity from a member holding the dormant role is not counted. A returning member starts over on purpose, with another 7 days and 3 days of activity: the point is to show they are present again. +- The 3-days role is held only while the activity-day count stays at or above 3. If a member's activity days are deleted or reset, the role is removed. +- A member who leaves keeps their Onboarding progress for a grace period of 7 days, counted from when the daily job first notices they are gone. If they rejoin within it, their activity-day count is as it was, however soon after leaving they return. The time away does not count toward the 7 days or the dormancy deadline: the restart anchor and last activity move forward by the time away, measured from when the daily job noticed the leave until the rejoin. After the grace period the progress is deleted and they start Onboarding again. Members who had already finished always start again, because their record is gone and Discord removes their roles when they leave. Activity-day records are only deleted in two cases: their onboarding record is deleted (they finished, or the grace period after leaving ran out), or they become dormant. - Bots and moderators (**exempt members**) are never touched. - Which channels each role can see or write in is configured by moderators in Discord. The bot only adds and removes roles. It never edits channel permission overwrites. ## 2. The decision -Onboarding is implemented as a role reconciler that works **without the GuildMembers privileged intent**. It relies on a small tracking table, the join system message, per-message events and a daily job that fetches members one by one. +Onboarding is implemented as a role reconciler that works **with the GuildMembers privileged intent**. It relies on proper Discord events for joins, leaves, and role updates, along with a tracking table for activity days and dormancy. -### Why not use the intent +**Fallback:** If the GuildMembers intent application is denied, the bot can fall back to the approach described in section 3.1, which uses system messages, per-message events, and a daily job to fetch members one by one. -The intent is the obvious route. It delivers join, leave and role-update events and lets the bot list every member. It is privileged. Discord's [Gateway documentation](https://docs.discord.com/developers/events/gateway) says apps with fewer than 10,000 users can switch privileged intents on in the Developer Portal, and approval is needed above that. This app already needed approval for the Message Content intent, so the same process would apply to GuildMembers. We chose not to depend on an approval that Discord controls and could refuse or revoke. +### Why use the intent -### What this costs +The intent is the most straightforward route. It delivers join, leave and role-update events in real-time and lets the bot list every member. This app already requires approval for the Message Content intent (privileged, see `src/index.ts:17`), so applying for GuildMembers follows the same process. Using the intent eliminates the technical debt of workarounds and provides cleaner, more reliable functionality. -- Roles granted outside the bot (Discord onboarding giving the start role) are noticed late: when the member next posts, or at the next daily run. +### What this gives us (with GuildMembers intent) + +- Roles granted outside the bot (Discord onboarding giving the start role) are noticed immediately via role-update events. +- Leaves are noticed immediately via leave events, enabling accurate grace period tracking. +- Time-based roles (7-day, dormant) can be applied in real-time or via scheduled jobs. +- Existing members can be listed, enabling a clean backfill of legacy members. +- Dormancy can be comprehensive: all members can be evaluated, not just those the bot saw join or post. +- The leave grace period starts exactly when the member left. + +### Fallback costs (without GuildMembers intent) + +If the intent is not approved, the fallback approach has these costs: + +- Roles granted outside the bot are noticed late: when the member next posts, or at the next daily run. - Leaves are noticed late, at the next daily run. - Time-based roles (7-day, dormant) are applied once a day, so they can arrive up to a day late. - Existing members can't be listed, so they become **legacy members** one at a time, as they post. @@ -75,116 +88,169 @@ The intent is the obvious route. It delivers join, leave and role-update events ## 3. Technical design -### Signals the bot uses +### Signals the bot uses (with GuildMembers intent) + +| Need | Source | Needs privileged intent | +| ------------------ | ------------------------------------------------- | ----------------------- | +| A member joined | `GuildMemberAdd` event | Yes (GuildMembers) | +| Join date, roles | `GuildMemberAdd` event or `guild.members.fetch()` | Yes (GuildMembers) | +| Activity tracked | `MessageCreate`, `VoiceStateUpdate` events | No | +| Member left | `GuildMemberRemove` event | Yes (GuildMembers) | +| Start role granted | `GuildMemberUpdate` event (role change) | Yes (GuildMembers) | + +**Fallback signals (without GuildMembers intent):** -| Need | Source | Needs privileged intent | -| ------------------ | --------------------------------------------------------------------------------------------------------------- | ----------------------- | -| A member joined | System join message (`MessageType.UserJoin`) in the server's system channel. `message.author` is the joiner | No (GuildMessages) | -| Join date, roles | `message.member` on every guild message (`joinedAt`, `roles`) | No | -| Message counted | `MessageCreate` | No | -| Message edited | `MessageUpdate` | No | -| Message uncounted | `MessageDelete` and `MessageDeleteBulk` | No | -| Member left | Daily run fetches each tracked member by ID and treats Discord's "Unknown Member" error (code 10007) as a leave | No | -| Start role granted | Seen on the member's next message, or when the daily run fetches them | No | +- A member joined: System join message (`MessageType.UserJoin`) in the server's system channel +- Join date, roles: `message.member` on every guild message +- Member left: Daily run fetches each tracked member by ID and treats Discord's "Unknown Member" error (code 10007) as a leave +- Start role granted: Seen on the member's next message, or when the daily run fetches them -Spam detection removes messages with `bulkDelete`. discord.js reports that with a separate bulk-delete event, and the bot has no delete listeners today. Both events must be handled, otherwise spam would still count toward the 400 characters. +Note: Spam detection removes messages with `bulkDelete`. discord.js reports that with a separate bulk-delete event, and the bot has no delete listeners today. ### Data -Two new Prisma models in [schema.prisma](../../prisma/schema.prisma): +Three Prisma models in [schema.prisma](../../prisma/schema.prisma): -- **Onboarding**: one row per member who is in progress or dormant. Columns are the user ID (primary key), join date, restart anchor, and the time the bot first noticed the member had left (empty while they are in the server). The row is deleted when the member finishes, or when the grace period after leaving has ended. -- **OnboardingMessage**: one row per counted message. Columns are the message ID (primary key), the user ID (a foreign key to `Onboarding` that cascades on delete) and the counted length. A member's total is the sum of their rows. A message's creation time is not stored: messages before the anchor are filtered when they arrive, and the time is encoded in the message ID if it is ever needed. A member's rows are deleted in exactly two cases: their `Onboarding` row is deleted, or they become dormant. +- **Onboarding**: one row per member who is actively in the onboarding process (not finished, not dormant). Columns are the user ID (primary key), join date, restart anchor, last activity date, activity days count (number of distinct days with activity), and the time the bot first noticed the member had left (empty while they are in the server). The row is deleted when the member finishes, becomes dormant, or when the grace period after leaving has ended. +- **Dormant**: one row per dormant member. Columns are the user ID (primary key), dormancy start date, and last activity date. The row is deleted when the member is removed from the server (after leaving with no rejoin within grace period) or when they reactivate. +- **OnboardingActivityDay**: one row per distinct day of activity for a member. Columns are a composite primary key of user ID and date. This table tracks which days a member was active. A member's total is the count of their rows. -Roles are not mirrored in the database. Discord's role list is the source of truth for the current stage, and the tables hold only what Discord cannot tell us (the anchor and the message lengths). +Roles are not mirrored in the database. Discord's role list is the source of truth for the current stage, and the tables hold only what Discord cannot tell us (the anchor, activity days, and dormancy state). -### Why message records get their own table +**Fallback note:** Without GuildMembers intent, the Dormant table would be merged into Onboarding with a status column, as the bot cannot reliably detect when members leave. -Counting a message, uncounting a deleted one and recounting an edited one are each a single statement keyed by message ID, so concurrent events for the same member cannot overwrite each other. A delete event for a message that is not in the bot's cache carries only the message ID and no author, and the primary key finds the owner directly. Replays (startup catch-up, duplicate events) become upserts on the primary key and cannot double-count. Message rows are removed by a cascade, not by application code. `OnboardingMessage` declares its relation to `Onboarding` with `onDelete: Cascade`, so deleting a member's `Onboarding` row deletes all of their `OnboardingMessage` rows in the same operation. The code that finishes a member or handles a leave only deletes the `Onboarding` row and cannot forget the messages. Becoming dormant is the exception: the `Onboarding` row stays, so the dormancy step deletes that member's message rows explicitly. Rectification and rejoining never delete message rows. +### Why activity days get their own table -SQLite only enforces foreign keys when they are switched on for the connection. A test must confirm the cascade really fires with the better-sqlite3 adapter in use, and the schema must not rely on it silently doing nothing. +Tracking activity per day in a separate table allows: -Storing the messages as a JSON column on `Onboarding` was considered and rejected; see section 4. +- Efficient counting of distinct days without complex queries +- Easy insertion of a new day's activity (insert if not exists) +- Clean deletion when a member finishes or becomes dormant (cascade delete) +- Replays (startup catch-up, duplicate events) become idempotent inserts on the composite primary key + +The `OnboardingActivityDay` table declares its relation to `Onboarding` with `onDelete: Cascade`, so deleting a member's `Onboarding` row deletes all of their activity day rows in the same operation. When a member becomes dormant, their activity day rows are explicitly deleted. + +SQLite only enforces foreign keys when they are switched on for the connection. A test must confirm the cascade really fires with the better-sqlite3 adapter in use. ### One decision function All entry points call the same two pieces: -1. A pure function takes the member's roles, restart anchor, counted characters, join date and the current time, and returns the roles to add and remove. It has no Discord calls, so it can be unit-tested. +1. A pure function takes the member's roles, restart anchor, activity days count, join date and the current time, and returns the roles to add and remove. It has no Discord calls, so it can be unit-tested. 2. An applier makes the role changes and writes a line to the onboarding log channel. Evaluation order inside the pure function: 1. **Exempt member**: nothing. -2. **Finished role present**: remove any leftover 7-day and messages roles, stop tracking. +2. **Finished role present**: remove any leftover 7-day and 3-days roles, stop tracking. 3. **Joined before the launch date**: add start, finished and legacy. 4. **Dormant role present**: nothing until rectification. -5. **3 months past the anchor**: remove 7-day and messages, add dormant, delete the member's message rows. +5. **3 months past the anchor**: remove 7-day and 3-days, add dormant, move to Dormant table, delete the member's activity day rows. 6. **7 days past the anchor**: add 7-day. -7. **400 counted characters**: add messages. Below 400 while holding the messages role: remove it. -8. **Start, 7-day and messages all held**: add finished, remove 7-day and messages. +7. **3 activity days**: add 3-days role. Below 3 while holding the 3-days role: remove it. +8. **Start, 7-day and 3-days all held**: add finished, remove 7-day and 3-days, delete Onboarding row. Role changes run one at a time, as the existing [addRoleToUser](../../src/util/add-role-to-user.ts) does, so a large first run stays inside Discord's rate limits. Functions throw when they cannot complete their job and the caller handles it. -### Entry points +### Entry points (with GuildMembers intent) + +- **Member join** (`GuildMemberAdd`): create the member's `Onboarding` row with join date as restart anchor and initial last activity. +- **Message create** (`MessageCreate`): if the member is not exempt and not finished, create tracking row if missing, record activity day if the message is after the anchor and it's a new calendar day, then evaluate. +- **Voice state update** (`VoiceStateUpdate`): if the member joined a voice channel and is not exempt and not finished, create tracking row if missing, record activity day if it's a new calendar day, then evaluate. +- **Member leave** (`GuildMemberRemove`): record leave time in Onboarding row. If no rejoin within 7 days, delete the row. +- **Member update** (`GuildMemberUpdate`): if start role is granted and member has Onboarding row, evaluate. +- **Daily job**: scheduled with `node-cron` at a fixed UTC time. For each tracked member, re-evaluate. For dormant members, check if they should be removed from the Dormant table. +- **Rectification**: a shared function checks the member holds the dormant role (throws if not), removes it, creates a new Onboarding row with anchor set to now, and evaluates. The `/onboarding reactivate` slash command and the dormant-channel button both call it. -- **Join message**: create the member's `Onboarding` row. If a row already exists, the bot clears its left time and changes nothing else, whether the join message is a replay or the member is returning within the grace period. The row keeps its original join date and restart anchor, so a member who had been in the server for 6 days does not restart their 7-day clock, and the message rows are untouched. The roles lost while away are re-granted by the next evaluation. -- **Any guild message from a member who is not exempt and not finished**: if there is no tracking row, create one from `message.member.joinedAt`. Record the counted length (messages before the restart anchor are ignored), then evaluate. -- **Message edit and delete events**: recompute or remove the message record, then evaluate. -- **Daily job**: scheduled with `node-cron` at a fixed UTC time, like the showcase digest. For each tracked member it fetches the member by ID. If the member is gone, it records when it first noticed, and deletes their data once the grace period has passed; members marked as left are not evaluated. If the member is present, it clears any recorded left time and re-evaluates them. +**Fallback entry points (without GuildMembers intent):** + +- **Join message**: create the member's `Onboarding` row. If a row already exists, the bot clears its left time. +- **Any guild message from a member who is not exempt and not finished**: if there is no tracking row, create one from `message.member.joinedAt`. Record activity day, then evaluate. +- **Daily job**: For each tracked member, fetch by ID. If gone, record leave time; if present after grace period, delete row. - **Startup catch-up**: fetch recent history of the system channel and replay join messages the bot missed while offline. -- **Rectification**: a shared function checks the member holds the dormant role (throws if not), removes it, sets the anchor to now and evaluates. It does not touch message rows. The `/onboarding reactivate` slash command and the dormant-channel button both call it. ### Dormant channel -At startup the bot makes sure one fixed message with a "Reactivate" button exists in the dormant channel, and posts it if missing. The placeholder text says the account has been dormant and the member should press the button to restart onboarding. The final wording is a constant that will be replaced later. The button is handled by the persistent interaction handlers so it survives restarts, and it rejects members without the dormant role. +At startup the bot makes sure one fixed message with a "Reactivate" button exists in the dormant channel, and posts it if missing. This serves as a reference point for dormant users. The placeholder text says the account has been dormant and the member should press the button to restart onboarding. The final wording is a constant that will be replaced later. The button is handled by the persistent interaction handlers so it survives restarts, and it rejects members without the dormant role. + +**Note:** With Option B chosen, this shared message is supplementary. The primary notification mechanism is the private thread created when a dormant member attempts to write. ### Configuration -- **Constants** in one onboarding config file: counted-character threshold (400), days until the 7-day role (7) and dormancy period (3 months), leave grace period (7 days), daily-job time, launch-date constant. +- **Constants** in one onboarding config file: days until the 7-day role (7), activity days required (3), dormancy period (3 months), leave grace period (7 days), daily-job time, launch-date constant. - **Required env vars** in [env.ts](../../src/env.ts): the five onboarding role IDs plus the legacy role ID, the dormant channel ID and the onboarding log channel ID, and the launch date. The optional `ONBOARDING_ROLE_ID` and `ONBOARDING_CHANNEL_ID` and the unused `/onboarding` stub are removed. -- **Permissions**: the bot's own role must sit above all onboarding roles and have Manage Roles, and it needs View Channel on the system channel. -- The GuildMembers intent stays off. No change to the intents in [index.ts](../../src/index.ts). +- **Permissions**: the bot's own role must sit above all onboarding roles and have Manage Roles, Moderate Members (for applying timeouts), View Channel on the system channel, and (with GuildMembers intent) the GuildMembers privileged intent enabled in the Developer Portal. +- **Intent configuration**: Enable GuildMembers intent in `src/index.ts`. If the intent is not approved, fall back to the system message approach. ## 4. Considered options -- **Apply for the GuildMembers intent.** Simplest design. It would also give events for role changes and leaves, and a list of all members for a clean backfill. Rejected because it depends on Discord's approval, which has been needed before and is not ours to decide. +- **Use GuildMembers intent (CHOSEN).** Simplest design. It provides real-time events for joins, leaves, and role changes, and allows listing all members for a clean backfill. Requires approval for servers with 10,000+ users, but this server may be below that threshold. Fallback to the system message approach if approval is denied. +- **Activity-based trust instead of character count (CHOSEN).** Using 3 days of activity is more meaningful than a 400-character threshold. It better aligns with Goal 1 (protecting regulars) by ensuring users return to engage, rather than just posting a long message once. +- **Separate Dormant table (CHOSEN).** Keeping active and dormant users in separate tables makes the data model clearer and allows the Onboarding table to only contain users actively progressing. - **Keep a running character total per member instead of per-message records.** Less storage. Rejected because deleted and spam-removed messages must stop counting, and a total alone cannot tell which characters to subtract. -- **One table, with the per-message lengths in a JSON column on `Onboarding`** (message ID mapped to counted length). Simpler schema, one read for a member's whole state, and one delete for cleanup. Rejected for three reasons. Every update is a read-modify-write of the whole column, so two events for the same member, such as a new message during a spam bulk-delete, can silently overwrite each other unless updates are serialised per member. A delete event for an uncached message gives only the message ID, so finding the member who owns it means scanning every member's JSON with raw SQL, since Prisma cannot filter inside JSON on SQLite as far as we know. And the total would be computed in code, with the whole blob rewritten on every message. Fixing the first two with a per-member lock and a message-to-member lookup would rebuild the second table in a worse form. +- **One table, with the per-message lengths in a JSON column on `Onboarding`** (message ID mapped to counted length). Simpler schema, one read for a member's whole state, and one delete for cleanup. Rejected for three reasons. Every update is a read-modify-write of the whole column, so two events for the same member can silently overwrite each other unless updates are serialised per member. A delete event for an uncached message gives only the message ID, so finding the member who owns it means scanning every member's JSON with raw SQL. And the total would be computed in code, with the whole blob rewritten on every message. Fixing the first two with a per-member lock and a message-to-member lookup would rebuild the second table in a worse form. - **Restore finished members who leave and rejoin.** Would need a permanent record of every finished member. Without the intent the bot only learns of a rejoin, never of the leave, so the 7-day grace period given to in-progress members cannot be applied: the choices are restoring access whenever they return, or guessing from the finish time. Rejected to keep the design small and not grant full access automatically; a finished member who returns starts again. - **Hourly job instead of daily.** Faster role delivery. Not chosen: a daily job was the requirement and the lag is acceptable. - **Bot manages channel permission overwrites.** Would let the bot enforce what "dormant" means. Rejected because channel permissions belong to moderators and the onboarding flow is meant to be roles only. ## 5. To verify before building +### With GuildMembers intent: + +- That the bot receives `GuildMemberAdd`, `GuildMemberRemove`, and `GuildMemberUpdate` events. +- That the bot can list all members to backfill legacy members. +- That `VoiceStateUpdate` events are received to track voice channel joins. + +### Fallback (without GuildMembers intent): + - That the bot can see and read the system join messages in the real server, and that welcome messages are enabled there. - That fetching a single member by ID works with the intent off. Discord's docs say only List Guild Members requires the intent. The Get Guild Member page does not mention one, and the existing stub in [features/onboarding](../../src/features/onboarding/index.ts) already calls it. -- How a dormant member is made read-only. Decision pending; the two options are described below. -### Open decision: making dormant members read-only +### For both approaches: + +- That activity day tracking correctly identifies distinct calendar days. +- That the separate Dormant table works as expected. +- That the private thread notification works as expected: creation, member addition, button functionality, and thread cleanup. + +### Decision: making dormant members read-only -A dormant member must be able to read the server but not write anywhere, including the lounge. The lounge stays open to every other member. Two ways to get there. +**Decision: Option B (Delete and notify with private thread) has been chosen.** -**Option A: channel permissions.** Dormancy removes the start role. Moderators configure the dormant role with read access everywhere and a Send Messages deny on every writable channel, the lounge included. Discord applies `@everyone` overwrites first and role overwrites after, so the dormant role's deny beats the lounge's `@everyone` allow. Another role's explicit allow would beat the deny, because Discord combines role overwrites so that an allow from any role wins over a deny from another. The start role's write allows are such an allow, which is why the start role has to be removed. Reactivation re-adds it. +A dormant member must be able to read the server but not write anywhere, including the lounge. The chosen approach uses a **soft block** pattern with **just-in-time education**: the bot allows the write attempt to proceed, then intercepts it to provide immediate, actionable feedback. This is also known as **forgiving degradation** or **progressive intervention**—the system doesn't silently fail or permanently block, but instead converts the failure into a learning moment. -- Discord enforces it. Nothing is posted, and it works when the bot is down. -- The bot still only moves roles and never edits overwrites. -- The member cannot write anywhere and gets no prompt on their own. They may not know why, or how to come back, unless they are told (see "Telling the member" below). -- Reactivation skips Discord onboarding the second time. +**Implementation:** When a dormant member writes a message, the bot: -**Option B: delete and notify.** Dormant members keep their channel permissions. When one writes a message, the bot deletes the message and tells them to press a reactivate button. This gives the member a clear, immediate task without searching the server. +1. Deletes the message immediately (requires Manage Messages permission) +2. Applies a **10-minute timeout** to the member (prevents further posts during this period) +3. Creates a private thread in the channel where the message was posted +4. Adds the member and the bot to the private thread +5. Posts an explanation that the account is dormant and provides a reactivate button -- Discord only allows an ephemeral message as the response to an interaction (a slash command or button press). The bot cannot send one in reply to a normal message. The message that carries the button is therefore a DM, which fails if the member has DMs closed, a visible reply that deletes itself, or a private thread (see "Telling the member" below). Pressing the button can then respond ephemerally. -- The offending message is posted before it is deleted. It can ping people and unfurl links for the moment it exists, and it stays if the bot is down or rate-limited. This weakens the hijacked-account goal, since a compromised account can post a scam before the delete lands. -- The bot needs the Manage Messages permission and must act on every guild message from a dormant member. -- Every dormant member who writes is deleted and notified, so a member with a legitimate message sees it removed. +When the member presses the reactivate button, the bot removes the timeout, removes the dormant role, and proceeds with rectification. The member is notified because they were added to the thread, giving them a clear, visible path to reactivation without having to search the server or leave to figure out what happened. + +**Why Option B over Option A:** + +- **User retention:** A user who cannot write at all (**hard block**) is more likely to churn and leave the server entirely. A soft block keeps them engaged and provides immediate guidance. +- **Discovery:** Users learn about their dormant status naturally through use, rather than having to investigate why they can't post. +- **Actionable:** The notification includes the reactivate button right next to the explanation, making the next step obvious. + +**Trade-offs accepted:** + +- The offending message is briefly visible before deletion. It can ping people and unfurl links, which weakens Goal 5 (hijacked dormant accounts). This is mitigated by the immediate 10-minute timeout that prevents follow-up posts. +- The bot must have Manage Messages permission and Moderate Members permission (for applying timeouts) and handle every guild message from dormant members. +- Each dormant member who writes creates a new private thread. The bot needs permission to create private threads and add members. The 10-minute timeout prevents rapid successive thread creation from the same user. ### Telling the member -A dormant member should be told what happened and given one action to take, without searching the server. A private thread does this better than a DM: it does not fail when DMs are closed, the message stays inside the server, and the reactivate button sits next to the explanation. When the member becomes dormant (or, in option B, when they first write), the bot creates a private thread, adds the member, and posts the explanation and the reactivate button. The member is notified because they were added to the thread. +With Option B chosen, the telling happens **just-in-time** when the dormant member attempts to write. A private thread is created in the channel where they tried to post, which does not fail when DMs are closed, keeps the message inside the server, and places the reactivate button next to the explanation. The member is notified because they were added to the thread. + +This approach replaces the single shared button message in the dormant channel, though that message can remain as a reference point. -This works with either option and replaces the single shared button message in the dormant channel, or sits beside it. To verify before relying on it: +**To verify before relying on it:** - A member added to a private thread can see and use it when the parent channel is otherwise hidden from them. - The bot has permission to create private threads and add members. -- The number of threads stays manageable, and an archived thread can still be used. Threads should be deleted or reused on reactivation. +- The bot has Moderate Members permission to apply and remove the 10-minute timeout. +- The number of threads stays manageable. Consider archiving threads after reactivation or after a period of inactivity. +- The delete-and-create-thread sequence is atomic enough that the message doesn't persist long enough to cause harm. +- The 10-minute timeout is applied correctly and removed upon reactivation. From 229b0e5fa29431e33bfd210c1350e0f7ab8cf5eb Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Fri, 6 Nov 2026 20:35:44 +0100 Subject: [PATCH 18/30] feat: info on attachments and type the rules --- ...onboarding-without-guild-members-intent.md | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.md b/docs/adr/0001-onboarding-without-guild-members-intent.md index 501d8bd..26b67c8 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.md @@ -1,7 +1,5 @@ # Onboarding: a role-based member journey -Terms in **bold** are defined in [GLOSSARY.md](../../GLOSSARY.md). - ## Problem and goals Regulars have said they are tired of dealing with low-quality content from new joiners every day: badly formed help requests, and "look at my site" posts for projects that are fully vibe coded. They would prefer a calmer environment, with newcomers kept out of general chat and given the lounge instead. The trigger for this work is low-effort help requests, not a specific spam incident. @@ -9,8 +7,8 @@ Regulars have said they are tired of dealing with low-quality content from new j Onboarding exists to serve these goals, in this order of weight: 1. **Protect regulars' attention.** Keep newcomers out of general chat and the rest of the server until they have had time to settle in. Newcomers cannot create showcases, because that goes through a bot button in a channel they cannot reach. They can comment on showcases and take part in the help forum from the start. -2. **Server comprehension.** The 7-day wait gives newcomers time to read the rules and see how good questions are asked before they reach the whole server. Nothing checks that they did. -3. **Spam and raids.** Fresh accounts have limited reach while trust is unknown. +2. **Server comprehension.** The 7-day wait gives newcomers time to read the rules and see how good questions are asked before they reach the whole server. To get the rules role, a newcomer must also type out the rules exactly, from an image, in the type-the-rules channel. The image stops copy and paste, so they have to read the text as they type. It checks effort, not understanding. +3. **Spam and raids.** Fresh accounts have limited reach while trust is unknown. Members who have not finished cannot attach files, which blocks file-based spam payloads. 4. **Drive-by accounts.** People who join, post once and leave should not cost moderators time. 5. **Limit hijacked dormant accounts.** An account that has shown no activity for months can later be compromised and used to post scam messages. A member who has not finished Onboarding and has no activity for 3 months loses the ability to write. Activity means writing a message anywhere, creating a forum post, or joining a voice channel. Finished members are never made dormant, so this goal does not cover them. 6. **Encourage participation.** Newcomers get a friendly place to take part straight away (the lounge and a few topic channels) and a path to the rest. @@ -33,12 +31,13 @@ The purpose is to let newcomers take part in a few friendly channels straight aw | Stage | How a member gets there | What they can do | | ----------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Everyone** | Joined, but has no onboarding role (for example skipped Discord onboarding) | Write in lounge, read a few channels (announcement, rules, etc.), read message mods to open a ticket | +| **Everyone** | Joined, but has no onboarding role (for example skipped Discord onboarding) | Write in lounge and the type-the-rules channel, read a few channels (announcement, rules, etc.), read message mods to open a ticket | | **Start role** | Granted by Discord onboarding | Also write in help forum (create and reply), ai, gaming, real-life, frontend, design-ui-ux and backend-and-cloud, and comment on showcases. Showcases are created through a bot button in a channel this role cannot reach | | **7-day role** | 7 days after the **restart anchor** | Milestone only | | **3-days role** | Activity recorded on 3 different days since the restart anchor | Milestone only | -| **Finished role** | Holds start, 7-day and 3-days | Full server access. Permanent. The 7-day and 3-days roles are removed | -| **Dormant role** | 3 months without **activity**, without the finished role | Keeps its channel access, but every message they write is deleted and they are told how to reactivate. The 7-day and 3-days roles are removed; the start role stays | +| **Rules role** | Passed the **rules check** in type-the-rules | Milestone only | +| **Finished role** | Holds start, rules, 7-day and 3-days | Full server access, including attaching files. Permanent. The rules, 7-day and 3-days roles are removed | +| **Dormant role** | 3 months without **activity**, without the finished role | Keeps its channel access, but every message they write is deleted and they are told how to reactivate. The rules, 7-day and 3-days roles are removed; the start role stays | | **Legacy member** | Joined before launch, first seen posting | Receives start, finished and **legacy role** at once | ### Rules @@ -48,9 +47,12 @@ The purpose is to let newcomers take part in a few friendly channels straight aw - Dormancy runs from last activity: 3 months with none, for anyone without the finished role, including members who never held the start role. Finished members are never made dormant. - When a dormant member writes, their message is deleted immediately, they receive a 10-minute timeout, and the bot creates a private thread in that channel, adds the member, and posts an explanation with a reactivate button. This **soft block** approach ensures the user learns about their dormant status at the moment of attempt and is given a clear, actionable path forward without having to search the server. The timeout prevents spam and is removed when the member reactivates. - A member who has no start role (for example, skipped Discord onboarding) stays limited to the lounge until they complete Discord onboarding themselves. They are not nudged. -- Finishing needs the start role. A member could meet the 7-day and 3-days conditions without it, but the finished role waits until Discord onboarding has been completed. +- Finishing needs the start role. A member could meet the 7-day, 3-days and rules conditions without it, but the finished role waits until Discord onboarding has been completed. +- The **rules check** replaces the quiz Discord onboarding used to show. In the type-the-rules channel, a new channel Everyone can write in, a pinned message tells the member what to do and the rules are shown as an image and the member types them out exactly as written, not in their own words. The bot compares the message with the rules text, ignoring case and extra whitespace; every word must match. A match grants the rules role. The message is deleted afterwards, whether it passed or failed, so the answer is not left for others to copy. The rules role is independent of the start role: a member can hold either first. +- The rules image carries the full rules text as its alt text, so screen reader users can complete the check. Members who cannot use the image for another reason are told in the type-the-rules channel to message mods, and a moderator can grant the rules role with a command. Alt text is easy to extract, so the check is friction, not a security control. +- Members who have not finished cannot attach files. Moderators set this in Discord by denying Attach Files to everyone except the finished role. The bot does nothing for it, in line with the roles-only rule below. Links and embeds are not restricted. This serves goal 3 only. - Activity on a day is recorded once per calendar day, regardless of how many messages are sent. Activity means writing a message anywhere (a forum post counts), or joining a voice channel. A member must record activity on 3 distinct days to earn the 3-days role. -- **Rectification** leaves dormancy. A moderator runs `/onboarding reactivate @member`, or the member presses the button in the dormant channel. Both remove the dormant role, set a new restart anchor and set last activity to now. The day count is already empty, because it is cleared when the member becomes dormant, and activity from a member holding the dormant role is not counted. A returning member starts over on purpose, with another 7 days and 3 days of activity: the point is to show they are present again. +- **Rectification** leaves dormancy. A moderator runs `/onboarding reactivate @member`, or the member presses the button in the dormant channel. Both remove the dormant role, set a new restart anchor and set last activity to now. The day count is already empty, because it is cleared when the member becomes dormant, and activity from a member holding the dormant role is not counted. A returning member starts over on purpose, with another 7 days, 3 days of activity and the rules check: the point is to show they are present again. - The 3-days role is held only while the activity-day count stays at or above 3. If a member's activity days are deleted or reset, the role is removed. - A member who leaves keeps their Onboarding progress for a grace period of 7 days, counted from when the daily job first notices they are gone. If they rejoin within it, their activity-day count is as it was, however soon after leaving they return. The time away does not count toward the 7 days or the dormancy deadline: the restart anchor and last activity move forward by the time away, measured from when the daily job noticed the leave until the rejoin. After the grace period the progress is deleted and they start Onboarding again. Members who had already finished always start again, because their record is gone and Discord removes their roles when they leave. Activity-day records are only deleted in two cases: their onboarding record is deleted (they finished, or the grace period after leaving ran out), or they become dormant. - Bots and moderators (**exempt members**) are never touched. From f7a9544e7d96a43289fdd797eb6e8206804d9bcb Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Fri, 6 Nov 2026 20:40:03 +0100 Subject: [PATCH 19/30] feat: clarify the dormancy --- docs/adr/0001-onboarding-without-guild-members-intent.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.md b/docs/adr/0001-onboarding-without-guild-members-intent.md index 26b67c8..e675589 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.md @@ -45,7 +45,7 @@ The purpose is to let newcomers take part in a few friendly channels straight aw - The restart anchor is the later of the member's join date and their latest rectification. The 7-day clock and the activity-day count run from it. - **Activity** is writing a message anywhere (a forum post counts), or joining a voice channel. The bot cannot tell whether someone speaks, only that they joined. It stores the time of the member's last activity, and updates it only when the stored time is older than a set interval, because dormancy is measured in months. A new member's last activity starts as their join date. - Dormancy runs from last activity: 3 months with none, for anyone without the finished role, including members who never held the start role. Finished members are never made dormant. -- When a dormant member writes, their message is deleted immediately, they receive a 10-minute timeout, and the bot creates a private thread in that channel, adds the member, and posts an explanation with a reactivate button. This **soft block** approach ensures the user learns about their dormant status at the moment of attempt and is given a clear, actionable path forward without having to search the server. The timeout prevents spam and is removed when the member reactivates. +- When a dormant member writes, their message is deleted immediately, they receive a 10-minute timeout, and the bot creates a private thread in that channel, adds the member, and posts an explanation with a reactivate button and the text of the deleted message, so the member can repost it once reactivated. This **soft block** approach ensures the user learns about their dormant status at the moment of attempt and is given a clear, actionable path forward without having to search the server. The timeout prevents spam and is removed when the member reactivates. - A member who has no start role (for example, skipped Discord onboarding) stays limited to the lounge until they complete Discord onboarding themselves. They are not nudged. - Finishing needs the start role. A member could meet the 7-day, 3-days and rules conditions without it, but the finished role waits until Discord onboarding has been completed. - The **rules check** replaces the quiz Discord onboarding used to show. In the type-the-rules channel, a new channel Everyone can write in, a pinned message tells the member what to do and the rules are shown as an image and the member types them out exactly as written, not in their own words. The bot compares the message with the rules text, ignoring case and extra whitespace; every word must match. A match grants the rules role. The message is deleted afterwards, whether it passed or failed, so the answer is not left for others to copy. The rules role is independent of the start role: a member can hold either first. @@ -193,6 +193,7 @@ At startup the bot makes sure one fixed message with a "Reactivate" button exist - **One table, with the per-message lengths in a JSON column on `Onboarding`** (message ID mapped to counted length). Simpler schema, one read for a member's whole state, and one delete for cleanup. Rejected for three reasons. Every update is a read-modify-write of the whole column, so two events for the same member can silently overwrite each other unless updates are serialised per member. A delete event for an uncached message gives only the message ID, so finding the member who owns it means scanning every member's JSON with raw SQL. And the total would be computed in code, with the whole blob rewritten on every message. Fixing the first two with a per-member lock and a message-to-member lookup would rebuild the second table in a worse form. - **Restore finished members who leave and rejoin.** Would need a permanent record of every finished member. Without the intent the bot only learns of a rejoin, never of the leave, so the 7-day grace period given to in-progress members cannot be applied: the choices are restoring access whenever they return, or guessing from the finish time. Rejected to keep the design small and not grant full access automatically; a finished member who returns starts again. - **Hourly job instead of daily.** Faster role delivery. Not chosen: a daily job was the requirement and the lag is acceptable. +- **Deny Send Messages to the dormant role (hard block).** No bot handling of messages, no brief visibility of the message, and no Manage Messages or Moderate Members permission. Rejected for two reasons. The member sees a disabled or blocked message box with Discord's generic notice and no path to reactivate, and members rarely investigate, so they churn or ask in chat why they see nothing. And it needs a dormant-role overwrite in every channel, which moderators would have to maintain, against the rule that onboarding only moves roles. - **Bot manages channel permission overwrites.** Would let the bot enforce what "dormant" means. Rejected because channel permissions belong to moderators and the onboarding flow is meant to be roles only. ## 5. To verify before building @@ -226,7 +227,7 @@ A dormant member must be able to read the server but not write anywhere, includi 2. Applies a **10-minute timeout** to the member (prevents further posts during this period) 3. Creates a private thread in the channel where the message was posted 4. Adds the member and the bot to the private thread -5. Posts an explanation that the account is dormant and provides a reactivate button +5. Posts an explanation that the account is dormant, the text of the deleted message so the member does not lose it, and a reactivate button. Only the text is copied; attachments are not, and unfinished members cannot attach files anyway When the member presses the reactivate button, the bot removes the timeout, removes the dormant role, and proceeds with rectification. The member is notified because they were added to the thread, giving them a clear, visible path to reactivation without having to search the server or leave to figure out what happened. @@ -239,6 +240,7 @@ When the member presses the reactivate button, the bot removes the timeout, remo **Trade-offs accepted:** - The offending message is briefly visible before deletion. It can ping people and unfurl links, which weakens Goal 5 (hijacked dormant accounts). This is mitigated by the immediate 10-minute timeout that prevents follow-up posts. +- A member who wrote a long, good-faith reply loses it from the channel and gets a timeout, which can feel like a punishment. Copying the text into the thread lets them repost it right after reactivating, which is immediate. The 10-minute timeout is kept because it stops follow-up posts and thread spam; this friction is accepted. - The bot must have Manage Messages permission and Moderate Members permission (for applying timeouts) and handle every guild message from dormant members. - Each dormant member who writes creates a new private thread. The bot needs permission to create private threads and add members. The 10-minute timeout prevents rapid successive thread creation from the same user. From acb6b64e67be57ed1eef7bc26f1d5046871d0e54 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Fri, 6 Nov 2026 20:40:49 +0100 Subject: [PATCH 20/30] chore: remove glossary --- GLOSSARY.md | 54 ----------------------------------------------------- 1 file changed, 54 deletions(-) delete mode 100644 GLOSSARY.md diff --git a/GLOSSARY.md b/GLOSSARY.md deleted file mode 100644 index 40035f4..0000000 --- a/GLOSSARY.md +++ /dev/null @@ -1,54 +0,0 @@ -# Web Dev & Design Bot - -A Discord bot for the Web Dev & Design server. This glossary covers the language of the new-member onboarding flow. - -## Onboarding - -**Onboarding**: -The bot's own, custom flow that moves a new member from limited access to full access through a series of roles. -_Avoid_: Discord onboarding (that is a different thing), welcome flow, verification - -**Discord onboarding**: -Discord's built-in question flow shown when someone joins, in which the member picks a role such as Developer or Designer. -_Avoid_: Onboarding (reserved for the bot's flow) - -**Everyone**: -A member holding no onboarding role, for example because they skipped Discord onboarding. They can only write in lounge and can read a few channels, including message mods. -_Avoid_: Roleless member, guest - -**Start role** (`onboarding:start`): -The role a member receives from Discord onboarding. It unlocks write access to the first set of channels and is the entry point of Onboarding. - -**7-day role** (`onboarding:7day`): -Granted once a member has been in the server for 7 days. - -**Messages role** (`onboarding:messages`): -Granted once a member's counted messages reach 400 characters in total. - -**Finished role** (`onboarding:finished`): -Granted when a member holds both the 7-day role and the messages role. It unlocks the rest of the server. The 7-day and messages roles are removed when it is granted. - -**Dormant role** (`onboarding:dormant`): -Granted to a member who has not reached the finished role 3 months after joining. It strips their onboarding progress and signals that they must take action to return. - -**Rectification**: -Leaving dormancy, either by the member pressing the button in the dormant channel or by a moderator using a command. It removes the dormant role and restarts the 7-day clock, the message count and the 3-month deadline from that moment. - -**Restart anchor**: -The moment from which a member's Onboarding clocks run: their join date, or their latest rectification if that is later. - -**Dormant channel**: -The one channel a dormant member can read besides message mods. It explains that their account is dormant and holds the button for rectification. - -**Counted message**: -A message that contributes characters toward the messages role. Messages sent before the start role count; messages deleted or caught by spam detection do not. Only the text counts, stripped of whitespace, markdown formatting and links; attachments add nothing. - -**Exempt member**: -A bot or moderator. They never receive onboarding roles and never go dormant. - -**Legacy member**: -A member who joined before Onboarding launched. They skip the flow and receive the start, finished and legacy roles the first time the bot sees them post. -_Avoid_: Grandfathered member (gendered term) - -**Legacy role** (`onboarding:legacy`): -A permanent marker that tells moderators a member was given the finished role by being a legacy member and did not earn it. From 9a9777fc58dea003f854d7da5e3014424a697a2c Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Fri, 6 Nov 2026 20:41:05 +0100 Subject: [PATCH 21/30] chore: update gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3247662..c31bcbf 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,5 @@ src/generated # Database files *.db *.sqlite -*.sqlite3 \ No newline at end of file +*.sqlite3 +*.ai-scratchpad.md From 7ba0310156f26506e099ea039cbfd3bcc5fa8423 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Fri, 6 Nov 2026 20:56:59 +0100 Subject: [PATCH 22/30] feat: clarify atachments and rule writing --- .../0001-onboarding-without-guild-members-intent.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.md b/docs/adr/0001-onboarding-without-guild-members-intent.md index e675589..6a1fdaf 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.md @@ -8,7 +8,7 @@ Onboarding exists to serve these goals, in this order of weight: 1. **Protect regulars' attention.** Keep newcomers out of general chat and the rest of the server until they have had time to settle in. Newcomers cannot create showcases, because that goes through a bot button in a channel they cannot reach. They can comment on showcases and take part in the help forum from the start. 2. **Server comprehension.** The 7-day wait gives newcomers time to read the rules and see how good questions are asked before they reach the whole server. To get the rules role, a newcomer must also type out the rules exactly, from an image, in the type-the-rules channel. The image stops copy and paste, so they have to read the text as they type. It checks effort, not understanding. -3. **Spam and raids.** Fresh accounts have limited reach while trust is unknown. Members who have not finished cannot attach files, which blocks file-based spam payloads. +3. **Spam and raids.** Fresh accounts have limited reach while trust is unknown. Members who have not finished cannot attach files outside the help forum, which blocks file-based spam payloads. 4. **Drive-by accounts.** People who join, post once and leave should not cost moderators time. 5. **Limit hijacked dormant accounts.** An account that has shown no activity for months can later be compromised and used to post scam messages. A member who has not finished Onboarding and has no activity for 3 months loses the ability to write. Activity means writing a message anywhere, creating a forum post, or joining a voice channel. Finished members are never made dormant, so this goal does not cover them. 6. **Encourage participation.** Newcomers get a friendly place to take part straight away (the lounge and a few topic channels) and a path to the rest. @@ -46,15 +46,16 @@ The purpose is to let newcomers take part in a few friendly channels straight aw - **Activity** is writing a message anywhere (a forum post counts), or joining a voice channel. The bot cannot tell whether someone speaks, only that they joined. It stores the time of the member's last activity, and updates it only when the stored time is older than a set interval, because dormancy is measured in months. A new member's last activity starts as their join date. - Dormancy runs from last activity: 3 months with none, for anyone without the finished role, including members who never held the start role. Finished members are never made dormant. - When a dormant member writes, their message is deleted immediately, they receive a 10-minute timeout, and the bot creates a private thread in that channel, adds the member, and posts an explanation with a reactivate button and the text of the deleted message, so the member can repost it once reactivated. This **soft block** approach ensures the user learns about their dormant status at the moment of attempt and is given a clear, actionable path forward without having to search the server. The timeout prevents spam and is removed when the member reactivates. -- A member who has no start role (for example, skipped Discord onboarding) stays limited to the lounge until they complete Discord onboarding themselves. They are not nudged. +- A member who has no start role (for example, skipped Discord onboarding) stays limited to the lounge until they complete Discord onboarding themselves. They are not nudged about Discord onboarding. - Finishing needs the start role. A member could meet the 7-day, 3-days and rules conditions without it, but the finished role waits until Discord onboarding has been completed. -- The **rules check** replaces the quiz Discord onboarding used to show. In the type-the-rules channel, a new channel Everyone can write in, a pinned message tells the member what to do and the rules are shown as an image and the member types them out exactly as written, not in their own words. The bot compares the message with the rules text, ignoring case and extra whitespace; every word must match. A match grants the rules role. The message is deleted afterwards, whether it passed or failed, so the answer is not left for others to copy. The rules role is independent of the start role: a member can hold either first. -- The rules image carries the full rules text as its alt text, so screen reader users can complete the check. Members who cannot use the image for another reason are told in the type-the-rules channel to message mods, and a moderator can grant the rules role with a command. Alt text is easy to extract, so the check is friction, not a security control. -- Members who have not finished cannot attach files. Moderators set this in Discord by denying Attach Files to everyone except the finished role. The bot does nothing for it, in line with the roles-only rule below. Links and embeds are not restricted. This serves goal 3 only. +- The **rules check** replaces the quiz Discord onboarding used to show. In the type-the-rules channel, a new channel Everyone can write in, a pinned message tells the member what to do and the rules are shown as an image and the member types them out exactly as written, not in their own words. The bot compares the message with the rules text, ignoring case and extra whitespace; every word must match. A match grants the rules role. The message is deleted afterwards, whether it passed or failed, so the answer is not left for others to copy. The bot replies to say whether the attempt was correct or wrong, and that reply is deleted after 1 minute. The rules role is independent of the start role: a member can hold either first. +- The rules image carries the full rules text as its alt text, so screen reader users can complete the check. Members who cannot use the image for another reason are told in the type-the-rules channel to message mods, and a moderator can grant the rules role with a command. Alt text is easy to extract, so the check is friction, not a security control. The image URL and the rules text the bot compares against are constants a human maintains by hand. Nothing checks that they match. +- A member without the rules role is pinged in the type-the-rules channel when they join, each time their last activity is updated, and each time they are granted a new onboarding role. Each ping message is deleted after 2 minutes. The pings stop once the rules role is granted. +- Members who have not finished cannot attach files, except in the help forum, where images are allowed. Moderators set this in Discord by denying Attach Files to everyone except the finished role, outside the help forum. The bot does nothing for it, in line with the roles-only rule below. Links and embeds are not restricted. This serves goal 3 only. - Activity on a day is recorded once per calendar day, regardless of how many messages are sent. Activity means writing a message anywhere (a forum post counts), or joining a voice channel. A member must record activity on 3 distinct days to earn the 3-days role. - **Rectification** leaves dormancy. A moderator runs `/onboarding reactivate @member`, or the member presses the button in the dormant channel. Both remove the dormant role, set a new restart anchor and set last activity to now. The day count is already empty, because it is cleared when the member becomes dormant, and activity from a member holding the dormant role is not counted. A returning member starts over on purpose, with another 7 days, 3 days of activity and the rules check: the point is to show they are present again. - The 3-days role is held only while the activity-day count stays at or above 3. If a member's activity days are deleted or reset, the role is removed. -- A member who leaves keeps their Onboarding progress for a grace period of 7 days, counted from when the daily job first notices they are gone. If they rejoin within it, their activity-day count is as it was, however soon after leaving they return. The time away does not count toward the 7 days or the dormancy deadline: the restart anchor and last activity move forward by the time away, measured from when the daily job noticed the leave until the rejoin. After the grace period the progress is deleted and they start Onboarding again. Members who had already finished always start again, because their record is gone and Discord removes their roles when they leave. Activity-day records are only deleted in two cases: their onboarding record is deleted (they finished, or the grace period after leaving ran out), or they become dormant. +- A member who leaves keeps their Onboarding progress for a grace period of 7 days, counted from when the daily job first notices they are gone. If they rejoin within it, their activity-day count is as it was, however soon after leaving they return, and the rules role is given back because the Onboarding record also stores that they passed the rules check. The time away does not count toward the 7 days or the dormancy deadline: the restart anchor and last activity move forward by the time away, measured from when the daily job noticed the leave until the rejoin. After the grace period the progress is deleted and they start Onboarding again. Members who had already finished always start again, because their record is gone and Discord removes their roles when they leave. Activity-day records are only deleted in two cases: their onboarding record is deleted (they finished, or the grace period after leaving ran out), or they become dormant. - Bots and moderators (**exempt members**) are never touched. - Which channels each role can see or write in is configured by moderators in Discord. The bot only adds and removes roles. It never edits channel permission overwrites. From f8045d96770f09ccfbd6e1daad128cc25ccab5fc Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Fri, 6 Nov 2026 21:10:32 +0100 Subject: [PATCH 23/30] feat: add charts for the business process --- ...onboarding-without-guild-members-intent.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.md b/docs/adr/0001-onboarding-without-guild-members-intent.md index 6a1fdaf..83a9d76 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.md @@ -59,6 +59,56 @@ The purpose is to let newcomers take part in a few friendly channels straight aw - Bots and moderators (**exempt members**) are never touched. - Which channels each role can see or write in is configured by moderators in Discord. The bot only adds and removes roles. It never edits channel permission overwrites. +### The flow at a glance + +```mermaid +flowchart TD + Join([Member joins]) --> Progress + Join -. "joined before launch,
first seen posting" .-> Legacy["Legacy member
start + finished + legacy roles"] + Legacy --> Finished + + subgraph Progress["In progress: four independent milestones"] + Start["Start role
Discord onboarding"] + Rules["Rules role
type the rules exactly
in type-the-rules"] + Seven["7-day role
7 days after the restart anchor"] + Three["3-days role
activity on 3 different days"] + end + + Progress --> All{"All four held?"} + All -- yes --> Finished(["Finished
full server, permanent
milestone roles removed"]) + Progress -- "3 months without activity" --> Dormant["Dormant
can read, writes are deleted
rules, 7-day and 3-days roles removed"] + Dormant -- "reactivate button or
/onboarding reactivate
(start role kept, new restart anchor)" --> Progress +``` + +### A newcomer's journey + +The scores are a rough 1 to 5 guess at how the step feels to the member, where 1 is frustrating and 5 is pleasant. They mark where the design asks the most of people. + +```mermaid +journey + title A newcomer's path to the full server + section Joining + Join the server: 3: Member + Choose Developer or Designer in Discord onboarding: 4: Member + Get pinged in type-the-rules: 3: Member, Bot + section Rules check + Type the rules from the image: 2: Member + Get told if it was correct or wrong: 3: Member, Bot + Receive the rules role: 4: Member, Bot + section Settling in + Ask a question in the help forum: 4: Member + Chat in the lounge and topic channels: 4: Member + Hit the file attachment block outside the help forum: 2: Member + section Earning trust + Wait out the 7 days: 3: Member + Be active on 3 different days: 4: Member + Receive the finished role and the full server: 5: Member, Bot + section Returning after 3 months away + Write a message and see it deleted: 1: Member, Bot + Read the explanation in a private thread: 3: Member, Bot + Press reactivate and repost the saved message: 4: Member, Bot +``` + ## 2. The decision Onboarding is implemented as a role reconciler that works **with the GuildMembers privileged intent**. It relies on proper Discord events for joins, leaves, and role updates, along with a tracking table for activity days and dormancy. From f24ff2f3962e94e5a7ccc7912fe8cff16ebb7edb Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sat, 7 Nov 2026 12:21:24 +0100 Subject: [PATCH 24/30] feat: clarified the flow more --- docs/adr/0001-onboarding-without-guild-members-intent.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.md b/docs/adr/0001-onboarding-without-guild-members-intent.md index 83a9d76..aab00b9 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.md @@ -6,12 +6,12 @@ Regulars have said they are tired of dealing with low-quality content from new j Onboarding exists to serve these goals, in this order of weight: -1. **Protect regulars' attention.** Keep newcomers out of general chat and the rest of the server until they have had time to settle in. Newcomers cannot create showcases, because that goes through a bot button in a channel they cannot reach. They can comment on showcases and take part in the help forum from the start. +1. **Protect regulars' attention.** Keep newcomers out of general chat and the rest of the server until they have had time to settle in. Channels that newcomers cannot access are **hidden entirely** rather than shown as read-only; showing read-only channels would invite questions in write-accessible channels about why they cannot participate, creating the very distraction this goal seeks to avoid. Newcomers cannot create showcases, because that goes through a bot button in a channel they cannot reach. They can comment on showcases and take part in the help forum from the start. 2. **Server comprehension.** The 7-day wait gives newcomers time to read the rules and see how good questions are asked before they reach the whole server. To get the rules role, a newcomer must also type out the rules exactly, from an image, in the type-the-rules channel. The image stops copy and paste, so they have to read the text as they type. It checks effort, not understanding. 3. **Spam and raids.** Fresh accounts have limited reach while trust is unknown. Members who have not finished cannot attach files outside the help forum, which blocks file-based spam payloads. 4. **Drive-by accounts.** People who join, post once and leave should not cost moderators time. 5. **Limit hijacked dormant accounts.** An account that has shown no activity for months can later be compromised and used to post scam messages. A member who has not finished Onboarding and has no activity for 3 months loses the ability to write. Activity means writing a message anywhere, creating a forum post, or joining a voice channel. Finished members are never made dormant, so this goal does not cover them. -6. **Encourage participation.** Newcomers get a friendly place to take part straight away (the lounge and a few topic channels) and a path to the rest. +6. **Encourage participation.** Newcomers get a friendly place to take part straight away (the lounge and a few topic channels) and a path to the rest. The start role intentionally grants access to multiple channels after Discord onboarding, not just one or two, because newcomers need enough spaces to chat and be welcomed into the community. Without access to several active channels, newcomers may feel the server has little to offer and leave before completing onboarding. Reading is not restricted by this flow. Members who read but rarely post keep their access to read the server. Onboarding gates where a member can write, not whether they may be here. A dormant member can read but cannot write until they reactivate. @@ -25,7 +25,7 @@ Every gate in section 1 must trace back to one of these goals. A gate that serve New members of the Web Dev & Design server start with very little access and earn the rest of the server over time. **Onboarding** is the bot's own flow for this. It is separate from **Discord onboarding**, the question Discord shows at join, where the member picks a role such as Developer or Designer. -The purpose is to let newcomers take part in a few friendly channels straight away, while the rest of the server is held back until they have shown they are present and engaged. Members who never finish lose the ability to write again, though they can still read. +The purpose is to let newcomers take part in a few friendly channels straight away, while the rest of the server is held back until they have shown they are present and engaged. Access expands in **stages**: initial access to lounge and type-the-rules, broader access after Discord onboarding (start role), and full server access after completing all four milestones (finished role). Members who never finish lose the ability to write again, though they can still read. ### The journey @@ -36,7 +36,7 @@ The purpose is to let newcomers take part in a few friendly channels straight aw | **7-day role** | 7 days after the **restart anchor** | Milestone only | | **3-days role** | Activity recorded on 3 different days since the restart anchor | Milestone only | | **Rules role** | Passed the **rules check** in type-the-rules | Milestone only | -| **Finished role** | Holds start, rules, 7-day and 3-days | Full server access, including attaching files. Permanent. The rules, 7-day and 3-days roles are removed | +| **Finished role** | Holds start, rules, 7-day and 3-days | Full server access, including all remaining channels and the ability to attach files. Permanent. The rules, 7-day and 3-days roles are removed | | **Dormant role** | 3 months without **activity**, without the finished role | Keeps its channel access, but every message they write is deleted and they are told how to reactivate. The rules, 7-day and 3-days roles are removed; the start role stays | | **Legacy member** | Joined before launch, first seen posting | Receives start, finished and **legacy role** at once | From 013ccb4fec9f04231ced2d2f3afba448c81e15e3 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sat, 7 Nov 2026 12:39:23 +0100 Subject: [PATCH 25/30] feat: be more concrete with some of the points --- ...onboarding-without-guild-members-intent.md | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.md b/docs/adr/0001-onboarding-without-guild-members-intent.md index aab00b9..01ba3b6 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.md @@ -23,26 +23,34 @@ Every gate in section 1 must trace back to one of these goals. A gate that serve ## 1. What Onboarding is -New members of the Web Dev & Design server start with very little access and earn the rest of the server over time. **Onboarding** is the bot's own flow for this. It is separate from **Discord onboarding**, the question Discord shows at join, where the member picks a role such as Developer or Designer. +Regulars are tired of low-effort posts from newcomers. **Onboarding** solves this by restricting new members' access until they prove they are trustworthy. It is a bot-managed role progression, separate from Discord's built-in onboarding (which only assigns a role like Developer or Designer). -The purpose is to let newcomers take part in a few friendly channels straight away, while the rest of the server is held back until they have shown they are present and engaged. Access expands in **stages**: initial access to lounge and type-the-rules, broader access after Discord onboarding (start role), and full server access after completing all four milestones (finished role). Members who never finish lose the ability to write again, though they can still read. +The bot grants access in stages by assigning Discord roles, which control which channels a member can see and write in. Newcomers must complete four milestones to gain full server access: + +1. Complete Discord onboarding (start role) +2. Wait 7 days from the **restart anchor** +3. Be active on 3 different days +4. Pass a rules check + +Members who do not finish and then go 3 months without activity become **dormant**: they can read but cannot write. They can reactivate at any time. + +> **Restart anchor:** The later of the member's join date or their latest rectification (if they were dormant and reactivated). The 7-day clock and activity-day count both run from this point. ### The journey -| Stage | How a member gets there | What they can do | -| ----------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Everyone** | Joined, but has no onboarding role (for example skipped Discord onboarding) | Write in lounge and the type-the-rules channel, read a few channels (announcement, rules, etc.), read message mods to open a ticket | -| **Start role** | Granted by Discord onboarding | Also write in help forum (create and reply), ai, gaming, real-life, frontend, design-ui-ux and backend-and-cloud, and comment on showcases. Showcases are created through a bot button in a channel this role cannot reach | -| **7-day role** | 7 days after the **restart anchor** | Milestone only | -| **3-days role** | Activity recorded on 3 different days since the restart anchor | Milestone only | -| **Rules role** | Passed the **rules check** in type-the-rules | Milestone only | -| **Finished role** | Holds start, rules, 7-day and 3-days | Full server access, including all remaining channels and the ability to attach files. Permanent. The rules, 7-day and 3-days roles are removed | -| **Dormant role** | 3 months without **activity**, without the finished role | Keeps its channel access, but every message they write is deleted and they are told how to reactivate. The rules, 7-day and 3-days roles are removed; the start role stays | -| **Legacy member** | Joined before launch, first seen posting | Receives start, finished and **legacy role** at once | +| **Role** | **How Earned** | **Grants Access To** | **Notes** | +| ------------ | ---------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| **Everyone** | Default state (no onboarding roles) | Lounge, type-the-rules, announcements, rules | Can open mod tickets | +| **Start** | Discord onboarding | Help forum, ai, gaming, real-life, frontend, design-ui-ux, backend-and-cloud | Can comment on showcases | +| **7-day** | 7 days after restart anchor | _None (milestone only)_ | Auto-granted | +| **3-days** | Activity on 3 distinct days | _None (milestone only)_ | Auto-granted | +| **Rules** | Type rules from image in #type-the-rules | _None (milestone only)_ | Bot checks message against rules text, deletes message, informs user of result, grants role if correct | +| **Finished** | Holds Start + 7-day + 3-days + Rules | All channels, can attach files | Permanent; milestone roles removed | +| **Dormant** | 3 months of inactivity (no Finished) | Read-only | Writes deleted, 10-min timeout, private thread with reactivate button | +| **Legacy** | Joined before launch | All channels | Receives Start + Finished + Legacy at once | ### Rules -- The restart anchor is the later of the member's join date and their latest rectification. The 7-day clock and the activity-day count run from it. - **Activity** is writing a message anywhere (a forum post counts), or joining a voice channel. The bot cannot tell whether someone speaks, only that they joined. It stores the time of the member's last activity, and updates it only when the stored time is older than a set interval, because dormancy is measured in months. A new member's last activity starts as their join date. - Dormancy runs from last activity: 3 months with none, for anyone without the finished role, including members who never held the start role. Finished members are never made dormant. - When a dormant member writes, their message is deleted immediately, they receive a 10-minute timeout, and the bot creates a private thread in that channel, adds the member, and posts an explanation with a reactivate button and the text of the deleted message, so the member can repost it once reactivated. This **soft block** approach ensures the user learns about their dormant status at the moment of attempt and is given a clear, actionable path forward without having to search the server. The timeout prevents spam and is removed when the member reactivates. From 65e3738bae112cf3b23614db6ea2d48aa3a03932 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sat, 7 Nov 2026 13:03:33 +0100 Subject: [PATCH 26/30] docs: clarify onboarding role structure and channel permissions --- ...onboarding-without-guild-members-intent.md | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.md b/docs/adr/0001-onboarding-without-guild-members-intent.md index 01ba3b6..db8fe35 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.md @@ -25,36 +25,33 @@ Every gate in section 1 must trace back to one of these goals. A gate that serve Regulars are tired of low-effort posts from newcomers. **Onboarding** solves this by restricting new members' access until they prove they are trustworthy. It is a bot-managed role progression, separate from Discord's built-in onboarding (which only assigns a role like Developer or Designer). -The bot grants access in stages by assigning Discord roles, which control which channels a member can see and write in. Newcomers must complete four milestones to gain full server access: +The bot assigns Discord roles to track progress. There are four **milestone roles** (Start, 7-day, 3-days, Rules) that a member must hold to receive the **Finished** role, which grants full server write access. Each onboarding role in Discord is prefixed with `onboarding:` (e.g., `onboarding:7-day`), but the prefix is omitted in this document for clarity. -1. Complete Discord onboarding (start role) -2. Wait 7 days from the **restart anchor** -3. Be active on 3 different days -4. Pass a rules check +New members without the Start role (**Everyone** state) have limited write access to #lounge and #type-the-rules, with broad read access to most channels including #message-mods which allows them to open mod tickets via #staff-support-. After completing Discord onboarding, they receive the Start role and gain write access to topic channels. The Finished role unlocks write access to all remaining channels. -Members who do not finish and then go 3 months without activity become **dormant**: they can read but cannot write. They can reactivate at any time. +Members without the Finished role who go 3 months without activity become **dormant**. Dormant members retain their Discord permissions but the bot enforces a soft-block: any message they write is deleted, they receive a 10-minute timeout, and a private thread explains how to reactivate. They can reactivate at any time. > **Restart anchor:** The later of the member's join date or their latest rectification (if they were dormant and reactivated). The 7-day clock and activity-day count both run from this point. ### The journey -| **Role** | **How Earned** | **Grants Access To** | **Notes** | -| ------------ | ---------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| **Everyone** | Default state (no onboarding roles) | Lounge, type-the-rules, announcements, rules | Can open mod tickets | -| **Start** | Discord onboarding | Help forum, ai, gaming, real-life, frontend, design-ui-ux, backend-and-cloud | Can comment on showcases | -| **7-day** | 7 days after restart anchor | _None (milestone only)_ | Auto-granted | -| **3-days** | Activity on 3 distinct days | _None (milestone only)_ | Auto-granted | -| **Rules** | Type rules from image in #type-the-rules | _None (milestone only)_ | Bot checks message against rules text, deletes message, informs user of result, grants role if correct | -| **Finished** | Holds Start + 7-day + 3-days + Rules | All channels, can attach files | Permanent; milestone roles removed | -| **Dormant** | 3 months of inactivity (no Finished) | Read-only | Writes deleted, 10-min timeout, private thread with reactivate button | -| **Legacy** | Joined before launch | All channels | Receives Start + Finished + Legacy at once | +| **Role** | **How Earned** | **Write Access** | **Read Access** | **Notes** | +| ------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| **Everyone** | Default state (no onboarding roles) | #lounge, #type-the-rules, #staff-support- | #rules, #announcement, #welcome, #introduction, #message-mods, #how-to-get-help, #guides, #help, #showcase, #showcase-spotlight, #ai, #gaming, #real-life, #frontend, #design-ui-ux, #backend-and-cloud | Can open mod tickets via #message-mods | +| **Start** | Discord onboarding | #introduction, #help, #showcase, #ai, #gaming, #real-life, #frontend, #design-ui-ux, #backend-and-cloud | Inherits from Everyone | Can comment on showcases; showcases created via bot button in restricted channel | +| **7-day** | 7 days after restart anchor | Inherit | Inherit | Milestone role; auto-granted | +| **3-days** | Activity on 3 distinct days | Inherit | Inherit | Milestone role; auto-granted | +| **Rules** | Type rules from image in #type-the-rules | Inherit | Inherit | Milestone role; bot checks message, deletes it, informs user, grants role if correct | +| **Finished** | Holds Start + 7-day + 3-days + Rules | All remaining channels | All channels | Permanent; milestone roles removed | +| **Dormant** | 3 months of inactivity (no Finished) | Inherit (soft-blocked by bot) | Inherit | Writes deleted, 10-min timeout, private thread with reactivate button | +| **Legacy** | Joined before launch, first seen posting | All channels | All channels | Receives Start + Finished + Legacy at once | ### Rules - **Activity** is writing a message anywhere (a forum post counts), or joining a voice channel. The bot cannot tell whether someone speaks, only that they joined. It stores the time of the member's last activity, and updates it only when the stored time is older than a set interval, because dormancy is measured in months. A new member's last activity starts as their join date. - Dormancy runs from last activity: 3 months with none, for anyone without the finished role, including members who never held the start role. Finished members are never made dormant. - When a dormant member writes, their message is deleted immediately, they receive a 10-minute timeout, and the bot creates a private thread in that channel, adds the member, and posts an explanation with a reactivate button and the text of the deleted message, so the member can repost it once reactivated. This **soft block** approach ensures the user learns about their dormant status at the moment of attempt and is given a clear, actionable path forward without having to search the server. The timeout prevents spam and is removed when the member reactivates. -- A member who has no start role (for example, skipped Discord onboarding) stays limited to the lounge until they complete Discord onboarding themselves. They are not nudged about Discord onboarding. +- A member who has no start role (for example, skipped Discord onboarding) has write access to #lounge and #type-the-rules, with broad read access to most channels including #message-mods which allows them to open mod tickets to gain write-access in #staff-support-, until they complete Discord onboarding to receive the Start role. They are not nudged about Discord onboarding. - Finishing needs the start role. A member could meet the 7-day, 3-days and rules conditions without it, but the finished role waits until Discord onboarding has been completed. - The **rules check** replaces the quiz Discord onboarding used to show. In the type-the-rules channel, a new channel Everyone can write in, a pinned message tells the member what to do and the rules are shown as an image and the member types them out exactly as written, not in their own words. The bot compares the message with the rules text, ignoring case and extra whitespace; every word must match. A match grants the rules role. The message is deleted afterwards, whether it passed or failed, so the answer is not left for others to copy. The bot replies to say whether the attempt was correct or wrong, and that reply is deleted after 1 minute. The rules role is independent of the start role: a member can hold either first. - The rules image carries the full rules text as its alt text, so screen reader users can complete the check. Members who cannot use the image for another reason are told in the type-the-rules channel to message mods, and a moderator can grant the rules role with a command. Alt text is easy to extract, so the check is friction, not a security control. The image URL and the rules text the bot compares against are constants a human maintains by hand. Nothing checks that they match. From 7e56642dc1590a24fc5d0134a5da4b13427658d8 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sat, 7 Nov 2026 13:09:27 +0100 Subject: [PATCH 27/30] feat: clean up wording --- ...onboarding-without-guild-members-intent.md | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.md b/docs/adr/0001-onboarding-without-guild-members-intent.md index db8fe35..371dabb 100644 --- a/docs/adr/0001-onboarding-without-guild-members-intent.md +++ b/docs/adr/0001-onboarding-without-guild-members-intent.md @@ -27,7 +27,7 @@ Regulars are tired of low-effort posts from newcomers. **Onboarding** solves thi The bot assigns Discord roles to track progress. There are four **milestone roles** (Start, 7-day, 3-days, Rules) that a member must hold to receive the **Finished** role, which grants full server write access. Each onboarding role in Discord is prefixed with `onboarding:` (e.g., `onboarding:7-day`), but the prefix is omitted in this document for clarity. -New members without the Start role (**Everyone** state) have limited write access to #lounge and #type-the-rules, with broad read access to most channels including #message-mods which allows them to open mod tickets via #staff-support-. After completing Discord onboarding, they receive the Start role and gain write access to topic channels. The Finished role unlocks write access to all remaining channels. +New members without the Start role (**Everyone** state) have write access to #lounge and #type-the-rules, with broad read access to most channels. Read access to #message-mods enables creating #staff-support-{username} for mod tickets. After completing Discord onboarding, they receive the Start role and gain write access to topic channels. The Finished role unlocks write access to all remaining channels. Members without the Finished role who go 3 months without activity become **dormant**. Dormant members retain their Discord permissions but the bot enforces a soft-block: any message they write is deleted, they receive a 10-minute timeout, and a private thread explains how to reactivate. They can reactivate at any time. @@ -35,23 +35,23 @@ Members without the Finished role who go 3 months without activity become **dorm ### The journey -| **Role** | **How Earned** | **Write Access** | **Read Access** | **Notes** | -| ------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | -| **Everyone** | Default state (no onboarding roles) | #lounge, #type-the-rules, #staff-support- | #rules, #announcement, #welcome, #introduction, #message-mods, #how-to-get-help, #guides, #help, #showcase, #showcase-spotlight, #ai, #gaming, #real-life, #frontend, #design-ui-ux, #backend-and-cloud | Can open mod tickets via #message-mods | -| **Start** | Discord onboarding | #introduction, #help, #showcase, #ai, #gaming, #real-life, #frontend, #design-ui-ux, #backend-and-cloud | Inherits from Everyone | Can comment on showcases; showcases created via bot button in restricted channel | -| **7-day** | 7 days after restart anchor | Inherit | Inherit | Milestone role; auto-granted | -| **3-days** | Activity on 3 distinct days | Inherit | Inherit | Milestone role; auto-granted | -| **Rules** | Type rules from image in #type-the-rules | Inherit | Inherit | Milestone role; bot checks message, deletes it, informs user, grants role if correct | -| **Finished** | Holds Start + 7-day + 3-days + Rules | All remaining channels | All channels | Permanent; milestone roles removed | -| **Dormant** | 3 months of inactivity (no Finished) | Inherit (soft-blocked by bot) | Inherit | Writes deleted, 10-min timeout, private thread with reactivate button | -| **Legacy** | Joined before launch, first seen posting | All channels | All channels | Receives Start + Finished + Legacy at once | +| **Role** | **How Earned** | **Write Access** | **Read Access** | **Notes** | +| ------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| **Everyone** | Default state (no onboarding roles) | #lounge, #type-the-rules | #rules, #announcement, #welcome, #introduction, #message-mods, #how-to-get-help, #guides, #help, #showcase, #showcase-spotlight, #ai, #gaming, #real-life, #frontend, #design-ui-ux, #backend-and-cloud | Can open mod tickets via #staff-support-{username} (enabled by read access to #message-mods) | +| **Start** | Discord onboarding | #introduction, #help, #showcase, #ai, #gaming, #real-life, #frontend, #design-ui-ux, #backend-and-cloud | Inherit | Can comment on showcases; showcases created via bot button in restricted channel | +| **7-day** | 7 days after restart anchor | Inherit | Inherit | Milestone role; auto-granted | +| **3-days** | Activity on 3 distinct days | Inherit | Inherit | Milestone role; auto-granted | +| **Rules** | Type rules from image in #type-the-rules | Inherit | Inherit | Milestone role; bot checks message, deletes it, informs user, grants role if correct | +| **Finished** | Holds Start + 7-day + 3-days + Rules | All remaining channels | All channels | Permanent; milestone roles removed | +| **Dormant** | 3 months of inactivity (no Finished) | Inherit (soft-blocked by bot) | Inherit | Writes deleted, 10-min timeout, private thread with reactivate button | +| **Legacy** | Joined before launch, first seen posting | All channels | All channels | Receives Start + Finished + Legacy at once | ### Rules - **Activity** is writing a message anywhere (a forum post counts), or joining a voice channel. The bot cannot tell whether someone speaks, only that they joined. It stores the time of the member's last activity, and updates it only when the stored time is older than a set interval, because dormancy is measured in months. A new member's last activity starts as their join date. - Dormancy runs from last activity: 3 months with none, for anyone without the finished role, including members who never held the start role. Finished members are never made dormant. - When a dormant member writes, their message is deleted immediately, they receive a 10-minute timeout, and the bot creates a private thread in that channel, adds the member, and posts an explanation with a reactivate button and the text of the deleted message, so the member can repost it once reactivated. This **soft block** approach ensures the user learns about their dormant status at the moment of attempt and is given a clear, actionable path forward without having to search the server. The timeout prevents spam and is removed when the member reactivates. -- A member who has no start role (for example, skipped Discord onboarding) has write access to #lounge and #type-the-rules, with broad read access to most channels including #message-mods which allows them to open mod tickets to gain write-access in #staff-support-, until they complete Discord onboarding to receive the Start role. They are not nudged about Discord onboarding. +- A member who has no start role (for example, skipped Discord onboarding) has write access to #lounge and #type-the-rules, with broad read access to most channels. Read access to #message-mods enables creating #staff-support-{username} for mod tickets, until they complete Discord onboarding to receive the Start role. They are not nudged about Discord onboarding. - Finishing needs the start role. A member could meet the 7-day, 3-days and rules conditions without it, but the finished role waits until Discord onboarding has been completed. - The **rules check** replaces the quiz Discord onboarding used to show. In the type-the-rules channel, a new channel Everyone can write in, a pinned message tells the member what to do and the rules are shown as an image and the member types them out exactly as written, not in their own words. The bot compares the message with the rules text, ignoring case and extra whitespace; every word must match. A match grants the rules role. The message is deleted afterwards, whether it passed or failed, so the answer is not left for others to copy. The bot replies to say whether the attempt was correct or wrong, and that reply is deleted after 1 minute. The rules role is independent of the start role: a member can hold either first. - The rules image carries the full rules text as its alt text, so screen reader users can complete the check. Members who cannot use the image for another reason are told in the type-the-rules channel to message mods, and a moderator can grant the rules role with a command. Alt text is easy to extract, so the check is friction, not a security control. The image URL and the rules text the bot compares against are constants a human maintains by hand. Nothing checks that they match. From 02a759aab9e52b0b4acd30043668cc18fd3631c3 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sat, 7 Nov 2026 13:14:20 +0100 Subject: [PATCH 28/30] chore: delete claude scratchpad --- ...ing-without-guild-members-intent.claude.md | 61 ------------------- 1 file changed, 61 deletions(-) delete mode 100644 docs/adr/0001-onboarding-without-guild-members-intent.claude.md diff --git a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md b/docs/adr/0001-onboarding-without-guild-members-intent.claude.md deleted file mode 100644 index 4c2b5f9..0000000 --- a/docs/adr/0001-onboarding-without-guild-members-intent.claude.md +++ /dev/null @@ -1,61 +0,0 @@ -# Scratchpad: ADR 0001 (onboarding without GuildMembers intent) - -Main doc: `0001-onboarding-without-guild-members-intent.md` (human controls it; Claude never commits it). -Workflow: iterative authoring skill. Ask, don't guess. Prefer AskUserQuestion (human asked for this). Commit only this scratchpad, with descriptive messages. - -## Where we are (end of session 1) - -Business half is DRAFTED in the main doc and the human finalized it, except one last read-through they have not explicitly done: -- "Problem and goals" (goals 1, 2, 5 and the gates paragraph reworded) and "1. What Onboarding is" (journey table + rules). -- Sections 2-5 (decision, technical design, considered options, to-verify/open decision) are UNTOUCHED and now STALE: they still describe anchor-based dormancy, Option A/B as undecided, no lastActivity. -- The human has already committed the main doc edits themselves (17e95a4 "clarify goal dormancy", 06d6704 "7 day gives users also enough time to get a read on the server and its community"). The working tree is clean for that file. They may have tweaked wording, so RE-READ the main doc at the start of the next session and treat it as the source of truth over this file. `pnpm exec oxfmt` fails on Node v20.17 (needs newer, see .nvmrc), so the widened table rows may still be unformatted; check `pnpm fmt:check` on a supported Node. -- Note: AGENTS.md and docs/agents/ are also uncommitted in the working tree and are not ours. - -## Decisions made (business, reasoning kept for context) - -- No status/front matter in the ADR. -- Goal 1 = protect GENERAL CHAT. Human: low-effort or low-quality messages should not pollute the main area where people hang out; newcomers (no roles) only have the lounge. Start role adds help forum (create+reply), ai, gaming, real life, frontend, design, backend, and showcase COMMENTS only. Showcase creation goes through a bot button in a channel the start role cannot reach. Finished = full server. -- Dormancy is ACTIVITY-based: 3 months without activity, unfinished members only (finished are permanent and never dormant; human chose this over covering everyone, so goal 5 does not cover finished accounts). Activity = message anywhere (forum post counts) or joining voice (bot cannot detect talking). lastActivity stored, writes throttled, starts at join date, rectification sets it to now. -- Dormant behaviour = Option B (delete and notify), chosen over my recommendation of Option A (permission deny). Known cost: message briefly visible, weakens goal 5, bot needs Manage Messages. Start role stays on dormancy. -- Messages from members holding the dormant role are not counted toward the 400. -- Rejoin within the 7-day grace: time away does NOT count; anchor and lastActivity shift forward by away time (measured from when the daily job noticed the leave). -- Dormant notice: member told privately and immediately, with a way to reactivate. Reasoning (human): a user who cannot write somewhere churns rather than investigates; this keeps them in the server and makes reactivating easy. -- No start role (skipped Discord onboarding): stays lounge-only, no nudge. -- Gates: 7 days = friction for drive-bys. 400 chars = rough engagement sign and friction; no quality judgement. Goal 2 reworded: the 7 days give time to read, nothing checks it. -- Legacy rule confirmed: start + finished + legacy role on first post. - -## Open items - -Business, for the TEAM to decide (not the human alone): dormant notice in a private thread (member, bot, mods only) vs a reply in the channel itself. Written into the ADR rules as an open point. - -Business, optional: human reads the business half once more and signs off. - -Technical (PARKED by human: "business definition first, technical details later"). Suggested order when resumed: -1. B1 Intent rationale. The bot already depends on MessageContent (privileged, `src/index.ts:17`) and the 400-char mechanic needs it, so "approval could be refused/revoked" applies to it equally. No evidence anyone applied for GuildMembers and was refused. The "10,000 users" claim is unverified. Ask: has GuildMembers been applied for, how big is the server, what is the real reason to avoid it? Revisit "Why not the intent" and "Considered options". -2. Rewrite the "open decision: making dormant members read-only" section: Option B chosen; keep A in Considered options. "Telling the member" (DM vs ephemeral vs private thread) ties to the team decision above. -3. Update section 3 for the new business rules: Data (lastActivity column; maybe left-time already exists), evaluation order step 5 (dormancy by lastActivity, skip recording while dormant), join-message entry point ("changes nothing else" now shifts anchor + lastActivity), message entry point (update lastActivity, throttle), new VoiceStateUpdate entry point, Option B handler (delete dormant members' messages, Manage Messages, exclude the dormant channel), daily job wording, entry-point step 3 legacy check. -4. Technical gaps from the review: - - T9 No `Partials` in `src/index.ts`: discord.js does not emit messageDelete/messageUpdate for uncached messages, and messageDeleteBulk drops uncached ones. The ADR relies on an ID-only delete event. - - T10 Deletes/edits while the bot is offline are lost, counts drift forever; startup catch-up replays only join messages. - - T11 No estimate of tracked-member count N; daily job does N fetches; dormant rows never expire. - - T13 Concurrency argument against the JSON column is applied selectively; evaluate() is also read-modify-write, and step 5 deletes rows while inserts may race. - - T12 (mostly moot under Option B) read-only would need more than Send Messages deny. -5. L8 Launch date is listed as both a config constant and an env var. Decide which. -6. S14 Split the spec (journey, rules, schema, eval order, env vars) out of the ADR? Human deferred; edit in place first. -7. Minor: who accepted the dormancy best-effort gap (invisible members) and when does it become unacceptable; cite the source of "a daily job was the requirement". -8. Verify before building: forum post starter message arrives as MessageCreate; VoiceStateUpdate available (GuildVoiceStates already on); join messages visible in the system channel; single-member fetch works without the intent. - -## New suggestions from the human (end of session 1), NOT yet discussed or scoped - -S1. Prevent media attachments while Onboarding is not finished. -- My notes (unconfirmed): could be a Discord permission (deny Attach Files / Embed Links on the unfinished roles, set by moderators, which fits the rule "the bot only moves roles") or the bot deleting such messages (Manage Messages, briefly visible, like Option B). Journey rules currently say "Attachments add nothing" to the count, so wording changes too. -- Questions to ask: which stage (Everyone, start role, 7-day/messages milestone holders, dormant)? Attachments only, or also embeds/links? All channels or only some? Which goal does it serve (spam/raids, goal 3, and general-chat quality, goal 1)? Does a finished member keep full access? Does dormant need it? - -S2. Replace the Discord onboarding rules quiz (rules shown with 4 answers, 3 wrong and 1 right) with the newcomer writing the rules out in their own words in a channel. -- My notes (unconfirmed): this is a bigger change than it looks. Today Discord onboarding grants the start role and is the only gate for goal 2. Writing in a channel means someone or something must grant the start role afterwards, so the bot (or a moderator) would take over granting it. That would also remove a technical cost in the ADR ("roles granted outside the bot are noticed late"). It touches the "Everyone" stage, "Discord onboarding" in the glossary, goal 2 (the "nothing checks it" wording could become false), the "No start role" rule and the technical sections. -- Questions to ask: what exactly do they write (rules in own words, an introduction, both)? Who validates it: moderators, the bot (length/keywords), or nobody? Which channel, and can the Everyone stage write there (it is currently lounge-only)? Does Discord onboarding still exist for picking the Developer/Designer role? What is the grant mechanism: moderator button/command or automatic? Is spam in that channel a risk (goal 3)? -- Do this as its own business-level discussion BEFORE the technical sections, because it changes the journey. - -## Next session: how to start - -Read the main doc and this file, then ask (AskUserQuestion) which to do: (a) business sign-off read-through, (b) the two new suggestions S1 and S2 (business level, S2 may reshape the journey, so probably first), or (c) start the technical sections at item 1 (B1). Do not edit sections 2-5 without confirming scope. From 7bc1bdea837d050ff301ff8b18820f7898bc0809 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sun, 8 Nov 2026 14:25:41 +0100 Subject: [PATCH 29/30] feat: new onboarding plan --- docs/spec/onboarding.md | 167 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 docs/spec/onboarding.md diff --git a/docs/spec/onboarding.md b/docs/spec/onboarding.md new file mode 100644 index 0000000..c6c1700 --- /dev/null +++ b/docs/spec/onboarding.md @@ -0,0 +1,167 @@ +# Onboarding + +> DRAFT: Business Definition under review. Milestones are not yet defined. + +## 1. Business Definition + +### Purpose + +Onboarding keeps new members in a contained space while they settle in, and gives them a clear path to the rest of the server. + +### Problem + +Regulars are annoyed by a steady stream of newcomer noise: + +- simple, badly formed help requests +- "look at my project" drop-ins, often fully vibe coded +- new joiners entering random voice channels +- solicitation and spam + +### Goals + +1. **Protect regulars' attention.** Newcomers chat in a separate space and do not reach general chat or open voice channels at first. +2. **Give newcomers somewhere to belong.** They can talk from the first minute, and they have a visible way to progress. +3. **Reward participation.** Members who stay and contribute gain access and recognition. +4. **Limit spam and abuse.** New and long-dormant accounts have limited reach: no attachments, no open voice. + +### Audience + +- New members (read this to know how to progress). +- Regulars and moderators (read this to know what newcomers can and cannot do). +- Maintainers of the bot. + +### Success criteria + +- Regulars report less newcomer noise in general chat and voice. +- Newcomers still participate and a meaningful share progress past level 0. +- Moderators do not have to manage progression by hand. + +## 2. Business Concepts + +> DRAFT: Concepts under review. + +### Activity + +An **activity** is one day on which a member interacted with the server in a meaningful way: writing a message, creating a thread or forum post, or joining a voice channel. It does not matter how long the interaction lasted or how many times the member interacted that day. A day with any interaction counts as exactly one activity. + +Everything in this document that is measured in "activity" is measured in days. Days and months are always in UTC. + +### Levels + +A member's **level** says how much of the server they can reach. There are four levels, numbered 0 to 3. + +Levels are earned strictly in order and none can be skipped: 0, then 1, then 2, then 3. A member only moves up when the previous level has been granted, so two levels are never granted at the same time. The only ways down are from level 3 to level 2, and a forced reset to level 0. + +Which channels each level can see or write in is decided by moderators in Discord. The bot only assigns and removes the level roles. + +### Level 0: New joiner + +Every new member starts here. + +- Cannot access general chat. +- Cannot access voice channels, except 1-on-1 voice channels. +- Cannot add attachments. + +### Level 1: Agreed to the rules + +A member reaches level 1 by agreeing to the rules. The bot asks in a dedicated channel (name to be defined), and the member presses a button to agree. This is the only requirement. + +- Gains access to general chat and all voice channels. +- Can add attachments. + +#### Welcome message and tasks + +Right after a member agrees, the bot writes a message in the same channel, welcoming them to the server and telling them to go to #bot-commands and run the `/onboarding` command. + +The command replies privately to the member who ran it, with a **task list**. Tasks are suggestions that help a member join in. Unless a task is marked (required), it is optional. There are no rewards. Tasks exist to motivate people to take part. + +A task that the member has completed is shown as checked off. The starting tasks are: + +- Agree to the rules (required). +- Run `/onboarding` in #bot-commands. +- Introduce yourself in #introduction, using a template. +- Post a meme in #memes. +- Share a hobby you have in real life, in #real-life. +- Say hi to the community in #lounge. + +Any member can run `/onboarding`, at any level. + +The bot starts checking a member's tasks from the moment they join, and keeps checking at all times. A task is checked off when: + +- Agree to the rules: the member has pressed the agree button. +- Run `/onboarding` in #bot-commands: the member has run the command in #bot-commands. Running it in another channel does not count. +- Introduce yourself, share a hobby, say hi: the member has posted any message in that channel. +- Post a meme: the member has posted a message with an attachment in #memes. Level 0 members cannot add attachments, so they must reach level 1 first. + +More tasks can be added over time. + +### Level 2: Regular + +A member at level 1 reaches level 2 by being active on at least **12 days in each of the last 3 calendar months**. The 12 days must be reached in every one of the 3 months. A total of 36 days across 3 months is not enough, because those days could be packed into a stretch shorter than 3 months. + +- Gains access to the VIP area. + +Level 2 does not depend on later activity. Once earned, it is not lost for being less active. Members who held the "Regular" or "VC Regular" role before this system launched start at level 2. + +### Level 3: Highly active + +A member at level 2 reaches level 3 by being active on at least **21 days in each of the last 3 calendar months**. It is an aesthetic recognition and grants no access. + +Unlike the other levels, level 3 can be lost. When a member no longer meets the requirement, they return to level 2. They can earn level 3 again by meeting the requirement again. + +### Level overview + +```mermaid +flowchart LR + L0["Level 0
New joiner"] -- "agrees to the rules" --> L1["Level 1
Agreed to the rules"] + L1 -- "forced reset" --> L0 + L1 -- "12 active days in each of the last 3 months" --> L2["Level 2
Regular"] + L2 -- "21 active days in each of the last 3 months" --> L3["Level 3
Highly active"] + L3 -- "requirement no longer met" --> L2 +``` + +### Monthly evaluation + +Levels 0 and 1 are granted as soon as the member qualifies. Levels 2 and 3 are decided on the **first day of each month**. On that day the bot looks at each member's activity in the 3 calendar months before the current one, and grants or removes levels 2 and 3. The current month is never counted. For example, on April 1st the bot looks at January, February and March. + +Because a member can only move up one level per check, and level 3 needs level 2 first: + +- A member who qualifies for both levels 2 and 3 gets level 2 on the first check and must wait for the next check for level 3. +- Reaching level 2 takes at least 3 months in the server, and reaching level 3 at least 4 months. + +### Dormancy + +Once a week, the bot checks every member at level 0 or 1, except bots and moderators. A member whose last activity was 90 or more days ago becomes **dormant** and is reset to level 0 at that moment, and all of their completed tasks are cleared. Activity is counted in whole days, so the time of day is ignored. Members at level 2 or above never become dormant. + +A dormant member can still read the server, but cannot write. The member learns of the dormancy when they try to write: + +1. The bot deletes the message and times the member out for 10 minutes. +2. The bot opens a private thread with the member, explaining that the account is dormant. It repeats the text of the deleted message so it is not lost, and offers a reactivate button. +3. The member presses the reactivate button in that thread. A moderator can also reactivate a member. +4. The timeout and the dormant state are removed, and the member's last activity is set to now. The member is at level 0 and starts again from there. +5. In the same thread, the bot posts a message that pings the member, shows the rules, and has a button to agree, as for level 1. + +```mermaid +flowchart TD + Weekly["Weekly check of members at level 0 or 1
(bots and moderators excluded)"] --> Old{"Last activity
90 or more days ago?"} + Old -- no --> Nothing["Nothing happens"] + Old -- yes --> Dormant["Member becomes dormant
reset to level 0, tasks cleared"] + Dormant --> Write["Member tries to write"] + Write --> Block["Message deleted
10-minute timeout
private thread opened with reactivate button"] + Block --> Reactivate{"Member presses reactivate
or a moderator reactivates"} + Reactivate --> Cleared["Timeout and dormant state removed
last activity set to now"] + Cleared --> Rules["Bot posts the rules in the thread
and pings the member"] + Rules --> Agree["Member presses agree
and reaches level 1"] +``` + +### Leaving and rejoining + +When a member leaves the server, the bot keeps their progress (level, activity and completed tasks) for **seven days**. This applies to every level. If they rejoin within those seven days, their progress is restored as it was. After seven days the progress is deleted, and a member who returns starts at level 0 as a new joiner. + +### Existing members at launch + +Members who joined before the system launched start at level 1, except holders of the "Regular" or "VC Regular" role, who start at level 2. + +### Exempt members + +Bots are never assigned levels and are never made dormant. Moderators gain levels like any other member, but are never made dormant. From 21c1981f0ecf27e854a4f74487482fdec342d5d3 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sun, 8 Nov 2026 14:27:23 +0100 Subject: [PATCH 30/30] feat: remove audience from the doc --- docs/spec/onboarding.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/spec/onboarding.md b/docs/spec/onboarding.md index c6c1700..3051204 100644 --- a/docs/spec/onboarding.md +++ b/docs/spec/onboarding.md @@ -24,12 +24,6 @@ Regulars are annoyed by a steady stream of newcomer noise: 3. **Reward participation.** Members who stay and contribute gain access and recognition. 4. **Limit spam and abuse.** New and long-dormant accounts have limited reach: no attachments, no open voice. -### Audience - -- New members (read this to know how to progress). -- Regulars and moderators (read this to know what newcomers can and cannot do). -- Maintainers of the bot. - ### Success criteria - Regulars report less newcomer noise in general chat and voice.