From f928abd6ceaaae9fabf211e83ef7c8b23b2138fc Mon Sep 17 00:00:00 2001 From: Hibryda Date: Wed, 11 Mar 2026 22:56:52 +0100 Subject: [PATCH] refactor(types): add GroupId and AgentId branded types to ids.ts Extend the branded type system with two new domain types for btmsg/bttask agent and group identifiers. Apply to groups.ts interfaces including agentToProject() domain crossing cast. --- v2/src/lib/types/groups.ts | 12 +++++---- v2/src/lib/types/ids.test.ts | 48 ++++++++++++++++++++++++++++++++++-- v2/src/lib/types/ids.ts | 18 +++++++++++++- 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/v2/src/lib/types/groups.ts b/v2/src/lib/types/groups.ts index ff6e73d..efe7988 100644 --- a/v2/src/lib/types/groups.ts +++ b/v2/src/lib/types/groups.ts @@ -1,8 +1,9 @@ import type { ProviderId } from '../providers/types'; import type { AnchorBudgetScale } from './anchors'; +import type { ProjectId, GroupId, AgentId } from './ids'; export interface ProjectConfig { - id: string; + id: ProjectId; name: string; identifier: string; description: string; @@ -35,8 +36,9 @@ export const AGENT_ROLE_ICONS: Record = { /** Convert a GroupAgentConfig to a ProjectConfig for unified rendering */ export function agentToProject(agent: GroupAgentConfig, groupCwd: string): ProjectConfig { + // Agent IDs serve as project IDs in the workspace (agents render as project boxes) return { - id: agent.id, + id: agent.id as unknown as ProjectId, name: agent.name, identifier: agent.role, description: `${agent.role.charAt(0).toUpperCase() + agent.role.slice(1)} agent`, @@ -58,7 +60,7 @@ export type GroupAgentStatus = 'active' | 'sleeping' | 'stopped'; /** Group-level agent configuration */ export interface GroupAgentConfig { - id: string; + id: AgentId; name: string; role: GroupAgentRole; model?: string; @@ -70,7 +72,7 @@ export interface GroupAgentConfig { } export interface GroupConfig { - id: string; + id: GroupId; name: string; projects: ProjectConfig[]; /** Group-level orchestration agents (Tier 1) */ @@ -80,7 +82,7 @@ export interface GroupConfig { export interface GroupsFile { version: number; groups: GroupConfig[]; - activeGroupId: string; + activeGroupId: GroupId; } /** Derive a project identifier from a name: lowercase, spaces to dashes */ diff --git a/v2/src/lib/types/ids.test.ts b/v2/src/lib/types/ids.test.ts index 43798b3..9cd1f41 100644 --- a/v2/src/lib/types/ids.test.ts +++ b/v2/src/lib/types/ids.test.ts @@ -1,5 +1,11 @@ import { describe, it, expect } from 'vitest'; -import { SessionId, ProjectId, type SessionId as SessionIdType, type ProjectId as ProjectIdType } from './ids'; +import { + SessionId, ProjectId, GroupId, AgentId, + type SessionId as SessionIdType, + type ProjectId as ProjectIdType, + type GroupId as GroupIdType, + type AgentId as AgentIdType, +} from './ids'; describe('branded types', () => { describe('SessionId', () => { @@ -41,12 +47,50 @@ describe('branded types', () => { }); }); + describe('GroupId', () => { + it('creates a GroupId from a string', () => { + const id = GroupId('grp-abc'); + expect(id).toBe('grp-abc'); + }); + + it('is usable as a Map key', () => { + const map = new Map(); + const id = GroupId('grp-1'); + map.set(id, 'test-group'); + expect(map.get(id)).toBe('test-group'); + }); + }); + + describe('AgentId', () => { + it('creates an AgentId from a string', () => { + const id = AgentId('agent-manager'); + expect(id).toBe('agent-manager'); + }); + + it('is usable as a Map key', () => { + const map = new Map(); + const id = AgentId('a1'); + map.set(id, 99); + expect(map.get(id)).toBe(99); + }); + + it('equality works between two AgentIds with same value', () => { + const a = AgentId('a1'); + const b = AgentId('a1'); + expect(a === b).toBe(true); + }); + }); + describe('type safety (compile-time)', () => { - it('both types are strings at runtime', () => { + it('all four types are strings at runtime', () => { const sid = SessionId('s1'); const pid = ProjectId('p1'); + const gid = GroupId('g1'); + const aid = AgentId('a1'); expect(typeof sid).toBe('string'); expect(typeof pid).toBe('string'); + expect(typeof gid).toBe('string'); + expect(typeof aid).toBe('string'); }); }); }); diff --git a/v2/src/lib/types/ids.ts b/v2/src/lib/types/ids.ts index 1a51aca..f704e91 100644 --- a/v2/src/lib/types/ids.ts +++ b/v2/src/lib/types/ids.ts @@ -1,4 +1,4 @@ -// Branded types for domain identifiers — prevents accidental swapping of sessionId/projectId +// Branded types for domain identifiers — prevents accidental swapping of IDs across domains. // These are compile-time only; at runtime they are plain strings. /** Unique identifier for an agent session */ @@ -7,6 +7,12 @@ export type SessionId = string & { readonly __brand: 'SessionId' }; /** Unique identifier for a project */ export type ProjectId = string & { readonly __brand: 'ProjectId' }; +/** Unique identifier for a project group */ +export type GroupId = string & { readonly __brand: 'GroupId' }; + +/** Unique identifier for an agent in the btmsg/bttask system */ +export type AgentId = string & { readonly __brand: 'AgentId' }; + /** Create a SessionId from a raw string */ export function SessionId(value: string): SessionId { return value as SessionId; @@ -16,3 +22,13 @@ export function SessionId(value: string): SessionId { export function ProjectId(value: string): ProjectId { return value as ProjectId; } + +/** Create a GroupId from a raw string */ +export function GroupId(value: string): GroupId { + return value as GroupId; +} + +/** Create an AgentId from a raw string */ +export function AgentId(value: string): AgentId { + return value as AgentId; +}