@lmzhen/dsh-tool-skill-manage 0.3.70 → 0.3.72

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,7 +1,7 @@
1
1
  import { effectiveSessionPolicy } from "@lmzhen/dsh-evolution-approval";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { defineTool } from "@deepseek-ai/dsh-tools";
4
- import { DEFAULT_SKILL_LIMITS, DSH_AUTHORING_STANDARDS, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SkillLibrary, authoringFeedback, clampedNumber, computeDedupGroups, contentHash, evolutionIoAdapter, parseFrontmatter, resolveOrigins, resolveSkillsRoot } from "@lmzhen/dsh-evolution-core";
4
+ import { DEFAULT_SKILL_LIMITS, DSH_AUTHORING_STANDARDS, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SkillLibrary, authoringFeedback, clampedNumber, computeDedupGroups, contentHash, evolutionIoAdapter, parseFrontmatter, resolveOrigins, resolveSkillsRoot } from "@lmzhen/dsh-evolution-core";
5
5
  //#region lib/types/index.js
6
6
  /**
7
7
  * Model-facing skill_manage tool over ctx.evolutionIo + ctx.skillUsage.
@@ -35,7 +35,8 @@ const Config = z.object({
35
35
  maxSkillContentChars: z.number().min(1).default(DEFAULT_SKILL_LIMITS.maxSkillContentChars),
36
36
  maxSkillFileBytes: z.number().min(1).default(DEFAULT_SKILL_LIMITS.maxSkillFileBytes),
37
37
  descriptionStrict: z.boolean().default(false),
38
- threatExemptLabels: z.array(z.string()).default([])
38
+ threatExemptLabels: z.array(z.string()).default([]),
39
+ strictCrossSource: z.boolean().default(false)
39
40
  });
40
41
  /** v30 REV-02: read the protected-skill list off the (optional) policy
41
42
  * snapshot through an `unknown` boundary — the Context augmentation types the
@@ -67,6 +68,15 @@ function apply(ctx, rawConfig = {}) {
67
68
  ctx.emit("evolution/skill-mutated", event);
68
69
  }, void 0, [...rawConfig.threatExemptLabels ?? []]);
69
70
  if (numericClamped.length > 0) ctx.logger.warn(`tool-skill-manage: ${numericClamped.join(", ")} provided an invalid value; falling back to the default`);
71
+ const catalogWinner = async (name) => {
72
+ const catalog = ctx.get("skills");
73
+ if (catalog === void 0) return void 0;
74
+ try {
75
+ return (await catalog.list()).find((summary) => summary.name === name);
76
+ } catch {
77
+ return;
78
+ }
79
+ };
70
80
  async function executeCore(args, origin = "foreground") {
71
81
  const action = args.action;
72
82
  const name = args.name ?? "";
@@ -121,26 +131,7 @@ function apply(ctx, rawConfig = {}) {
121
131
  skills: []
122
132
  };
123
133
  }
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
- };
134
+ const REQUIRED_ARGS = SKILL_ACTION_REQUIRED_FIELDS;
144
135
  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
136
  if (missing.length > 0) return {
146
137
  ok: false,
@@ -148,24 +139,7 @@ function apply(ctx, rawConfig = {}) {
148
139
  skills: []
149
140
  };
150
141
  let feedbackLines = [];
151
- if ((action === "edit" || action === "update") && typeof args.staged_from_sha256 === "string" && args.staged_from_sha256 !== "") {
152
- const currentContent = await library.read(name).catch(() => null);
153
- const actual = contentHash(currentContent ?? "");
154
- if (currentContent === null || actual !== args.staged_from_sha256) return {
155
- ok: false,
156
- message: `Skill "${name}" changed after this write was staged (content ${currentContent === null ? "no longer exists" : "hash mismatch"}); refusing to replay the stale snapshot. Re-apply the edit to stage a fresh copy.`,
157
- skills: []
158
- };
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
- }
142
+ const stagedAnchor = typeof args.staged_from_sha256 === "string" && args.staged_from_sha256 !== "" ? args.staged_from_sha256 === "absent" ? { absent: true } : { sha256: args.staged_from_sha256 } : void 0;
169
143
  if ((action === "create" || action === "edit" || action === "update") && args.content) {
170
144
  const parsed = parseFrontmatter(args.content);
171
145
  if (parsed) {
@@ -180,11 +154,11 @@ function apply(ctx, rawConfig = {}) {
180
154
  }
181
155
  let result;
182
156
  if (action === "create") result = await library.create(name, args.content ?? "", origin);
183
- else if (action === "edit" || action === "update") result = await library.update(name, args.content ?? "", origin);
157
+ else if (action === "edit" || action === "update") result = await library.update(name, args.content ?? "", origin, stagedAnchor);
184
158
  else if (action === "patch") result = await library.patch(name, args.old_string ?? "", args.new_string ?? "", args.file_path ?? "", args.replace_all === true, origin);
185
159
  else if (action === "delete") result = await library.archive(name, args.absorbed_into ? { absorbedInto: args.absorbed_into } : {});
186
- else if (action === "write_file") result = await library.writeSupportFile(name, args.file_path ?? "", args.file_content ?? "", origin);
187
- else if (action === "remove_file") result = await library.removeSupportFile(name, args.file_path ?? "", origin);
160
+ else if (action === "write_file") result = await library.writeSupportFile(name, args.file_path ?? "", args.file_content ?? "", origin, stagedAnchor);
161
+ else if (action === "remove_file") result = await library.removeSupportFile(name, args.file_path ?? "", origin, stagedAnchor);
188
162
  else if (action === "restructure") {
189
163
  const moves = Array.isArray(args.restructure) ? args.restructure : [];
190
164
  if (moves.some((move) => {
@@ -204,6 +178,11 @@ function apply(ctx, rawConfig = {}) {
204
178
  ok: false,
205
179
  message: `Unknown action "${action}".`
206
180
  };
181
+ if (result.stale === true) return {
182
+ ok: false,
183
+ message: action === "write_file" || action === "remove_file" ? `Support file "${args.file_path ?? ""}" of "${name}" changed after this write was staged (or its state could not be verified).` : `Skill "${name}" changed after this write was staged (content ${result.anchor === "missing" ? "no longer exists" : "hash mismatch"}).`,
184
+ skills: []
185
+ };
207
186
  if (result.ok) {
208
187
  if (result.normalizedFrontmatterFields && result.normalizedFrontmatterFields.length > 0) feedbackLines.push(`frontmatter auto-quoted (YAML compatibility): ${result.normalizedFrontmatterFields.join(", ")}`);
209
188
  const mutating = action !== "list" && action !== "review" && action !== "pin" && action !== "unpin";
@@ -318,25 +297,44 @@ function apply(ctx, rawConfig = {}) {
318
297
  },
319
298
  isConcurrencySafe: () => false,
320
299
  async execute(args, exec) {
300
+ let operation = args;
321
301
  const origins = resolveOrigins(exec.agent?.session?.header.origin);
322
302
  const reviewOrigin = origins.approval;
323
303
  const libraryOrigin = origins.library;
324
304
  const sessionPolicy = effectiveSessionPolicy(ctx, exec.agent?.session);
305
+ if (typeof args.name === "string" && args.name !== "" && args.action !== "list" && args.action !== "review" && args.action !== "pin" && args.action !== "unpin") {
306
+ const winner = await catalogWinner(args.name);
307
+ if (winner !== void 0 && winner.provider !== "dsh-evolution") {
308
+ const message = `skill "${args.name}" resolves to a higher-priority "${winner.source}" skill (provider "${winner.provider}"); this write lands on the family copy, which the catalog does NOT serve — edit the "${winner.source}" copy or rename.`;
309
+ if (rawConfig.strictCrossSource === true) return {
310
+ ok: false,
311
+ message: `Refused: ${message}`,
312
+ skills: []
313
+ };
314
+ ctx.logger.warn(`skill_manage: ${message}`);
315
+ }
316
+ }
325
317
  const approval = ctx.get("evolutionApproval");
326
318
  if (approval && args.action !== "list" && args.action !== "review" && args.action !== "pin" && args.action !== "unpin") {
327
319
  if ((args.action === "update" || args.action === "edit") && typeof args.name === "string" && args.name !== "") {
328
320
  const stageCurrent = await library.read(args.name).catch(() => null);
329
- if (stageCurrent !== null) args.staged_from_sha256 = contentHash(stageCurrent);
321
+ if (stageCurrent !== null) operation = {
322
+ ...args,
323
+ staged_from_sha256: contentHash(stageCurrent)
324
+ };
330
325
  }
331
326
  if ((args.action === "write_file" || args.action === "remove_file") && typeof args.name === "string" && args.name !== "" && typeof args.file_path === "string" && args.file_path !== "") {
332
327
  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);
328
+ if (stageFile !== void 0) operation = {
329
+ ...args,
330
+ staged_from_sha256: stageFile === null ? "absent" : contentHash(stageFile)
331
+ };
334
332
  }
335
333
  const decision = await approval.request({
336
334
  kind: "skill",
337
335
  summary: `skill ${args.action ?? "?"} ${args.name ?? ""}`.trim(),
338
336
  args: {
339
- operation: args,
337
+ operation,
340
338
  origin: reviewOrigin,
341
339
  libraryOrigin
342
340
  },
@@ -352,7 +350,7 @@ function apply(ctx, rawConfig = {}) {
352
350
  ...decision.pendingId !== void 0 ? { pending_id: decision.pendingId } : {}
353
351
  };
354
352
  }
355
- return await executeCore(args, libraryOrigin);
353
+ return await executeCore(operation, libraryOrigin);
356
354
  }
357
355
  }));
358
356
  ctx.inject(["evolutionApproval"], (approvalCtx) => {
@@ -32,6 +32,11 @@ export interface Config {
32
32
  * strict ANY-hit-blocks behavior is unchanged; deployments opt in per
33
33
  * label for known false-positive content. */
34
34
  threatExemptLabels?: string[];
35
+ /** OPT-19 (2026-09, plan D3): when true, a write whose name the platform
36
+ * catalog resolves to a NON-family source (project/custom sources outrank
37
+ * the family tree) is REFUSED instead of warned about. Default false —
38
+ * warn only, keeping the family tree an autonomous evolution zone. */
39
+ strictCrossSource?: boolean;
35
40
  }
36
41
  export declare const Config: z<Config>;
37
42
  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.3.70",
4
+ "version": "0.3.72",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -31,24 +31,26 @@
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
33
  "@deepseek-ai/schemastery": "^3.18.1",
34
- "@lmzhen/dsh-evolution-approval": "^0.3.70",
35
- "@lmzhen/dsh-evolution-core": "^0.3.70"
34
+ "@lmzhen/dsh-evolution-approval": "^0.3.72",
35
+ "@lmzhen/dsh-evolution-core": "^0.3.72"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@deepseek-ai/cordis": "^4.0.1",
39
39
  "@deepseek-ai/dsh-invariants": "^0.1.5-rc.2",
40
+ "@deepseek-ai/dsh-skill": "^0.1.5-rc.2",
40
41
  "@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.2",
41
42
  "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
42
- "@lmzhen/dsh-evolution-io": "^0.3.70",
43
- "@lmzhen/dsh-skill-usage": "^0.3.70"
43
+ "@lmzhen/dsh-evolution-io": "^0.3.72",
44
+ "@lmzhen/dsh-skill-usage": "^0.3.72"
44
45
  },
45
46
  "devDependencies": {
46
47
  "@deepseek-ai/dsh-agent-loop-testkit": "^0.1.5-rc.2",
47
48
  "@deepseek-ai/dsh-invariants": "^0.1.5-rc.2",
49
+ "@deepseek-ai/dsh-skill": "^0.1.5-rc.2",
48
50
  "@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.2",
49
51
  "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
50
- "@lmzhen/dsh-evolution-core": "^0.3.70",
51
- "@lmzhen/dsh-evolution-io": "^0.3.70",
52
- "@lmzhen/dsh-skill-usage": "^0.3.70"
52
+ "@lmzhen/dsh-evolution-core": "^0.3.72",
53
+ "@lmzhen/dsh-evolution-io": "^0.3.72",
54
+ "@lmzhen/dsh-skill-usage": "^0.3.72"
53
55
  }
54
56
  }