${projectsDropdown}
- ${eventsDropdown}
+ ${eventsLink}
${latestDropdown}
${committeesDropdown}
${aboutDropdown}
@@ -295,9 +285,7 @@
Agents
Publishers
Properties
- ${membershipEnabled ? `` : ''}
- ${membershipEnabled ? `
All Events` : ''}
- ${membershipEnabled ? `
Industry Gatherings` : ''}
+ ${membershipEnabled ? `
Events` : ''}
${membershipEnabled ? `` : ''}
${membershipEnabled ? `
Research & Ideas` : ''}
${membershipEnabled ? `
Industry News` : ''}
@@ -308,6 +296,7 @@
${membershipEnabled ? `
Working Groups` : ''}
${membershipEnabled ? `
Industry Councils` : ''}
${membershipEnabled ? `
Regional Chapters` : ''}
+ ${membershipEnabled ? `
Industry Gatherings` : ''}
About
Membership
Governance
diff --git a/server/src/addie/mcp/admin-tools.ts b/server/src/addie/mcp/admin-tools.ts
index e8a94f28c3..6647d03757 100644
--- a/server/src/addie/mcp/admin-tools.ts
+++ b/server/src/addie/mcp/admin-tools.ts
@@ -983,6 +983,75 @@ Example: For CES 2026, create an industry gathering with name "CES 2026", locati
},
},
+ // ============================================
+ // COMMITTEE LEADERSHIP TOOLS
+ // ============================================
+ {
+ name: 'add_committee_leader',
+ description: `Add a user as a leader of a committee (working group, council, chapter, or industry gathering).
+Leaders have management access to their committee - they can create events, manage posts, and manage members.
+
+To find the user_id:
+1. Look up the user via Slack (use their Slack user ID to find their WorkOS user ID in the mapping)
+2. Or ask for their email and look up via get_organization_details
+
+To find the committee:
+1. Use list_working_groups, list_chapters, or list_industry_gatherings
+2. Use the committee's slug (e.g., "ces-2026", "technical-standards-wg")`,
+ usage_hints: 'Use when an admin wants to give someone committee management access. Get the user_id from Slack mapping or org details.',
+ input_schema: {
+ type: 'object',
+ properties: {
+ committee_slug: {
+ type: 'string',
+ description: 'The slug of the committee (e.g., "ces-2026", "creative-wg", "austin-chapter")',
+ },
+ user_id: {
+ type: 'string',
+ description: 'The WorkOS user ID of the person to make a leader',
+ },
+ user_email: {
+ type: 'string',
+ description: 'Optional: The user email (for confirmation/logging)',
+ },
+ },
+ required: ['committee_slug', 'user_id'],
+ },
+ },
+ {
+ name: 'remove_committee_leader',
+ description: `Remove a user from the leadership of a committee.
+The user will lose management access but will remain a regular member.`,
+ input_schema: {
+ type: 'object',
+ properties: {
+ committee_slug: {
+ type: 'string',
+ description: 'The slug of the committee',
+ },
+ user_id: {
+ type: 'string',
+ description: 'The WorkOS user ID of the leader to remove',
+ },
+ },
+ required: ['committee_slug', 'user_id'],
+ },
+ },
+ {
+ name: 'list_committee_leaders',
+ description: `List all leaders of a specific committee. Shows their user IDs, names, and organizations.`,
+ input_schema: {
+ type: 'object',
+ properties: {
+ committee_slug: {
+ type: 'string',
+ description: 'The slug of the committee to list leaders for',
+ },
+ },
+ required: ['committee_slug'],
+ },
+ },
+
// ============================================
// ORGANIZATION MANAGEMENT TOOLS
// ============================================
@@ -3847,6 +3916,160 @@ export function createAdminToolHandlers(
}
});
+ // ============================================
+ // COMMITTEE LEADERSHIP HANDLERS
+ // ============================================
+
+ // Add committee leader
+ handlers.set('add_committee_leader', async (input) => {
+ const adminCheck = requireAdminFromContext();
+ if (adminCheck) return adminCheck;
+
+ const committeeSlug = (input.committee_slug as string)?.trim();
+ const userId = (input.user_id as string)?.trim();
+ const userEmail = input.user_email as string | undefined;
+
+ if (!committeeSlug) {
+ return 'â Please provide a committee_slug (e.g., "ces-2026", "creative-wg").';
+ }
+
+ if (!userId) {
+ return 'â Please provide a user_id (WorkOS user ID).';
+ }
+
+ try {
+ // Find the committee
+ const committee = await wgDb.getWorkingGroupBySlug(committeeSlug);
+ if (!committee) {
+ return `â Committee "${committeeSlug}" not found. Use list_working_groups, list_chapters, or list_industry_gatherings to find the correct slug.`;
+ }
+
+ // Check if already a leader
+ const leaders = await wgDb.getLeaders(committee.id);
+ if (leaders.some((l: { user_id: string }) => l.user_id === userId)) {
+ return `âšī¸ User is already a leader of "${committee.name}".`;
+ }
+
+ // Add as leader
+ await wgDb.addLeader(committee.id, userId);
+
+ // Also ensure they're a member
+ const memberships = await wgDb.getMembershipsByWorkingGroup(committee.id);
+ if (!memberships.some(m => m.workos_user_id === userId)) {
+ await wgDb.addMembership({
+ working_group_id: committee.id,
+ workos_user_id: userId,
+ user_email: userEmail,
+ });
+ }
+
+ logger.info({ committeeSlug, committeeName: committee.name, userId, userEmail }, 'Added committee leader via Addie');
+
+ const emailInfo = userEmail ? ` (${userEmail})` : '';
+ return `â
Successfully added user ${userId}${emailInfo} as a leader of **${committee.name}**.
+
+They now have management access to:
+- Create and manage events
+- Create and manage posts
+- Manage committee members
+
+Committee management page: https://agenticadvertising.org/working-groups/${committeeSlug}/manage`;
+ } catch (error) {
+ logger.error({ error, committeeSlug, userId }, 'Error adding committee leader');
+ return 'â Failed to add committee leader. Please try again.';
+ }
+ });
+
+ // Remove committee leader
+ handlers.set('remove_committee_leader', async (input) => {
+ const adminCheck = requireAdminFromContext();
+ if (adminCheck) return adminCheck;
+
+ const committeeSlug = (input.committee_slug as string)?.trim();
+ const userId = (input.user_id as string)?.trim();
+
+ if (!committeeSlug) {
+ return 'â Please provide a committee_slug.';
+ }
+
+ if (!userId) {
+ return 'â Please provide a user_id.';
+ }
+
+ try {
+ const committee = await wgDb.getWorkingGroupBySlug(committeeSlug);
+ if (!committee) {
+ return `â Committee "${committeeSlug}" not found.`;
+ }
+
+ // Check if they are a leader
+ const leaders = await wgDb.getLeaders(committee.id);
+ if (!leaders.some((l: { user_id: string }) => l.user_id === userId)) {
+ return `âšī¸ User ${userId} is not a leader of "${committee.name}".`;
+ }
+
+ await wgDb.removeLeader(committee.id, userId);
+
+ logger.info({ committeeSlug, committeeName: committee.name, userId }, 'Removed committee leader via Addie');
+
+ return `â
Successfully removed user ${userId} as a leader of **${committee.name}**.
+
+They are still a member but no longer have management access.`;
+ } catch (error) {
+ logger.error({ error, committeeSlug, userId }, 'Error removing committee leader');
+ return 'â Failed to remove committee leader. Please try again.';
+ }
+ });
+
+ // List committee leaders
+ handlers.set('list_committee_leaders', async (input) => {
+ const adminCheck = requireAdminFromContext();
+ if (adminCheck) return adminCheck;
+
+ const committeeSlug = (input.committee_slug as string)?.trim();
+
+ if (!committeeSlug) {
+ return 'â Please provide a committee_slug.';
+ }
+
+ try {
+ const committee = await wgDb.getWorkingGroupBySlug(committeeSlug);
+ if (!committee) {
+ return `â Committee "${committeeSlug}" not found.`;
+ }
+
+ const leaders = await wgDb.getLeaders(committee.id);
+
+ if (leaders.length === 0) {
+ return `âšī¸ **${committee.name}** has no assigned leaders.
+
+Use add_committee_leader to assign a leader.`;
+ }
+
+ let response = `## Leaders of ${committee.name}\n\n`;
+ response += `**Committee type:** ${committee.committee_type}\n`;
+ response += `**Slug:** ${committeeSlug}\n\n`;
+
+ for (const leader of leaders) {
+ response += `- **User ID:** ${leader.user_id}\n`;
+ if (leader.name) {
+ response += ` **Name:** ${leader.name}\n`;
+ }
+ if (leader.org_name) {
+ response += ` **Org:** ${leader.org_name}\n`;
+ }
+ if (leader.created_at) {
+ response += ` Added: ${new Date(leader.created_at).toLocaleDateString()}\n`;
+ }
+ }
+
+ return response;
+ } catch (error) {
+ logger.error({ error, committeeSlug }, 'Error listing committee leaders');
+ return 'â Failed to list committee leaders. Please try again.';
+ }
+ });
+
// ============================================
// ORGANIZATION MANAGEMENT HANDLERS
// ============================================
diff --git a/server/src/db/migrations/137_seed_dev_committee_leader.sql b/server/src/db/migrations/137_seed_dev_committee_leader.sql
new file mode 100644
index 0000000000..eb643af9db
--- /dev/null
+++ b/server/src/db/migrations/137_seed_dev_committee_leader.sql
@@ -0,0 +1,35 @@
+-- Migration: 137_seed_dev_committee_leader.sql
+-- Set up dev leader user as a leader of the Technical Standards Working Group
+-- This is only for local development testing
+
+-- Insert the dev leader user into working_group_leaders table
+-- Makes them a leader of the Technical Standards Working Group
+INSERT INTO working_group_leaders (working_group_id, user_id)
+SELECT
+ wg.id,
+ 'user_dev_leader_001'
+FROM working_groups wg
+WHERE wg.slug = 'technical-standards-wg'
+ON CONFLICT (working_group_id, user_id) DO NOTHING;
+
+-- Also make them a member of the working group
+INSERT INTO working_group_memberships (working_group_id, workos_user_id, status, joined_at)
+SELECT
+ wg.id,
+ 'user_dev_leader_001',
+ 'active',
+ NOW()
+FROM working_groups wg
+WHERE wg.slug = 'technical-standards-wg'
+ON CONFLICT (working_group_id, workos_user_id) DO NOTHING;
+
+-- Also make the dev member user a member of a working group for testing
+INSERT INTO working_group_memberships (working_group_id, workos_user_id, status, joined_at)
+SELECT
+ wg.id,
+ 'user_dev_member_001',
+ 'active',
+ NOW()
+FROM working_groups wg
+WHERE wg.slug = 'creative-wg'
+ON CONFLICT (working_group_id, workos_user_id) DO NOTHING;
diff --git a/server/src/db/migrations/138_seed_industry_gatherings.sql b/server/src/db/migrations/138_seed_industry_gatherings.sql
new file mode 100644
index 0000000000..993b71ac77
--- /dev/null
+++ b/server/src/db/migrations/138_seed_industry_gatherings.sql
@@ -0,0 +1,73 @@
+-- Migration: 138_seed_industry_gatherings.sql
+-- Seed industry gatherings (external events like CES, Cannes Lions)
+-- These are committee-like groups where members coordinate attendance/activities at major industry events
+
+-- Insert CES 2026
+INSERT INTO working_groups (
+ name, slug, description, committee_type, status, is_private, display_order,
+ event_start_date, event_end_date, auto_archive_after_event,
+ logo_url, website_url
+)
+VALUES (
+ 'CES 2026',
+ 'ces-2026',
+ 'Connect with AgenticAdvertising.org members attending CES 2026 in Las Vegas. Coordinate meetups, share schedules, and network with fellow members at the world''s most influential tech event.',
+ 'industry_gathering',
+ 'active',
+ false,
+ 100,
+ '2026-01-07',
+ '2026-01-10',
+ true,
+ 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/0e/CES_logo.svg/300px-CES_logo.svg.png',
+ 'https://www.ces.tech/'
+)
+ON CONFLICT (slug) DO UPDATE SET
+ name = EXCLUDED.name,
+ description = EXCLUDED.description,
+ event_start_date = EXCLUDED.event_start_date,
+ event_end_date = EXCLUDED.event_end_date,
+ logo_url = EXCLUDED.logo_url,
+ website_url = EXCLUDED.website_url;
+
+-- Insert Cannes Lions 2026
+INSERT INTO working_groups (
+ name, slug, description, committee_type, status, is_private, display_order,
+ event_start_date, event_end_date, auto_archive_after_event,
+ logo_url, website_url
+)
+VALUES (
+ 'Cannes Lions 2026',
+ 'cannes-lions-2026',
+ 'Join AgenticAdvertising.org members at the Cannes Lions International Festival of Creativity. Network, attend sessions together, and explore the future of advertising creativity.',
+ 'industry_gathering',
+ 'active',
+ false,
+ 101,
+ '2026-06-15',
+ '2026-06-19',
+ true,
+ 'https://www.canneslions.com/images/canneslions-logo.svg',
+ 'https://www.canneslions.com/'
+)
+ON CONFLICT (slug) DO UPDATE SET
+ name = EXCLUDED.name,
+ description = EXCLUDED.description,
+ event_start_date = EXCLUDED.event_start_date,
+ event_end_date = EXCLUDED.event_end_date,
+ logo_url = EXCLUDED.logo_url,
+ website_url = EXCLUDED.website_url;
+
+-- Make the dev leader user a leader of CES 2026
+INSERT INTO working_group_leaders (working_group_id, user_id)
+SELECT wg.id, 'user_dev_leader_001'
+FROM working_groups wg
+WHERE wg.slug = 'ces-2026'
+ON CONFLICT (working_group_id, user_id) DO NOTHING;
+
+-- Also make them a member
+INSERT INTO working_group_memberships (working_group_id, workos_user_id, status, joined_at)
+SELECT wg.id, 'user_dev_leader_001', 'active', NOW()
+FROM working_groups wg
+WHERE wg.slug = 'ces-2026'
+ON CONFLICT (working_group_id, workos_user_id) DO NOTHING;
diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts
index 61dbb526a4..52f78215c0 100644
--- a/server/src/middleware/auth.ts
+++ b/server/src/middleware/auth.ts
@@ -173,6 +173,16 @@ export const DEV_USERS: Record
= {
isMember: false,
description: 'User without any organization membership',
},
+ // Committee leader (member who leads a working group but is not a site admin)
+ leader: {
+ id: 'user_dev_leader_001',
+ email: 'leader@test.local',
+ firstName: 'Committee',
+ lastName: 'Leader',
+ isAdmin: false,
+ isMember: true,
+ description: 'Committee leader with working group management access',
+ },
};
// Dev session cookie name