Ensure Compound Engineering stages request and expose their registered skills when launching interactive sessions. - Add plugin-local skill resolution checks that warn when a stage skill install is missing. - Pass a stable additional skill path and requested stage skill into every CE interactive session. - Add coverage for stage skill loading options and update skill reachability expectations. Files changed: ...d-skills-not-loading-in-interactive-sessions.md | 1 + .../src/__tests__/skill-reachability.test.ts | 71 +++++++++++++--------- .../src/__tests__/stage-skill-loading.test.ts | 69 +++++++++++++++++++++ .../src/session/orchestrator.ts | 48 ++++++++++++++- 4 files changed, 157 insertions(+), 32 deletions(-) Fusion-Task-Id: FN-7107 Fusion-Task-Lineage: 0504e9ba-c2e7-4317-9c90-55693baf8170 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
8.6 KiB
title, date, category, module, problem_type, component, severity, symptoms, root_cause, resolution_type, related_components, tags
| title | date | category | module | problem_type | component | severity | symptoms | root_cause | resolution_type | related_components | tags | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Plugin-bundled skills silently fail to load in interactive sessions | 2026-06-03 | integration-issues | packages/engine | integration_issue | tooling | high |
|
incomplete_setup | code_fix |
|
|
Plugin-bundled skills silently fail to load in interactive sessions
Problem
Bundled ce-* skills declared via PluginSkillContribution.skillFiles never loaded into live interactive agent sessions. The engine's skill resolver only filters skills it already discovered on disk and never ingests a contribution's skillFiles, so a name-only contribution produced no loadable skill — silently.
Symptoms
- A stage session runs, but the agent behaves as if the
ce-*skill is absent — its instructions are never applied. - The resolver returns an empty/unchanged skill set for the requested name; the filter has nothing matching to keep.
- No error is raised. A
PluginSkillContributionis name-only ({ skillId, name, skillFiles }), so declaring it is structurally valid; the session just starts without the skill. - Tests using a scripted/fake session pass, hiding the gap — only a real resource loader surfaces it.
What Didn't Work
1. Declaring skills: PluginSkillContribution[] alone. The engine resolver (skill-resolver.ts) computes an allow/exclude filter over skills the loader already discovered on disk. createSkillsOverrideFromSelection returns a callback that only ever runs base.skills.filter(...) — it never adds skills. If the bundled SKILL.md was never physically on a discoverable path, it isn't in base.skills, so filtering by its name yields []. The contribution's skillFiles are never read for live sessions.
2. Setting the session cwd to the install root + naming the skill in the system prompt. DefaultResourceLoader discovers skills by scanning standard skill roots (e.g. <cwd>/.claude/skills/<id>/SKILL.md), not by treating an arbitrary cwd as a skills directory. Pointing cwd at <installRoot> (which holds <id>/SKILL.md directly) does not match the layout the loader scans, so the skill still isn't discovered — and it relocates the session away from the project root where it must read context and write artifacts. A prompt mention cannot inject skill content the loader never loaded.
Solution
Two parts: physically install the bundled skill to a discoverable plugin-local dir, and forward both the requested name and the install dir through a new seam option, end to end.
Physical install (skill-installation.ts) — copy each bundled <skillId>/SKILL.md into a plugin-local target, with a hard isolation guard (never a global ~/.claude|.codex|.gemini/skills), idempotently:
assertPluginLocalTarget(targetRoot); // isolation invariant: never a global skills dir
if (!existsSync(join(targetRoot, skillId))) { // skip-if-exists
mkdirSync(targetRoot, { recursive: true });
cpSync(join(sourceRoot, skillId), join(targetRoot, skillId), { recursive: true });
}
Layer 1 — engine loader seam (pi.ts). A new AgentOptions.additionalSkillPaths, forwarded into DefaultResourceLoader as a real discovery path (distinct from the filtering skillsOverride):
// AgentOptions
skills?: string[]; // convenience → auto-builds a SkillSelectionContext (requestedSkillNames)
additionalSkillPaths?: string[]; // extra dirs (each holding <id>/SKILL.md) for the loader to SCAN
const resourceLoader = new DefaultResourceLoader({
cwd: resolvedProjectRoot,
...(options.additionalSkillPaths?.length
? { additionalSkillPaths: [...options.additionalSkillPaths] } : {}),
...(skillsOverrideFn ? { skillsOverride: skillsOverrideFn } : {}),
});
Layer 2 — core seam type (plugin-types.ts). CreateInteractiveAiSessionOptions gains the matching fields so a plugin route can request them:
requestedSkillNames?: string[]; // names the session should load
additionalSkillPaths?: string[]; // dirs to scan so requestedSkillNames are discoverable
Layer 3 — engine adapter (index.ts). Forwards both into createFnAgent, mapping requestedSkillNames → the convenience skills param:
...(opts.requestedSkillNames?.length ? { skills: opts.requestedSkillNames } : {}),
...(opts.additionalSkillPaths?.length ? { additionalSkillPaths: opts.additionalSkillPaths } : {}),
Caller — orchestrator (orchestrator.ts). Passes the stage's skill id as the requested name AND the plugin-local install root as a discovery path, while keeping cwd on the project root:
private buildSessionOptions(stage: CeStageDefinition) {
return {
cwd: this.projectRoot, // project root — NOT the skills dir
requestedSkillNames: [stage.skillId],
additionalSkillPaths: resolveStageSkillPaths(), // [resolveDefaultInstallTargetRoot()]
// ...systemPrompt, tools, model
};
}
Why This Works
skillsOverride(built bycreateSkillsOverrideFromSelection) is purely a filter overbase.skills. To make a new skill exist inbase.skills, discovery must be fed the path — exactly whatadditionalSkillPathsdoes onDefaultResourceLoader: it scans those dirs for the<id>/SKILL.mdlayout, so the physically-installed skill now appears inbase.skills.- The convenience
skills/requestedSkillNamesparam auto-builds aSkillSelectionContext, which makes the filter include that name instead of passing everything or nothing. - Discovery (add via
additionalSkillPaths) and selection (keep viarequestedSkillNames) are now both satisfied, so the skill is loaded and retained — withcwdstill on the project root, so context reads and artifact writes are unaffected.
Prevention
A plugin author shipping a bundled skill should:
- Physically install the
SKILL.mdto a plugin-local, discoverable dir laid out as<root>/<skillId>/SKILL.md(usecpSync+ skip-if-exists). Never install into a global~/.claude|.codex|.gemini/skills; keep an explicitassertPluginLocalTarget()guard so a global install is never clobbered. - Forward both seam options when starting the session:
requestedSkillNames: [skillId](so the resolver keeps it) andadditionalSkillPaths: [installRoot](so the loader discovers it). One without the other silently no-ops — a name with no discovered file filters to[]; a discovered file with no requested name can be filtered out. - Remember
skillsOverrideonly filters — declaring aPluginSkillContributionis name-only and never injects skill content into a live session. - Prove it with a real
DefaultResourceLoader(seepackages/engine/src/__tests__/compound-engineering-skill-resolution.test.ts) that asserts the skill actually appears in the resolved session skills — a scripted/fake session cannot catch a discovery gap. - For stage registries, iterate the registry instead of sampling. Compound Engineering now treats each
listStages()entry as a skill-loading invariant:<stage.skillId>must have bundled source frontmatter, an installed<skillId>/SKILL.md, a plugin-local discovery path, and session options that request the skill. A missing installed stage skill should emit a clear guard warning before session start, not silently run a degraded skill-less stage.
Related Issues
docs/PLUGIN_AUTHORING.md(§skills) presentsskillFilesas sufficient for surfacing bundled skills in sessions — now misleading; warrants a note that plugins must physically install + forwardadditionalSkillPaths.docs/plans/2026-06-02-001-feat-compound-engineering-plugin-plan.mdassumed "PluginSkillContribution.skillFilescovers bundled skills" (KTD5) — corrected by this learning.docs/brainstorms/2026-06-02-compound-engineering-plugin-requirements.md(R11–R13) defines the plugin-local, never-global install rules this fix implements.- No related GitHub issue exists (searched
plugin skill discovery,compound engineering skill— zero matches).