Align plugin skill enable/disable reads with the resolved skillFiles path so Skills-view toggles persist and sessions honor them. - Accept optional skillRelativePath in resolvePluginSkillEnabled for custom skillFiles keys - Pass resolved relativePath from dashboard skills adapter when merging plugin skills - Reuse resolved body path in session-skill-context for enable checks and additionalSkillPaths - Add unit coverage for custom-path round-trip, enable, and disable behavior - Ship patch changeset for @runfusion/fusion Files changed: .changeset/fn-7954-plugin-skill-toggle-key-fix.md | 7 +++ packages/core/src/__tests__/skill-settings.test.ts | 15 ++++++ packages/core/src/skill-settings.ts | 8 +++- .../dashboard/src/__tests__/skills-adapter.test.ts | 55 ++++++++++++++++++++++ packages/dashboard/src/skills-adapter.ts | 1 + .../src/__tests__/session-skill-context.test.ts | 45 ++++++++++++++++++ packages/engine/src/session-skill-context.ts | 29 +++++++----- 7 files changed, 147 insertions(+), 13 deletions(-) Fusion-Task-Id: FN-7954 Fusion-Task-Lineage: 3399186b-7325-4ed7-8900-85eb2ef98c7e Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
109 lines
3.9 KiB
TypeScript
109 lines
3.9 KiB
TypeScript
export type SkillSettingState = "enabled" | "disabled";
|
|
|
|
export interface SkillSettingsScope {
|
|
skills?: string[];
|
|
packages?: Array<string | { source: string; skills?: string[] }>;
|
|
}
|
|
|
|
/**
|
|
* Compute deterministic skill ID from metadata.
|
|
* Format: encodeURIComponent(metadata.source) + "::" + relativePath
|
|
*
|
|
* @param source - The package source identifier
|
|
* @param relativePath - Path relative to the skill directory
|
|
* @returns Deterministic skill ID
|
|
*/
|
|
export function computeSkillId(source: string, relativePath: string): string {
|
|
const normalizedPath = relativePath.replaceAll("\\", "/");
|
|
return `${encodeURIComponent(source)}::${normalizedPath}`;
|
|
}
|
|
|
|
/**
|
|
* Parse a skill ID back into source and relativePath components.
|
|
*/
|
|
export function parseSkillId(skillId: string): { source: string; relativePath: string } | null {
|
|
const parts = skillId.split("::");
|
|
if (parts.length !== 2) return null;
|
|
try {
|
|
return {
|
|
source: decodeURIComponent(parts[0]!),
|
|
relativePath: parts[1]!,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function normalizeStoredSkillPath(path: string): string {
|
|
return path.replaceAll("\\", "/").replace(/^skills\//, "");
|
|
}
|
|
|
|
function settingEntryState(entry: string): SkillSettingState {
|
|
return entry.startsWith("+") ? "enabled" : "disabled";
|
|
}
|
|
|
|
/**
|
|
* FNXC:PluginSkills 2026-07-12-00:00:
|
|
* Plugin-skill effective enablement must have one resolver shared by dashboard discovery and engine session assembly. FN-7858 fixed drift where Skills view honored per-project toggles but collectPluginSkillNames only used static plugin defaults.
|
|
*/
|
|
export function getSkillSettingState(
|
|
skillId: string,
|
|
settings: SkillSettingsScope,
|
|
): SkillSettingState | undefined {
|
|
const parsedSkillId = parseSkillId(skillId);
|
|
if (!parsedSkillId) {
|
|
return undefined;
|
|
}
|
|
|
|
const normalizedSkillPath = normalizeStoredSkillPath(parsedSkillId.relativePath);
|
|
|
|
const skills = settings.skills ?? [];
|
|
for (const entry of skills) {
|
|
if (typeof entry !== "string") continue;
|
|
const entryPath = normalizeStoredSkillPath(
|
|
entry.startsWith("+") || entry.startsWith("-") ? entry.slice(1) : entry,
|
|
);
|
|
const entryId = computeSkillId("*", `skills/${entryPath}`);
|
|
if (entryId === skillId || entryPath === normalizedSkillPath) {
|
|
return settingEntryState(entry);
|
|
}
|
|
}
|
|
|
|
const packages = settings.packages ?? [];
|
|
for (const pkg of packages) {
|
|
const source = typeof pkg === "string" ? pkg : pkg.source;
|
|
const pkgSkills = typeof pkg === "object" && pkg !== null ? pkg.skills : undefined;
|
|
if (!pkgSkills) continue;
|
|
|
|
for (const entry of pkgSkills) {
|
|
if (typeof entry !== "string") continue;
|
|
const entryPath = normalizeStoredSkillPath(
|
|
entry.startsWith("+") || entry.startsWith("-") ? entry.slice(1) : entry,
|
|
);
|
|
const entryId = computeSkillId(source, `skills/${entryPath}`);
|
|
if (entryId === skillId || entryPath === normalizedSkillPath) {
|
|
return settingEntryState(entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* FNXC:PluginSkills 2026-07-14-00:00:
|
|
* FN-7954 closes the plugin-skill toggle write/read key-schema mismatch for custom skillFiles paths. Dashboard toggles persist entries under the resolved plugin-root-relative SKILL.md path, so read paths must pass that same relative path when it is known; omitting skillRelativePath intentionally preserves the legacy name-derived skills/<name>/SKILL.md lookup for callers without a pluginRoot.
|
|
*/
|
|
export function resolvePluginSkillEnabled(
|
|
settings: SkillSettingsScope,
|
|
pluginId: string,
|
|
skillName: string,
|
|
staticEnabled: boolean | undefined,
|
|
skillRelativePath?: string,
|
|
): boolean {
|
|
const relativePath = skillRelativePath ?? `skills/${skillName}/SKILL.md`;
|
|
const skillId = computeSkillId(`plugin:${pluginId}`, relativePath);
|
|
const settingState = getSkillSettingState(skillId, settings);
|
|
return settingState === undefined ? staticEnabled !== false : settingState === "enabled";
|
|
}
|