@lmzhen/dsh-tool-skill-manage 0.1.0-rc.8 → 0.1.0

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,77 @@ 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 === "pin") result = await library.setPinned(name, true, origin);
97
+ else if (action === "unpin") result = await library.setPinned(name, false, origin);
53
98
  else result = {
54
99
  ok: false,
55
100
  message: `Unknown action "${action}".`
56
101
  };
57
102
  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
- });
103
+ const mutating = action !== "list" && action !== "review" && action !== "skip" && action !== "pin" && action !== "unpin";
104
+ if (name && action === "create") {
105
+ await ctx.skillUsage.ensureRecord(name);
106
+ if (origin !== "foreground") await ctx.skillUsage.markAgentCreated(name);
107
+ }
108
+ if (name && action === "delete") await ctx.skillUsage.markArchived(name);
109
+ else if (name && mutating && action !== "create") await ctx.skillUsage.record(name, "patch");
65
110
  }
66
111
  return {
67
112
  ok: result.ok,
68
- message: result.message,
113
+ message: result.ok && feedbackLines.length > 0 ? `${result.message}\n\nAuthoring check:\n${feedbackLines.map((line) => `- ${line}`).join("\n")}` : result.message,
69
114
  skills: []
70
115
  };
71
116
  }
117
+ async function buildSkillReviewText() {
118
+ const list = await library.list();
119
+ const report = await ctx.skillUsage.report();
120
+ const lines = list.map((summary) => {
121
+ const record = report.get(summary.name);
122
+ const quality = record?.quality_score !== void 0 ? ` quality:${record.quality_score.toFixed(2)}${record.quality_warn ? "⚠" : ""}` : "";
123
+ 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}]` : ""}`;
124
+ });
125
+ const groups = computeDedupGroups({ contents: new Map(await Promise.all(list.map(async (summary) => [summary.name, await library.read(summary.name) ?? ""]))) });
126
+ const dedupLines = groups.slice(0, 3).map((group) => `- ${group.join(" ~ ")}`);
127
+ const warned = list.filter((summary) => report.get(summary.name)?.quality_warn === true).map((summary) => summary.name);
128
+ const warningLine = warned.length === 0 ? "" : `\nWarning skills (${warned.length}): ${warned.join(", ")} — low quality; consider consolidating them before authoring new skills.`;
129
+ return [
130
+ 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}):` : ""}`,
131
+ "",
132
+ ...lines,
133
+ ...dedupLines,
134
+ warningLine
135
+ ].join("\n");
136
+ }
72
137
  ctx.tools.register(defineTool({
73
138
  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,
139
+ 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. Created/edited SKILL.md MUST start with YAML frontmatter (a name/description block), or creation is rejected. " + DSH_AUTHORING_STANDARDS,
75
140
  parameters: {
76
141
  action: {
77
142
  type: "string",
78
143
  required: true,
79
144
  enum: [
145
+ "review",
146
+ "list",
80
147
  "create",
81
148
  "edit",
82
149
  "update",
@@ -84,7 +151,9 @@ function apply(ctx, rawConfig = {}) {
84
151
  "delete",
85
152
  "write_file",
86
153
  "remove_file",
87
- "list"
154
+ "skip",
155
+ "pin",
156
+ "unpin"
88
157
  ]
89
158
  },
90
159
  name: { type: "string" },
@@ -124,17 +193,22 @@ function apply(ctx, rawConfig = {}) {
124
193
  },
125
194
  isConcurrencySafe: () => false,
126
195
  async execute(args, exec) {
127
- const origin = exec.agent?.session.header.origin === "subagent" ? "background_review" : "foreground";
196
+ const origins = resolveOrigins(exec.agent?.session.header.origin);
197
+ const reviewOrigin = origins.approval;
198
+ const libraryOrigin = origins.library;
199
+ const sessionPolicy = effectiveSessionPolicy(ctx, exec.agent?.session);
128
200
  const approval = ctx.get("evolutionApproval");
129
- if (approval && args.action !== "list") {
201
+ if (approval && args.action !== "list" && args.action !== "review" && args.action !== "skip" && args.action !== "pin" && args.action !== "unpin") {
130
202
  const decision = await approval.request({
131
203
  kind: "skill",
132
204
  summary: `skill ${args.action ?? "?"} ${args.name ?? ""}`.trim(),
133
205
  args: {
134
206
  operation: args,
135
- origin
207
+ origin: reviewOrigin,
208
+ libraryOrigin
136
209
  },
137
- origin
210
+ origin: reviewOrigin,
211
+ ...sessionPolicy !== void 0 ? { sessionPolicy } : {}
138
212
  });
139
213
  if (decision.action === "staged") return {
140
214
  ok: true,
@@ -143,13 +217,13 @@ function apply(ctx, rawConfig = {}) {
143
217
  pending_id: decision.pendingId ?? ""
144
218
  };
145
219
  }
146
- return await executeCore(args, origin);
220
+ return await executeCore(args, libraryOrigin);
147
221
  }
148
222
  }));
149
223
  ctx.inject(["evolutionApproval"], (approvalCtx) => {
150
224
  const dispose = approvalCtx.evolutionApproval.registerRunner("skill", (args) => {
151
225
  const wrapped = args ?? {};
152
- return executeCore(wrapped.operation ?? {}, wrapped.origin ?? "background_review");
226
+ return executeCore(wrapped.operation ?? {}, wrapped.libraryOrigin ?? wrapped.origin ?? "background_review");
153
227
  });
154
228
  approvalCtx.effect(() => dispose, "tool-skill-manage.approval-runner");
155
229
  });
@@ -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.8",
4
+ "version": "0.1.0",
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.8"
36
+ "@lmzhen/dsh-evolution-core": "^0.1.0"
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.8",
43
- "@lmzhen/dsh-skill-usage": "^0.1.0-rc.8"
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.1.0",
43
+ "@lmzhen/dsh-skill-usage": "^0.1.0"
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.8",
50
- "@lmzhen/dsh-evolution-io": "^0.1.0-rc.8",
51
- "@lmzhen/dsh-skill-usage": "^0.1.0-rc.8"
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.1.0",
50
+ "@lmzhen/dsh-evolution-io": "^0.1.0",
51
+ "@lmzhen/dsh-skill-usage": "^0.1.0"
52
52
  }
53
53
  }