feat: skills
CI / verify (pull_request) Successful in 10s

This commit is contained in:
2026-09-26 21:35:59 +02:00
parent 4f956effff
commit d132060697
14 changed files with 338 additions and 96 deletions
+47
View File
@@ -8,6 +8,8 @@ import type { DeepMutable } from "@opencode/plugin/promise/types";
import type { Info, ToolContext, ToolEditor } from "@opencode/plugin/promise/tool";
import type { JsonValue, ModelInfo, SessionGetInput } from "@opencode/client";
import type { SessionContext } from "@opencode/plugin/promise/session";
import type { SkillEditor } from "@opencode/plugin/promise/skill";
import type { Skill } from "@opencode/schema/skill";
import { z } from "zod";
import { gptAgents, settings, workerNames } from "../src/config.js";
import { basePlugin as plugin } from "../src/index.js";
@@ -107,6 +109,7 @@ async function host(
});
const storage = new Map<string, JsonValue>();
const tools = new Map<string, Info>();
const skills = new Map<string, Skill.Info>();
const mcpTools = new Set(configuredMcpTools);
const mcpServers = new Map(Object.entries(structuredClone(configuredMcp)));
let mcpTransform!: (editor: {
@@ -204,6 +207,25 @@ async function host(
return registration;
},
},
skill: {
async transform(callback: (editor: SkillEditor) => void) {
callback({
list: () => [...skills.values()],
get: (id) => skills.get(id),
add: (skill) => {
skills.set(skill.id, skill);
},
update(id, update) {
const skill = skills.get(id);
if (skill) update(skill);
},
remove: (id) => {
skills.delete(id);
},
});
return registration;
},
},
agent: {
async list() {
return { data: [...agents.values()] };
@@ -272,6 +294,7 @@ async function host(
if (cleanup) cleanups.push(cleanup);
return {
agents,
skills,
build,
mcpServers,
defaultAgent,
@@ -416,6 +439,30 @@ test("orchestration inherits deep-lo and falls back to good-lo with available wo
).toBe(false);
});
test("registers the embedded anti-overengineering skill and makes it available to coordinate", async () => {
const result = await host();
const skill = result.skills.get("anti-overengineering")!;
expect(String(skill.name)).toBe("anti-overengineering");
expect(skill.content).toContain("## Correctness floor");
expect(skill.content).toContain("## Final subtraction pass");
expect(await Bun.file(skill.path).exists()).toBe(true);
const coordinate = result.agents.get("coordinate")!;
expect(coordinate.permissions).toContainEqual({
action: "skill",
resource: "*",
effect: "allow",
});
expect(coordinate.permissions.some((rule) => rule.action === "shell")).toBe(false);
for (const models of [allModels, allModels.slice(0, 2)]) {
const prompt = (await host(models)).agents.get("coordinate")?.system;
expect(prompt).toContain("Audit subagent review");
expect(prompt).toContain("reject overkill or");
expect(prompt).toContain("BEFORE delegating it");
expect(prompt).toContain("Ordinary in-scope bugfixes need no extra approval");
expect(prompt).toContain("Never waive correctness, security or data integrity");
}
});
test("custom assignments resolve duplicate model IDs by provider and refresh provider-aware fallback", async () => {
const agents = customAssignments("alpha");
agents["fast-lo"] = { ...agents["fast-lo"], model: "beta/shared" };