feat(core,engine,compound-engineering): make CE sub-agent sessions load their skills

Closes the U2/U5 skill-discovery carry-forward so the plugin's interactive ce-*
sessions actually load the stage's bundled skill in a live agent (not just in
scripted-fake tests).

Root cause: createFnAgent built its DefaultResourceLoader without forwarding any
skill-discovery path, and the interactive seam options couldn't carry one. The
loader's skillsOverride only *filters* skills already discovered from cwd's
standard roots, so the plugin-local .fusion-ce-skills/<id>/SKILL.md was never
discoverable.

Fix (end-to-end):
- AgentOptions.additionalSkillPaths forwarded into DefaultResourceLoader
- CreateInteractiveAiSessionOptions gains requestedSkillNames + additionalSkillPaths
- the interactive engine adapter forwards them to createFnAgent (skills +
  additionalSkillPaths)
- the orchestrator runs the session with cwd on the real project root and hands
  it [stage.skillId] + the install root

Proven: a real DefaultResourceLoader with additionalSkillPaths discovers ce-plan
and filters out ce-work; the orchestrator passes the right id/path/cwd. Plugin 96,
engine 136, core 99 tests green.
This commit is contained in:
gsxdsm
2026-06-02 20:55:01 -07:00
parent 08312bb59d
commit f5be21178f
7 changed files with 191 additions and 91 deletions

View File

@@ -30,7 +30,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { loadSkills } from "@earendil-works/pi-coding-agent";
import { DefaultResourceLoader, loadSkills, type Skill } from "@earendil-works/pi-coding-agent";
import {
createSkillsOverrideFromSelection,
resolveSessionSkills,
@@ -156,4 +156,39 @@ describe("U2: CE bundled skill session-resolution (empirical)", () => {
expect(resolved).toContain(s);
}
});
it("PASSING (real loader): DefaultResourceLoader with additionalSkillPaths + skillsOverride discovers ce-plan — the exact path createFnAgent now feeds", async () => {
const installRoot = join(tmp, ".fusion-ce-skills");
materializeInstalledSkills(installRoot, ["ce-plan", "ce-work"]);
mkdirSync(projectRootDir, { recursive: true });
mkdirSync(agentDir, { recursive: true });
// Build the same skillsOverride createFnAgent builds from `skills: ["ce-plan"]`.
const selection = resolveSessionSkills({
projectRootDir,
requestedSkillNames: ["ce-plan"],
sessionPurpose: "executor",
});
const skillsOverride = createSkillsOverrideFromSelection(selection, {
requestedSkillNames: ["ce-plan"],
sessionPurpose: "executor",
});
// Construct the loader exactly as pi.ts createFnAgent now does: cwd on the
// project root, the install dir passed via additionalSkillPaths, and the
// requested-name filter as skillsOverride.
const loader = new DefaultResourceLoader({
cwd: projectRootDir,
agentDir,
additionalSkillPaths: [installRoot],
skillsOverride,
});
await loader.reload();
const names = loader.getSkills().skills.map((s: Skill) => s.name);
// ce-plan is discoverable (via additionalSkillPaths) AND survives the filter;
// ce-work is discovered but filtered out by the requested-name override.
expect(names).toContain("ce-plan");
expect(names).not.toContain("ce-work");
});
});

View File

@@ -156,6 +156,11 @@ const _createInteractiveAiSessionAdapter: CreateInteractiveAiSessionFactory = (
tools: opts.tools,
defaultProvider: opts.defaultProvider,
defaultModelId: opts.defaultModelId,
// Forward skill selection so a plugin can load a specific bundled skill.
// `skills` (convenience) auto-builds a SkillSelectionContext; the extra
// discovery dirs make those skills actually visible to the loader.
...(opts.requestedSkillNames?.length ? { skills: opts.requestedSkillNames } : {}),
...(opts.additionalSkillPaths?.length ? { additionalSkillPaths: opts.additionalSkillPaths } : {}),
}),
options,
);

View File

@@ -964,6 +964,11 @@ export interface AgentOptions {
* (and `skillSelection` is not), auto-constructs a SkillSelectionContext
* from the cwd and these names. Ignored when `skillSelection` is set. */
skills?: string[];
/** Extra directories to scan for skills (each holding `<id>/SKILL.md`), in
* addition to the default cwd/agent-dir roots. Forwarded to the resource
* loader so callers (e.g. plugins that install skills to a private dir) can
* make `skills`/`skillSelection` names discoverable in the live session. */
additionalSkillPaths?: string[];
/** Optional task-scoped env injected into this session's subprocess tools only. */
taskEnv?: NodeJS.ProcessEnv;
/** Last-chance abort hook fired immediately before `createAgentSession`.
@@ -1987,6 +1992,9 @@ export async function createFnAgent(options: AgentOptions): Promise<AgentResult>
? [options.systemPromptLayers.dynamic]
: [],
...(effectiveExtensionPaths.length > 0 ? { additionalExtensionPaths: [...effectiveExtensionPaths] } : {}),
...(options.additionalSkillPaths && options.additionalSkillPaths.length > 0
? { additionalSkillPaths: [...options.additionalSkillPaths] }
: {}),
...(skillsOverrideFn ? { skillsOverride: skillsOverrideFn } : {}),
});
await resourceLoader.reload();