Scope SOP and API catalogues to the agent's role

Every agent received the full 15-SOP, 11-API-doc bundle regardless of
what the agent was authorised to do — individual contributors were
paying token cost on hiring pipelines, budget requests, and sprint
management they had no authority to touch. The prompt-cache prefix
also grew monotonically with every SOP added across the product
lifetime.

Introduce role-scoped bundles (executive / manager / IC) keyed on the
same isCEO / isManager split the rest of the system prompt already
uses. Each bundle lists the procedures and endpoints the role can
actually execute under its decision authority. Shared SOPs (standup,
communication, quality, incident response, knowledge) appear in every
bundle so no role loses coverage of universal behaviour.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-19 19:15:24 -04:00
1 parent 9c65291973
commit 5dd74a477d
3 files changed
+112 -15

No files matched your search

+50 -6
View File
@@ -13,10 +13,52 @@ import meetingsEn from "./api-docs/meetings.en.md?raw";
import agentsEn from "./api-docs/agents.en.md?raw";
import sprintsEn from "./api-docs/sprints.en.md?raw";
const API_DOC_SECTIONS: string[] = [
headerEn, agentControlEn, tasksEn, goalsProjectsEn, approvalsEn,
messagesEn, filesEn, knowledgeEn, meetingsEn, agentsEn, sprintsEn,
];
/**
* Role-scoped API documentation bundles.
*
* Every agent used to receive the full endpoint catalogue. Individual
* contributors paid the token cost for hiring / budget / org-design
* endpoints they had no authority to call; cache churn followed any
* api-docs edit. Scope each bundle to the operations the role can
* actually perform under its decision authority.
*/
export type ApiAudience = "executive" | "manager" | "individual_contributor";
const SECTIONS_BY_ROLE: Record<ApiAudience, string[]> = {
executive: [
headerEn,
agentControlEn,
tasksEn,
goalsProjectsEn,
approvalsEn,
messagesEn,
filesEn,
knowledgeEn,
meetingsEn,
agentsEn,
sprintsEn,
],
manager: [
headerEn,
agentControlEn,
tasksEn,
goalsProjectsEn,
approvalsEn,
messagesEn,
filesEn,
knowledgeEn,
meetingsEn,
sprintsEn,
],
individual_contributor: [
headerEn,
tasksEn,
messagesEn,
filesEn,
knowledgeEn,
meetingsEn,
],
};
function render(template: string, ctx: PromptContext): string {
const connectorIds = ctx.allConnectors && ctx.allConnectors.length > 0
@@ -30,7 +72,9 @@ function render(template: string, ctx: PromptContext): string {
.replace(/\{\{connectorIds\}\}/g, connectorIds);
}
export function buildAPISection(ctx: PromptContext): string {
const body = API_DOC_SECTIONS.map((s) => render(s, ctx).trimEnd()).join("\n\n");
export function buildAPISection(ctx: PromptContext, audience: ApiAudience): string {
const body = SECTIONS_BY_ROLE[audience]
.map((s) => render(s, ctx).trimEnd())
.join("\n\n");
return `${body}\n${buildSocialSection(ctx)}`;
}
+59 -7
View File
@@ -14,15 +14,67 @@ import sop013En from "./sops/sop-013-incident.en.md?raw";
import sop014En from "./sops/sop-014-hiring-pipeline.en.md?raw";
import sop015En from "./sops/sop-015-knowledge.en.md?raw";
const SOPS_IN_ORDER: string[] = [
sop001En, sop002En, sop003En, sop004En, sop005En,
sop006En, sop007En, sop008En, sop009En, sop010En,
sop011En, sop012En, sop013En, sop014En, sop015En,
];
/**
* Role-scoped SOP bundles.
*
* The system prompt previously concatenated all 15 SOPs for every agent
* regardless of role. Individual contributors were receiving the hiring
* pipeline, cadence, and budget SOPs they had no authority to act on;
* every wake paid the token bill. The prompt-cache prefix also grew
* with each SOP added over the life of the product.
*
* Each role receives the procedures it can execute under its decision
* authority (see `operating-en.ts#authorityRules`). SOPs that every
* agent must know regardless of seat — standup, communication,
* incident response, knowledge contribution — appear in every bundle.
*/
const SOP_BY_ROLE = {
executive: [
sop001En, // standup
sop003En, // delegation
sop004En, // code-review (CEO still reviews top-level deliverables)
sop005En, // hiring
sop006En, // escalation
sop007En, // communication
sop008En, // cross-department
sop009En, // documents
sop010En, // budget
sop012En, // cadence
sop013En, // incident
sop014En, // hiring-pipeline
sop015En, // knowledge
],
manager: [
sop001En, // standup
sop002En, // task-lifecycle
sop003En, // delegation
sop004En, // code-review
sop006En, // escalation
sop007En, // communication
sop008En, // cross-department
sop009En, // documents
sop011En, // quality
sop013En, // incident
sop015En, // knowledge
],
individual_contributor: [
sop001En, // standup
sop002En, // task-lifecycle
sop004En, // code-review
sop006En, // escalation
sop007En, // communication
sop009En, // documents
sop011En, // quality
sop015En, // knowledge
],
} as const;
export type SopAudience = keyof typeof SOP_BY_ROLE;
const SOP_HEADING = "# Standard Operating Procedures";
export function buildSOPSection(): string {
const body = SOPS_IN_ORDER.map((text) => text.trimEnd()).join("\n\n");
export function buildSOPSection(audience: SopAudience): string {
const body = SOP_BY_ROLE[audience].map((text) => text.trimEnd()).join("\n\n");
return `${SOP_HEADING}\n\n${body}\n`;
}
+3 -2
View File
@@ -198,8 +198,9 @@ export function buildSystemPrompt(ctx: PromptContext): string {
const approvalsSection = buildPendingApprovalsSection(ctx.pendingApprovals);
if (approvalsSection) sections.push(approvalsSection);
sections.push(buildSOPSection());
sections.push(buildAPISection(ctx));
const audience = isCEO ? "executive" : isManager ? "manager" : "individual_contributor";
sections.push(buildSOPSection(audience));
sections.push(buildAPISection(ctx, audience));
if (ctx.recentComments.length > 0) {
const lines = ctx.recentComments.slice(0, 10)