@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 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
- if (name && action === "create" && origin === "background_review") await ctx.skillUsage.markAgentCreated(name);
59
- if (name && action !== "list") await ctx.skillUsage.record(name, "patch");
60
- ctx.emit("evolution/skill-mutated", {
61
- action: action ?? "?",
62
- name,
63
- ...result.path && action === "delete" ? { archivedPath: result.path } : result.path ? { filePath: result.path } : {}
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 delete only. Prefer patching an umbrella over creating narrow skills. Created/edited SKILL.md MUST start with YAML frontmatter (a name/description block), or creation is rejected. " + DSH_AUTHORING_STANDARDS,
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
- "list"
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 origin = exec.agent?.session.header.origin === "subagent" ? "background_review" : "foreground";
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, origin);
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
  });
@@ -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.1.0-rc.9",
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.1.0-rc.9"
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.0-rc.6",
41
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
42
- "@lmzhen/dsh-evolution-io": "^0.1.0-rc.9",
43
- "@lmzhen/dsh-skill-usage": "^0.1.0-rc.9"
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.0-rc.6",
47
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
48
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
49
- "@lmzhen/dsh-evolution-core": "^0.1.0-rc.9",
50
- "@lmzhen/dsh-evolution-io": "^0.1.0-rc.9",
51
- "@lmzhen/dsh-skill-usage": "^0.1.0-rc.9"
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
  }