@lmzhen/dsh-tool-skill-manage 0.3.67 → 0.3.69

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.
Files changed (2) hide show
  1. package/lib/index.js +79 -8
  2. package/package.json +8 -8
package/lib/index.js CHANGED
@@ -37,6 +37,12 @@ const Config = z.object({
37
37
  descriptionStrict: z.boolean().default(false),
38
38
  threatExemptLabels: z.array(z.string()).default([])
39
39
  });
40
+ /** v30 REV-02: read the protected-skill list off the (optional) policy
41
+ * snapshot through an `unknown` boundary — the Context augmentation types the
42
+ * getter non-optionally, but at runtime the row can be absent. */
43
+ function policySnapshotOf(source) {
44
+ return source?.get?.();
45
+ }
40
46
  function apply(ctx, rawConfig = {}) {
41
47
  const systemPrompt = ctx.get("systemPrompt");
42
48
  if (systemPrompt) ctx.effect(() => systemPrompt.section({
@@ -64,18 +70,32 @@ function apply(ctx, rawConfig = {}) {
64
70
  async function executeCore(args, origin = "foreground") {
65
71
  const action = args.action;
66
72
  const name = args.name ?? "";
67
- if (action === "review") return {
68
- ok: true,
69
- message: await buildSkillReviewText(),
70
- skills: []
71
- };
72
- if (action === "list") {
73
+ if (action === "review") try {
74
+ return {
75
+ ok: true,
76
+ message: await buildSkillReviewText(),
77
+ skills: []
78
+ };
79
+ } catch (error) {
80
+ return {
81
+ ok: false,
82
+ message: `skill_manage review: skill tree scan failed (${error instanceof Error ? error.message : String(error)}).`,
83
+ skills: []
84
+ };
85
+ }
86
+ if (action === "list") try {
73
87
  const list = await library.list();
74
88
  return {
75
89
  ok: true,
76
90
  message: `Listed ${list.length} skills.`,
77
91
  skills: list.map((s) => s.name)
78
92
  };
93
+ } catch (error) {
94
+ return {
95
+ ok: false,
96
+ message: `skill_manage list: skill tree scan failed (${error instanceof Error ? error.message : String(error)}).`,
97
+ skills: []
98
+ };
79
99
  }
80
100
  const scalarArgs = args;
81
101
  for (const field of [
@@ -94,6 +114,39 @@ function apply(ctx, rawConfig = {}) {
94
114
  skills: []
95
115
  };
96
116
  }
117
+ if (origin !== "foreground") {
118
+ if ((policySnapshotOf(ctx.get("evolutionPolicy"))?.protectedSkillNames)?.includes(name)) return {
119
+ ok: false,
120
+ message: `skill_manage: "${name}" is protected by the current policy (protectedSkillNames); replayed/autonomous writes are refused.`,
121
+ skills: []
122
+ };
123
+ }
124
+ const REQUIRED_ARGS = {
125
+ create: ["name", "content"],
126
+ edit: ["name", "content"],
127
+ update: ["name", "content"],
128
+ patch: [
129
+ "name",
130
+ "old_string",
131
+ "new_string"
132
+ ],
133
+ delete: ["name"],
134
+ write_file: [
135
+ "name",
136
+ "file_path",
137
+ "file_content"
138
+ ],
139
+ remove_file: ["name", "file_path"],
140
+ restructure: ["name"],
141
+ pin: ["name"],
142
+ unpin: ["name"]
143
+ };
144
+ const missing = (action === void 0 || typeof action !== "string" || !Object.hasOwn(REQUIRED_ARGS, action) ? [] : REQUIRED_ARGS[action] ?? []).filter((field) => scalarArgs[field] === void 0 || scalarArgs[field] === null);
145
+ if (missing.length > 0) return {
146
+ ok: false,
147
+ message: `skill_manage ${action} requires ${missing.join(", ")}; the tool description lists the arguments per action.`,
148
+ skills: []
149
+ };
97
150
  let feedbackLines = [];
98
151
  if ((action === "edit" || action === "update") && typeof args.staged_from_sha256 === "string" && args.staged_from_sha256 !== "") {
99
152
  const currentContent = await library.read(name).catch(() => null);
@@ -104,6 +157,15 @@ function apply(ctx, rawConfig = {}) {
104
157
  skills: []
105
158
  };
106
159
  }
160
+ if ((action === "write_file" || action === "remove_file") && typeof args.staged_from_sha256 === "string" && args.staged_from_sha256 !== "") {
161
+ const currentBytes = await library.readSupportFile(name, args.file_path ?? "").catch(() => void 0);
162
+ const actual = currentBytes === void 0 || currentBytes === null ? "absent" : contentHash(currentBytes);
163
+ if (currentBytes === void 0 || actual !== args.staged_from_sha256) return {
164
+ ok: false,
165
+ message: `Support file "${args.file_path ?? ""}" of "${name}" changed after this write was staged (or its state could not be verified); refusing to replay the stale snapshot. Re-stage the file operation.`,
166
+ skills: []
167
+ };
168
+ }
107
169
  if ((action === "create" || action === "edit" || action === "update") && args.content) {
108
170
  const parsed = parseFrontmatter(args.content);
109
171
  if (parsed) {
@@ -169,7 +231,12 @@ function apply(ctx, rawConfig = {}) {
169
231
  const protection = summary.protectionUnknown ? " [protection:unknown]" : summary.protectedBy ? ` [${summary.protectedBy}]` : "";
170
232
  return `- ${summary.name} | ${record?.state ?? "active"} | use:${record?.use_count ?? 0} view:${record?.view_count ?? 0} patch:${record?.patch_count ?? 0}${quality}${protection}`;
171
233
  });
172
- const groups = computeDedupGroups({ contents: new Map(await Promise.all(list.map(async (summary) => [summary.name, await library.read(summary.name) ?? ""]))) });
234
+ const contents = /* @__PURE__ */ new Map();
235
+ for (let offset = 0; offset < list.length; offset += 16) await Promise.all(list.slice(offset, offset + 16).map(async (summary) => {
236
+ const body = await library.read(summary.name).catch(() => null);
237
+ contents.set(summary.name, body ?? "");
238
+ }));
239
+ const groups = computeDedupGroups({ contents });
173
240
  const dedupLines = groups.slice(0, MAX_DEDUP_GROUPS_IN_REVIEW).map((group) => `- ${group.join(" ~ ")}`);
174
241
  const warned = list.filter((summary) => warnedFlag(summary.name)).map((summary) => summary.name);
175
242
  const warningLine = warned.length === 0 ? "" : `\nWarning skills (${warned.length}): ${warned.join(", ")} — low quality; consider consolidating them before authoring new skills.`;
@@ -183,7 +250,7 @@ function apply(ctx, rawConfig = {}) {
183
250
  }
184
251
  ctx.tools.register(defineTool({
185
252
  name: "skill_manage",
186
- description: "Manage reusable skills. review returns the library review text (state/usage/quality per skill); list returns names only; create/edit/update take full SKILL.md content (edit is an alias of update); patch applies old_string -> new_string; delete archives to .archive (absorbed_into names an umbrella skill); write_file/remove_file add or remove one support file under references/ or scripts/; 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,
253
+ description: "Manage reusable skills. review returns the library review text (state/usage/quality per skill); list returns names only; create/edit/update take full SKILL.md content (edit is an alias of update); patch applies old_string -> new_string; delete archives to .archive (absorbed_into names an umbrella skill); write_file/remove_file add or remove one support file under references/ or scripts/; 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). Per-action arguments: patch needs name + old_string + new_string and patches SKILL.md unless file_path names a support file (e.g. \"references/topic.md\"); patch replaces the FIRST occurrence only unless replace_all=true, and the result message reports how many anchors the file carried. write_file needs name + file_path + file_content; remove_file needs name + file_path; create/edit/update need name + content; delete needs name. A rejected patch names the reason and the remedy (missing anchor, stale snapshot, frontmatter that cannot be auto-quoted, byte/char limit). 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,
187
254
  parameters: {
188
255
  action: {
189
256
  type: "string",
@@ -261,6 +328,10 @@ function apply(ctx, rawConfig = {}) {
261
328
  const stageCurrent = await library.read(args.name).catch(() => null);
262
329
  if (stageCurrent !== null) args.staged_from_sha256 = contentHash(stageCurrent);
263
330
  }
331
+ if ((args.action === "write_file" || args.action === "remove_file") && typeof args.name === "string" && args.name !== "" && typeof args.file_path === "string" && args.file_path !== "") {
332
+ const stageFile = await library.readSupportFile(args.name, args.file_path).catch(() => void 0);
333
+ if (stageFile !== void 0) args.staged_from_sha256 = stageFile === null ? "absent" : contentHash(stageFile);
334
+ }
264
335
  const decision = await approval.request({
265
336
  kind: "skill",
266
337
  summary: `skill ${args.action ?? "?"} ${args.name ?? ""}`.trim(),
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.3.67",
4
+ "version": "0.3.69",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -31,24 +31,24 @@
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
33
  "@deepseek-ai/schemastery": "^3.18.1",
34
- "@lmzhen/dsh-evolution-approval": "^0.3.67",
35
- "@lmzhen/dsh-evolution-core": "^0.3.67"
34
+ "@lmzhen/dsh-evolution-approval": "^0.3.69",
35
+ "@lmzhen/dsh-evolution-core": "^0.3.69"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@deepseek-ai/cordis": "^4.0.1",
39
39
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
40
40
  "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.2",
41
41
  "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
42
- "@lmzhen/dsh-evolution-io": "^0.3.67",
43
- "@lmzhen/dsh-skill-usage": "^0.3.67"
42
+ "@lmzhen/dsh-evolution-io": "^0.3.69",
43
+ "@lmzhen/dsh-skill-usage": "^0.3.69"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@deepseek-ai/dsh-agent-loop-testkit": "^0.1.1-rc.2",
47
47
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
48
48
  "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.2",
49
49
  "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
50
- "@lmzhen/dsh-evolution-core": "^0.3.67",
51
- "@lmzhen/dsh-evolution-io": "^0.3.67",
52
- "@lmzhen/dsh-skill-usage": "^0.3.67"
50
+ "@lmzhen/dsh-evolution-core": "^0.3.69",
51
+ "@lmzhen/dsh-evolution-io": "^0.3.69",
52
+ "@lmzhen/dsh-skill-usage": "^0.3.69"
53
53
  }
54
54
  }