Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .codearbiter/plans/arbiter-self-assign.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Plan — arbiter-self-assign

Spec: `.codearbiter/specs/arbiter-self-assign.md`. Stack: Node + TS strict + discord.js v14 + vitest.
Reuses `@discord-bots/bot-core`. Bot package: `@discord-bots/arbiter-self-assign`; token env
`DISCORD_TOKEN_ARBITER_SELF_ASSIGN`. Component interactions tested with mocked discord.js objects (no
live connection). Verification cites tech-stack.md commands.

## AC ledger
AC-01..AC-12 per spec. Bijective coverage proven at end.

## Task table

| id | path(s) | verification | maps-to | covers | depends-on | status |
|---|---|---|---|---|---|---|
| T-01 | (scaffold) `bots/arbiter-self-assign/{package.json,tsconfig.json,src/index.ts,data/.gitkeep}` via `tools/new-bot` then customize | `node tools/new-bot ... ` produces folder; `npx tsc -b` clean with bot in references | scaffold convention | AC-12 | — | ACCEPTED |
| T-02 | `packages/bot-core/src/json-value-store.ts` + `.test.ts`; export from `index.ts` | `vitest run packages/bot-core` — read/write persists one JSON value across reload; missing/corrupt → undefined | generic state store | AC-08,AC-10 | — | ACCEPTED |
| T-03 | `bots/arbiter-self-assign/src/roles.ts` + `.test.ts` (the 12 role defs: colors+hex, Helldivers+3 placeholders, GameNight; managed-name set + guards) | tests: managed set = exactly 12; `isManaged`/partition correct; placeholders flagged | role catalog + guard | AC-01,AC-02 | T-01 | ACCEPTED |
| T-04 | `bots/arbiter-self-assign/src/config.ts` + `.test.ts` | tests: intents == `[Guilds, GuildMembers]` (no MessageContent); guild `1119021914086703194`, channel `1515850507103633641`; token via env | config/intents | AC-09,AC-11 | T-01 | ACCEPTED |
| T-05 | `bots/arbiter-self-assign/src/ensure-roles.ts` + `.test.ts` (mocked guild.roles) | tests: creates only missing managed roles w/ correct props (color/no-perm/not-hoist; ping/GameNight mentionable); adopts existing by name; ZERO ops on a non-managed role present in the guild | ensure-roles + only-12 | AC-01,AC-02 | T-03 | ACCEPTED |
| T-06 | `bots/arbiter-self-assign/src/menu.ts` + `.test.ts` | tests: builds 1 string-select (7 colors, min0/max1) + button row(s) (pings+GameNight); custom IDs stable + namespaced (`selfassign:color`, `selfassign:ping:<slug>`) | menu builder | AC-03,AC-08 | T-03 | ACCEPTED |
| T-07 | `bots/arbiter-self-assign/src/handlers/color.ts` + `.test.ts` (mock select interaction + member roles) | tests: assigns chosen color, removes other managed colors only, ephemeral reply; non-managed roles untouched | color single-select | AC-04,AC-06,AC-02 | T-05,T-06 | ACCEPTED |
| T-08 | `bots/arbiter-self-assign/src/handlers/ping.ts` + `.test.ts` | tests: toggles ping/GameNight (add if absent, remove if held), ephemeral reply; only managed roles touched | ping toggle | AC-05,AC-06,AC-02 | T-05,T-06 | ACCEPTED |
| T-09 | `bots/arbiter-self-assign/src/handlers/post-menu.ts` + `.test.ts` (+ command builder) | tests: admin-gated (Manage Roles) check; posts menu to #roles; persists message id; non-admin rejected ephemerally | /post-role-menu | AC-07,AC-08 | T-06,T-02 | ACCEPTED |
| T-10 | `bots/arbiter-self-assign/src/index.ts` + `.test.ts` | tests: client intents == `[Guilds,GuildMembers]`; registers `/post-role-menu` idempotently; ready→ensure roles + load-or-post menu; interaction routing; guarded main; no token/PII log | entrypoint wiring | AC-07,AC-08,AC-09,AC-10,AC-11 | T-04,T-05,T-07,T-08,T-09 | ACCEPTED |
| T-11 | `bots/arbiter-self-assign/src/deploy-commands.ts` + `.test.ts` | tests (mock REST): idempotent register of `/post-role-menu` | deploy script | AC-07 | T-09 | ACCEPTED |
| T-12 | `bots/arbiter-self-assign/README.md` | doc covers app setup, role-hierarchy (bot role above the 12), Manage Roles + GuildMembers intent, env token, placeholder-rename `[CONFIRM-03]`, live-verify | operator handoff | (docs) | T-10 | ACCEPTED |

## Order & MVP slice
Order via depends-on (no cycles): T-01,T-02 → T-03,T-04 → T-05,T-06 → T-07,T-08,T-09 → T-10 → T-11 → T-12.
**MVP slice = T-01..T-11** (functional, tested bot + deploy script). T-12 (operator README) is the tail.

## Out-of-scope (tagged)
- `[NEEDS-TRIAGE: operator-live-verify]` — app creation, role-hierarchy placement (bot role above the
12), Manage Roles invite, set `DISCORD_TOKEN_ARBITER_SELF_ASSIGN`, run, click the menu. Operator, post-PR.
- `[CONFIRM-03]` — names of the 3 placeholder game-ping roles (config-driven; rename before live).

## Coverage proof (bijective)
AC-01→T-03,T-05 · AC-02→T-03,T-05,T-07,T-08 · AC-03→T-06 · AC-04→T-07 · AC-05→T-08 · AC-06→T-07,T-08 ·
AC-07→T-09,T-10,T-11 · AC-08→T-02,T-06,T-09,T-10 · AC-09→T-04,T-10 · AC-10→T-02,T-10 · AC-11→T-04,T-10 ·
AC-12→T-01. Every task covers ≥1 AC; every AC covered. (T-12 is the documented operator handoff.)
75 changes: 75 additions & 0 deletions .codearbiter/specs/arbiter-self-assign.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Sprint Spec — arbiterGaming Self-Assign Role Bot

**Slug:** arbiter-self-assign
**Date:** 2026-06-15
**Stage:** 1
**Source:** charter `../discordArbiter/charters/arbiter-self-assign-bot.md` + user decisions (UI style,
roster, color exclusivity, straight-to-sprint).

## Problem
arbiterGaming members should self-serve cosmetic **color** roles and opt-in **game-ping** roles in
`#roles` without bothering a mod. The guild isn't Community-enabled (no native onboarding), so a real
bot runtime is required. Bot #2 in the `discordBots` monorepo — the first real exercise of the
`tools/new-bot` convention.

## Scope (in)
- A role-selection UI in `#roles` (guild `1119021914086703194`, channel `1515850507103633641`):
**select-menu for colors** (single-select), **toggle buttons for game pings + GameNight** (decided).
- The bot **owns exactly 12 roles** and ensures they exist on startup.
- Component interaction handlers (color select, ping toggle, admin `/post-role-menu`).
- Per-guild persistence of the menu message id so the UI re-binds after restart.
- Reuse `@discord-bots/bot-core` (config loader, idempotent command registration). Generate the bot
via `tools/new-bot` to validate the per-bot convention.

## Scope (out)
- **Live Discord operator steps** — create the app, set the token, place the bot's role above the 12,
invite with Manage Roles, run the bot. `[NEEDS-TRIAGE: operator-live-verify]`.
- Any role outside the 12; any moderation; the tier roles (Admin/Mod/Member/Founding Four); auto-mod.
- Dice / scheduled events (that's dungeon-herald).

## The 12 managed roles (the ONLY roles the bot may create/modify/assign)
- **Colors** (cosmetic: no permissions, not hoisted): Red `#E74C3C`, Orange `#E67E22`, Yellow
`#F1C40F`, Green `#2ECC71`, Cyan `#1ABC9C`, Blue `#3498DB`, Pink `#E84393`.
- **Game pings** (mentionable, no permissions, no color): **Helldivers** + **3 config placeholders**
(`[CONFIRM-03]` — operator renames in config before live deploy).
- **GameNight** (mentionable, no permissions).

## Acceptance criteria
- **AC-01** — On startup (and on demand), the bot creates any of its 12 roles that are missing, with
the exact name; colors get the palette hex, no permissions, not hoisted; game-ping/GameNight roles
are mentionable, no color, no permissions; all positioned below the bot's own highest role. Existing
roles with a managed name are adopted, not duplicated.
- **AC-02** (safety-critical) — The bot never creates, edits, deletes, or assigns any role NOT in its
12 managed set (matched by name). Tier roles and all others are never touched. Proven by tests that
feed a guild containing non-managed roles and assert zero operations on them.
- **AC-03** — The bot posts a selection message in `#roles`: a string-select listing the 7 colors
(min 0 / max 1), and button row(s) for the game pings + GameNight. Component custom IDs are stable
and namespaced (e.g. `selfassign:color`, `selfassign:ping:<role>`).
- **AC-04** — Selecting a color assigns that color role and removes any OTHER managed color role the
member holds (single color at a time).
- **AC-05** — Clicking a game-ping/GameNight button toggles that role: add if absent, remove if held.
- **AC-06** — Every interaction replies EPHEMERALLY to the invoking member ("You're now Blue", "Added
Helldivers", "Removed GameNight"); nothing is posted publicly per interaction.
- **AC-07** — An admin-gated `/post-role-menu` command (re)posts the menu in `#roles`; usable only by
members with Manage Roles/admin; registered idempotently via bot-core.
- **AC-08** — The menu survives a restart: the posted message id is persisted per guild and the bot
re-binds (stable custom IDs make handlers work after restart); if the stored message is gone, it
reposts cleanly.
- **AC-09** — Least-privilege: the client requests exactly `[Guilds, GuildMembers]` intents
(GuildMembers required to read/modify member roles) and NO others (no MessageContent). Requires the
Manage Roles permission. Asserted without login.
- **AC-10** — No member PII stored or logged: persisted state is only `{guildId, channelId,
messageId}`; no per-user data; no usernames/IDs of members in logs.
- **AC-11** — Token from env `DISCORD_TOKEN_ARBITER_SELF_ASSIGN`; never hardcoded or logged.
- **AC-12** — The bot is scaffolded via `tools/new-bot` and conforms to the per-bot convention; it
reuses `@discord-bots/bot-core` for config + command registration.

## Open questions
- `[CONFIRM-03]` — names of the 3 placeholder game-ping roles (Helldivers is fixed). Config-driven;
operator sets real names before the live run. Does not block the build (placeholders are config
defaults, clearly flagged).

## Security note (gates remain hard stops)
This bot mutates roles under the **GuildMembers privileged intent** + **Manage Roles**. The "only-12"
boundary (AC-02), least-privilege intents (AC-09), and no-PII (AC-10) are hard acceptance criteria and
security-review gates. Any reviewer CRITICAL halts the sprint. Token from env only (AC-11).
25 changes: 25 additions & 0 deletions .codearbiter/sprint-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,28 @@ Started: 2026-06-14 · Branch: `sprint/dungeon-herald-mvp` · Spec/plan: `.codea

## SPRINT COMPLETE
- 29/29 tasks ACCEPTED. 175 tests green. 4 reviewers PASS (0 CRITICAL/HIGH). Auto-decisions: D-01..D-04, all high-confidence/user-directed — zero low-confidence calls to review. Open items: 3 NEEDS-TRIAGE + 2 CONFIRM (all non-blocking, in open-questions.md / above).

---

# Sprint 2 — arbiter-self-assign (started 2026-06-15)
Branch: `sprint/arbiter-self-assign` · spec/plan: `.codearbiter/{specs,plans}/arbiter-self-assign.md`
User approved spec+plan at the Phase-1 gate ("Approve — run it"). UI decisions: select-menu colors
(single-select) + toggle buttons for pings/GameNight. Roster: Helldivers + 3 placeholders (config).

## D-05 — Hosting via Docker (resolves [CONFIRM-01] direction)
- User: "can this be in a docker container for hosting? easier to see in docker desktop" (2026-06-15).
- Decision: containerize as a DEDICATED follow-up PR after this bot lands — one image per bot
(ADR-0003 one-process-per-bot) + docker-compose for Docker Desktop + named volumes per bot `data/`
(also fixes the ephemeral-FS persistence risk for dungeon-herald's reminders.json). Not folded into
this sprint (cross-cutting; covers both bots). [CONFIRM-01] now leaning "self-host via Docker".
- Strength: n/a (user-directed) · Confidence: high

## ACCEPT — bot-core JsonValueStore (T-02) + arbiter-self-assign (T-01,T-03..T-12)
- Fresh-verified: vitest bot 52/52, full suite 239/239, tsc -b exit 0, lint exit 0, format:check exit 0.
- Intents exactly [Guilds, GuildMembers] (privileged GuildMembers required for role mutation; no MessageContent). Behavioral proof: catalog=12; isManagedRole Admin=false/Blue=true; forged roleByKey('ping-admin')=undefined.
- Quality review (Phase 4): security-reviewer over the role-mutation/privileged-intent surface → PASS, 0 CRITICAL/HIGH. Make-or-break checks pass: only-12 boundary (ensure-roles iterates catalog; handlers resolve via trusted map) + custom-id forgery resistance (forged key → undefined → ephemeral reject) + admin gate on /post-role-menu + no-PII + env-only token.
- 2 LOW findings. #1 (role-editor mutation layer lacked a managed-set guard) — APPLIED: added isManagedRole guard in asMemberRoleEditor add/remove + 3 guard tests (defense-in-depth for AC-02). #2 (name-based identity not unique in Discord) — NOTED, non-blocking (single-tenant operator-controlled guild).
- [NEEDS-TRIAGE: new-bot-convention] The generator + dungeon-herald use a Windows-fragile launch guard (`import.meta.url === file://${process.argv[1]}`) that silently no-ops on Windows (relative argv). Works on Linux/Docker (the hosting target). arbiter-self-assign uses the robust pathToFileURL idiom. FIX the generator + dungeon-herald guards in the Docker PR.
- Added `.gitattributes` (eol=lf) to fix Windows-CRLF vs Linux-LF format:check inconsistency; `prettier --write .` normalized the tree.

## SPRINT 2 landing — commits in progress
13 changes: 13 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Normalize line endings to LF for all text files, on every platform.
# Without this, Windows checkouts get CRLF and fail `prettier --check`, while
# Linux CI (LF) passes — a cross-platform inconsistency. LF is the source of truth.
* text=auto eol=lf

# Explicit binaries (never normalize).
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.woff binary
*.woff2 binary
113 changes: 113 additions & 0 deletions bots/arbiter-self-assign/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<!-- SPDX-License-Identifier: MIT -->

# arbiter-self-assign

A self-serve role bot for **arbiterGaming** (`1119021914086703194`). Members pick a cosmetic **color**
and opt into **game-ping** roles in `#roles` without bothering a mod.

- **Color select** — a single-select menu of 7 colors (Red, Orange, Yellow, Green, Cyan, Blue, Pink).
Picking one assigns it and removes any other color (one color at a time). Clearing removes it.
- **Ping toggles** — buttons for `Helldivers`, three operator-named game slots, and `GameNight`.
Clicking adds the role if you lack it, removes it if you have it. These roles are mentionable.
- **`/post-role-menu`** — admin-only (Manage Roles). (Re)posts the menu in `#roles`.

The bot **owns exactly 12 roles** and ensures they exist on startup. It never creates, edits, deletes,
or assigns any role outside those 12 — tier roles (Admin/Mod/Member/Founding Four) and everything else
are never touched.

Built from the discordArbiter `arbiter-self-assign` charter. Scaffolded via `tools/new-bot`. Part of
the `discordBots` monorepo.

---

## Operator setup (live run)

These steps require your bot token and a live server, so they are performed by you (the operator), not
by the build. `[NEEDS-TRIAGE: operator-live-verify]`

### 1. Create the bot application

1. In the [Discord Developer Portal](https://discord.com/developers/applications), create an
application (or reuse one). Note its **Application ID**.
2. Under **Bot**, create a bot and copy its **token**.
3. **Enable the privileged `Server Members Intent` (GuildMembers).** This bot _requires_ it to read
and modify member roles. Leave **Message Content** and **Presence** OFF — they are not used.

### 2. Rename the placeholder ping roles (before deploy) — `[CONFIRM-03]`

Open `bots/arbiter-self-assign/src/roles.ts` and rename the three placeholder game-ping roles
(`Game Slot 2`, `Game Slot 3`, `Game Slot 4`, flagged `placeholder: true`) to the real game names.
`Helldivers` and `GameNight` are fixed. Rebuild after editing.

### 3. Place the bot's role ABOVE the 12 managed roles

A bot can only manage roles **below** its own highest role. After inviting the bot, drag its role in
**Server Settings → Roles** so it sits **above** all 12 managed roles (the bot creates them just below
its own highest role on startup, but if you create them by hand or reorder, keep the bot on top).

### 4. Invite the bot

Invite it to arbiterGaming with the `bot` and `applications.commands` scopes, and grant:

- **Manage Roles** (server permission — required to create/assign the 12 roles).
- On **`#roles`**: **View Channel** and **Send Messages** (to post the menu).

### 5. Provide secrets via environment (never commit these)

| Variable | Value |
| ----------------------------------- | ------------------------------ |
| `DISCORD_TOKEN_ARBITER_SELF_ASSIGN` | the bot token from step 1 |
| `DISCORD_APPLICATION_ID` | the Application ID from step 1 |

The bot reads the token from `DISCORD_TOKEN_ARBITER_SELF_ASSIGN` only; it is never logged or written
to disk.

### 6. Build, register commands, run

From the **repo root**:

```bash
npm install
npx tsc -b # compile all workspaces to dist/
npm run deploy-commands -w @discord-bots/arbiter-self-assign # one-time + after any command change (idempotent)
npm start -w @discord-bots/arbiter-self-assign # start the always-on bot process
```

On first start the bot ensures the 12 roles exist (positioned just below its own highest role) and
posts the menu in `#roles`.

### 7. Verify it works

In arbiterGaming, in `#roles`:

- Pick **Blue** in the select → you get the Blue role and an ephemeral "You're now Blue"; pick **Red**
→ Blue is removed and Red added (only one color). Clear the select → your color is removed.
- Click **Helldivers** → ephemeral "Added Helldivers"; click again → "Removed Helldivers".
- Confirm tier roles (Admin/Mod/Member) are untouched by any of the above.
- As an admin, run `/post-role-menu` → the menu (re)posts. As a non-admin, the command is hidden /
rejected.
- Restart the bot → the existing menu still works (handlers re-bind via stable custom IDs); if the
stored message was deleted, the bot reposts it.

---

## Runtime notes

- **Durable state:** the menu location lives at `bots/arbiter-self-assign/data/menu-location.json`
(gitignored). It holds only `{guildId, channelId, messageId}` — no member PII. If it is lost, the
bot simply reposts the menu on next start.
- **Least privilege (AC-09):** the client requests EXACTLY `[Guilds, GuildMembers]` — no Message
Content, presences, or message intents.
- **Hosting (`[CONFIRM-01]`):** a plain always-on Node process. On an ephemeral-filesystem host the
menu-location file is wiped on redeploy; the bot just reposts the menu (no data loss of consequence).

## Development

```bash
npm test -w @discord-bots/arbiter-self-assign # unit tests (mocked Discord; no live connection)
npm run typecheck -w @discord-bots/arbiter-self-assign
```

Component handlers are unit-tested against narrow interfaces with mocked discord.js objects — no live
connection. The bot imports `@discord-bots/bot-core` (config token convention, `JsonValueStore`,
idempotent command registration).
2 changes: 2 additions & 0 deletions bots/arbiter-self-assign/data/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Durable runtime data lives here (e.g. reminders.json — gitignored).
# This file only keeps the directory present in git; it holds no data and no PII.
20 changes: 20 additions & 0 deletions bots/arbiter-self-assign/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@discord-bots/arbiter-self-assign",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "arbiter-self-assign — self-serve cosmetic color + game-ping roles for arbiterGaming's #roles channel.",
"license": "MIT",
"author": "Brennon Huff",
"main": "./dist/index.js",
"scripts": {
"start": "node dist/index.js",
"deploy-commands": "node dist/deploy-commands.js",
"test": "vitest run",
"typecheck": "tsc -b"
},
"dependencies": {
"@discord-bots/bot-core": "*",
"discord.js": "^14.26.4"
}
}
Loading
Loading