import { tool } from "@opencode-ai/plugin"; import { join, posix } from "node:path"; import { ResultReferenceStore } from "../../src/results/store.js"; import { ToolSessionContextStore } from "../../src/session/toolContextStore.js"; import { atomicWriteFile, ensureDirectory, readTextFile, } from "../../src/utils/fileStore.js"; import { sanitizePersistentLine } from "../../src/utils/persistencePolicy.js"; const resultStore = new ResultReferenceStore(); const toolContextStore = new ToolSessionContextStore(); const initializePromise = Promise.all([ resultStore.initialize(), toolContextStore.initialize(), ]); const SKILLS_ROOT_DIR = ".opencode/skills"; const LEARNED_PATTERNS_MARKER = "## Learned Patterns"; let writeQueue: Promise = Promise.resolve(); export default tool({ description: "将已验证、可复用、非敏感的 workflow 或方法模式写入指定的 .opencode/skills 目录,由 opencode 自动识别和加载。", args: { reason: tool.schema .string() .describe("Why this workflow or method should be learned for future reuse."), skill_path: tool.schema .string() .describe("Target skill directory path relative to .opencode/skills, for example analytics/simulation-analysis/leakage or platform/governance-observability/meta."), pattern: tool.schema .string() .describe("A reusable workflow pattern written as one concise bullet-like sentence."), signal_type: tool.schema .string() .describe("Signal type, e.g. validated_workflow, successful_complex_convergence, analysis_method, tool_recovery_pattern."), confidence: tool.schema .number() .describe("Confidence between 0 and 1. Only very high-confidence patterns are stored as learned skills."), result_refs: tool.schema .array(tool.schema.string()) .optional() .describe("Optional authorized result_ref list used only for evidence validation before persisting the skill."), }, async execute(args, context) { await initializePromise; const sessionContext = await toolContextStore.read(context.sessionID); if (!sessionContext) { throw new Error(`session context not found for ${context.sessionID}`); } const skillPath = normalizeSkillPath(args.skill_path); if (!skillPath) { return JSON.stringify({ ok: true, kind: "skill", decision: "rejected", detail: "invalid skill_path; expected a relative path under .opencode/skills", }); } const pattern = sanitizePersistentLine(args.pattern, 320); if (!pattern) { return JSON.stringify({ ok: true, kind: "skill", decision: "rejected", detail: "pattern rejected by persistence policy", }); } if (args.confidence < 0.85) { return JSON.stringify({ ok: true, kind: "skill", decision: "rejected", detail: "only very high-confidence patterns can be stored as skills", }); } if (args.result_refs?.length) { await Promise.all( args.result_refs.map(async (resultRef) => { const record = await resultStore.peekAuthorized(resultRef, { actorKey: sessionContext.actorKey, projectId: sessionContext.projectId, }); if (!record) { throw new Error(`unauthorized or missing result_ref: ${resultRef}`); } }), ); } const result = await appendLearnedSkillPattern(skillPath, pattern); return JSON.stringify({ ok: true, kind: "skill", decision: result.changed ? "accepted" : "deduped", detail: result.changed ? "skill file updated" : "pattern already existed", target: result.target, }); }, }); const appendLearnedSkillPattern = async (skillPath: string, pattern: string) => { return serializeWrite(async () => { const target = join(SKILLS_ROOT_DIR, skillPath, "SKILL.md"); const current = (await readTextFile(target)) ?? defaultLearnedSkill(skillPath); const existingPatterns = extractLearnedPatterns(current); if (existingPatterns.includes(pattern)) { return { changed: false, target }; } const next = current.includes(LEARNED_PATTERNS_MARKER) ? current.replace( LEARNED_PATTERNS_MARKER, `${LEARNED_PATTERNS_MARKER}\n- ${pattern}`, ) : `${current.trimEnd()}\n\n${LEARNED_PATTERNS_MARKER}\n- ${pattern}\n`; await ensureDirectory(join(SKILLS_ROOT_DIR, skillPath)); await atomicWriteFile(target, next); return { changed: true, target }; }); }; const serializeWrite = async (task: () => Promise) => { const run = writeQueue.catch(() => undefined).then(task); writeQueue = run.then( () => undefined, () => undefined, ); return run; }; const defaultLearnedSkill = (skillPath: string) => `--- name: tjwater-action-${toSkillName(skillPath)} description: 由 skill_manager 在线追加的高置信度可复用 workflow。 version: 1.0.0 --- # learned skill ## 简介 记录由 \`skill_manager\` 在线追加的高置信度 workflow 模式。 ## Learned Patterns `; const normalizeSkillPath = (rawSkillPath: string) => { const normalized = posix.normalize(rawSkillPath.trim().replace(/^\/+|\/+$/g, "")); if (!normalized || normalized === "." || normalized.startsWith("..")) { return null; } if (normalized === "SKILL.md" || normalized.endsWith("/SKILL.md")) { return null; } if (!/^[a-z0-9._/-]+$/i.test(normalized)) { return null; } return normalized; }; const toSkillName = (skillPath: string) => skillPath .split("/") .filter(Boolean) .join("-") .replace(/[^a-z0-9._-]+/gi, "-") .replace(/^-+|-+$/g, "") .slice(0, 120) || "generated-skill"; const extractLearnedPatterns = (content: string) => { if (!content.includes(LEARNED_PATTERNS_MARKER)) { return []; } return (content.split(LEARNED_PATTERNS_MARKER)[1] ?? "") .split("\n") .filter((line) => line.trim().startsWith("- ")) .map((line) => line.trim().slice(2)); };