@lmzhen/dsh-tool-skill-manage 0.9.0 → 0.11.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/README.md CHANGED
@@ -72,6 +72,28 @@ only the second one refuses a write:
72
72
  The third row is a view, not a rule: a description the family accepts can still be cut in a catalog
73
73
  viewer, which is why the authoring feedback names the bar rather than treating the cut as a limit.
74
74
 
75
+ ### The create-time duplicate hint
76
+
77
+ A `create` compares its candidate against the library listing before it writes, over the name +
78
+ description projection (core `nearDuplicateSummaries`, at `SUMMARY_DUPLICATE_HINT_THRESHOLD`), and
79
+ names the closest existing skills in the same "Authoring check" block. Three properties are
80
+ deliberate: it adds no read of its own (`library.list()` already reads every SKILL.md to parse
81
+ its frontmatter — that listing is what publishes `description` — and the hint never calls
82
+ `library.read`, never holds a body and never compares content); it is a HINT (the write proceeds
83
+ — only the model's next decision changes); and a listing failure degrades to a named line instead of
84
+ silence, because "nothing similar was checked" and "nothing similar exists" must not look alike. The
85
+ library-wide BODY scan stays where it was, in the `review` action.
86
+
87
+ ### The retention line
88
+
89
+ A whole-body replacement that keeps less than half of the previous SKILL.md says so in the same result
90
+ message (`Content kept 26% of the previous body (2104 of 8123 characters); the replaced version is
91
+ preserved in this skill's history.`). It is computed where the write path still holds both bodies — the
92
+ library's own in-lock read, core `contentRetentionFeedback` — because a caller re-reading the file
93
+ would race the writers the library serializes. Like the duplicate hint it is FEEDBACK, not a gate: no ratio
94
+ refuses a write, small skills stay quiet (a 40-character skill cut in half is not news), and patches,
95
+ support-file writes, creates and archives have no ratio to report.
96
+
75
97
  ## Known limitations
76
98
 
77
99
  - No known durable consumer gaps at this time. Runtime contracts are covered by package and boundary tests.
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_ARCHIVE_RETENTION_POLICY, DEFAULT_CITATION_POLICY, DEFAULT_REFERENCE_REWRITE_POLICY, DEFAULT_SKILL_LIMITS, DEFAULT_SUPPORT_FILE_CHAR_POLICY, DSH_AUTHORING_STANDARDS, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, authoringFeedback, callingScope, clampedNumber, computeDedupGroups, contentHash, errorText, evolutionIoAdapter, installParamSection, isPresent, isUnknown, isUnreadWrite, newSkillLibrary, paramNamespace, parseFrontmatter, policyStageLimits, probePresent, probeUnknown, readNumberParam, resolveExecOrigins, sessionReadSkillNames } from "@lmzhen/dsh-evolution-core";
4
+ import { DEFAULT_ARCHIVE_RETENTION_POLICY, DEFAULT_CITATION_POLICY, DEFAULT_REFERENCE_REWRITE_POLICY, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_VERSION_KEEP, DEFAULT_SUPPORT_FILE_CHAR_POLICY, DSH_AUTHORING_STANDARDS, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, authoringFeedback, callingScope, clampedNumber, computeDedupGroups, contentHash, errorText, evolutionIoAdapter, installParamSection, isPresent, isUnknown, isUnreadWrite, nearDuplicateSummaries, newSkillLibrary, paramNamespace, parseFrontmatter, policyStageLimits, probePresent, probeUnknown, readNumberParam, resolveExecOrigins, sessionReadSkillNames } from "@lmzhen/dsh-evolution-core";
5
5
  //#region lib/types/write-gates.js
6
6
  /**
7
7
  * The write-admission sequence: ONE ordered table of the reasons a skill write is refused.
@@ -213,7 +213,8 @@ const Config = z.object({
213
213
  maxSkillFileBytes: z.number().min(1).default(DEFAULT_SKILL_LIMITS.maxSkillFileBytes),
214
214
  descriptionStrict: z.boolean().default(false),
215
215
  threatExemptLabels: z.array(z.string()).default([]),
216
- strictCrossSource: z.boolean().default(false)
216
+ strictCrossSource: z.boolean().default(false),
217
+ skillVersionKeep: z.number().min(1).default(DEFAULT_SKILL_LIMITS.versionKeep ?? DEFAULT_SKILL_VERSION_KEEP)
217
218
  });
218
219
  /** Schema the platform validates the user layer against; defaults mirror the core
219
220
  * constants and the row schema, so an empty document resolves to today's behaviour. */
@@ -376,7 +377,8 @@ function apply(ctx, rawConfig = {}) {
376
377
  maxNameLength: limit("maxSkillNameLength", rawConfig.maxSkillNameLength, DEFAULT_SKILL_LIMITS.maxNameLength),
377
378
  maxDescriptionLength: limit("maxDescriptionLength", rawConfig.maxDescriptionLength, DEFAULT_SKILL_LIMITS.maxDescriptionLength),
378
379
  maxSkillContentChars: limit("maxSkillContentChars", readNumberParam(rawConfig, "skillContentChars"), DEFAULT_SKILL_LIMITS.maxSkillContentChars),
379
- maxSkillFileBytes: limit("maxSkillFileBytes", rawConfig.maxSkillFileBytes, DEFAULT_SKILL_LIMITS.maxSkillFileBytes)
380
+ maxSkillFileBytes: limit("maxSkillFileBytes", rawConfig.maxSkillFileBytes, DEFAULT_SKILL_LIMITS.maxSkillFileBytes),
381
+ versionKeep: limit("skillVersionKeep", rawConfig.skillVersionKeep, DEFAULT_SKILL_LIMITS.versionKeep ?? DEFAULT_SKILL_VERSION_KEEP)
380
382
  };
381
383
  const library = newSkillLibrary({
382
384
  config: rawConfig,
@@ -523,10 +525,12 @@ function apply(ctx, rawConfig = {}) {
523
525
  };
524
526
  let feedbackLines = [];
525
527
  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;
528
+ let candidateDescription = "";
526
529
  if ((action === "create" || action === "edit" || action === "update") && args.content) {
527
530
  const parsed = parseFrontmatter(args.content);
528
531
  if (parsed) {
529
532
  const feedback = authoringFeedback(parsed.frontmatter);
533
+ candidateDescription = parsed.frontmatter.description ?? "";
530
534
  feedbackLines = feedback.lines;
531
535
  if (settings().descriptionStrict && feedback.over60) return {
532
536
  ok: false,
@@ -536,8 +540,10 @@ function apply(ctx, rawConfig = {}) {
536
540
  }
537
541
  }
538
542
  let result;
539
- if (action === "create") result = await library.create(name, args.content ?? "", origin);
540
- else if (action === "edit" || action === "update") result = await library.update(name, args.content ?? "", origin, stagedAnchor);
543
+ if (action === "create") {
544
+ feedbackLines.push(...await duplicateHintLines(name, candidateDescription));
545
+ result = await library.create(name, args.content ?? "", origin);
546
+ } else if (action === "edit" || action === "update") result = await library.update(name, args.content ?? "", origin, stagedAnchor);
541
547
  else if (action === "patch") result = await library.patch(name, args.old_string ?? "", args.new_string ?? "", args.file_path ?? "", args.replace_all === true, origin);
542
548
  else if (action === "delete") result = await library.archive(name, args.absorbed_into ? { absorbedInto: args.absorbed_into } : {});
543
549
  else if (action === "write_file") result = await library.writeSupportFile(name, args.file_path ?? "", args.file_content ?? "", origin, stagedAnchor);
@@ -579,6 +585,39 @@ function apply(ctx, rawConfig = {}) {
579
585
  skills: []
580
586
  };
581
587
  }
588
+ /** Batch C (2026-09-27, design §3C-2): the create-time near-duplicate hint — one
589
+ * line naming the existing skill(s) this candidate is about to near-copy, over
590
+ * core's `summary` projection (name + description). Three properties are the
591
+ * design's, not stylistic choices:
592
+ *
593
+ * - It adds NO read of its own. `library.list()` reads each SKILL.md to parse its
594
+ * frontmatter — that listing is what publishes `description` — and the hint never
595
+ * calls `library.read`, never holds a body and never compares content, so a create
596
+ * pays no SECOND whole-tree pass for it.
597
+ * - It is a HINT: the write proceeds either way (G4 — create's foreground
598
+ * behavior is unchanged). The model gets the news, not a refusal.
599
+ * - A failed listing degrades to a NAMED line, never to silence: "nothing
600
+ * similar was checked" and "nothing similar exists" must not look alike
601
+ * (the PLAN-R2 P2-5 posture the review path already takes).
602
+ *
603
+ * The level and the math live in core (`nearDuplicateSummaries`,
604
+ * `SUMMARY_DUPLICATE_HINT_THRESHOLD`) — this function owns the wording only. */
605
+ async function duplicateHintLines(name, description) {
606
+ let matches;
607
+ try {
608
+ matches = nearDuplicateSummaries({
609
+ candidate: {
610
+ name,
611
+ description
612
+ },
613
+ existing: await library.list()
614
+ });
615
+ } catch (error) {
616
+ return [`Duplicate check skipped: the skill listing failed (${error instanceof Error ? error.message : String(error)}).`];
617
+ }
618
+ if (matches.length === 0) return [];
619
+ return [`Near-duplicate of existing skill(s): ${matches.map((match) => `${match.name} (${match.score.toFixed(2)})`).join(", ")} — prefer patch/update on that skill over adding a near-copy.`];
620
+ }
582
621
  async function buildSkillReviewText() {
583
622
  const list = await library.list();
584
623
  const report = await ctx.skillUsage.report();
@@ -46,6 +46,10 @@ export interface Config {
46
46
  * the family tree) is REFUSED instead of warned about. Default false —
47
47
  * warn only, keeping the family tree an autonomous evolution zone. */
48
48
  strictCrossSource?: boolean;
49
+ /** Content versions retained per skill (skill-history.ts). E2: a deployment value on this row,
50
+ * like the four caps above — the settings layer deliberately has no card for it (retention is
51
+ * storage policy, not an authoring knob). */
52
+ skillVersionKeep?: number;
49
53
  }
50
54
  export declare const Config: z<Config>;
51
55
  /** Write behaviour a user may change (G3/S3.4). Field names are the CANONICAL
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.9.0",
4
+ "version": "0.11.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,24 +27,24 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@deepseek-ai/schemastery": "^3.18.1",
30
- "@lmzhen/dsh-evolution-approval": "^0.9.0",
31
- "@lmzhen/dsh-evolution-core": "^0.9.0"
30
+ "@lmzhen/dsh-evolution-approval": "^0.11.1",
31
+ "@lmzhen/dsh-evolution-core": "^0.11.1"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@deepseek-ai/cordis": "^4.0.1",
35
35
  "@deepseek-ai/dsh-skill": "^0.1.5-rc.2",
36
36
  "@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.2",
37
37
  "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
38
- "@lmzhen/dsh-evolution-io": "^0.9.0",
39
- "@lmzhen/dsh-skill-usage": "^0.9.0"
38
+ "@lmzhen/dsh-evolution-io": "^0.11.1",
39
+ "@lmzhen/dsh-skill-usage": "^0.11.1"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@deepseek-ai/dsh-agent-loop-testkit": "^0.1.5-rc.2",
43
43
  "@deepseek-ai/dsh-skill": "^0.1.5-rc.2",
44
44
  "@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.2",
45
45
  "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
46
- "@lmzhen/dsh-evolution-core": "^0.9.0",
47
- "@lmzhen/dsh-evolution-io": "^0.9.0",
48
- "@lmzhen/dsh-skill-usage": "^0.9.0"
46
+ "@lmzhen/dsh-evolution-core": "^0.11.1",
47
+ "@lmzhen/dsh-evolution-io": "^0.11.1",
48
+ "@lmzhen/dsh-skill-usage": "^0.11.1"
49
49
  }
50
50
  }