Skip to content

Commit a02ceb3

Browse files
committed
fix(skills): align prompt exposure with runtime entries
1 parent 16e18bf commit a02ceb3

7 files changed

Lines changed: 68 additions & 119 deletions

src/agents/skills.available-skills-visibility.test.ts

Lines changed: 0 additions & 35 deletions
This file was deleted.

src/agents/skills.issue-60716-design.test.ts

Lines changed: 0 additions & 37 deletions
This file was deleted.

src/agents/skills.issue-60716-proof.test.ts

Lines changed: 0 additions & 42 deletions
This file was deleted.

src/agents/skills.loadworkspaceskillentries.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,26 @@ describe("loadWorkspaceSkillEntries", () => {
149149
expect(entries.map((entry) => entry.skill.name)).toContain("fallback-name");
150150
});
151151

152+
it("keeps disable-model-invocation skills visible in exposure metadata for newly loaded entries", async () => {
153+
const workspaceDir = await createTempWorkspaceDir();
154+
await writeSkill({
155+
dir: path.join(workspaceDir, "skills", "hidden-skill"),
156+
name: "hidden-skill",
157+
description: "Visible prompt entry",
158+
frontmatterExtra: "disable-model-invocation: true",
159+
});
160+
161+
const entries = loadWorkspaceSkillEntries(workspaceDir, {
162+
managedSkillsDir: path.join(workspaceDir, ".managed"),
163+
bundledSkillsDir: path.join(workspaceDir, ".bundled"),
164+
});
165+
166+
const hiddenEntry = entries.find((entry) => entry.skill.name === "hidden-skill");
167+
168+
expect(hiddenEntry?.invocation?.disableModelInvocation).toBe(true);
169+
expect(hiddenEntry?.exposure?.includeInAvailableSkillsPrompt).toBe(true);
170+
});
171+
152172
it("inherits agents.defaults.skills when an agent omits skills", async () => {
153173
const workspaceDir = await createTempWorkspaceDir();
154174
await writeSkill({

src/agents/skills.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,24 @@ describe("buildWorkspaceSkillsPrompt", () => {
332332
expect(prompt).toContain("Does demo things");
333333
expect(prompt).toContain(path.join(skillDir, "SKILL.md"));
334334
});
335+
336+
it("keeps runtime-eligible skills visible in available_skills when disable-model-invocation is set", async () => {
337+
const workspaceDir = await makeWorkspace();
338+
const skillDir = path.join(workspaceDir, "skills", "hidden-skill");
339+
340+
await writeSkill({
341+
dir: skillDir,
342+
name: "hidden-skill",
343+
description: "Still visible in the prompt",
344+
frontmatterExtra: "disable-model-invocation: true",
345+
});
346+
347+
const prompt = buildWorkspaceSkillsPrompt(workspaceDir, resolveTestSkillDirs(workspaceDir));
348+
349+
expect(prompt).toContain("hidden-skill");
350+
expect(prompt).toContain("Still visible in the prompt");
351+
expect(prompt).toContain(path.join(skillDir, "SKILL.md"));
352+
});
335353
});
336354

337355
describe("applySkillEnvOverrides", () => {

src/agents/skills/compact-format.test.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ function makeEntry(skill: Skill): SkillEntry {
2727
frontmatter: {},
2828
exposure: {
2929
includeInRuntimeRegistry: true,
30-
includeInAvailableSkillsPrompt: skill.disableModelInvocation !== true,
30+
includeInAvailableSkillsPrompt: true,
3131
userInvocable: true,
3232
},
3333
};
@@ -59,6 +59,13 @@ describe("formatSkillsCompact", () => {
5959
expect(formatSkillsForPrompt(skills)).toBe(upstreamFormatSkillsForPrompt(skills));
6060
});
6161

62+
it("renders all passed skills in the full formatter without reapplying visibility policy", () => {
63+
const hidden: Skill = { ...makeSkill("hidden"), disableModelInvocation: true };
64+
const out = formatSkillsForPrompt([makeSkill("visible"), hidden]);
65+
expect(out).toContain("visible");
66+
expect(out).toContain("hidden");
67+
});
68+
6269
it("returns empty string for no skills", () => {
6370
expect(formatSkillsCompact([])).toBe("");
6471
});
@@ -93,9 +100,25 @@ describe("formatSkillsCompact", () => {
93100
});
94101

95102
describe("applySkillsPromptLimits (via buildWorkspaceSkillsPrompt)", () => {
96-
it("applies disableModelInvocation filtering before compact formatting", () => {
97-
const hidden: Skill = { ...makeSkill("hidden"), disableModelInvocation: true };
98-
const prompt = buildPrompt([makeSkill("visible"), hidden], { maxChars: 4_000 });
103+
it("respects explicit exposure metadata before compact formatting", () => {
104+
const hidden = makeEntry({ ...makeSkill("hidden"), disableModelInvocation: true });
105+
hidden.exposure = {
106+
includeInRuntimeRegistry: true,
107+
includeInAvailableSkillsPrompt: false,
108+
userInvocable: true,
109+
};
110+
111+
const prompt = buildWorkspaceSkillsPrompt("/fake", {
112+
entries: [makeEntry(makeSkill("visible")), hidden],
113+
config: {
114+
skills: {
115+
limits: {
116+
maxSkillsPromptChars: 4_000,
117+
},
118+
},
119+
} satisfies OpenClawConfig,
120+
});
121+
99122
expect(prompt).toContain("visible");
100123
expect(prompt).not.toContain("hidden");
101124
});

src/agents/skills/workspace.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,9 @@ function loadSkillEntries(
487487
invocation,
488488
exposure: {
489489
includeInRuntimeRegistry: true,
490-
includeInAvailableSkillsPrompt: invocation.disableModelInvocation !== true,
490+
// Prompt visibility now follows runtime inclusion for freshly loaded skills.
491+
// Legacy entries without exposure metadata still use the fallback below.
492+
includeInAvailableSkillsPrompt: true,
491493
userInvocable: invocation.userInvocable !== false,
492494
},
493495
};

0 commit comments

Comments
 (0)