diff --git a/docs/architecture.md b/docs/architecture.md index a953b08..f23f857 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -5,7 +5,7 @@ Single unified service (`ejclaw`) manages all three Discord bots in one process: - `ejclaw.service` — Single unified process -- Discord bots: `DISCORD_BOT_TOKEN` (Claude), `DISCORD_CODEX_BOT_TOKEN` (Codex-main), `DISCORD_REVIEW_BOT_TOKEN` (Codex-review) +- Discord bots: `DISCORD_OWNER_BOT_TOKEN` (owner), `DISCORD_REVIEWER_BOT_TOKEN` (reviewer), `DISCORD_ARBITER_BOT_TOKEN` (arbiter) - Paired review: owner ↔ reviewer (agent types configurable per role) - Reviewer fallback: Claude exhaustion → codex-review auto-handoff - Shared dirs: `store/`, `groups/`, `data/` diff --git a/docs/configuration.md b/docs/configuration.md index 349e86e..36c3ffe 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -5,9 +5,9 @@ All configuration in a single `.env` file. ## Discord Bots ```bash -DISCORD_BOT_TOKEN= # Claude bot -DISCORD_CODEX_BOT_TOKEN= # Codex-main bot (owner) -DISCORD_REVIEW_BOT_TOKEN= # Codex-review bot (arbiter) +DISCORD_OWNER_BOT_TOKEN= # Owner bot +DISCORD_REVIEWER_BOT_TOKEN= # Reviewer bot +DISCORD_ARBITER_BOT_TOKEN= # Arbiter bot ``` ## Agent Types & Models diff --git a/setup/verify-state.test.ts b/setup/verify-state.test.ts index 0b59b37..054ffcb 100644 --- a/setup/verify-state.test.ts +++ b/setup/verify-state.test.ts @@ -11,7 +11,6 @@ import { buildVerifySummary, detectChannelAuth, detectCredentials, - detectLegacyDiscordTokenKeys, loadRegisteredGroupsSummary, loadRoleRoutingRequirementsSummary, } from './verify-state.js'; @@ -53,37 +52,17 @@ describe('verify state helpers', () => { }); }); - it('does not treat legacy service-based channel auth names as configured channels', () => { + it('does not treat unknown discord token names as configured channels', () => { expect( detectChannelAuth( {}, { - DISCORD_CLAUDE_BOT_TOKEN: 'legacy-owner-token', - DISCORD_CODEX_MAIN_BOT_TOKEN: 'legacy-reviewer-token', - DISCORD_CODEX_REVIEW_BOT_TOKEN: 'legacy-arbiter-token', + DISCORD_UNUSED_BOT_TOKEN: 'unknown-token', }, ), ).toEqual({}); }); - it('detects legacy service-based discord token names from env file and process env', () => { - expect( - detectLegacyDiscordTokenKeys( - { - DISCORD_BOT_TOKEN: 'legacy-owner-token', - }, - { - DISCORD_CODEX_MAIN_BOT_TOKEN: 'legacy-reviewer-token', - DISCORD_CODEX_REVIEW_BOT_TOKEN: 'legacy-arbiter-token', - }, - ), - ).toEqual([ - 'DISCORD_BOT_TOKEN', - 'DISCORD_CODEX_MAIN_BOT_TOKEN', - 'DISCORD_CODEX_REVIEW_BOT_TOKEN', - ]); - }); - it('loads paired-room routing requirements from the sqlite store', () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-verify-')); tempRoots.push(tempRoot); @@ -270,26 +249,4 @@ describe('verify state helpers', () => { }); }); - it('fails verification when legacy discord token names are still configured', () => { - const services: ServiceCheck[] = [{ name: 'ejclaw', status: 'running' }]; - - expect( - buildVerifySummary( - services, - [], - 'configured', - { - discord: 'configured', - 'discord-review': 'configured', - 'discord-arbiter': 'configured', - }, - 1, - {}, - { legacyDiscordTokenKeys: ['DISCORD_BOT_TOKEN'] }, - ), - ).toMatchObject({ - status: 'failed', - legacyDiscordTokenKeys: ['DISCORD_BOT_TOKEN'], - }); - }); }); diff --git a/setup/verify-state.ts b/setup/verify-state.ts index 349b502..b435f70 100644 --- a/setup/verify-state.ts +++ b/setup/verify-state.ts @@ -11,15 +11,6 @@ import type { ServiceCheck } from './verify-services.js'; export type CredentialsStatus = 'configured' | 'missing'; export type VerifyStatus = 'success' | 'failed'; -const LEGACY_DISCORD_TOKEN_KEYS = [ - 'DISCORD_BOT_TOKEN', - 'DISCORD_CODEX_BOT_TOKEN', - 'DISCORD_REVIEW_BOT_TOKEN', - 'DISCORD_CLAUDE_BOT_TOKEN', - 'DISCORD_CODEX_MAIN_BOT_TOKEN', - 'DISCORD_CODEX_REVIEW_BOT_TOKEN', -]; - export interface RegisteredGroupsSummary { registeredGroups: number; groupsByAgent: Record; @@ -35,7 +26,6 @@ export interface VerifySummary extends RegisteredGroupsSummary { servicesSummary: Record; configuredChannels: string[]; channelAuth: Record; - legacyDiscordTokenKeys: string[]; tribunalRooms: number; activeArbiterTasks: number; // Legacy status fields kept for backward-compatible setup output. @@ -80,15 +70,6 @@ export function detectChannelAuth( return channelAuth; } -export function detectLegacyDiscordTokenKeys( - envVars = readEnvFile(LEGACY_DISCORD_TOKEN_KEYS), - processEnv: NodeJS.ProcessEnv = process.env, -): string[] { - return LEGACY_DISCORD_TOKEN_KEYS.filter( - (key) => !!(processEnv[key] || envVars[key]), - ); -} - export function loadRegisteredGroupsSummary( dbPath = path.join(STORE_DIR, 'messages.db'), ): RegisteredGroupsSummary { @@ -211,7 +192,6 @@ export function buildVerifySummary( registeredGroups: number, groupsByAgent: Record, options: { - legacyDiscordTokenKeys?: string[]; tribunalRooms?: number; activeArbiterTasks?: number; } = {}, @@ -224,7 +204,6 @@ export function buildVerifySummary( const hasOwnerCapableChannel = 'discord' in channelAuth; const codexConfigured = 'discord-review' in channelAuth; const reviewConfigured = 'discord-arbiter' in channelAuth; - const legacyDiscordTokenKeys = options.legacyDiscordTokenKeys ?? []; const tribunalRooms = options.tribunalRooms ?? 0; const activeArbiterTasks = options.activeArbiterTasks ?? 0; const reviewerConfigured = tribunalRooms === 0 || codexConfigured; @@ -234,7 +213,6 @@ export function buildVerifySummary( allConfiguredServicesRunning && credentials === 'configured' && hasOwnerCapableChannel && - legacyDiscordTokenKeys.length === 0 && reviewerConfigured && arbiterConfigured && registeredGroups > 0 @@ -251,7 +229,6 @@ export function buildVerifySummary( servicesSummary, configuredChannels, channelAuth, - legacyDiscordTokenKeys, tribunalRooms, activeArbiterTasks, registeredGroups, diff --git a/setup/verify.ts b/setup/verify.ts index 52df47c..ddd0ba1 100644 --- a/setup/verify.ts +++ b/setup/verify.ts @@ -21,7 +21,6 @@ import { buildVerifySummary, detectChannelAuth, detectCredentials, - detectLegacyDiscordTokenKeys, loadRegisteredGroupsSummary, loadRoleRoutingRequirementsSummary, } from './verify-state.js'; @@ -58,7 +57,6 @@ export async function run(_args: string[]): Promise { const credentials = detectCredentials(projectRoot); const channelAuth = detectChannelAuth(); - const legacyDiscordTokenKeys = detectLegacyDiscordTokenKeys(); const { registeredGroups, groupsByAgent } = loadRegisteredGroupsSummary(); const { tribunalRooms, activeArbiterTasks } = loadRoleRoutingRequirementsSummary(); @@ -66,7 +64,6 @@ export async function run(_args: string[]): Promise { status: baseStatus, servicesSummary, configuredChannels, - legacyDiscordTokenKeys: legacyDiscordTokens, tribunalRooms: detectedTribunalRooms, activeArbiterTasks: detectedActiveArbiterTasks, codexConfigured, @@ -79,7 +76,6 @@ export async function run(_args: string[]): Promise { registeredGroups, groupsByAgent, { - legacyDiscordTokenKeys, tribunalRooms, activeArbiterTasks, }, @@ -93,7 +89,6 @@ export async function run(_args: string[]): Promise { { status, channelAuth, - legacyDiscordTokens, tribunalRooms: detectedTribunalRooms, activeArbiterTasks: detectedActiveArbiterTasks, servicesSummary, @@ -101,16 +96,6 @@ export async function run(_args: string[]): Promise { }, 'Verification complete', ); - if (legacyDiscordTokens.length > 0) { - logger.error( - { - legacyDiscordTokens, - migration: - 'Rename Discord bot tokens to DISCORD_OWNER_BOT_TOKEN / DISCORD_REVIEWER_BOT_TOKEN / DISCORD_ARBITER_BOT_TOKEN', - }, - 'Verification failed due to legacy service-based Discord bot token names', - ); - } if (legacyServiceIssues.length > 0) { logger.error( { @@ -133,7 +118,6 @@ export async function run(_args: string[]): Promise { CREDENTIALS: credentials, CONFIGURED_CHANNELS: configuredChannels.join(','), CHANNEL_AUTH: JSON.stringify(channelAuth), - LEGACY_DISCORD_TOKENS: legacyDiscordTokens.join(','), TRIBUNAL_ROOMS: detectedTribunalRooms, ACTIVE_ARBITER_TASKS: detectedActiveArbiterTasks, REGISTERED_GROUPS: registeredGroups, diff --git a/src/channels/discord.test.ts b/src/channels/discord.test.ts index e2eebca..84388c4 100644 --- a/src/channels/discord.test.ts +++ b/src/channels/discord.test.ts @@ -3,7 +3,15 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; // --- Mocks --- // Mock registry (registerChannel runs at import time) -vi.mock('./registry.js', () => ({ registerChannel: vi.fn() })); +const registeredChannelFactories = vi.hoisted( + () => new Map any>(), +); + +vi.mock('./registry.js', () => ({ + registerChannel: vi.fn((name: string, factory: (...args: any[]) => any) => { + registeredChannelFactories.set(name, factory); + }), +})); // Mock env reader (used by the factory, not needed in unit tests) vi.mock('../env.js', () => ({ @@ -113,6 +121,8 @@ vi.mock('discord.js', () => { }); import { DiscordChannel, DiscordChannelOpts } from './discord.js'; +import { registerChannel } from './registry.js'; +import { getEnv } from '../env.js'; import { logger } from '../logger.js'; // --- Test helpers --- @@ -214,6 +224,31 @@ describe('DiscordChannel', () => { vi.restoreAllMocks(); }); + describe('channel registration', () => { + it('warns when only legacy owner token names are configured', () => { + const ownerFactory = registeredChannelFactories.get('discord'); + + vi.mocked(getEnv).mockImplementation((key: string) => { + if (key === 'DISCORD_BOT_TOKEN') return 'legacy-owner-token'; + return undefined; + }); + + expect(ownerFactory).toBeTypeOf('function'); + expect(ownerFactory?.(createTestOpts() as any)).toBeNull(); + expect(logger.warn).toHaveBeenCalledWith( + { + role: 'owner', + canonicalKey: 'DISCORD_OWNER_BOT_TOKEN', + legacyKeys: ['DISCORD_BOT_TOKEN'], + }, + 'Discord: legacy bot token names detected; rename them to canonical role-based names', + ); + expect(logger.warn).toHaveBeenCalledWith( + 'Discord: DISCORD_OWNER_BOT_TOKEN not set', + ); + }); + }); + // --- Connection lifecycle --- describe('connection lifecycle', () => { diff --git a/src/channels/discord.ts b/src/channels/discord.ts index 47186c7..e689e45 100644 --- a/src/channels/discord.ts +++ b/src/channels/discord.ts @@ -43,31 +43,24 @@ const DISCORD_ARBITER_LEGACY_TOKEN_KEYS = [ 'DISCORD_CODEX_REVIEW_BOT_TOKEN', ]; -function getConfiguredLegacyDiscordTokenKeys(keys: string[]): string[] { - return keys.filter((key) => !!getEnv(key)); -} - -function getRoleTokenOrLogMigrationError( +function warnIfLegacyDiscordTokenConfigured( canonicalKey: string, legacyKeys: string[], role: 'owner' | 'reviewer' | 'arbiter', -): string { - const canonicalValue = getEnv(canonicalKey) || ''; - if (canonicalValue) return canonicalValue; +): void { + if (getEnv(canonicalKey)) return; - const configuredLegacyKeys = getConfiguredLegacyDiscordTokenKeys(legacyKeys); - if (configuredLegacyKeys.length > 0) { - logger.error( - { - role, - canonicalKey, - legacyKeys: configuredLegacyKeys, - }, - 'Discord: legacy service-based bot token names are no longer supported; rename them to canonical role-based names', - ); - } + const configuredLegacyKeys = legacyKeys.filter((key) => !!getEnv(key)); + if (configuredLegacyKeys.length === 0) return; - return ''; + logger.warn( + { + role, + canonicalKey, + legacyKeys: configuredLegacyKeys, + }, + 'Discord: legacy bot token names detected; rename them to canonical role-based names', + ); } /** @@ -834,11 +827,12 @@ export class DiscordChannel implements Channel { } registerChannel(DISCORD_OWNER_CHANNEL, (opts: ChannelOpts) => { - const token = getRoleTokenOrLogMigrationError( + warnIfLegacyDiscordTokenConfigured( DISCORD_OWNER_TOKEN_KEY, DISCORD_OWNER_LEGACY_TOKEN_KEYS, 'owner', ); + const token = getEnv(DISCORD_OWNER_TOKEN_KEY) || ''; if (!token) { logger.warn('Discord: DISCORD_OWNER_BOT_TOKEN not set'); return null; @@ -847,12 +841,13 @@ registerChannel(DISCORD_OWNER_CHANNEL, (opts: ChannelOpts) => { }); registerChannel(DISCORD_REVIEWER_CHANNEL, (opts: ChannelOpts) => { - const ownerToken = getEnv(DISCORD_OWNER_TOKEN_KEY) || ''; - const token = getRoleTokenOrLogMigrationError( + warnIfLegacyDiscordTokenConfigured( DISCORD_REVIEWER_TOKEN_KEY, DISCORD_REVIEWER_LEGACY_TOKEN_KEYS, 'reviewer', ); + const ownerToken = getEnv(DISCORD_OWNER_TOKEN_KEY) || ''; + const token = getEnv(DISCORD_REVIEWER_TOKEN_KEY) || ''; if (!token) return null; if (token === ownerToken) { logger.warn( @@ -871,13 +866,14 @@ registerChannel(DISCORD_REVIEWER_CHANNEL, (opts: ChannelOpts) => { }); registerChannel(DISCORD_ARBITER_CHANNEL, (opts: ChannelOpts) => { - const ownerToken = getEnv(DISCORD_OWNER_TOKEN_KEY) || ''; - const reviewerToken = getEnv(DISCORD_REVIEWER_TOKEN_KEY) || ''; - const token = getRoleTokenOrLogMigrationError( + warnIfLegacyDiscordTokenConfigured( DISCORD_ARBITER_TOKEN_KEY, DISCORD_ARBITER_LEGACY_TOKEN_KEYS, 'arbiter', ); + const ownerToken = getEnv(DISCORD_OWNER_TOKEN_KEY) || ''; + const reviewerToken = getEnv(DISCORD_REVIEWER_TOKEN_KEY) || ''; + const token = getEnv(DISCORD_ARBITER_TOKEN_KEY) || ''; if (!token) return null; if (token === ownerToken || token === reviewerToken) { logger.warn(