Skip to content

Commit 35044fd

Browse files
recuu-pfegclaude
andcommitted
feat: integrate skills into Playbook — tag-based knowledge injection
Replace hardcoded skills/index.ts with Playbook-based skill rules. Skills are now category="skill" playbook rules with tags, injected only when task labels match rule tags (pinpoint injection). - fetchPlaybookPrompt now accepts taskTags parameter - All 3 skill injection points (spawn_reviewer, review_changes, toban review) use Playbook API instead of hardcoded getSkillKnowledge - Task labels are extracted and passed as tags for matching Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4975686 commit 35044fd

3 files changed

Lines changed: 34 additions & 27 deletions

File tree

src/agent-templates.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -670,16 +670,15 @@ export async function executeActions(
670670
const { PROMPT_TEMPLATES } = await import("./prompts/templates.js");
671671
const typeHints = JSON.parse(PROMPT_TEMPLATES["reviewer-type-hints"] || "{}") as Record<string, string>;
672672

673+
// Fetch playbook rules for reviewer, including skill rules matching task labels
673674
let customRules = "";
674-
try { customRules = await ctx.api.fetchPlaybookPrompt("reviewer") || ""; } catch { /* non-fatal */ }
675-
676-
// Inject skills knowledge if task has skills defined
677-
const taskSkills = (ctx.task as Record<string, unknown>).skills as string[] | null;
678-
if (taskSkills && taskSkills.length > 0) {
679-
const { getSkillKnowledge } = await import("./prompts/skills/index.js");
680-
const skillRules = getSkillKnowledge(taskSkills);
681-
if (skillRules) customRules += "\n\n" + skillRules;
682-
}
675+
const taskLabels: string[] = (() => {
676+
const raw = (ctx.task as Record<string, unknown>).labels;
677+
if (Array.isArray(raw)) return raw;
678+
if (typeof raw === "string") { try { return JSON.parse(raw); } catch { return []; } }
679+
return [];
680+
})();
681+
try { customRules = await ctx.api.fetchPlaybookPrompt("reviewer", taskLabels) || ""; } catch { /* non-fatal */ }
683682

684683
const reviewerTemplate = DEFAULT_TEMPLATES.find((t) => t.id === "reviewer")!;
685684
const reviewCriteria = [
@@ -851,13 +850,14 @@ export async function executeActions(
851850
taskDescription: ctx.task.description || "(no description)",
852851
taskTypeHint: typeHints[taskType] || typeHints.implementation || "",
853852
customReviewRules: await (async () => {
853+
const labels: string[] = (() => {
854+
const raw = (ctx.task as Record<string, unknown>).labels;
855+
if (Array.isArray(raw)) return raw;
856+
if (typeof raw === "string") { try { return JSON.parse(raw); } catch { return []; } }
857+
return [];
858+
})();
854859
let rules = "";
855-
try { rules = await ctx.api.fetchPlaybookPrompt("reviewer") || ""; } catch { /* */ }
856-
const taskSkills = (ctx.task as Record<string, unknown>).skills as string[] | null;
857-
if (taskSkills?.length) {
858-
const { getSkillKnowledge } = await import("./prompts/skills/index.js");
859-
rules += "\n\n" + getSkillKnowledge(taskSkills);
860-
}
860+
try { rules = await ctx.api.fetchPlaybookPrompt("reviewer", labels) || ""; } catch { /* */ }
861861
return rules ? `\n## Project-Specific Review Rules\n${rules}` : "";
862862
})()
863863
});

src/api-client.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export interface ApiClient {
9494
}): Promise<void>;
9595
submitRetroComment(sprintNumber: number, data: RetroCommentInput): Promise<void>;
9696
reportProgress(data: ProgressReport): Promise<void>;
97-
fetchPlaybookPrompt(agentName?: string): Promise<string>;
97+
fetchPlaybookPrompt(agentName?: string, taskTags?: string[]): Promise<string>;
9898
fetchMessages(channel: string): Promise<Message[]>;
9999
sendMessage(from: string, to: string, content: string): Promise<void>;
100100
fetchMySecrets(): Promise<Record<string, string>>;
@@ -265,9 +265,12 @@ export function createApiClient(apiUrl: string, apiKey: string): ApiClient {
265265
}
266266
},
267267

268-
async fetchPlaybookPrompt(agentName?: string): Promise<string> {
268+
async fetchPlaybookPrompt(agentName?: string, taskTags?: string[]): Promise<string> {
269269
try {
270-
const qs = agentName ? `?agent=${encodeURIComponent(agentName)}` : "";
270+
const params = new URLSearchParams();
271+
if (agentName) params.set("agent", agentName);
272+
if (taskTags?.length) params.set("tags", taskTags.join(","));
273+
const qs = params.toString() ? `?${params.toString()}` : "";
271274
const res = await fetch(`${apiUrl}/api/v1/playbook/prompt${qs}`, { headers });
272275
if (!res.ok) return "";
273276
const data = (await res.json()) as { prompt: string };

src/cli.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,8 @@ async function runLoop(cliArgs: CliArgs, runner: AgentRunner): Promise<void> {
265265
const agentName = task.owner ?? "builder";
266266
const apiDocs = await api.fetchApiDocs(agentName);
267267
const taskType = (task as Record<string, unknown>).type as string | undefined;
268+
const rawLabels = (task as Record<string, unknown>).labels;
269+
const taskLabels: string[] = Array.isArray(rawLabels) ? rawLabels : typeof rawLabels === "string" ? (() => { try { return JSON.parse(rawLabels); } catch { return []; } })() : [];
268270
const agentTemplate = matchTemplate(taskType, agentName);
269271
const isReadOnly = agentTemplate.tools !== "all";
270272
ui.info(`[task] Template: "${agentTemplate.id}"${isReadOnly ? ` (read-only: ${(agentTemplate.tools as string[]).join(", ")})` : ""}`);
@@ -344,7 +346,7 @@ async function runLoop(cliArgs: CliArgs, runner: AgentRunner): Promise<void> {
344346
taskPriority: typeof task.priority === "string" ? task.priority : `p${task.priority}`,
345347
taskType, apiUrl: cliArgs.apiUrl, apiKey: cliArgs.apiKey,
346348
language: ctx.language,
347-
playbookRules: (await ctx.api.fetchPlaybookPrompt(agentName)) || ctx.playbookRules,
349+
playbookRules: (await ctx.api.fetchPlaybookPrompt(agentName, taskLabels)) || ctx.playbookRules,
348350
targetRepo: task.target_repo ?? undefined,
349351
apiDocs: apiDocs || undefined, engineHint: getEngine(cliArgs.engine).promptHint,
350352
pastFailures: pastFailures.length > 0 ? pastFailures : undefined,
@@ -753,15 +755,17 @@ async function handleReview(apiUrl: string, apiKey: string, taskId?: string, ski
753755
const taskType = (task as Record<string, unknown>).type as string || "implementation";
754756
const typeHints = JSON.parse(PROMPT_TEMPLATES["reviewer-type-hints"] || "{}") as Record<string, string>;
755757

758+
// Fetch playbook rules including skill rules matching task labels or --skill args
759+
const reviewTags = skills || (() => {
760+
const raw = (task as Record<string, unknown>).labels;
761+
if (Array.isArray(raw)) return raw as string[];
762+
if (typeof raw === "string") { try { return JSON.parse(raw) as string[]; } catch { return []; } }
763+
return [];
764+
})();
756765
let customRules = "";
757-
try { customRules = await api.fetchPlaybookPrompt("reviewer") || ""; } catch { /* non-fatal */ }
758-
759-
// Inject skills knowledge
760-
const activeSkills = skills || (task as Record<string, unknown>).skills as string[] | null || [];
761-
if (activeSkills.length > 0) {
762-
const { getSkillKnowledge } = await import("./prompts/skills/index.js");
763-
customRules += "\n\n" + getSkillKnowledge(activeSkills);
764-
ui.info(`[review] Skills injected: ${activeSkills.join(", ")}`);
766+
try { customRules = await api.fetchPlaybookPrompt("reviewer", reviewTags) || ""; } catch { /* non-fatal */ }
767+
if (reviewTags.length > 0) {
768+
ui.info(`[review] Tags for skill matching: ${reviewTags.join(", ")}`);
765769
}
766770

767771
const reviewSystem = interpolate(PROMPT_TEMPLATES["reviewer-system"] || "", {

0 commit comments

Comments
 (0)