/** * Team roster builder (specs/2026-10-10-team-roster.md). * * Pure functions that turn the harness's own records (teams, projects, * project tickets, WorkItems, Slack channel mappings) into a short roster of * who owns what, render it under a token budget, and rank teams and members * for a free-text owner request. No I/O and no LLM: the same input always * gives the same roster, so every part is unit-testable. * * @module services/team-roster/team-roster.builder */ import type { Project, Team } from '../../types/index.js'; import type { ProjectTicket } from '../../types/project-ticket.types.js'; import type { WorkItem } from '../../types/v2/work-item.types.js'; /** Ticket fields the roster reads. */ export type RosterTicket = Pick & { /** Absolute path of the project the ticket lives in */ projectPath: string; }; /** WorkItem fields the roster reads. */ export type RosterWorkItem = Pick & Partial>; /** Everything the roster is built from. */ export interface RosterSources { teams: readonly Team[]; projects: ReadonlyArray>; tickets: readonly RosterTicket[]; workItems: readonly RosterWorkItem[]; /** Slack channel name per team id */ channels?: ReadonlyMap; now: Date; } /** Where a team's charter text came from. */ export type CharterSource = 'lead' | 'description' | 'derived'; /** One member as the roster shows it. */ export interface RosterMember { name: string; role: string; isLead: boolean; /** Skill tags, job title and owned domains, most specific first */ specialties: string[]; /** Session name, for delegating to the member directly */ session: string; /** Start of the member's own brief, used for matching only */ about: string; } /** One line of "currently working on" or "finished this week". */ export interface RosterWork { /** Ticket id (`TKT-12`), or the first 8 chars of a WorkItem id */ ref: string; title: string; status: string; /** Member name, when known */ who?: string; /** ISO time of the last change */ at: string; } /** One team as the roster shows it. */ export interface RosterTeam { id: string; name: string; parent?: string; charter: string; charterSource: CharterSource; leads: string[]; members: RosterMember[]; projects: string[]; channel?: string; paused: boolean; open: RosterWork[]; /** Finished in the last {@link TEAM_ROSTER_CONSTANTS.RECENT_DAYS} days */ recentDone: RosterWork[]; /** Scheduled runs of recurring triggers seen this week (not listed) */ routineRuns: number; backlog: number; /** Older ticket titles, for matching only */ history: string[]; } /** * Collapse whitespace and cut to a length, marking the cut. * * @param text - Any text * @param max - Longest result (chars) * @returns One line of at most `max` chars */ export declare function clip(text: string, max: number): string; /** * Build the roster from the harness's records. * * Charter precedence: the lead's `charter`, then `description` (when it says * something), then one derived from roles, projects and recent titles. Work * is attributed to a team by ticket `team`, assignee session, WorkItem target * session or `metadata.teamId`; a ticket with no owner goes to the only team * on its project. Recurring-trigger runs are counted, not listed. * * @param src - Records and the clock * @returns One entry per live team, busiest first */ export declare function buildRoster(src: RosterSources): RosterTeam[]; /** * Token estimate that counts each CJK character as a token (the plain * 4-chars-per-token rule undercounts Chinese and Japanese several times over). * * @param text - Any text * @returns Estimated tokens */ export declare function estimateRosterTokens(text: string): number; /** * Render the roster under a token budget. The richest detail tier that fits * wins; when even the leanest does not, the least active teams drop to a * name-only line. * * @param teams - Roster from {@link buildRoster} * @param maxTokens - Budget (defaults to the prompt budget) * @returns Markdown, or '' when there are no teams */ export declare function renderRoster(teams: readonly RosterTeam[], maxTokens?: number): string; /** One ranked answer of {@link findOwnersFor}. */ export interface OwnerCandidate { team: string; teamId: string; /** Who to hand the request to: the team lead, else the best member */ handTo: string; /** Member whose recent work or specialties fit best, when one stands out */ likelyDoer?: string; score: number; /** Request words that matched */ matched: string[]; paused: boolean; /** Another team scored close to this one */ ambiguous: boolean; charterSource: CharterSource; } /** * Rank teams (and the member within each) for a free-text owner request. * * BM25 over each team's charter, name, projects, member roles and specialties * and recent work titles, plus a flat boost when the request names a member * or team outright. The hand-off target is the team lead (the rule for new * work); `likelyDoer` names the member whose own work or skills fit best. * * @param teams - Roster from {@link buildRoster} * @param request - What the owner asked for * @param limit - Candidates to return * @returns Best first; empty when nothing matches */ export declare function findOwnersFor(teams: readonly RosterTeam[], request: string, limit?: number): OwnerCandidate[]; //# sourceMappingURL=team-roster.builder.d.ts.map