mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:30:57 +00:00
Harden skills visibility reporting
This commit is contained in:
@@ -99,6 +99,29 @@ describe("buildWorkspaceSkillStatus", () => {
|
||||
expect(skill?.commandVisible).toBe(false);
|
||||
});
|
||||
|
||||
it("uses default-visible exposure semantics when older entries omit exposure fields", () => {
|
||||
const entry: SkillEntry = {
|
||||
skill: createFixtureSkill({
|
||||
name: "legacy-exposure",
|
||||
description: "test",
|
||||
filePath: "/tmp/legacy-exposure/SKILL.md",
|
||||
baseDir: "/tmp/legacy-exposure",
|
||||
source: "test",
|
||||
}),
|
||||
frontmatter: {},
|
||||
exposure: {
|
||||
includeInRuntimeRegistry: true,
|
||||
} as SkillEntry["exposure"],
|
||||
};
|
||||
|
||||
const report = buildWorkspaceSkillStatus("/tmp/ws", { entries: [entry] });
|
||||
const skill = report.skills[0];
|
||||
expect(skill?.eligible).toBe(true);
|
||||
expect(skill?.modelVisible).toBe(true);
|
||||
expect(skill?.userInvocable).toBe(true);
|
||||
expect(skill?.commandVisible).toBe(true);
|
||||
});
|
||||
|
||||
it("reports skills blocked by an agent skill filter", () => {
|
||||
const alpha: SkillEntry = {
|
||||
skill: createFixtureSkill({
|
||||
|
||||
@@ -176,20 +176,20 @@ function normalizeInstallOptions(
|
||||
|
||||
function isSkillVisibleInAvailableSkillsPrompt(entry: SkillEntry): boolean {
|
||||
if (entry.exposure) {
|
||||
return entry.exposure.includeInAvailableSkillsPrompt;
|
||||
return entry.exposure.includeInAvailableSkillsPrompt !== false;
|
||||
}
|
||||
if (entry.invocation) {
|
||||
return !entry.invocation.disableModelInvocation;
|
||||
return entry.invocation.disableModelInvocation !== true;
|
||||
}
|
||||
return !entry.skill.disableModelInvocation;
|
||||
return entry.skill.disableModelInvocation !== true;
|
||||
}
|
||||
|
||||
function isSkillUserInvocable(entry: SkillEntry): boolean {
|
||||
if (entry.exposure) {
|
||||
return entry.exposure.userInvocable;
|
||||
return entry.exposure.userInvocable !== false;
|
||||
}
|
||||
if (entry.invocation) {
|
||||
return entry.invocation.userInvocable;
|
||||
return entry.invocation.userInvocable !== false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -28,15 +28,18 @@ function appendClawHubHint(output: string, json?: boolean): string {
|
||||
}
|
||||
|
||||
function formatSkillStatus(skill: SkillStatusEntry): string {
|
||||
if (skill.eligible) {
|
||||
return theme.success("✓ ready");
|
||||
}
|
||||
if (skill.disabled) {
|
||||
return theme.warn("⏸ disabled");
|
||||
}
|
||||
if (skill.blockedByAllowlist) {
|
||||
return theme.warn("🚫 blocked");
|
||||
}
|
||||
if (skill.blockedByAgentFilter) {
|
||||
return theme.warn("🚫 excluded");
|
||||
}
|
||||
if (skill.eligible) {
|
||||
return theme.success("✓ ready");
|
||||
}
|
||||
return theme.warn("△ needs setup");
|
||||
}
|
||||
|
||||
@@ -96,7 +99,9 @@ function formatSkillMissingSummary(skill: SkillStatusEntry): string {
|
||||
}
|
||||
|
||||
export function formatSkillsList(report: SkillStatusReport, opts: SkillsListOptions): string {
|
||||
const skills = opts.eligible ? report.skills.filter((s) => s.eligible) : report.skills;
|
||||
const isReadyForAgent = (skill: SkillStatusEntry) =>
|
||||
skill.eligible && !skill.blockedByAgentFilter;
|
||||
const skills = opts.eligible ? report.skills.filter(isReadyForAgent) : report.skills;
|
||||
|
||||
if (opts.json) {
|
||||
const jsonReport = sanitizeJsonValue({
|
||||
@@ -130,7 +135,7 @@ export function formatSkillsList(report: SkillStatusReport, opts: SkillsListOpti
|
||||
return appendClawHubHint(message, opts.json);
|
||||
}
|
||||
|
||||
const eligible = skills.filter((s) => s.eligible);
|
||||
const ready = skills.filter(isReadyForAgent);
|
||||
const tableWidth = getTerminalTableWidth();
|
||||
const rows = skills.map((skill) => {
|
||||
const missing = formatSkillMissingSummary(skill);
|
||||
@@ -155,7 +160,7 @@ export function formatSkillsList(report: SkillStatusReport, opts: SkillsListOpti
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(
|
||||
`${theme.heading("Skills")} ${theme.muted(`(${eligible.length}/${skills.length} ready)`)}`,
|
||||
`${theme.heading("Skills")} ${theme.muted(`(${ready.length}/${skills.length} ready)`)}`,
|
||||
);
|
||||
lines.push(
|
||||
renderTable({
|
||||
@@ -191,13 +196,15 @@ export function formatSkillInfo(
|
||||
|
||||
const lines: string[] = [];
|
||||
const emoji = normalizeSkillEmoji(skill.emoji);
|
||||
const status = skill.eligible
|
||||
? theme.success("✓ Ready")
|
||||
: skill.disabled
|
||||
? theme.warn("⏸ Disabled")
|
||||
: skill.blockedByAllowlist
|
||||
? theme.warn("🚫 Blocked by allowlist")
|
||||
: theme.warn("△ Needs setup");
|
||||
const status = skill.disabled
|
||||
? theme.warn("⏸ Disabled")
|
||||
: skill.blockedByAllowlist
|
||||
? theme.warn("🚫 Blocked by allowlist")
|
||||
: skill.blockedByAgentFilter
|
||||
? theme.warn("🚫 Excluded by agent allowlist")
|
||||
: skill.eligible
|
||||
? theme.success("✓ Ready")
|
||||
: theme.warn("△ Needs setup");
|
||||
|
||||
const safeName = sanitizeForLog(skill.name);
|
||||
const safeHomepage = skill.homepage ? sanitizeForLog(skill.homepage) : undefined;
|
||||
@@ -214,6 +221,15 @@ export function formatSkillInfo(
|
||||
if (safeHomepage) {
|
||||
lines.push(`${theme.muted(" Homepage:")} ${safeHomepage}`);
|
||||
}
|
||||
lines.push(
|
||||
`${theme.muted(" Visible to model:")} ${skill.modelVisible ? theme.success("yes") : theme.warn("no")}`,
|
||||
);
|
||||
lines.push(
|
||||
`${theme.muted(" Available as command:")} ${skill.commandVisible ? theme.success("yes") : theme.warn("no")}`,
|
||||
);
|
||||
if (skill.blockedByAgentFilter) {
|
||||
lines.push(`${theme.muted(" Agent allowlist:")} excludes this skill`);
|
||||
}
|
||||
if (skill.primaryEnv) {
|
||||
lines.push(`${theme.muted(" Primary env:")} ${skill.primaryEnv}`);
|
||||
}
|
||||
@@ -377,6 +393,9 @@ export function formatSkillsCheck(report: SkillStatusReport, opts: SkillsCheckOp
|
||||
if (modelVisible.length > 0 || commandVisible.length > 0 || promptHidden.length > 0) {
|
||||
lines.push("");
|
||||
lines.push(theme.heading("What this means:"));
|
||||
lines.push(
|
||||
` ${theme.muted("Eligible:")} installed and requirements pass; the agent may still exclude it.`,
|
||||
);
|
||||
if (modelVisible.length > 0) {
|
||||
lines.push(
|
||||
` ${theme.muted("Visible to model:")} the agent can see the skill instructions during normal chat.`,
|
||||
@@ -389,7 +408,7 @@ export function formatSkillsCheck(report: SkillStatusReport, opts: SkillsCheckOp
|
||||
}
|
||||
if (promptHidden.length > 0) {
|
||||
lines.push(
|
||||
` ${theme.muted("Hidden from model prompt:")} installed and ready, but kept out of normal chat unless called explicitly.`,
|
||||
` ${theme.muted("Hidden from model prompt:")} installed and ready, but kept out of normal chat.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -408,9 +427,10 @@ export function formatSkillsCheck(report: SkillStatusReport, opts: SkillsCheckOp
|
||||
lines.push(theme.heading("Ready but hidden from model prompt:"));
|
||||
for (const skill of promptHidden) {
|
||||
const emoji = normalizeSkillEmoji(skill.emoji);
|
||||
lines.push(
|
||||
` ${emoji} ${sanitizeForLog(skill.name)} ${theme.muted("(skill hides its instructions from the model; commands/cron may still use it)")}`,
|
||||
);
|
||||
const reason = skill.commandVisible
|
||||
? "skill hides its instructions from the model; commands/cron may still use it"
|
||||
: "skill hides its instructions from the model and is not exposed as a command";
|
||||
lines.push(` ${emoji} ${sanitizeForLog(skill.name)} ${theme.muted(`(${reason})`)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -119,6 +119,26 @@ describe("skills-cli", () => {
|
||||
expect(output).toContain("eligible-one");
|
||||
expect(output).not.toContain("not-eligible");
|
||||
});
|
||||
|
||||
it("does not label agent-excluded skills as ready", () => {
|
||||
const report = createMockReport([
|
||||
createMockSkill({ name: "ready-one", eligible: true }),
|
||||
createMockSkill({
|
||||
name: "agent-excluded",
|
||||
eligible: true,
|
||||
blockedByAgentFilter: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const output = formatSkillsList(report, {});
|
||||
expect(output).toContain("1/2 ready");
|
||||
expect(output).toContain("agent-excluded");
|
||||
expect(output).toContain("excluded");
|
||||
|
||||
const eligibleOnly = formatSkillsList(report, { eligible: true });
|
||||
expect(eligibleOnly).toContain("ready-one");
|
||||
expect(eligibleOnly).not.toContain("agent-excluded");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSkillInfo", () => {
|
||||
@@ -201,6 +221,22 @@ describe("skills-cli", () => {
|
||||
const output = formatSkillInfo(report, "info-emoji", {});
|
||||
expect(output).toContain("🎛️");
|
||||
});
|
||||
|
||||
it("shows agent exclusion and visibility details in skill info", () => {
|
||||
const report = createMockReport([
|
||||
createMockSkill({
|
||||
name: "agent-excluded",
|
||||
eligible: true,
|
||||
blockedByAgentFilter: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const output = formatSkillInfo(report, "agent-excluded", {});
|
||||
expect(output).toContain("Excluded by agent allowlist");
|
||||
expect(output).toContain("Visible to model");
|
||||
expect(output).toContain("Available as command");
|
||||
expect(output).toContain("excludes this skill");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSkillsCheck", () => {
|
||||
@@ -270,8 +306,27 @@ describe("skills-cli", () => {
|
||||
expect(output).toContain("Excluded by agent allowlist");
|
||||
expect(output).toContain("not-assigned");
|
||||
expect(output).toContain("What this means");
|
||||
expect(output).toContain("the agent may still exclude it");
|
||||
expect(output).toContain("people, scripts, or cron jobs can call the skill explicitly");
|
||||
expect(output).toContain("kept out of normal chat unless called explicitly");
|
||||
expect(output).toContain("kept out of normal chat");
|
||||
expect(output).toContain("commands/cron may still use it");
|
||||
});
|
||||
|
||||
it("does not imply prompt-hidden non-command skills can be called explicitly", () => {
|
||||
const report = createMockReport([
|
||||
createMockSkill({
|
||||
name: "internal-hidden",
|
||||
eligible: true,
|
||||
modelVisible: false,
|
||||
commandVisible: false,
|
||||
userInvocable: false,
|
||||
}),
|
||||
]);
|
||||
|
||||
const output = formatSkillsCheck(report, {});
|
||||
expect(output).toContain("internal-hidden");
|
||||
expect(output).toContain("is not exposed as a command");
|
||||
expect(output).not.toContain("commands/cron may still use it");
|
||||
});
|
||||
|
||||
it("summarizes a mixed bad skill pack in JSON", () => {
|
||||
|
||||
Reference in New Issue
Block a user