feat(FN-3097): wire plugin prompt contributions into execution surfaces

- Add plugin prompt contribution support across executor, triage, reviewer, and heartbeat instruction builders
- Thread plugin runner context into execution prompt assembly and preserve source issue commit reference hints
- Expand engine tests to cover prompt contribution injection behavior and regression scenarios
- Update architecture and plugin authoring docs with prompt surface integration details

Fusion-Task-Id: FN-3097
This commit is contained in:
Fusion
2026-05-05 23:31:06 -07:00
committed by gsxdsm
parent b312ca4122
commit ef1ae3eac3
10 changed files with 335 additions and 20 deletions

View File

@@ -20,7 +20,7 @@ A comprehensive guide to creating Fusion plugins that extend the task board with
14. [Example Plugins](#14-example-plugins) 14. [Example Plugins](#14-example-plugins)
15. [Registering Skills](#15-registering-skills) 15. [Registering Skills](#15-registering-skills)
16. [Registering Workflow Steps](#16-registering-workflow-steps) 16. [Registering Workflow Steps](#16-registering-workflow-steps)
17. [Plugin Prompt Contributions](#17-plugin-prompt-contributions) 17. [Contributing Prompt Modifications](#17-contributing-prompt-modifications)
18. [Plugin Binary Setup Hooks](#18-plugin-binary-setup-hooks) 18. [Plugin Binary Setup Hooks](#18-plugin-binary-setup-hooks)
--- ---
@@ -1240,7 +1240,7 @@ const workflowSteps: PluginWorkflowStepContribution[] = [
Use `mode: "prompt" | "script"` and `toolMode: "readonly" | "coding"`. Use `mode: "prompt" | "script"` and `toolMode: "readonly" | "coding"`.
## 17. Plugin Prompt Contributions ## 17. Contributing Prompt Modifications
Prompt contributions let a plugin inject additional instructions into specific prompt surfaces. Prompt contributions let a plugin inject additional instructions into specific prompt surfaces.
@@ -1251,6 +1251,12 @@ Supported surfaces:
- `reviewer` - `reviewer`
- `heartbeat` - `heartbeat`
Each contribution uses the `PluginPromptContribution` shape:
- `surface`: one of the five supported surfaces
- `content`: prompt text to inject
- `position?`: `"append"` (default) or `"prepend"`
- `condition?`: optional human-readable condition note
```typescript ```typescript
import type { PluginPromptContributions } from "@fusion/plugin-sdk"; import type { PluginPromptContributions } from "@fusion/plugin-sdk";
@@ -1258,10 +1264,10 @@ const promptContributions: PluginPromptContributions = {
enabledByDefault: false, enabledByDefault: false,
contributions: [ contributions: [
{ {
surface: "reviewer", surface: "executor-system",
position: "append", position: "append",
content: "Always call out missing tests and unsafe assumptions.", content: "Always summarize browser-derived evidence with source URLs.",
condition: "Only for backend code changes", condition: "When browser tooling is available",
}, },
], ],
}; };

View File

@@ -210,6 +210,18 @@ Concrete references:
- `prompt-overrides.ts` defines prompt key catalogs and per-role override validation - `prompt-overrides.ts` defines prompt key catalogs and per-role override validation
- Provides override resolution/validation helpers (`resolvePrompt`, `resolveRolePrompts`, `assertValidPromptOverrideMap`) - Provides override resolution/validation helpers (`resolvePrompt`, `resolveRolePrompts`, `assertValidPromptOverrideMap`)
### Plugin Prompt Contributions
- Plugin prompt contributions are filtered per surface through `PluginRunner.getPromptContributionsForSurface(surface)`.
- Prompt assembly uses `buildPluginPromptSection(surface, pluginRunner)` in `packages/engine/src/agent-instructions.ts`.
- Supported prompt surfaces:
- `executor-system`
- `executor-task`
- `triage`
- `reviewer`
- `heartbeat`
- Integration points append the built plugin section to the role-specific system/task prompt only when contributions exist, preserving existing prompts when no plugins contribute.
### Agent Permissions ### Agent Permissions
- `agent-permissions.ts` normalizes permissions and computes effective access state - `agent-permissions.ts` normalizes permissions and computes effective access state

View File

@@ -8,6 +8,7 @@ import {
resolveAgentInstructionsWithRatings, resolveAgentInstructionsWithRatings,
buildAgentChatPrompt, buildAgentChatPrompt,
buildSystemPromptWithInstructions, buildSystemPromptWithInstructions,
buildPluginPromptSection,
ensureDefaultHeartbeatProcedureFile, ensureDefaultHeartbeatProcedureFile,
resolveAgentHeartbeatProcedure, resolveAgentHeartbeatProcedure,
} from "../agent-instructions.js"; } from "../agent-instructions.js";
@@ -555,6 +556,38 @@ describe("buildSystemPromptWithInstructions", () => {
}); });
}); });
describe("buildPluginPromptSection", () => {
it("returns empty string when pluginRunner is undefined", () => {
expect(buildPluginPromptSection("triage", undefined)).toBe("");
});
it("returns empty string when no contributions match", () => {
const pluginRunner = {
getPromptContributionsForSurface: vi.fn().mockReturnValue([]),
};
expect(buildPluginPromptSection("triage", pluginRunner as any)).toBe("");
});
it("formats grouped plugin sections and prepend-before-append ordering", () => {
const pluginRunner = {
getPromptContributionsForSurface: vi.fn().mockReturnValue([
{ pluginId: "plugin-b", contribution: { surface: "triage", content: "append B1" }, config: {} },
{ pluginId: "plugin-a", contribution: { surface: "triage", content: "prepend A1", position: "prepend" }, config: {} },
{ pluginId: "plugin-a", contribution: { surface: "triage", content: "prepend A2", position: "prepend" }, config: {} },
{ pluginId: "plugin-c", contribution: { surface: "triage", content: "append C1", position: "append" }, config: {} },
]),
};
const result = buildPluginPromptSection("triage", pluginRunner as any);
expect(result).toContain("## Plugin: plugin-a\n\nprepend A1\n\nprepend A2");
expect(result).toContain("## Plugin: plugin-b\n\nappend B1");
expect(result).toContain("## Plugin: plugin-c\n\nappend C1");
expect(result.indexOf("## Plugin: plugin-a")).toBeLessThan(result.indexOf("## Plugin: plugin-b"));
});
});
describe("diagnostics logging", () => { describe("diagnostics logging", () => {
let testDir: string; let testDir: string;

View File

@@ -146,6 +146,47 @@ describe("reviewStep — spec review type", () => {
expect(opts.systemPrompt).toContain("Mission clarity"); expect(opts.systemPrompt).toContain("Mission clarity");
}); });
it("appends reviewer plugin prompt contributions when provided", async () => {
mockedCreateFnAgent.mockResolvedValue(
createMockSession("### Verdict: APPROVE\n### Summary\nGood spec."),
);
const pluginRunner = {
getPromptContributionsForSurface: vi.fn().mockReturnValue([
{ pluginId: "plugin-review", contribution: { content: "Follow plugin reviewer rubric." } },
]),
};
await reviewStep(
"/tmp/worktree", "FN-050", 0, "Spec Review", "spec", "# Task: KB-050",
undefined,
{ pluginRunner: pluginRunner as any },
);
const opts = mockedCreateFnAgent.mock.calls[0][0];
expect(opts.systemPrompt).toContain("## Plugin: plugin-review");
expect(opts.systemPrompt).toContain("Follow plugin reviewer rubric.");
});
it("keeps reviewer system prompt unchanged when no reviewer plugin contributions exist", async () => {
mockedCreateFnAgent.mockResolvedValue(
createMockSession("### Verdict: APPROVE\n### Summary\nGood spec."),
);
const pluginRunner = {
getPromptContributionsForSurface: vi.fn().mockReturnValue([]),
};
await reviewStep(
"/tmp/worktree", "FN-050", 0, "Spec Review", "spec", "# Task: KB-050",
undefined,
{ pluginRunner: pluginRunner as any },
);
const opts = mockedCreateFnAgent.mock.calls[0][0];
expect(opts.systemPrompt).not.toContain("## Plugin:");
});
it("injects read-only memory instructions and tools when project memory is enabled", async () => { it("injects read-only memory instructions and tools when project memory is enabled", async () => {
mockedCreateFnAgent.mockResolvedValue( mockedCreateFnAgent.mockResolvedValue(
createMockSession("### Verdict: APPROVE\n### Summary\nGood spec."), createMockSession("### Verdict: APPROVE\n### Summary\nGood spec."),

View File

@@ -697,6 +697,103 @@ describe("fast-mode triage", () => {
expect(capturedSystemPrompt).toContain("## Review Level"); expect(capturedSystemPrompt).toContain("## Review Level");
}); });
it("includes triage plugin contributions when provided", async () => {
const task = createTriageTask({ id: "FN-FAST-PLUGIN-001", executionMode: "standard" });
const store = createMockStore({
getTask: vi.fn().mockResolvedValue({ ...mockTaskDetail, id: task.id, attachments: [], comments: [] }),
});
const pluginRunner = {
getPromptContributionsForSurface: vi.fn().mockImplementation((surface: string) => {
if (surface !== "triage") return [];
return [{ pluginId: "plugin-triage", contribution: { surface: "triage", content: "Use plugin triage policy." }, config: {} }];
}),
getPluginSkills: vi.fn().mockReturnValue([]),
};
let capturedSystemPrompt = "";
mockCreateFnAgent.mockImplementationOnce(async (opts: any) => {
capturedSystemPrompt = opts.systemPrompt;
return {
session: {
state: {},
sessionManager: { getLeafId: vi.fn().mockReturnValue(null) },
prompt: vi.fn().mockResolvedValue(undefined),
dispose: vi.fn(),
navigateTree: vi.fn(),
},
};
});
const processor = new TriageProcessor(store, "/tmp/root", { pluginRunner: pluginRunner as any });
await processor.specifyTask(task);
expect(capturedSystemPrompt).toContain("## Plugin: plugin-triage");
expect(capturedSystemPrompt).toContain("Use plugin triage policy.");
});
it("keeps triage prompt unchanged when no triage plugin contributions exist", async () => {
const task = createTriageTask({ id: "FN-FAST-PLUGIN-002", executionMode: "standard" });
const store = createMockStore({
getTask: vi.fn().mockResolvedValue({ ...mockTaskDetail, id: task.id, attachments: [], comments: [] }),
});
const pluginRunner = {
getPromptContributionsForSurface: vi.fn().mockReturnValue([]),
getPluginSkills: vi.fn().mockReturnValue([]),
};
let capturedSystemPrompt = "";
mockCreateFnAgent.mockImplementationOnce(async (opts: any) => {
capturedSystemPrompt = opts.systemPrompt;
return {
session: {
state: {},
sessionManager: { getLeafId: vi.fn().mockReturnValue(null) },
prompt: vi.fn().mockResolvedValue(undefined),
dispose: vi.fn(),
navigateTree: vi.fn(),
},
};
});
const processor = new TriageProcessor(store, "/tmp/root", { pluginRunner: pluginRunner as any });
await processor.specifyTask(task);
expect(capturedSystemPrompt).not.toContain("## Plugin:");
});
it("applies triage plugin contributions in fast mode too", async () => {
const task = createTriageTask({ id: "FN-FAST-PLUGIN-003", executionMode: "fast" });
const store = createMockStore({
getTask: vi.fn().mockResolvedValue({ ...mockTaskDetail, id: task.id, attachments: [], comments: [] }),
});
const pluginRunner = {
getPromptContributionsForSurface: vi.fn().mockReturnValue([
{ pluginId: "plugin-fast", contribution: { surface: "triage", content: "Fast mode plugin note." }, config: {} },
]),
getPluginSkills: vi.fn().mockReturnValue([]),
};
let capturedSystemPrompt = "";
mockCreateFnAgent.mockImplementationOnce(async (opts: any) => {
capturedSystemPrompt = opts.systemPrompt;
return {
session: {
state: {},
sessionManager: { getLeafId: vi.fn().mockReturnValue(null) },
prompt: vi.fn().mockResolvedValue(undefined),
dispose: vi.fn(),
navigateTree: vi.fn(),
},
};
});
const processor = new TriageProcessor(store, "/tmp/root", { pluginRunner: pluginRunner as any });
await processor.specifyTask(task);
expect(capturedSystemPrompt).toContain("This task is running in **fast mode**");
expect(capturedSystemPrompt).toContain("## Plugin: plugin-fast");
});
it("auto-approves fn_review_spec in fast mode without calling reviewer", async () => { it("auto-approves fn_review_spec in fast mode without calling reviewer", async () => {
const rootDir = await createTriageFixtureRoot("fusion-triage-fast-review-"); const rootDir = await createTriageFixtureRoot("fusion-triage-fast-review-");
try { try {

View File

@@ -24,7 +24,12 @@ import { Type, type Static } from "@mariozechner/pi-ai";
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import { createTaskCreateTool, createTaskLogToolWithContext, createTaskDocumentWriteTool, createTaskDocumentReadTool, createListAgentsTool, createDelegateTaskTool, createGetAgentConfigTool, createUpdateAgentConfigTool, createSendMessageTool, createReadMessagesTool, createMemoryTools, createReadEvaluationsTool, createUpdateIdentityTool, createReflectOnPerformanceTool, taskCreateParams } from "./agent-tools.js"; import { createTaskCreateTool, createTaskLogToolWithContext, createTaskDocumentWriteTool, createTaskDocumentReadTool, createListAgentsTool, createDelegateTaskTool, createGetAgentConfigTool, createUpdateAgentConfigTool, createSendMessageTool, createReadMessagesTool, createMemoryTools, createReadEvaluationsTool, createUpdateIdentityTool, createReflectOnPerformanceTool, taskCreateParams } from "./agent-tools.js";
import { AgentLogger } from "./agent-logger.js"; import { AgentLogger } from "./agent-logger.js";
import { resolveAgentInstructionsWithRatings, buildSystemPromptWithInstructions, resolveAgentHeartbeatProcedure } from "./agent-instructions.js"; import {
resolveAgentInstructionsWithRatings,
buildSystemPromptWithInstructions,
buildPluginPromptSection,
resolveAgentHeartbeatProcedure,
} from "./agent-instructions.js";
import { heartbeatLog, formatError } from "./logger.js"; import { heartbeatLog, formatError } from "./logger.js";
import { createRunAuditor, type EngineRunContext } from "./run-audit.js"; import { createRunAuditor, type EngineRunContext } from "./run-audit.js";
import { promptWithFallback } from "./pi.js"; import { promptWithFallback } from "./pi.js";
@@ -1689,6 +1694,19 @@ export class HeartbeatMonitor {
baseHeartbeatSystemPrompt, baseHeartbeatSystemPrompt,
[resolvedInstructionsForIdentity, memoryInstructions, selfImprovePrompt].filter((part) => part.trim()).join("\n\n"), [resolvedInstructionsForIdentity, memoryInstructions, selfImprovePrompt].filter((part) => part.trim()).join("\n\n"),
); );
const heartbeatContributions = this.pluginRunner
?.getPromptContributionsForSurface("heartbeat")
?? [];
if (heartbeatContributions.length > 0) {
heartbeatLog.log(`applied ${heartbeatContributions.length} plugin prompt contributions for heartbeat surface`);
}
const heartbeatPluginContributions = buildPluginPromptSection(
"heartbeat",
this.pluginRunner,
);
const systemPromptFinal = heartbeatPluginContributions
? `${systemPrompt}\n\n${heartbeatPluginContributions}`
: systemPrompt;
// fn_heartbeat_done must be the last tool in the array (stable terminal signal) // fn_heartbeat_done must be the last tool in the array (stable terminal signal)
heartbeatTools.push(heartbeatDoneTool); heartbeatTools.push(heartbeatDoneTool);
@@ -1717,7 +1735,7 @@ export class HeartbeatMonitor {
runtimeHint: extractRuntimeHint(agent.runtimeConfig), runtimeHint: extractRuntimeHint(agent.runtimeConfig),
pluginRunner: this.pluginRunner, pluginRunner: this.pluginRunner,
cwd: rootDir, cwd: rootDir,
systemPrompt, systemPrompt: systemPromptFinal,
tools: "coding", tools: "coding",
customTools: heartbeatTools, customTools: heartbeatTools,
...(() => { ...(() => {
@@ -1956,7 +1974,7 @@ export class HeartbeatMonitor {
try { try {
const runWithPrompts: AgentHeartbeatRun = { const runWithPrompts: AgentHeartbeatRun = {
...run, ...run,
systemPrompt: truncatePrompt(systemPrompt, 100_000), systemPrompt: truncatePrompt(systemPromptFinal, 100_000),
executionPrompt: truncatePrompt(executionPrompt, 100_000), executionPrompt: truncatePrompt(executionPrompt, 100_000),
heartbeatProcedureSource: customProcedure ? "custom" : "default", heartbeatProcedureSource: customProcedure ? "custom" : "default",
}; };

View File

@@ -6,7 +6,9 @@ import {
type Agent, type Agent,
type AgentRatingSummary, type AgentRatingSummary,
type AgentStore, type AgentStore,
type PluginPromptSurface,
} from "@fusion/core"; } from "@fusion/core";
import type { PluginRunner } from "./plugin-runner.js";
import { createLogger } from "./logger.js"; import { createLogger } from "./logger.js";
const log = createLogger("agent-instructions"); const log = createLogger("agent-instructions");
@@ -383,3 +385,38 @@ export function buildSystemPromptWithInstructions(
if (!instructions.trim()) return basePrompt; if (!instructions.trim()) return basePrompt;
return `${basePrompt}\n\n## Custom Instructions\n\n${instructions}`; return `${basePrompt}\n\n## Custom Instructions\n\n${instructions}`;
} }
export function buildPluginPromptSection(
surface: PluginPromptSurface,
pluginRunner: PluginRunner | undefined,
): string {
if (!pluginRunner) {
return "";
}
const contributions = pluginRunner.getPromptContributionsForSurface(surface);
if (contributions.length === 0) {
return "";
}
const prependByPlugin = new Map<string, string[]>();
const appendByPlugin = new Map<string, string[]>();
for (const { pluginId, contribution } of contributions) {
const target = contribution.position === "prepend" ? prependByPlugin : appendByPlugin;
const existing = target.get(pluginId) ?? [];
existing.push(contribution.content);
target.set(pluginId, existing);
}
const toSections = (group: Map<string, string[]>): string[] => {
return Array.from(group.entries()).map(([pluginId, contents]) => {
return `## Plugin: ${pluginId}\n\n${contents.join("\n\n")}`;
});
};
const sections = [...toSections(prependByPlugin), ...toSections(appendByPlugin)];
log.log(`Applied ${contributions.length} prompt contributions for surface '${surface}'`);
return sections.join("\n\n");
}

View File

@@ -42,7 +42,11 @@ import type { StuckTaskDetector, StuckTaskEvent } from "./stuck-task-detector.js
import type { PluginRunner } from "./plugin-runner.js"; import type { PluginRunner } from "./plugin-runner.js";
import { isContextLimitError } from "./context-limit-detector.js"; import { isContextLimitError } from "./context-limit-detector.js";
import { StepSessionExecutor } from "./step-session-executor.js"; import { StepSessionExecutor } from "./step-session-executor.js";
import { resolveAgentInstructions, buildSystemPromptWithInstructions } from "./agent-instructions.js"; import {
resolveAgentInstructions,
buildSystemPromptWithInstructions,
buildPluginPromptSection,
} from "./agent-instructions.js";
import type { AgentReflectionService } from "./agent-reflection.js"; import type { AgentReflectionService } from "./agent-reflection.js";
import { createRunAuditor, generateSyntheticRunId, type EngineRunContext } from "./run-audit.js"; import { createRunAuditor, generateSyntheticRunId, type EngineRunContext } from "./run-audit.js";
import { evaluateSpecStaleness, getPromptPath } from "./spec-staleness.js"; import { evaluateSpecStaleness, getPromptPath } from "./spec-staleness.js";
@@ -2884,6 +2888,17 @@ export class TaskExecutor {
getExecutorSystemPrompt(settings), getExecutorSystemPrompt(settings),
executorInstructions, executorInstructions,
); );
const executorSystemContributions = this.options.pluginRunner?.getPromptContributionsForSurface("executor-system") ?? [];
if (executorSystemContributions.length > 0) {
executorLog.log(`${task.id}: applied ${executorSystemContributions.length} plugin prompt contributions for executor-system surface`);
}
const executorPluginContributions = buildPluginPromptSection(
"executor-system",
this.options.pluginRunner,
);
const executorSystemPromptFinal = executorPluginContributions
? `${executorSystemPrompt}\n\n${executorPluginContributions}`
: executorSystemPrompt;
// sessionFile must be let because it's destructured alongside session which is reassigned // sessionFile must be let because it's destructured alongside session which is reassigned
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
@@ -2892,7 +2907,7 @@ export class TaskExecutor {
runtimeHint: executorRuntimeHint, runtimeHint: executorRuntimeHint,
pluginRunner: this.options.pluginRunner, pluginRunner: this.options.pluginRunner,
cwd: worktreePath, cwd: worktreePath,
systemPrompt: executorSystemPrompt, systemPrompt: executorSystemPromptFinal,
tools: "coding", tools: "coding",
customTools, customTools,
onText: agentLogger.onText, onText: agentLogger.onText,
@@ -2970,7 +2985,13 @@ export class TaskExecutor {
"Review the current state of your worktree and proceed with the next pending step.", "Review the current state of your worktree and proceed with the next pending step.",
].join("\n")); ].join("\n"));
} else { } else {
const agentPrompt = buildExecutionPrompt(detail, this.rootDir, settings, worktreePath); const agentPrompt = buildExecutionPrompt(
detail,
this.rootDir,
settings,
worktreePath,
this.options.pluginRunner,
);
await promptWithFallback(session, agentPrompt); await promptWithFallback(session, agentPrompt);
} }
@@ -3196,7 +3217,7 @@ export class TaskExecutor {
runtimeHint: executorRuntimeHint, runtimeHint: executorRuntimeHint,
pluginRunner: this.options.pluginRunner, pluginRunner: this.options.pluginRunner,
cwd: worktreePath, cwd: worktreePath,
systemPrompt: executorSystemPrompt, systemPrompt: executorSystemPromptFinal,
tools: "coding", tools: "coding",
customTools, customTools,
onText: agentLogger.onText, onText: agentLogger.onText,
@@ -3248,7 +3269,7 @@ export class TaskExecutor {
"Do NOT ask for permission. Do NOT write a summary. Just call a tool and keep working.", "Do NOT ask for permission. Do NOT write a summary. Just call a tool and keep working.",
"", "",
"Original task:", "Original task:",
buildExecutionPrompt(detail, this.rootDir, settings, worktreePath), buildExecutionPrompt(detail, this.rootDir, settings, worktreePath, this.options.pluginRunner),
].join("\n"); ].join("\n");
} else { } else {
retryPrompt = [ retryPrompt = [
@@ -3258,7 +3279,7 @@ export class TaskExecutor {
"2. If there is remaining work, finish it and then call fn_task_done.", "2. If there is remaining work, finish it and then call fn_task_done.",
"", "",
"Original task:", "Original task:",
buildExecutionPrompt(detail, this.rootDir, settings, worktreePath), buildExecutionPrompt(detail, this.rootDir, settings, worktreePath, this.options.pluginRunner),
].join("\n"); ].join("\n");
} }
@@ -6926,7 +6947,13 @@ function buildSourceIssueRef(sourceIssue: TaskDetail["sourceIssue"]): string {
return `${sourceIssue.repository}#${issueNumber}`; return `${sourceIssue.repository}#${issueNumber}`;
} }
export function buildExecutionPrompt(task: TaskDetail, rootDir?: string, settings?: Settings, worktreePath?: string): string { export function buildExecutionPrompt(
task: TaskDetail,
rootDir?: string,
settings?: Settings,
worktreePath?: string,
pluginRunner?: PluginRunner,
): string {
const prompt = scopePromptToWorktree(task.prompt, rootDir, worktreePath); const prompt = scopePromptToWorktree(task.prompt, rootDir, worktreePath);
const reviewMatch = prompt.match(/##\s*Review Level[:\s]*(\d)/); const reviewMatch = prompt.match(/##\s*Review Level[:\s]*(\d)/);
const reviewLevel = reviewMatch ? parseInt(reviewMatch[1], 10) : 0; const reviewLevel = reviewMatch ? parseInt(reviewMatch[1], 10) : 0;
@@ -7020,6 +7047,12 @@ git log --oneline
steeringSection = lines.join("\n"); steeringSection = lines.join("\n");
} }
const taskPromptContributions = pluginRunner?.getPromptContributionsForSurface("executor-task") ?? [];
if (taskPromptContributions.length > 0) {
executorLog.log(`${task.id}: applied ${taskPromptContributions.length} plugin prompt contributions for executor-task surface`);
}
const pluginTaskContributions = buildPluginPromptSection("executor-task", pluginRunner);
return `Execute this task. return `Execute this task.
## Task: ${task.id} ## Task: ${task.id}
@@ -7038,6 +7071,10 @@ ${reviewLevel >= 1 ? `Before implementing each step (except Step 0 and the final
${reviewLevel >= 2 ? `After implementing + committing each step, call: ${reviewLevel >= 2 ? `After implementing + committing each step, call:
\`fn_review_step(step=N, type="code", step_name="...", baseline="<SHA from before step>")\`` : ""} \`fn_review_step(step=N, type="code", step_name="...", baseline="<SHA from before step>")\`` : ""}
${reviewLevel >= 3 ? `After tests, also call fn_review_step with type="code" for test review.` : ""} ${reviewLevel >= 3 ? `After tests, also call fn_review_step with type="code" for test review.` : ""}
${pluginTaskContributions ? `
${pluginTaskContributions}
` : ""}
## Worktree Boundaries ## Worktree Boundaries

View File

@@ -16,7 +16,11 @@ import { buildSessionSkillContext } from "./session-skill-context.js";
import { AgentLogger } from "./agent-logger.js"; import { AgentLogger } from "./agent-logger.js";
import { reviewerLog } from "./logger.js"; import { reviewerLog } from "./logger.js";
import { checkSessionError } from "./usage-limit-detector.js"; import { checkSessionError } from "./usage-limit-detector.js";
import { resolveAgentInstructions, buildSystemPromptWithInstructions } from "./agent-instructions.js"; import {
resolveAgentInstructions,
buildSystemPromptWithInstructions,
buildPluginPromptSection,
} from "./agent-instructions.js";
import { createFallbackModelObserver } from "./fallback-model-observer.js"; import { createFallbackModelObserver } from "./fallback-model-observer.js";
import { createMemoryGetTool, createMemorySearchTool } from "./agent-tools.js"; import { createMemoryGetTool, createMemorySearchTool } from "./agent-tools.js";
@@ -408,6 +412,19 @@ export async function reviewStep(
reviewerBasePrompt + memorySection, reviewerBasePrompt + memorySection,
reviewerInstructions, reviewerInstructions,
); );
const reviewerContributions = options.pluginRunner
?.getPromptContributionsForSurface("reviewer")
?? [];
if (reviewerContributions.length > 0) {
reviewerLog.log(`applied ${reviewerContributions.length} plugin prompt contributions for reviewer surface`);
}
const reviewerPluginContributions = buildPluginPromptSection(
"reviewer",
options.pluginRunner,
);
const reviewerSystemPromptFinal = reviewerPluginContributions
? `${reviewerSystemPrompt}\n\n${reviewerPluginContributions}`
: reviewerSystemPrompt;
// Build skill selection context (assigned agent skills take precedence over role fallback) // Build skill selection context (assigned agent skills take precedence over role fallback)
let skillContext = undefined; let skillContext = undefined;
@@ -477,7 +494,7 @@ export async function reviewStep(
runtimeHint: extractRuntimeHint(memoryAgent?.runtimeConfig), runtimeHint: extractRuntimeHint(memoryAgent?.runtimeConfig),
pluginRunner: options.pluginRunner, pluginRunner: options.pluginRunner,
cwd, cwd,
systemPrompt: reviewerSystemPrompt, systemPrompt: reviewerSystemPromptFinal,
tools: "readonly", tools: "readonly",
customTools: memoryTools, customTools: memoryTools,
onText: agentLogger ? agentLogger.onText : (delta) => options.onText?.(delta), onText: agentLogger ? agentLogger.onText : (delta) => options.onText?.(delta),

View File

@@ -23,7 +23,11 @@ import { reviewStep, type ReviewVerdict } from "./reviewer.js";
import { buildSessionSkillContext } from "./session-skill-context.js"; import { buildSessionSkillContext } from "./session-skill-context.js";
import { PRIORITY_SPECIFY, type AgentSemaphore } from "./concurrency.js"; import { PRIORITY_SPECIFY, type AgentSemaphore } from "./concurrency.js";
import { AgentLogger } from "./agent-logger.js"; import { AgentLogger } from "./agent-logger.js";
import { resolveAgentInstructions, buildSystemPromptWithInstructions } from "./agent-instructions.js"; import {
resolveAgentInstructions,
buildSystemPromptWithInstructions,
buildPluginPromptSection,
} from "./agent-instructions.js";
import { createFallbackModelObserver } from "./fallback-model-observer.js"; import { createFallbackModelObserver } from "./fallback-model-observer.js";
import { planLog, reviewerLog, formatError } from "./logger.js"; import { planLog, reviewerLog, formatError } from "./logger.js";
import { import {
@@ -975,6 +979,19 @@ export class TriageProcessor {
|| (isFast ? FAST_TRIAGE_SYSTEM_PROMPT : TRIAGE_SYSTEM_PROMPT), || (isFast ? FAST_TRIAGE_SYSTEM_PROMPT : TRIAGE_SYSTEM_PROMPT),
triageInstructions, triageInstructions,
); );
const triageContributions = this.options.pluginRunner
?.getPromptContributionsForSurface("triage")
?? [];
if (triageContributions.length > 0) {
planLog.log(`${task.id}: applied ${triageContributions.length} plugin prompt contributions for triage surface`);
}
const triagePluginContributions = buildPluginPromptSection(
"triage",
this.options.pluginRunner,
);
const triageSystemPromptFinal = triagePluginContributions
? `${triageSystemPrompt}\n\n${triagePluginContributions}`
: triageSystemPrompt;
// Build skill selection context (assigned agent skills take precedence over role fallback) // Build skill selection context (assigned agent skills take precedence over role fallback)
const skillContext = await buildSessionSkillContext({ const skillContext = await buildSessionSkillContext({
@@ -990,7 +1007,7 @@ export class TriageProcessor {
runtimeHint: triageRuntimeHint, runtimeHint: triageRuntimeHint,
pluginRunner: this.options.pluginRunner, pluginRunner: this.options.pluginRunner,
cwd: this.rootDir, cwd: this.rootDir,
systemPrompt: triageSystemPrompt, systemPrompt: triageSystemPromptFinal,
tools: "coding", tools: "coding",
customTools, customTools,
onText: agentLogger.onText, onText: agentLogger.onText,
@@ -1238,7 +1255,7 @@ export class TriageProcessor {
runtimeHint: triageRuntimeHint, runtimeHint: triageRuntimeHint,
pluginRunner: this.options.pluginRunner, pluginRunner: this.options.pluginRunner,
cwd: this.rootDir, cwd: this.rootDir,
systemPrompt: triageSystemPrompt, systemPrompt: triageSystemPromptFinal,
tools: "coding", tools: "coding",
customTools, customTools,
onText: agentLogger.onText, onText: agentLogger.onText,