feat(pi-workflow): reuse standard agent profiles

This commit is contained in:
jb
2026-08-26 22:51:20 -04:00
parent a4784b75c3
commit aa50f0fd16
12 changed files with 606 additions and 54 deletions
+61
View File
@@ -29,6 +29,13 @@ test("configuration defaults to concurrency 8 with immutable ceiling 32", async
});
assert.deepEqual(config, DEFAULT_WORKFLOW_CONFIG);
assert.equal(config.concurrency.ceiling, MAX_CONCURRENCY);
assert.deepEqual(config.standardAgents, {
enabled: true,
roles: {
scout: ["discover"],
worker: ["implement"],
},
});
});
});
@@ -106,6 +113,60 @@ test("untrusted project configuration is never applied", async () => {
});
});
test("trusted configuration can replace or disable standard-agent role overlays", async () => {
await withTempDir(async (directory) => {
const agentDir = path.join(directory, "agent");
await writeJson(path.join(agentDir, "pi-workflow.json"), {
standardAgents: {
roles: {
"codebase-analyzer": ["discover"],
worker: ["implement", "verify"],
},
},
});
let config = await loadWorkflowConfig({
cwd: directory,
agentDir,
projectTrusted: false,
});
assert.deepEqual(config.standardAgents.roles, {
"codebase-analyzer": ["discover"],
worker: ["implement", "verify"],
});
await writeJson(path.join(directory, ".pi", "pi-workflow.json"), {
standardAgents: { enabled: false },
});
config = await loadWorkflowConfig({
cwd: directory,
agentDir,
projectTrusted: true,
});
assert.equal(config.standardAgents.enabled, false);
});
});
test("invalid standard-agent names and roles fail before a run starts", async () => {
await withTempDir(async (directory) => {
const agentDir = path.join(directory, "agent");
await writeJson(path.join(agentDir, "pi-workflow.json"), {
standardAgents: { roles: { Worker: ["implement"] } },
});
await assert.rejects(
loadWorkflowConfig({ cwd: directory, agentDir, projectTrusted: false }),
/lowercase kebab case/,
);
await writeJson(path.join(agentDir, "pi-workflow.json"), {
standardAgents: { roles: { worker: ["remediate"] } },
});
await assert.rejects(
loadWorkflowConfig({ cwd: directory, agentDir, projectTrusted: false }),
/unsupported role remediate/,
);
});
});
test("configuration cannot raise ceilings or activate unaudited tools", async () => {
await withTempDir(async (directory) => {
const agentDir = path.join(directory, "agent");
+65
View File
@@ -7,6 +7,8 @@ import {
MAX_ASSIGNMENTS_PER_RUN,
MAX_CONCURRENCY,
MAX_REVIEW_CYCLES,
PROFILE_ROLES,
type ProfileRole,
type WorkflowConfig,
} from "./domain.ts";
@@ -15,10 +17,13 @@ const TOP_LEVEL_KEYS = new Set([
"maxAssignmentsPerRun",
"maxReviewCycles",
"analysisTools",
"standardAgents",
"worktrees",
]);
const CONCURRENCY_KEYS = new Set(["default", "ceiling"]);
const STANDARD_AGENT_KEYS = new Set(["enabled", "roles"]);
const WORKTREE_KEYS = new Set(["root"]);
const PROFILE_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
export interface LoadWorkflowConfigOptions {
cwd: string;
@@ -97,6 +102,15 @@ function cloneConfig(config: WorkflowConfig): WorkflowConfig {
maxAssignmentsPerRun: config.maxAssignmentsPerRun,
maxReviewCycles: config.maxReviewCycles,
analysisTools: [...config.analysisTools],
standardAgents: {
enabled: config.standardAgents.enabled,
roles: Object.fromEntries(
Object.entries(config.standardAgents.roles).map(([name, roles]) => [
name,
[...roles],
]),
),
},
worktrees: { ...config.worktrees },
};
}
@@ -192,6 +206,57 @@ function applyConfigLayer(
);
}
if (value.standardAgents !== undefined) {
if (!isRecord(value.standardAgents)) {
throw new Error(`${label}.standardAgents must be an object`);
}
assertKnownKeys(
value.standardAgents,
STANDARD_AGENT_KEYS,
`${label}.standardAgents`,
);
if (value.standardAgents.enabled !== undefined) {
if (typeof value.standardAgents.enabled !== "boolean") {
throw new Error(`${label}.standardAgents.enabled must be a boolean`);
}
next.standardAgents.enabled = value.standardAgents.enabled;
}
if (value.standardAgents.roles !== undefined) {
if (!isRecord(value.standardAgents.roles)) {
throw new Error(`${label}.standardAgents.roles must be an object`);
}
const roles: Record<string, ProfileRole[]> = {};
for (const [name, rawRoles] of Object.entries(
value.standardAgents.roles,
)) {
if (!PROFILE_NAME.test(name)) {
throw new Error(
`${label}.standardAgents.roles agent names must be lowercase kebab case`,
);
}
const requested = stringArray(
rawRoles,
`${label}.standardAgents.roles.${name}`,
);
if (requested.length === 0) {
throw new Error(
`${label}.standardAgents.roles.${name} must not be empty`,
);
}
const invalid = requested.filter(
(role) => !PROFILE_ROLES.includes(role as ProfileRole),
);
if (invalid.length > 0) {
throw new Error(
`${label}.standardAgents.roles.${name} contains unsupported role ${invalid.join(", ")}`,
);
}
roles[name] = requested as ProfileRole[];
}
next.standardAgents.roles = roles;
}
}
if (value.worktrees !== undefined) {
if (!isRecord(value.worktrees)) {
throw new Error(`${label}.worktrees must be an object`);
+19 -1
View File
@@ -46,7 +46,12 @@ export interface AgentProfile {
tools?: string[];
selection: ProfileSelection;
instructions: string;
source: "bundled" | "project";
source:
| "bundled"
| "project"
| "standard-package"
| "standard-global"
| "standard-project";
sourcePath: string;
resolvedPath: string;
digest: string;
@@ -446,6 +451,11 @@ export const MAX_CONCURRENCY = 32;
export const MAX_ASSIGNMENTS_PER_RUN = 1_024;
export const MAX_REVIEW_CYCLES = 20;
export interface StandardAgentConfig {
enabled: boolean;
roles: Record<string, ProfileRole[]>;
}
export interface WorkflowConfig {
concurrency: {
default: number;
@@ -454,6 +464,7 @@ export interface WorkflowConfig {
maxAssignmentsPerRun: number;
maxReviewCycles: number;
analysisTools: string[];
standardAgents: StandardAgentConfig;
worktrees: {
root: string;
};
@@ -467,6 +478,13 @@ export const DEFAULT_WORKFLOW_CONFIG: WorkflowConfig = {
maxAssignmentsPerRun: 128,
maxReviewCycles: 3,
analysisTools: [...ANALYSIS_TOOL_ALLOWLIST],
standardAgents: {
enabled: true,
roles: {
scout: ["discover"],
worker: ["implement"],
},
},
worktrees: {
root: ".worktrees",
},
+7 -1
View File
@@ -415,6 +415,12 @@ test("mutating children receive guarded file tools without process execution", a
).profiles;
const profile = profiles.find((candidate) => candidate.roles.includes("implement"));
assert.ok(profile);
const standardWorker = {
...profile,
name: "worker",
source: "standard-package" as const,
tools: [...(profile.tools ?? []), "bash", "subagent"],
};
const inherited = model("provider-a", "parent");
const executor = new PiAssignmentExecutor({
modelRegistry: registry([inherited]),
@@ -476,7 +482,7 @@ test("mutating children receive guarded file tools without process execution", a
runId: "run-mutation",
role: "design",
request: "Generate an owned file",
profile,
profile: standardWorker,
facts: {
stage: "design",
taskKind: "feature",
+1
View File
@@ -61,6 +61,7 @@ export function createWorkflowRuntime(
const catalog = await loadAgentProfiles({
cwd: ctx.cwd,
projectTrusted: ctx.isProjectTrusted(),
standardAgents: config.standardAgents,
});
const executor = new PiAssignmentExecutor({
modelRegistry: ctx.modelRegistry,
+224 -19
View File
@@ -30,6 +30,17 @@ function profileDocument(options: {
return `---\nname: ${options.name}\ndescription: ${options.description ?? options.name}\nroles: ${options.roles ?? "[discover]"}\n${options.extra ?? ""}selection:\n${options.selection ?? " stages: [discover]\n anyTags: []\n allTags: []\n pathGlobs: []\n keywords: []\n"}---\n\n${options.body ?? "Follow the assignment."}\n`;
}
function standardAgentDocument(options: {
name: string;
description?: string;
model?: string;
tools?: string;
thinking?: string;
body?: string;
}) {
return `---\nname: ${options.name}\ndescription: ${options.description ?? options.name}\n${options.model ? `model: ${options.model}\n` : ""}${options.tools ? `tools: ${options.tools}\n` : ""}${options.thinking ? `thinking: ${options.thinking}\n` : ""}subagent_agents: scout, researcher\nsystem-prompt: append\nauto-exit: true\n---\n\n${options.body ?? "Follow the standard assignment."}\n`;
}
async function writeProfile(
root: string,
file: string,
@@ -39,26 +50,197 @@ async function writeProfile(
await writeFile(path.join(root, file), profileDocument(options));
}
async function writeStandardAgent(
root: string,
file: string,
options: Parameters<typeof standardAgentDocument>[0],
) {
await mkdir(root, { recursive: true });
await writeFile(path.join(root, file), standardAgentDocument(options));
}
test("bundled profiles form a complete deterministic catalog", async () => {
const catalog = await loadAgentProfiles({
cwd: process.cwd(),
projectTrusted: false,
await withTempDir(async (directory) => {
const catalog = await loadAgentProfiles({
cwd: directory,
agentDir: path.join(directory, "agent"),
projectTrusted: false,
});
assert.deepEqual(
catalog.profiles.map((profile) => profile.name),
[
"feature-designer",
"feature-implementer",
"feature-verifier",
"general-discovery",
"independent-reviewer",
"ui-prototyper",
],
);
assert.equal(catalog.diagnostics.length, 0);
assert.ok(
catalog.profiles.every((profile) => /^[a-f0-9]{64}$/.test(profile.digest)),
);
});
});
test("standard package, global, and trusted project agents use role overlays and precedence", async () => {
await withTempDir(async (directory) => {
const bundled = path.join(directory, "bundled");
const packageRoot = path.join(directory, "package-agents");
const agentDir = path.join(directory, "agent");
const globalRoot = path.join(agentDir, "agents");
const projectRoot = path.join(directory, ".pi", "agents");
await writeProfile(bundled, "general.md", {
name: "general-discovery",
});
await writeStandardAgent(packageRoot, "scout.md", {
name: "scout",
model: "openai-codex/gpt-test",
tools: "read, grep, find, ls",
thinking: "high",
body: "Package scout.",
});
await writeStandardAgent(packageRoot, "worker.md", {
name: "worker",
tools: "read, write, edit, bash",
body: "Package worker.",
});
await writeStandardAgent(globalRoot, "scout.md", {
name: "scout",
body: "Global scout.",
});
await writeStandardAgent(globalRoot, "unmapped.md", {
name: "unmapped",
});
await writeStandardAgent(projectRoot, "worker.md", {
name: "worker",
body: "Project worker.",
});
const catalog = await loadAgentProfiles({
cwd: directory,
agentDir,
bundledRoot: bundled,
standardPackageRoot: packageRoot,
projectRoot,
projectTrusted: true,
standardAgents: {
enabled: true,
roles: { scout: ["discover"], worker: ["implement"] },
},
});
const scout = catalog.profiles.find((profile) => profile.name === "scout");
assert.equal(scout?.source, "standard-global");
assert.equal(scout?.instructions, "Global scout.");
assert.deepEqual(scout?.roles, ["discover"]);
assert.deepEqual(scout?.selection.stages, ["discover"]);
const worker = catalog.profiles.find((profile) => profile.name === "worker");
assert.equal(worker?.source, "standard-project");
assert.equal(worker?.instructions, "Project worker.");
assert.deepEqual(worker?.roles, ["implement"]);
assert.equal(catalog.profiles.some((profile) => profile.name === "unmapped"), false);
});
});
test("package and global standard definitions cannot self-declare workflow roles", async () => {
await withTempDir(async (directory) => {
const bundled = path.join(directory, "bundled");
const packageRoot = path.join(directory, "package-agents");
await writeProfile(bundled, "general.md", { name: "general-discovery" });
await writeProfile(packageRoot, "worker.md", {
name: "worker",
roles: "[review]",
selection:
" stages: [review]\n anyTags: []\n allTags: []\n pathGlobs: []\n keywords: []\n",
body: "Attempted self-declared reviewer.",
});
await writeProfile(packageRoot, "unmapped.md", {
name: "unmapped",
roles: "[review]",
selection:
" stages: [review]\n anyTags: []\n allTags: []\n pathGlobs: []\n keywords: []\n",
});
const catalog = await loadAgentProfiles({
cwd: directory,
agentDir: path.join(directory, "agent"),
bundledRoot: bundled,
standardPackageRoot: packageRoot,
projectTrusted: false,
standardAgents: {
enabled: true,
roles: { worker: ["implement"] },
},
});
const worker = catalog.profiles.find((profile) => profile.name === "worker");
assert.equal(worker?.source, "standard-package");
assert.deepEqual(worker?.roles, ["implement"]);
assert.deepEqual(worker?.selection.stages, ["implement"]);
assert.equal(catalog.profiles.some((profile) => profile.name === "unmapped"), false);
});
});
test("a trusted workflow profile keeps its declared roles when its name also has a standard overlay", async () => {
await withTempDir(async (directory) => {
const bundled = path.join(directory, "bundled");
const packageRoot = path.join(directory, "package-agents");
const projectRoot = path.join(directory, ".pi", "agents");
await writeProfile(bundled, "general.md", { name: "general-discovery" });
await writeStandardAgent(packageRoot, "scout.md", { name: "scout" });
await writeProfile(projectRoot, "scout.md", {
name: "scout",
roles: "[verify]",
selection:
" stages: [verify]\n anyTags: []\n allTags: []\n pathGlobs: []\n keywords: []\n",
body: "Project verifier.",
});
const catalog = await loadAgentProfiles({
cwd: directory,
agentDir: path.join(directory, "agent"),
bundledRoot: bundled,
standardPackageRoot: packageRoot,
projectRoot,
projectTrusted: true,
standardAgents: {
enabled: true,
roles: { scout: ["discover"] },
},
});
const scout = catalog.profiles.find((profile) => profile.name === "scout");
assert.equal(scout?.source, "project");
assert.deepEqual(scout?.roles, ["verify"]);
assert.deepEqual(scout?.selection.stages, ["verify"]);
assert.equal(scout?.instructions, "Project verifier.");
});
});
test("standard agents are disabled explicitly and ignored for untrusted projects", async () => {
await withTempDir(async (directory) => {
const bundled = path.join(directory, "bundled");
const packageRoot = path.join(directory, "package-agents");
const projectRoot = path.join(directory, ".pi", "agents");
await writeProfile(bundled, "general.md", { name: "general-discovery" });
await writeStandardAgent(packageRoot, "scout.md", { name: "scout" });
await writeStandardAgent(projectRoot, "worker.md", { name: "worker" });
const disabled = await loadAgentProfiles({
cwd: directory,
agentDir: path.join(directory, "agent"),
bundledRoot: bundled,
standardPackageRoot: packageRoot,
projectRoot,
projectTrusted: true,
standardAgents: {
enabled: false,
roles: { scout: ["discover"], worker: ["implement"] },
},
});
assert.deepEqual(disabled.profiles.map((profile) => profile.name), ["general-discovery"]);
});
assert.deepEqual(
catalog.profiles.map((profile) => profile.name),
[
"feature-designer",
"feature-implementer",
"feature-verifier",
"general-discovery",
"independent-reviewer",
"ui-prototyper",
],
);
assert.equal(catalog.diagnostics.length, 0);
assert.ok(
catalog.profiles.every((profile) => /^[a-f0-9]{64}$/.test(profile.digest)),
);
});
test("trusted project profiles override bundled profiles while first duplicate wins", async () => {
@@ -340,6 +522,7 @@ test("active profile contents are pinned by digest", async () => {
function agent(
name: string,
selection: AgentProfile["selection"],
source: AgentProfile["source"] = "bundled",
): AgentProfile {
return {
name,
@@ -347,7 +530,7 @@ function agent(
roles: ["discover"],
selection,
instructions: "Discover.",
source: "bundled",
source,
sourcePath: name,
resolvedPath: name,
digest: name.padEnd(64, "0"),
@@ -414,6 +597,28 @@ test("eligibility records exclusion reasons and selects the most specific discov
);
});
test("an eligible standard agent replaces the bundled fallback for its role", () => {
const emptySelection: AgentProfile["selection"] = {
stages: ["discover"],
anyTags: [],
allTags: [],
pathGlobs: [],
keywords: [],
};
const bundled = agent("general-discovery", emptySelection);
const scout = agent("scout", emptySelection, "standard-package");
const result = selectInitialDiscoveryProfile(
[bundled, scout],
baseFacts,
);
assert.equal(result.selected.name, "scout");
assert.deepEqual(
result.evaluations.find((item) => item.profile.name === "general-discovery")?.reasons,
["eligible standard agent available for role discover"],
);
});
test("eligibility enforces every criterion and uses a code-point name tie-breaker", () => {
const emptySelection: AgentProfile["selection"] = {
stages: ["discover"],
+169 -18
View File
@@ -1,6 +1,7 @@
import { createHash } from "node:crypto";
import { lstat, readdir, readFile, realpath, stat } from "node:fs/promises";
import * as path from "node:path";
import { getAgentDir } from "@earendil-works/pi-coding-agent";
import {
PROFILE_ROLES,
REASONING_EFFORTS,
@@ -11,6 +12,7 @@ import {
type ProfileDiagnostic,
type ProfileRole,
type ProfileSelection,
type StandardAgentConfig,
type WorkflowFacts,
} from "./domain.ts";
@@ -44,11 +46,16 @@ interface LoadSourceResult {
diagnostics: ProfileDiagnostic[];
}
type ProfileSourceMode = "workflow" | "standard" | "project";
export interface LoadAgentProfilesOptions {
cwd: string;
projectTrusted: boolean;
agentDir?: string;
bundledRoot?: string;
standardPackageRoot?: string;
projectRoot?: string;
standardAgents?: StandardAgentConfig;
}
function reason(error: unknown) {
@@ -219,7 +226,7 @@ function parseFrontmatter(
throw new Error(`frontmatter line ${lineIndex + 1}: expected key: value`);
const key = text.slice(0, separator).trim();
const rawValue = text.slice(separator + 1).trim();
if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(key)) {
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(key)) {
throw new Error(
`frontmatter line ${lineIndex + 1}: invalid key ${JSON.stringify(key)}`,
);
@@ -341,6 +348,61 @@ function parseSelection(value: unknown) {
return { selection, warnings };
}
function buildStandardProfile(
parsed: ParsedFrontmatter,
contents: string,
sourcePath: string,
resolvedPath: string,
source: AgentProfile["source"],
fallbackName: string,
roles: ProfileRole[],
) {
const name = optionalString(parsed.values.name, "name") ?? fallbackName;
if (!PROFILE_NAME.test(name)) {
throw new Error("name must be lowercase kebab case");
}
const description =
optionalString(parsed.values.description, "description") ?? name;
const model = optionalString(parsed.values.model, "model");
const effort = optionalString(parsed.values.thinking, "thinking");
if (effort && !REASONING_EFFORTS.includes(effort as never)) {
throw new Error(`thinking must be one of ${REASONING_EFFORTS.join(", ")}`);
}
const tools =
parsed.values.tools === undefined
? undefined
: stringList(parsed.values.tools, "tools", true);
if (tools?.some((tool) => !TOOL_NAME.test(tool))) {
throw new Error("tools contains an invalid tool name");
}
if (parsed.body.length === 0) {
throw new Error("agent instructions must not be empty");
}
const selection: ProfileSelection = {
stages: [...roles],
anyTags: [],
allTags: [],
pathGlobs: [],
keywords: [],
};
const digestInput = `${contents}\nworkflow-role-overlay:${JSON.stringify(roles)}`;
const profile: AgentProfile = {
name,
description,
roles: [...roles],
...(model ? { model } : {}),
...(effort ? { effort: effort as AgentProfile["effort"] } : {}),
...(tools ? { tools } : {}),
selection,
instructions: parsed.body,
source,
sourcePath,
resolvedPath,
digest: createHash("sha256").update(digestInput).digest("hex"),
};
return { profile, warnings: [] as string[] };
}
function buildProfile(
contents: string,
sourcePath: string,
@@ -415,6 +477,8 @@ async function loadSource(
root: string,
source: AgentProfile["source"],
required: boolean,
mode: ProfileSourceMode = "workflow",
standardRoles?: Record<string, ProfileRole[]>,
): Promise<LoadSourceResult> {
const diagnostics: ProfileDiagnostic[] = [];
let resolvedRoot: string;
@@ -466,12 +530,30 @@ async function loadSource(
throw new Error("profile is not a regular file");
}
const contents = await readFile(resolvedPath, "utf8");
const { profile, warnings } = buildProfile(
contents,
sourcePath,
resolvedPath,
source,
);
const parsed = parseFrontmatter(contents, sourcePath);
const declaredName =
optionalString(parsed.values.name, "name") ??
entry.name.replace(/\.md$/, "");
const isStandardDefinition =
mode === "standard" ||
(mode === "project" && parsed.values.roles === undefined);
let built;
if (isStandardDefinition) {
const roles = standardRoles?.[declaredName];
if (!roles) continue;
built = buildStandardProfile(
parsed,
contents,
sourcePath,
resolvedPath,
source === "project" ? "standard-project" : source,
entry.name.replace(/\.md$/, ""),
roles,
);
} else {
built = buildProfile(contents, sourcePath, resolvedPath, source);
}
const { profile, warnings } = built;
for (const message of warnings) {
diagnostics.push({ sourcePath, severity: "warning", message });
}
@@ -535,23 +617,73 @@ export async function loadAgentProfiles(
const bundledRoot =
options.bundledRoot ?? path.join(import.meta.dirname, "agents");
const bundled = await loadSource(bundledRoot, "bundled", true);
const standardAgents = options.standardAgents ?? {
enabled: false,
roles: {},
} satisfies StandardAgentConfig;
const agentDir = options.agentDir ?? getAgentDir();
const standardPackageRoot =
options.standardPackageRoot ??
path.join(
agentDir,
"extensions",
"pi-interactive-subagents",
"agents",
);
const standardPackage = standardAgents.enabled
? await loadSource(
standardPackageRoot,
"standard-package",
false,
"standard",
standardAgents.roles,
)
: { profiles: [], diagnostics: [] };
const standardGlobal = standardAgents.enabled
? await loadSource(
path.join(agentDir, "agents"),
"standard-global",
false,
"standard",
standardAgents.roles,
)
: { profiles: [], diagnostics: [] };
const projectRoot = options.projectTrusted
? (options.projectRoot ?? (await findNearestProjectRoot(options.cwd)))
: undefined;
if (projectRoot) await assertProjectProfileAncestry(projectRoot);
const project = projectRoot
? await loadSource(projectRoot, "project", false)
? await loadSource(
projectRoot,
"project",
false,
"project",
standardAgents.enabled ? standardAgents.roles : undefined,
)
: { profiles: [], diagnostics: [] };
const byName = new Map(
bundled.profiles.map((profile) => [profile.name, profile]),
);
for (const profile of project.profiles) byName.set(profile.name, profile);
for (const profile of standardPackage.profiles) {
byName.set(profile.name, profile);
}
for (const profile of standardGlobal.profiles) {
byName.set(profile.name, profile);
}
for (const profile of project.profiles) {
byName.set(profile.name, profile);
}
return {
profiles: [...byName.values()].sort((left, right) =>
compareText(left.name, right.name),
),
diagnostics: [...bundled.diagnostics, ...project.diagnostics],
diagnostics: [
...bundled.diagnostics,
...standardPackage.diagnostics,
...standardGlobal.diagnostics,
...project.diagnostics,
],
};
}
@@ -637,14 +769,33 @@ export function eligibleProfiles(
facts: WorkflowFacts,
role: ProfileRole,
) {
return profiles
.map((profile) => evaluateProfileEligibility(profile, facts, role))
.sort(
(left, right) =>
Number(right.eligible) - Number(left.eligible) ||
right.specificity - left.specificity ||
compareText(left.profile.name, right.profile.name),
);
const evaluations = profiles.map((profile) =>
evaluateProfileEligibility(profile, facts, role),
);
const hasEligibleStandard = evaluations.some(
(evaluation) =>
evaluation.eligible && evaluation.profile.source.startsWith("standard-"),
);
const ranked = hasEligibleStandard
? evaluations.map((evaluation) =>
evaluation.eligible && evaluation.profile.source === "bundled"
? {
...evaluation,
eligible: false,
reasons: [
...evaluation.reasons,
`eligible standard agent available for role ${role}`,
],
}
: evaluation,
)
: evaluations;
return ranked.sort(
(left, right) =>
Number(right.eligible) - Number(left.eligible) ||
right.specificity - left.specificity ||
compareText(left.profile.name, right.profile.name),
);
}
export function selectInitialDiscoveryProfile(
+6
View File
@@ -161,6 +161,8 @@ test("supported version-zero snapshots migrate missing lifecycle collections", a
delete record.affectedUiSurfaces;
delete record.reviewCycles;
delete record.verificationCycles;
delete (legacy.effectiveConfig as Partial<typeof legacy.effectiveConfig>)
.standardAgents;
const runDirectory = path.join(
directory,
"state",
@@ -181,6 +183,10 @@ test("supported version-zero snapshots migrate missing lifecycle collections", a
assert.deepEqual(loaded.prototypeCandidates, []);
assert.deepEqual(loaded.commandReceipts, []);
assert.equal(loaded.prototypeCleanupCompleted, false);
assert.deepEqual(
loaded.effectiveConfig.standardAgents,
DEFAULT_WORKFLOW_CONFIG.standardAgents,
);
});
});
+8 -4
View File
@@ -12,10 +12,11 @@ import {
stat,
} from "node:fs/promises";
import * as path from "node:path";
import type {
PlanArtifact,
WorkflowReportArtifact,
WorkflowRun,
import {
DEFAULT_WORKFLOW_CONFIG,
type PlanArtifact,
type WorkflowReportArtifact,
type WorkflowRun,
} from "./domain.ts";
import { deriveRunLabel } from "./run-label.ts";
@@ -299,6 +300,9 @@ export class WorkflowStore {
run.affectedUiSurfaces ??= [];
run.reviewCycles ??= 0;
run.verificationCycles ??= 0;
run.effectiveConfig.standardAgents ??= structuredClone(
DEFAULT_WORKFLOW_CONFIG.standardAgents,
);
for (const assignment of run.assignments) {
if (assignment.gitBefore) assignment.gitBefore.pathStatuses ??= {};
if (assignment.gitAfter) assignment.gitAfter.pathStatuses ??= {};