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:
@@ -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",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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."),
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user