Add agent visibility to skills check (#75983)

Merged via squash.

Prepared head SHA: 63bac4340f
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
Mariano
2026-05-02 20:50:38 +02:00
committed by GitHub
parent 91cc1df128
commit 3b347d1c7e
18 changed files with 903 additions and 42 deletions

View File

@@ -184,6 +184,8 @@ describe("command-path-policy", () => {
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "skills", "info", "browser"])).toBe(
"bypass",
);
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "skills", "check"])).toBe("bypass");
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "skills", "list"])).toBe("bypass");
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "skills", "search", "browser"])).toBe(
"default",
);

View File

@@ -362,6 +362,9 @@ describe("runCli exit behavior", () => {
["agents list", ["node", "openclaw", "agents", "list"]],
["models list", ["node", "openclaw", "models", "list"]],
["models status without live probe", ["node", "openclaw", "models", "status"]],
["skills check", ["node", "openclaw", "skills", "check"]],
["skills info", ["node", "openclaw", "skills", "info", "weather"]],
["skills list", ["node", "openclaw", "skills", "list"]],
["tasks list", ["node", "openclaw", "tasks", "list"]],
["legacy singular tool namespace", ["node", "openclaw", "tool", "image_generate"]],
["gateway tools namespace typo", ["node", "openclaw", "tools", "effective"]],
@@ -386,6 +389,16 @@ describe("runCli exit behavior", () => {
expect(startProxyMock).toHaveBeenCalledWith(undefined);
});
it("does not install the env proxy dispatcher for bypassed skills inspection commands", async () => {
hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(true);
tryRouteCliMock.mockResolvedValueOnce(true);
await runCli(["node", "openclaw", "skills", "check"]);
expect(hasEnvHttpProxyAgentConfiguredMock).not.toHaveBeenCalled();
expect(ensureGlobalUndiciEnvProxyDispatcherMock).not.toHaveBeenCalled();
});
it.each([
["tool", ["node", "openclaw", "tool", "image_generate"]],
["tools", ["node", "openclaw", "tools", "effective"]],

View File

@@ -265,6 +265,7 @@ function shouldBootstrapCliProxyBeforeFastPath(env: NodeJS.ProcessEnv = process.
async function bootstrapCliProxyCaptureAndDispatcher(
startupTrace: ReturnType<typeof createGatewayCliMainStartupTrace>,
options: { ensureDispatcher?: boolean } = {},
): Promise<void> {
const [
{ initializeDebugProxyCapture, finalizeDebugProxyCapture },
@@ -276,7 +277,9 @@ async function bootstrapCliProxyCaptureAndDispatcher(
process.once("exit", () => {
finalizeDebugProxyCapture();
});
await startupTrace.measure("proxy-dispatcher", () => ensureCliEnvProxyDispatcher());
if (options.ensureDispatcher !== false) {
await startupTrace.measure("proxy-dispatcher", () => ensureCliEnvProxyDispatcher());
}
maybeWarnAboutDebugProxyCoverage();
}
@@ -440,7 +443,9 @@ export async function runCli(argv: string[] = process.argv) {
return;
}
const bootstrapProxyBeforeFastPath = shouldBootstrapCliProxyBeforeFastPath();
const shouldUseCliEnvProxy = shouldStartProxyForCli(normalizedArgv);
const bootstrapProxyBeforeFastPath =
shouldUseCliEnvProxy && shouldBootstrapCliProxyBeforeFastPath();
if (
!bootstrapProxyBeforeFastPath &&
(await tryRunGatewayRunFastPath(normalizedArgv, startupTrace))
@@ -448,7 +453,9 @@ export async function runCli(argv: string[] = process.argv) {
return;
}
await bootstrapCliProxyCaptureAndDispatcher(startupTrace);
await bootstrapCliProxyCaptureAndDispatcher(startupTrace, {
ensureDispatcher: shouldUseCliEnvProxy,
});
if (
bootstrapProxyBeforeFastPath &&

View File

@@ -59,6 +59,9 @@ const mocks = vi.hoisted(() => {
runtimeStdout.push(JSON.stringify(value, null, space > 0 ? space : undefined));
}),
exit: vi.fn((code: number) => {
if (code === 0) {
return;
}
throw new Error(`__exit__:${code}`);
}),
};
@@ -142,7 +145,16 @@ describe("skills cli commands", () => {
return program;
};
const runCommand = (argv: string[]) => createProgram().parseAsync(argv, { from: "user" });
const runCommand = async (argv: string[]) => {
try {
await createProgram().parseAsync(argv, { from: "user" });
} catch (error) {
if (error instanceof Error && error.message === "__exit__:0") {
return;
}
throw error;
}
};
beforeEach(() => {
runtimeLogs.length = 0;
@@ -414,9 +426,10 @@ describe("skills cli commands", () => {
])("routes skills $label JSON output through stdout", async ({ argv, assert }) => {
await runCommand(argv);
expect(buildWorkspaceSkillStatusMock).toHaveBeenCalledWith("/tmp/workspace", {
config: {},
});
expect(buildWorkspaceSkillStatusMock).toHaveBeenCalledWith(
"/tmp/workspace",
expect.objectContaining({ config: {} }),
);
expect(
defaultRuntime.writeStdout.mock.calls.length + defaultRuntime.writeJson.mock.calls.length,
).toBeGreaterThan(0);
@@ -441,9 +454,10 @@ describe("skills cli commands", () => {
await runCommand(argv);
});
expect(buildWorkspaceSkillStatusMock).toHaveBeenCalledWith("/tmp/workspace-writer", {
config: {},
});
expect(buildWorkspaceSkillStatusMock).toHaveBeenCalledWith(
"/tmp/workspace-writer",
expect.objectContaining({ config: {} }),
);
});
it.each([
@@ -460,9 +474,10 @@ describe("skills cli commands", () => {
});
expect(resolveAgentIdByWorkspacePathMock).not.toHaveBeenCalled();
expect(buildWorkspaceSkillStatusMock).toHaveBeenCalledWith("/tmp/workspace-writer", {
config: {},
});
expect(buildWorkspaceSkillStatusMock).toHaveBeenCalledWith(
"/tmp/workspace-writer",
expect.objectContaining({ config: {} }),
);
});
it("falls back to the default agent outside configured workspaces", async () => {
@@ -476,9 +491,10 @@ describe("skills cli commands", () => {
expect(resolveAgentIdByWorkspacePathMock).toHaveBeenCalledWith({}, "/tmp/unrelated");
expect(resolveDefaultAgentIdMock).toHaveBeenCalledWith({});
expect(buildWorkspaceSkillStatusMock).toHaveBeenCalledWith("/tmp/workspace-main", {
config: {},
});
expect(buildWorkspaceSkillStatusMock).toHaveBeenCalledWith(
"/tmp/workspace-main",
expect.objectContaining({ config: {} }),
);
});
it("keeps non-JSON skills list output on stdout with human-readable formatting", async () => {

View File

@@ -17,6 +17,7 @@ export type SkillInfoOptions = {
export type SkillsCheckOptions = {
json?: boolean;
agent?: string;
};
function appendClawHubHint(output: string, json?: boolean): string {
@@ -27,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");
}
@@ -95,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({
@@ -108,6 +114,10 @@ export function formatSkillsList(report: SkillStatusReport, opts: SkillsListOpti
eligible: s.eligible,
disabled: s.disabled,
blockedByAllowlist: s.blockedByAllowlist,
blockedByAgentFilter: s.blockedByAgentFilter,
modelVisible: s.modelVisible,
userInvocable: s.userInvocable,
commandVisible: s.commandVisible,
source: s.source,
bundled: s.bundled,
primaryEnv: s.primaryEnv,
@@ -125,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);
@@ -150,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({
@@ -186,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;
@@ -209,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}`);
}
@@ -291,25 +312,47 @@ export function formatSkillInfo(
export function formatSkillsCheck(report: SkillStatusReport, opts: SkillsCheckOptions): string {
const eligible = report.skills.filter((s) => s.eligible);
const modelVisible = report.skills.filter((s) => s.modelVisible);
const commandVisible = report.skills.filter((s) => s.commandVisible);
const disabled = report.skills.filter((s) => s.disabled);
const blocked = report.skills.filter((s) => s.blockedByAllowlist && !s.disabled);
const missingReqs = report.skills.filter(
(s) => !s.eligible && !s.disabled && !s.blockedByAllowlist,
const agentFiltered = report.skills.filter((s) => s.eligible && s.blockedByAgentFilter);
const promptHidden = report.skills.filter(
(s) => s.eligible && !s.blockedByAgentFilter && !s.modelVisible,
);
const missingReqs = report.skills.filter(
(s) => !s.eligible && !s.disabled && !s.blockedByAllowlist && !s.blockedByAgentFilter,
);
const agentId = report.agentId ?? opts.agent;
if (opts.json) {
return JSON.stringify(
sanitizeJsonValue({
agentId,
agentSkillFilter: report.agentSkillFilter,
workspaceDir: report.workspaceDir,
managedSkillsDir: report.managedSkillsDir,
summary: {
total: report.skills.length,
eligible: eligible.length,
modelVisible: modelVisible.length,
commandVisible: commandVisible.length,
disabled: disabled.length,
blocked: blocked.length,
agentFiltered: agentFiltered.length,
notInjected: promptHidden.length,
missingRequirements: missingReqs.length,
},
eligible: eligible.map((s) => s.name),
modelVisible: modelVisible.map((s) => s.name),
commandVisible: commandVisible.map((s) => s.name),
disabled: disabled.map((s) => s.name),
blocked: blocked.map((s) => s.name),
agentFiltered: agentFiltered.map((s) => s.name),
notInjected: promptHidden.map((s) => ({
name: s.name,
reason: "disable-model-invocation",
})),
missingRequirements: missingReqs.map((s) => ({
name: s.name,
missing: s.missing,
@@ -323,22 +366,85 @@ export function formatSkillsCheck(report: SkillStatusReport, opts: SkillsCheckOp
const lines: string[] = [];
lines.push(theme.heading("Skills Status Check"));
if (agentId) {
lines.push(`${theme.muted("Agent:")} ${sanitizeForLog(agentId)}`);
}
lines.push("");
lines.push(`${theme.muted("Total:")} ${report.skills.length}`);
lines.push(`${theme.success("✓")} ${theme.muted("Eligible:")} ${eligible.length}`);
lines.push(`${theme.success("✓")} ${theme.muted("Visible to model:")} ${modelVisible.length}`);
lines.push(
`${theme.success("✓")} ${theme.muted("Available as command:")} ${commandVisible.length}`,
);
lines.push(`${theme.warn("⏸")} ${theme.muted("Disabled:")} ${disabled.length}`);
lines.push(`${theme.warn("🚫")} ${theme.muted("Blocked by allowlist:")} ${blocked.length}`);
if (agentId || agentFiltered.length > 0) {
lines.push(
`${theme.warn("🚫")} ${theme.muted("Excluded by agent allowlist:")} ${agentFiltered.length}`,
);
}
if (promptHidden.length > 0) {
lines.push(
`${theme.warn("△")} ${theme.muted("Ready but hidden from model prompt:")} ${promptHidden.length}`,
);
}
lines.push(`${theme.error("✗")} ${theme.muted("Missing requirements:")} ${missingReqs.length}`);
if (eligible.length > 0) {
if (modelVisible.length > 0 || commandVisible.length > 0 || promptHidden.length > 0) {
lines.push("");
lines.push(theme.heading("Ready to use:"));
for (const skill of eligible) {
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.`,
);
}
if (commandVisible.length > 0) {
lines.push(
` ${theme.muted("Available as command:")} people, scripts, or cron jobs can call the skill explicitly.`,
);
}
if (promptHidden.length > 0) {
lines.push(
` ${theme.muted("Hidden from model prompt:")} installed and ready, but kept out of normal chat.`,
);
}
}
if (modelVisible.length > 0) {
lines.push("");
lines.push(theme.heading("Ready and visible to model:"));
for (const skill of modelVisible) {
const emoji = normalizeSkillEmoji(skill.emoji);
lines.push(` ${emoji} ${sanitizeForLog(skill.name)}`);
}
}
if (promptHidden.length > 0) {
lines.push("");
lines.push(theme.heading("Ready but hidden from model prompt:"));
for (const skill of promptHidden) {
const emoji = normalizeSkillEmoji(skill.emoji);
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})`)}`);
}
}
if (agentFiltered.length > 0) {
lines.push("");
lines.push(theme.heading("Excluded by agent allowlist:"));
for (const skill of agentFiltered) {
const emoji = normalizeSkillEmoji(skill.emoji);
lines.push(
` ${emoji} ${sanitizeForLog(skill.name)} ${theme.muted("(loaded, but this agent is not allowed to see/use it)")}`,
);
}
}
if (missingReqs.length > 0) {
lines.push("");
lines.push(theme.heading("Missing requirements:"));

View File

@@ -10,7 +10,7 @@ vi.mock("@mariozechner/pi-coding-agent", () => ({
}));
function createMockSkill(overrides: Partial<SkillStatusEntry> = {}): SkillStatusEntry {
return {
const skill: SkillStatusEntry = {
name: "test-skill",
description: "A test skill",
source: "bundled",
@@ -23,10 +23,21 @@ function createMockSkill(overrides: Partial<SkillStatusEntry> = {}): SkillStatus
always: false,
disabled: false,
blockedByAllowlist: false,
blockedByAgentFilter: false,
eligible: true,
modelVisible: true,
userInvocable: true,
commandVisible: true,
...createEmptyInstallChecks(),
...overrides,
};
if (overrides.modelVisible === undefined) {
skill.modelVisible = skill.eligible && !skill.blockedByAgentFilter;
}
if (overrides.commandVisible === undefined) {
skill.commandVisible = skill.eligible && !skill.blockedByAgentFilter && skill.userInvocable;
}
return skill;
}
function createMockReport(skills: SkillStatusEntry[]): SkillStatusReport {
@@ -108,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", () => {
@@ -190,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", () => {
@@ -228,6 +275,128 @@ describe("skills-cli", () => {
expect(output).toContain("🎛️ ready-emoji");
expect(output).toContain("🎙️ missing-emoji");
});
it("shows agent-filtered and loaded-but-not-injected skills", () => {
const report = {
...createMockReport([
createMockSkill({ name: "visible", eligible: true, modelVisible: true }),
createMockSkill({
name: "prompt-hidden",
eligible: true,
modelVisible: false,
commandVisible: true,
}),
createMockSkill({
name: "not-assigned",
eligible: true,
blockedByAgentFilter: true,
}),
]),
agentId: "specialist",
agentSkillFilter: ["visible", "prompt-hidden"],
};
const output = formatSkillsCheck(report, {});
expect(output).toContain("Agent:");
expect(output).toContain("specialist");
expect(output).toContain("Ready and visible to model");
expect(output).toContain("visible");
expect(output).toContain("Ready but hidden from model prompt");
expect(output).toContain("prompt-hidden");
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");
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", () => {
const output = formatSkillsCheck(
{
...createMockReport([
createMockSkill({ name: "ready", eligible: true }),
createMockSkill({
name: "prompt-hidden",
eligible: true,
modelVisible: false,
commandVisible: true,
}),
createMockSkill({
name: "slash-hidden",
eligible: true,
modelVisible: true,
userInvocable: false,
commandVisible: false,
}),
createMockSkill({
name: "agent-filtered",
eligible: true,
blockedByAgentFilter: true,
}),
createMockSkill({
name: "missing-bin",
eligible: false,
missing: { bins: ["missing-tool"], anyBins: [], env: [], config: [], os: [] },
}),
createMockSkill({ name: "disabled", eligible: false, disabled: true }),
createMockSkill({
name: "blocked-bundled",
eligible: false,
blockedByAllowlist: true,
}),
]),
agentId: "specialist",
agentSkillFilter: ["ready", "prompt-hidden", "slash-hidden", "missing-bin"],
},
{ json: true },
);
const parsed = JSON.parse(output) as {
summary: Record<string, number>;
modelVisible: string[];
commandVisible: string[];
agentFiltered: string[];
notInjected: Array<{ name: string; reason: string }>;
missingRequirements: Array<{ name: string }>;
};
expect(parsed.summary).toMatchObject({
total: 7,
eligible: 4,
modelVisible: 2,
commandVisible: 2,
disabled: 1,
blocked: 1,
agentFiltered: 1,
notInjected: 1,
missingRequirements: 1,
});
expect(parsed.modelVisible).toEqual(["ready", "slash-hidden"]);
expect(parsed.commandVisible).toEqual(["ready", "prompt-hidden"]);
expect(parsed.agentFiltered).toEqual(["agent-filtered"]);
expect(parsed.notInjected).toEqual([
{ name: "prompt-hidden", reason: "disable-model-invocation" },
]);
expect(parsed.missingRequirements.map((entry) => entry.name)).toEqual(["missing-bin"]);
});
});
describe("JSON output", () => {
@@ -266,6 +435,7 @@ describe("skills-cli", () => {
assert: (parsed: Record<string, unknown>) => {
const summary = parsed.summary as Record<string, unknown>;
expect(summary.eligible).toBe(1);
expect(summary.modelVisible).toBe(1);
expect(summary.total).toBe(2);
},
},

View File

@@ -37,6 +37,7 @@ type ResolveSkillsWorkspaceOptions = {
function resolveSkillsWorkspace(options?: ResolveSkillsWorkspaceOptions): {
config: ReturnType<typeof getRuntimeConfig>;
workspaceDir: string;
agentId: string;
} {
const config = getRuntimeConfig();
const explicitAgentId = normalizeOptionalString(options?.agentId);
@@ -46,6 +47,7 @@ function resolveSkillsWorkspace(options?: ResolveSkillsWorkspaceOptions): {
const agentId = explicitAgentId ?? inferredAgentId ?? resolveDefaultAgentId(config);
return {
config,
agentId,
workspaceDir: resolveAgentWorkspaceDir(config, agentId),
};
}
@@ -60,9 +62,9 @@ function resolveAgentOption(
async function loadSkillsStatusReport(
options?: ResolveSkillsWorkspaceOptions,
): Promise<SkillStatusReport> {
const { config, workspaceDir } = resolveSkillsWorkspace(options);
const { config, workspaceDir, agentId } = resolveSkillsWorkspace(options);
const { buildWorkspaceSkillStatus } = await import("../agents/skills-status.js");
return buildWorkspaceSkillStatus(workspaceDir, { config });
return buildWorkspaceSkillStatus(workspaceDir, { config, agentId });
}
async function runSkillsAction(
@@ -72,6 +74,7 @@ async function runSkillsAction(
try {
const report = await loadSkillsStatusReport(options);
defaultRuntime.writeStdout(render(report));
defaultRuntime.exit(0);
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
@@ -256,9 +259,9 @@ export function registerSkillsCli(program: Command) {
skills
.command("check")
.description("Check which skills are ready vs missing requirements")
.option("--json", "Output as JSON", false)
.description("Check which skills are ready, visible, or missing requirements")
.option("--agent <id>", "Target agent workspace (defaults to cwd-inferred, then default agent)")
.option("--json", "Output as JSON", false)
.action(async (opts: { json?: boolean; agent?: string }, command: Command) => {
await runSkillsAction((report) => formatSkillsCheck(report, opts), {
agentId: resolveAgentOption(command, opts),