feat(fleet): add shared role semantics (#768)
This commit was merged in pull request #768.
This commit is contained in:
@@ -1,4 +1,15 @@
|
||||
import YAML from 'yaml';
|
||||
import {
|
||||
authorityForCanonicalClass,
|
||||
canonicalizeRoleClass,
|
||||
defaultOverrideDir,
|
||||
defaultRolesDir,
|
||||
extractClassesFromDir,
|
||||
resolvePersonaFrom,
|
||||
type PersonaDirs,
|
||||
type PersonaResolution,
|
||||
type RoleAuthority,
|
||||
} from '../commands/fleet-personas.js';
|
||||
|
||||
export const ROSTER_V2_SUPPORTED_RUNTIMES = ['claude', 'codex', 'opencode', 'pi'] as const;
|
||||
export const ROSTER_V2_REASONING_LEVELS = ['low', 'medium', 'high'] as const;
|
||||
@@ -58,6 +69,80 @@ export interface FleetRosterV2 {
|
||||
readonly agents: readonly FleetRosterV2Agent[];
|
||||
}
|
||||
|
||||
export interface SemanticallyValidatedRosterV2Agent extends FleetRosterV2Agent {
|
||||
readonly requestedClass: string;
|
||||
readonly canonicalClass: string;
|
||||
readonly canonicalToolPolicy: string;
|
||||
readonly persona: PersonaResolution;
|
||||
readonly authority: RoleAuthority;
|
||||
}
|
||||
|
||||
export interface SemanticallyValidatedRosterV2 extends Omit<FleetRosterV2, 'agents'> {
|
||||
readonly agents: readonly SemanticallyValidatedRosterV2Agent[];
|
||||
}
|
||||
|
||||
const PROTECTED_TOOL_POLICY_CLASSES = new Set([
|
||||
'merge-gate',
|
||||
'validator',
|
||||
'orchestrator',
|
||||
'team-leader',
|
||||
'interaction',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Validate filesystem-backed roster semantics after synchronous structural parsing.
|
||||
* Directory scans are batched once and every class must resolve to readable content.
|
||||
*/
|
||||
export async function validateRosterV2Semantics(
|
||||
roster: FleetRosterV2,
|
||||
opts: PersonaDirs = {},
|
||||
): Promise<SemanticallyValidatedRosterV2> {
|
||||
const rolesDir = opts.rolesDir ?? defaultRolesDir(opts.mosaicHome);
|
||||
const overrideDir = opts.overrideDir ?? defaultOverrideDir(opts.mosaicHome);
|
||||
const [base, over] = await Promise.all([
|
||||
extractClassesFromDir(rolesDir),
|
||||
extractClassesFromDir(overrideDir),
|
||||
]);
|
||||
|
||||
const agents: SemanticallyValidatedRosterV2Agent[] = [];
|
||||
for (const agent of roster.agents) {
|
||||
const requestedClass = agent.className;
|
||||
const { canonicalClass } = canonicalizeRoleClass(requestedClass);
|
||||
const canonicalToolPolicy = canonicalizeRoleClass(agent.toolPolicy).canonicalClass;
|
||||
const persona = await resolvePersonaFrom(requestedClass, {
|
||||
rolesDir,
|
||||
overrideDir,
|
||||
base,
|
||||
over,
|
||||
});
|
||||
if (!persona || persona.content.trim() === '') {
|
||||
throw new RosterV2ValidationError(
|
||||
`Roster v2 agent "${agent.name}" class "${requestedClass}" does not resolve to a readable persona.`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
(PROTECTED_TOOL_POLICY_CLASSES.has(canonicalClass) ||
|
||||
PROTECTED_TOOL_POLICY_CLASSES.has(canonicalToolPolicy)) &&
|
||||
canonicalToolPolicy !== canonicalClass
|
||||
) {
|
||||
throw new RosterV2ValidationError(
|
||||
`Roster v2 agent "${agent.name}" protected class "${canonicalClass}" tool policy must match its canonical class; received "${agent.toolPolicy}".`,
|
||||
);
|
||||
}
|
||||
agents.push(
|
||||
Object.freeze({
|
||||
...agent,
|
||||
requestedClass,
|
||||
canonicalClass,
|
||||
canonicalToolPolicy,
|
||||
persona,
|
||||
authority: authorityForCanonicalClass(canonicalClass),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return Object.freeze({ ...roster, agents: Object.freeze(agents) });
|
||||
}
|
||||
|
||||
export class RosterV2ValidationError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
|
||||
Reference in New Issue
Block a user