Make project-enabled skills available across agent sessions while preserving explicit per-agent skills as observable read-first requirements. - resolve all enabled discovered skills without using agent metadata as an availability filter - carry forced skill intent through PI, plugin, chat, workflow, merger, and heartbeat session paths - report resolved and unavailable forced skills in session summaries and diagnostics - document the updated skill model and add release notes and regression coverage Files changed: .changeset/fn-9114-forced-skills.md | 7 + docs/PLUGIN_AUTHORING.md | 2 +- docs/agents.md | 10 +- docs/diagnostics.md | 2 +- docs/settings-reference.md | 2 +- packages/core/src/__tests__/skill-settings.test.ts | 9 + .../dashboard/src/__tests__/chat-manager.test.ts | 26 ++- packages/dashboard/src/chat.ts | 10 + .../engine/src/__tests__/agent-skills-flow.test.ts | 14 +- .../compound-engineering-skill-resolution.test.ts | 2 +- .../engine/src/__tests__/heartbeat-skills.test.ts | 41 ++++ .../__tests__/hermes-runtime-integration.test.ts | 63 +++++- .../engine/src/__tests__/merger-skills.test.ts | 33 ++- packages/engine/src/__tests__/pi.test.ts | 35 ++- .../__tests__/plugin-skill-body-delivery.test.ts | 8 +- .../src/__tests__/plugin-skill-integration.test.ts | 3 +- .../src/__tests__/session-skill-context.test.ts | 40 ++-- .../engine/src/__tests__/skill-resolver.test.ts | 38 +++- .../__tests__/step-execute-skill-loading.test.ts | 6 + packages/engine/src/agents/agent-runtime.ts | 2 + .../engine/src/agents/agent-session-helpers.ts | 88 ++++++- .../src/cli-runtime/session-skill-context.ts | 117 ++++------ packages/engine/src/cli-runtime/skill-resolver.ts | 252 +++++++++------------ .../engine/src/executor/execute-workflow-step.ts | 14 ++ packages/engine/src/executor/run-implementation.ts | 5 + packages/engine/src/pi.ts | 28 ++- 26 files changed, 576 insertions(+), 281 deletions(-) Fusion-Task-Id: FN-9114 Fusion-Task-Lineage: fe63ebd1-f947-40d7-be7b-c13d7f1c35c9 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
88 lines
3.2 KiB
TypeScript
88 lines
3.2 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
import type { AgentStore } from "@fusion/core";
|
|
import type { PluginRunner } from "../plugins/plugin-runner.js";
|
|
import { buildSessionSkillContext } from "../cli-runtime/session-skill-context.js";
|
|
import { createSkillsOverrideFromSelection, resolveSessionSkills } from "../cli-runtime/skill-resolver.js";
|
|
import type { Skill } from "@earendil-works/pi-coding-agent";
|
|
|
|
describe("plugin skill integration", () => {
|
|
const projectRootDir = "/test/project";
|
|
|
|
it("merges enabled plugin skills, excludes disabled, and dedupes with agent skills", async () => {
|
|
const agentStore = {
|
|
getAgent: vi.fn().mockResolvedValue({
|
|
id: "agent-1",
|
|
metadata: { skills: ["fusion", "agent-only"] },
|
|
}),
|
|
} as unknown as AgentStore;
|
|
|
|
const pluginRunner = {
|
|
getPluginSkills: vi.fn().mockReturnValue([
|
|
{ pluginId: "plugin-a", skill: { name: "fusion", enabled: true } },
|
|
{ pluginId: "plugin-a", skill: { name: "plugin-enabled", enabled: true } },
|
|
{ pluginId: "plugin-b", skill: { name: "plugin-disabled", enabled: false } },
|
|
]),
|
|
} as unknown as PluginRunner;
|
|
|
|
const contextResult = await buildSessionSkillContext({
|
|
agentStore,
|
|
task: { assignedAgentId: "agent-1" },
|
|
sessionPurpose: "executor",
|
|
projectRootDir,
|
|
pluginRunner,
|
|
});
|
|
|
|
expect(contextResult.skillSelectionContext?.requestedSkillNames).toEqual([
|
|
"fusion",
|
|
"plugin-enabled",
|
|
]);
|
|
});
|
|
|
|
it("coexists with role fallback and flows through resolver pipeline", async () => {
|
|
const agentStore = {
|
|
getAgent: vi.fn().mockResolvedValue(null),
|
|
} as unknown as AgentStore;
|
|
const pluginRunner = {
|
|
getPluginSkills: vi.fn().mockReturnValue([
|
|
{ pluginId: "plugin-a", skill: { name: "plugin-skill", enabled: true } },
|
|
]),
|
|
} as unknown as PluginRunner;
|
|
|
|
const contextResult = await buildSessionSkillContext({
|
|
agentStore,
|
|
task: {},
|
|
sessionPurpose: "triage",
|
|
projectRootDir,
|
|
pluginRunner,
|
|
});
|
|
|
|
expect(contextResult.skillSelectionContext?.requestedSkillNames).toEqual(["fusion", "plugin-skill"]);
|
|
|
|
const resolved = resolveSessionSkills(contextResult.skillSelectionContext!);
|
|
const skillsOverride = createSkillsOverrideFromSelection(resolved, {
|
|
requestedSkillNames: contextResult.skillSelectionContext?.requestedSkillNames,
|
|
sessionPurpose: contextResult.skillSelectionContext?.sessionPurpose,
|
|
});
|
|
const makeSkill = (name: string, filePath: string): Skill => ({
|
|
name,
|
|
description: `${name} description`,
|
|
filePath,
|
|
baseDir: "/skills",
|
|
sourceInfo: {} as Skill["sourceInfo"],
|
|
disableModelInvocation: false,
|
|
});
|
|
|
|
const filtered = skillsOverride({
|
|
skills: [
|
|
makeSkill("fusion", "/skills/fusion/SKILL.md"),
|
|
makeSkill("plugin-skill", "/skills/plugin/SKILL.md"),
|
|
makeSkill("other", "/skills/other/SKILL.md"),
|
|
],
|
|
diagnostics: [],
|
|
});
|
|
|
|
expect(contextResult.skillSelectionContext?.requestedSkillNames).toEqual(["fusion", "plugin-skill"]);
|
|
expect(filtered.skills.map((skill) => skill.name)).toEqual(["fusion", "plugin-skill", "other"]);
|
|
});
|
|
});
|