rigjs
Version:
A multi-repos dev tool based on yarn and git.Rigjs is intended to be the simplest way to develop,share and deliver codes between different developers or different projects.
122 lines (107 loc) • 5.07 kB
text/typescript
import fs from 'fs';
import path from 'path';
import { crewPaths } from './paths';
export interface CrewRoleDefinition {
name: string;
title: string;
folder: string;
description?: string;
agent?: string;
defaultExecutor?: string;
promptPath?: string;
configPath?: string;
builtIn?: boolean;
}
export const BUILTIN_ROLE_NAMES = ['orchestrator', 'designer', 'pm', 'coder', 'tester', 'researcher'];
const BUILTIN_ROLES: CrewRoleDefinition[] = [
{ name: 'orchestrator', title: 'Orchestrator', folder: 'Orchestrator', description: 'Orchestrates across and within projects: selects projects, sequences cross-project dependencies, dispatches engines, manages pending questions and the dashboard. As project manager, decomposes analysis into tasks, schedules, dispatches develop/verify, tracks status, and drives merges.', builtIn: true },
{ name: 'designer', title: 'Designer', folder: 'Designer', description: 'Reviews user flows, interaction details, information architecture, and visual fit.', builtIn: true },
{ name: 'pm', title: 'PM', folder: 'PM', description: 'Turns goals into PRDs, scope boundaries, acceptance criteria, and open questions.', builtIn: true },
{ name: 'coder', title: 'Coder', folder: 'Coder', description: 'Implements project-scoped code tasks assigned by a Project Owner or the Orchestrator.', builtIn: true },
{ name: 'tester', title: 'Tester', folder: 'Tester', description: 'Plans and runs verification, defaulting frontend work to PRD-scoped Playwright E2E.', builtIn: true },
{ name: 'researcher', title: 'Researcher', folder: 'Researcher', description: 'Produces source-backed research reports and keeps a lightweight research index.', builtIn: true },
];
export function normalizeRoleName(input: string): string {
const name = input.trim().toLowerCase().replace(/\s+/g, '-');
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
throw new Error(`invalid role name "${input}". Use lowercase letters, numbers, and hyphens.`);
}
return name;
}
export function normalizeRoleNames(names: string[]): string[] {
const out: string[] = [];
const seen = new Set<string>();
for (const raw of names) {
const name = normalizeRoleName(raw);
if (seen.has(name)) continue;
seen.add(name);
out.push(name);
}
return out;
}
export function roleFolderName(name: string): string {
return titleFromRoleName(name).replace(/[^A-Za-z0-9]/g, '');
}
export function roleConfigDir(name: string): string {
return path.join(crewPaths.rolesDir, normalizeRoleName(name));
}
export function roleConfigPath(name: string): string {
return path.join(roleConfigDir(name), 'role.json');
}
export function loadGlobalRoleNames(): string[] {
return loadCustomRoleDefinitions().map(r => r.name);
}
export function roleDefinitionsForCrew(crew: { roles?: string[] }): CrewRoleDefinition[] {
const known = new Map<string, CrewRoleDefinition>();
for (const role of BUILTIN_ROLES) known.set(role.name, role);
for (const role of loadCustomRoleDefinitions()) known.set(role.name, role);
const names = normalizeRoleNames(crew.roles && crew.roles.length ? crew.roles : BUILTIN_ROLE_NAMES);
return names.map(name => known.get(name) || fallbackRole(name));
}
export function loadCustomRoleDefinitions(): CrewRoleDefinition[] {
if (!fs.existsSync(crewPaths.rolesDir)) return [];
const roles: CrewRoleDefinition[] = [];
for (const entry of fs.readdirSync(crewPaths.rolesDir)) {
const cfg = path.join(crewPaths.rolesDir, entry, 'role.json');
if (!fs.existsSync(cfg)) continue;
try {
const raw = JSON.parse(fs.readFileSync(cfg, 'utf8')) as Partial<CrewRoleDefinition>;
if (!raw.name) continue;
const name = normalizeRoleName(raw.name);
roles.push({
name,
title: raw.title || titleFromRoleName(name),
folder: raw.folder || path.join('Roles', name),
description: raw.description,
agent: raw.agent,
defaultExecutor: raw.defaultExecutor,
promptPath: raw.promptPath || path.join(roleConfigDir(name), 'prompt.md'),
configPath: cfg,
});
} catch {
// Invalid role configs are ignored by passive loaders; `rig crew role show`
// surfaces parse errors when the user inspects a specific role.
}
}
return roles.sort((a, b) => a.name.localeCompare(b.name));
}
export function roleByName(name: string, crew?: { roles?: string[] }): CrewRoleDefinition | undefined {
const normalized = normalizeRoleName(name);
const defs = crew ? roleDefinitionsForCrew(crew) : [...BUILTIN_ROLES, ...loadCustomRoleDefinitions()];
return defs.find(r => r.name === normalized);
}
export function titleFromRoleName(name: string): string {
return name
.split('-')
.filter(Boolean)
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
}
function fallbackRole(name: string): CrewRoleDefinition {
return {
name,
title: titleFromRoleName(name),
folder: path.join('Roles', name),
description: 'Custom role without a global role.json definition.',
};
}