@lmzhen/dsh-tool-skill-manage 0.1.0-rc.9 → 0.2.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +114 -24
- package/lib/types/index.d.ts +2 -0
- package/package.json +12 -12
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
2
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3
|
-
import { DEFAULT_SKILL_LIMITS, DSH_AUTHORING_STANDARDS, SkillLibrary, evolutionIoAdapter } from "@lmzhen/dsh-evolution-core";
|
|
3
|
+
import { DEFAULT_SKILL_LIMITS, DSH_AUTHORING_STANDARDS, SKILLS_GUIDANCE, SkillLibrary, authoringFeedback, computeDedupGroups, evolutionIoAdapter, parseFrontmatter, resolveOrigins } from "@lmzhen/dsh-evolution-core";
|
|
4
4
|
//#region lib/types/index.js
|
|
5
5
|
/**
|
|
6
6
|
* Model-facing skill_manage tool over ctx.evolutionIo + ctx.skillUsage.
|
|
@@ -22,19 +22,49 @@ const Config = z.object({
|
|
|
22
22
|
maxSkillNameLength: z.number().default(DEFAULT_SKILL_LIMITS.maxNameLength),
|
|
23
23
|
maxDescriptionLength: z.number().default(DEFAULT_SKILL_LIMITS.maxDescriptionLength),
|
|
24
24
|
maxSkillContentChars: z.number().default(DEFAULT_SKILL_LIMITS.maxSkillContentChars),
|
|
25
|
-
maxSkillFileBytes: z.number().default(DEFAULT_SKILL_LIMITS.maxSkillFileBytes)
|
|
25
|
+
maxSkillFileBytes: z.number().default(DEFAULT_SKILL_LIMITS.maxSkillFileBytes),
|
|
26
|
+
descriptionStrict: z.boolean().default(false)
|
|
26
27
|
});
|
|
28
|
+
/**
|
|
29
|
+
* The requesting session's effective approval policy, mirroring
|
|
30
|
+
* `dsh-user-approval` (override ?? configured default). Returns undefined when
|
|
31
|
+
* the approval service is not mounted or no session is available — callers
|
|
32
|
+
* keep their previous behavior.
|
|
33
|
+
*/
|
|
34
|
+
function effectiveSessionPolicy(ctx, session) {
|
|
35
|
+
const approval = ctx.get("approval");
|
|
36
|
+
if (!approval || session === void 0) return void 0;
|
|
37
|
+
return approval.overrideOf(session) ?? approval.config.policy ?? "ask";
|
|
38
|
+
}
|
|
27
39
|
function apply(ctx, rawConfig = {}) {
|
|
40
|
+
const systemPrompt = ctx.get("systemPrompt");
|
|
41
|
+
if (systemPrompt) ctx.effect(() => systemPrompt.section({
|
|
42
|
+
name: "evolution-skills-guidance",
|
|
43
|
+
order: 900,
|
|
44
|
+
text: SKILLS_GUIDANCE
|
|
45
|
+
}), "tool-skill-manage.skills-guidance");
|
|
28
46
|
const io = evolutionIoAdapter(() => ctx.evolutionIo.provider());
|
|
29
47
|
const library = new SkillLibrary(rawConfig.root || void 0, io, {
|
|
30
48
|
maxNameLength: rawConfig.maxSkillNameLength ?? DEFAULT_SKILL_LIMITS.maxNameLength,
|
|
31
49
|
maxDescriptionLength: rawConfig.maxDescriptionLength ?? DEFAULT_SKILL_LIMITS.maxDescriptionLength,
|
|
32
50
|
maxSkillContentChars: rawConfig.maxSkillContentChars ?? DEFAULT_SKILL_LIMITS.maxSkillContentChars,
|
|
33
51
|
maxSkillFileBytes: rawConfig.maxSkillFileBytes ?? DEFAULT_SKILL_LIMITS.maxSkillFileBytes
|
|
52
|
+
}, (event) => {
|
|
53
|
+
ctx.emit("evolution/skill-mutated", event);
|
|
34
54
|
});
|
|
35
55
|
async function executeCore(args, origin = "foreground") {
|
|
36
56
|
const action = args.action;
|
|
37
57
|
const name = args.name ?? "";
|
|
58
|
+
if (action === "review") return {
|
|
59
|
+
ok: true,
|
|
60
|
+
message: await buildSkillReviewText(),
|
|
61
|
+
skills: []
|
|
62
|
+
};
|
|
63
|
+
if (action === "skip") return {
|
|
64
|
+
ok: true,
|
|
65
|
+
message: "Skipped; no skill changes this pass.",
|
|
66
|
+
skills: []
|
|
67
|
+
};
|
|
38
68
|
if (action === "list") {
|
|
39
69
|
const list = await library.list();
|
|
40
70
|
return {
|
|
@@ -43,40 +73,81 @@ function apply(ctx, rawConfig = {}) {
|
|
|
43
73
|
skills: list.map((s) => s.name)
|
|
44
74
|
};
|
|
45
75
|
}
|
|
76
|
+
let feedbackLines = [];
|
|
77
|
+
if ((action === "create" || action === "edit" || action === "update") && args.content) {
|
|
78
|
+
const parsed = parseFrontmatter(args.content);
|
|
79
|
+
if (parsed) {
|
|
80
|
+
const feedback = authoringFeedback(parsed.frontmatter);
|
|
81
|
+
feedbackLines = feedback.lines;
|
|
82
|
+
if (rawConfig.descriptionStrict === true && feedback.over60) return {
|
|
83
|
+
ok: false,
|
|
84
|
+
message: `Authoring check: description ${feedback.descriptionChars}/60 characters exceeds the strict bar; tighten it to <=60 or set descriptionStrict=false.`,
|
|
85
|
+
skills: []
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
46
89
|
let result;
|
|
47
90
|
if (action === "create") result = await library.create(name, args.content ?? "", origin);
|
|
48
|
-
else if (action === "edit" || action === "update") result = await library.update(name, args.content ?? "");
|
|
49
|
-
else if (action === "patch") result = await library.patch(name, args.old_string ?? "", args.new_string ?? "", args.file_path ?? "", args.replace_all === true);
|
|
50
|
-
else if (action === "delete") result = await library.archive(name, args.absorbed_into
|
|
51
|
-
else if (action === "write_file") result = await library.writeSupportFile(name, args.file_path ?? "", args.file_content ?? "");
|
|
52
|
-
else if (action === "remove_file") result = await library.removeSupportFile(name, args.file_path ?? "");
|
|
91
|
+
else if (action === "edit" || action === "update") result = await library.update(name, args.content ?? "", origin);
|
|
92
|
+
else if (action === "patch") result = await library.patch(name, args.old_string ?? "", args.new_string ?? "", args.file_path ?? "", args.replace_all === true, origin);
|
|
93
|
+
else if (action === "delete") result = await library.archive(name, args.absorbed_into ? { absorbedInto: args.absorbed_into } : {});
|
|
94
|
+
else if (action === "write_file") result = await library.writeSupportFile(name, args.file_path ?? "", args.file_content ?? "", origin);
|
|
95
|
+
else if (action === "remove_file") result = await library.removeSupportFile(name, args.file_path ?? "", origin);
|
|
96
|
+
else if (action === "restructure") result = await library.restructure(name, (args.restructure ?? []).map((move) => ({
|
|
97
|
+
heading: move.heading ?? "",
|
|
98
|
+
toFile: move.to_file ?? ""
|
|
99
|
+
})), origin);
|
|
100
|
+
else if (action === "pin") result = await library.setPinned(name, true, origin);
|
|
101
|
+
else if (action === "unpin") result = await library.setPinned(name, false, origin);
|
|
53
102
|
else result = {
|
|
54
103
|
ok: false,
|
|
55
104
|
message: `Unknown action "${action}".`
|
|
56
105
|
};
|
|
57
106
|
if (result.ok) {
|
|
58
|
-
|
|
59
|
-
if (name && action
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
107
|
+
const mutating = action !== "list" && action !== "review" && action !== "skip" && action !== "pin" && action !== "unpin";
|
|
108
|
+
if (name && action === "create") {
|
|
109
|
+
await ctx.skillUsage.ensureRecord(name);
|
|
110
|
+
if (origin !== "foreground") await ctx.skillUsage.markAgentCreated(name);
|
|
111
|
+
}
|
|
112
|
+
if (name && action === "delete") await ctx.skillUsage.markArchived(name);
|
|
113
|
+
else if (name && mutating && action !== "create") await ctx.skillUsage.record(name, "patch");
|
|
65
114
|
}
|
|
66
115
|
return {
|
|
67
116
|
ok: result.ok,
|
|
68
|
-
message: result.message,
|
|
117
|
+
message: result.ok && feedbackLines.length > 0 ? `${result.message}\n\nAuthoring check:\n${feedbackLines.map((line) => `- ${line}`).join("\n")}` : result.message,
|
|
69
118
|
skills: []
|
|
70
119
|
};
|
|
71
120
|
}
|
|
121
|
+
async function buildSkillReviewText() {
|
|
122
|
+
const list = await library.list();
|
|
123
|
+
const report = await ctx.skillUsage.report();
|
|
124
|
+
const lines = list.map((summary) => {
|
|
125
|
+
const record = report.get(summary.name);
|
|
126
|
+
const quality = record?.quality_score !== void 0 ? ` quality:${record.quality_score.toFixed(2)}${record.quality_warn ? "⚠" : ""}` : "";
|
|
127
|
+
return `- ${summary.name} | ${record?.state ?? "active"} | use:${record?.use_count ?? 0} view:${record?.view_count ?? 0} patch:${record?.patch_count ?? 0}${quality}${summary.protectedBy ? ` [${summary.protectedBy}]` : ""}`;
|
|
128
|
+
});
|
|
129
|
+
const groups = computeDedupGroups({ contents: new Map(await Promise.all(list.map(async (summary) => [summary.name, await library.read(summary.name) ?? ""]))) });
|
|
130
|
+
const dedupLines = groups.slice(0, 3).map((group) => `- ${group.join(" ~ ")}`);
|
|
131
|
+
const warned = list.filter((summary) => report.get(summary.name)?.quality_warn === true).map((summary) => summary.name);
|
|
132
|
+
const warningLine = warned.length === 0 ? "" : `\nWarning skills (${warned.length}): ${warned.join(", ")} — low quality; consider consolidating them before authoring new skills.`;
|
|
133
|
+
return [
|
|
134
|
+
list.length === 0 ? "No skills yet. Create one with action=create, or it is safe to author a new class-level umbrella." : `Skills: ${list.length} total. Below each name: state, use/view/patch counts, quality (0-1, ⚠ = low) and protection.${groups.length > 0 ? `\n\nNear-duplicate groups (${groups.length}):` : ""}`,
|
|
135
|
+
"",
|
|
136
|
+
...lines,
|
|
137
|
+
...dedupLines,
|
|
138
|
+
warningLine
|
|
139
|
+
].join("\n");
|
|
140
|
+
}
|
|
72
141
|
ctx.tools.register(defineTool({
|
|
73
142
|
name: "skill_manage",
|
|
74
|
-
description: "Manage reusable skills. create/edit take full SKILL.md content; patch applies old_string -> new_string; delete archives to .archive. Protected bundled/hub skills reject mutation; pinned skills reject
|
|
143
|
+
description: "Manage reusable skills. review returns the library review text (state/usage/quality per skill); list returns names only; create/edit take full SKILL.md content; patch applies old_string -> new_string; delete archives to .archive; pin protects a skill from deletion, background review, and the lifecycle (pin/unpin are never allowed from a background review — foreground and delegated subagents may). Protected bundled/hub skills reject any mutation; pinned skills reject deletion and are read-only to the background review.Prefer patching an umbrella over creating narrow skills. restructure moves entire body sections (by their exact \"## heading\" line, via restructure: [{heading, to_file: \"references/<topic>.md\"}]) into a references/ file and replaces each with a pointer line — the skill name and directory never change.Created/edited SKILL.md MUST start with YAML frontmatter (a name/description block), or creation is rejected. " + DSH_AUTHORING_STANDARDS,
|
|
75
144
|
parameters: {
|
|
76
145
|
action: {
|
|
77
146
|
type: "string",
|
|
78
147
|
required: true,
|
|
79
148
|
enum: [
|
|
149
|
+
"review",
|
|
150
|
+
"list",
|
|
80
151
|
"create",
|
|
81
152
|
"edit",
|
|
82
153
|
"update",
|
|
@@ -84,7 +155,10 @@ function apply(ctx, rawConfig = {}) {
|
|
|
84
155
|
"delete",
|
|
85
156
|
"write_file",
|
|
86
157
|
"remove_file",
|
|
87
|
-
"
|
|
158
|
+
"restructure",
|
|
159
|
+
"skip",
|
|
160
|
+
"pin",
|
|
161
|
+
"unpin"
|
|
88
162
|
]
|
|
89
163
|
},
|
|
90
164
|
name: { type: "string" },
|
|
@@ -94,7 +168,18 @@ function apply(ctx, rawConfig = {}) {
|
|
|
94
168
|
replace_all: { type: "boolean" },
|
|
95
169
|
file_path: { type: "string" },
|
|
96
170
|
file_content: { type: "string" },
|
|
97
|
-
absorbed_into: { type: "string" }
|
|
171
|
+
absorbed_into: { type: "string" },
|
|
172
|
+
restructure: {
|
|
173
|
+
type: "array",
|
|
174
|
+
items: {
|
|
175
|
+
type: "object",
|
|
176
|
+
additionalProperties: false,
|
|
177
|
+
properties: {
|
|
178
|
+
heading: { type: "string" },
|
|
179
|
+
to_file: { type: "string" }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
98
183
|
},
|
|
99
184
|
output: {
|
|
100
185
|
schema: {
|
|
@@ -124,17 +209,22 @@ function apply(ctx, rawConfig = {}) {
|
|
|
124
209
|
},
|
|
125
210
|
isConcurrencySafe: () => false,
|
|
126
211
|
async execute(args, exec) {
|
|
127
|
-
const
|
|
212
|
+
const origins = resolveOrigins(exec.agent?.session.header.origin);
|
|
213
|
+
const reviewOrigin = origins.approval;
|
|
214
|
+
const libraryOrigin = origins.library;
|
|
215
|
+
const sessionPolicy = effectiveSessionPolicy(ctx, exec.agent?.session);
|
|
128
216
|
const approval = ctx.get("evolutionApproval");
|
|
129
|
-
if (approval && args.action !== "list") {
|
|
217
|
+
if (approval && args.action !== "list" && args.action !== "review" && args.action !== "skip" && args.action !== "pin" && args.action !== "unpin") {
|
|
130
218
|
const decision = await approval.request({
|
|
131
219
|
kind: "skill",
|
|
132
220
|
summary: `skill ${args.action ?? "?"} ${args.name ?? ""}`.trim(),
|
|
133
221
|
args: {
|
|
134
222
|
operation: args,
|
|
135
|
-
origin
|
|
223
|
+
origin: reviewOrigin,
|
|
224
|
+
libraryOrigin
|
|
136
225
|
},
|
|
137
|
-
origin
|
|
226
|
+
origin: reviewOrigin,
|
|
227
|
+
...sessionPolicy !== void 0 ? { sessionPolicy } : {}
|
|
138
228
|
});
|
|
139
229
|
if (decision.action === "staged") return {
|
|
140
230
|
ok: true,
|
|
@@ -143,13 +233,13 @@ function apply(ctx, rawConfig = {}) {
|
|
|
143
233
|
pending_id: decision.pendingId ?? ""
|
|
144
234
|
};
|
|
145
235
|
}
|
|
146
|
-
return await executeCore(args,
|
|
236
|
+
return await executeCore(args, libraryOrigin);
|
|
147
237
|
}
|
|
148
238
|
}));
|
|
149
239
|
ctx.inject(["evolutionApproval"], (approvalCtx) => {
|
|
150
240
|
const dispose = approvalCtx.evolutionApproval.registerRunner("skill", (args) => {
|
|
151
241
|
const wrapped = args ?? {};
|
|
152
|
-
return executeCore(wrapped.operation ?? {}, wrapped.origin ?? "background_review");
|
|
242
|
+
return executeCore(wrapped.operation ?? {}, wrapped.libraryOrigin ?? wrapped.origin ?? "background_review");
|
|
153
243
|
});
|
|
154
244
|
approvalCtx.effect(() => dispose, "tool-skill-manage.approval-runner");
|
|
155
245
|
});
|
package/lib/types/index.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface Config {
|
|
|
18
18
|
maxDescriptionLength?: number;
|
|
19
19
|
maxSkillContentChars?: number;
|
|
20
20
|
maxSkillFileBytes?: number;
|
|
21
|
+
/** When true, create/update refuse a description over the 60-char authoring bar (default: advisory feedback only). */
|
|
22
|
+
descriptionStrict?: boolean;
|
|
21
23
|
}
|
|
22
24
|
export declare const Config: z<Config>;
|
|
23
25
|
export declare function apply(ctx: Context, rawConfig?: Config): void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-tool-skill-manage",
|
|
3
3
|
"description": "Model-facing skill_manage tool (community build)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0-rc.1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -33,21 +33,21 @@
|
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
36
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
36
|
+
"@lmzhen/dsh-evolution-core": "^0.2.0-rc.1"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
40
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
41
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
42
|
-
"@lmzhen/dsh-evolution-io": "^0.
|
|
43
|
-
"@lmzhen/dsh-skill-usage": "^0.
|
|
40
|
+
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
41
|
+
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
|
|
42
|
+
"@lmzhen/dsh-evolution-io": "^0.2.0-rc.1",
|
|
43
|
+
"@lmzhen/dsh-skill-usage": "^0.2.0-rc.1"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@deepseek-ai/dsh-agent-loop-testkit": "^0.1.
|
|
47
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
48
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
49
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
50
|
-
"@lmzhen/dsh-evolution-io": "^0.
|
|
51
|
-
"@lmzhen/dsh-skill-usage": "^0.
|
|
46
|
+
"@deepseek-ai/dsh-agent-loop-testkit": "^0.1.1-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
48
|
+
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
|
|
49
|
+
"@lmzhen/dsh-evolution-core": "^0.2.0-rc.1",
|
|
50
|
+
"@lmzhen/dsh-evolution-io": "^0.2.0-rc.1",
|
|
51
|
+
"@lmzhen/dsh-skill-usage": "^0.2.0-rc.1"
|
|
52
52
|
}
|
|
53
53
|
}
|