@lmzhen/dsh-evolution-core 0.3.71 → 0.3.73

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,4 +1,4 @@
1
- import { basename, dirname, join } from "node:path";
1
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
2
2
  import { cp, lstat, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
4
  import { homedir } from "node:os";
@@ -956,6 +956,41 @@ async function updateSuppressedNames(root, io, task) {
956
956
  * threshold, which are intentionally left where they are used.
957
957
  * @module @lmzhen/dsh-evolution-core
958
958
  */
959
+ /**
960
+ * Required argument names per `skill_manage` action — the SINGLE SOURCE read
961
+ * by the tool's argument gate (tool-skill-manage executeCore) and the plan
962
+ * validator (evolution-plan-validator), so the two can never drift.
963
+ * OPT-05 (2026-09): the plan validator used to accept a `write_file`/
964
+ * `remove_file` op without `file_path` while the executor required it — the
965
+ * staged write then failed at EVERY approve until rejected.
966
+ * Rows here are the op-level requirements only: `delete` additionally
967
+ * requires `absorbed_into` at the PLAN layer (review passes may only delete
968
+ * into an umbrella) and `pin`/`unpin` are tool-only actions — each consumer
969
+ * adds its own extras on top of this table. An empty-string argument is NOT
970
+ * caught here (the tool's gate deliberately lets it reach the library for a
971
+ * more specific remedy message); the validator adds its own `.trim()`
972
+ * emptiness checks for payload fields.
973
+ */
974
+ const SKILL_ACTION_REQUIRED_FIELDS = {
975
+ create: ["name", "content"],
976
+ edit: ["name", "content"],
977
+ update: ["name", "content"],
978
+ patch: [
979
+ "name",
980
+ "old_string",
981
+ "new_string"
982
+ ],
983
+ delete: ["name"],
984
+ write_file: [
985
+ "name",
986
+ "file_path",
987
+ "file_content"
988
+ ],
989
+ remove_file: ["name", "file_path"],
990
+ restructure: ["name"],
991
+ pin: ["name"],
992
+ unpin: ["name"]
993
+ };
959
994
  /** Skill frontmatter `name` validated for the file name (lowercase + hyphen).
960
995
  * 计划 B-4 (v18): tightened to the UPSTREAM `SKILL_NAME` shape
961
996
  * (`/^[a-z0-9]+(?:-[a-z0-9]+)*$/`, packages/skill/skill/src/index.ts:20). The
@@ -2137,13 +2172,22 @@ function buildLearnPrompt(userRequest) {
2137
2172
  * C-11: the adoption test and the RETURNED value now come from the
2138
2173
  * SAME trimmed source — the old form tested `trim()` but returned the raw
2139
2174
  * value, so `DSH_HOME=" /x "` was accepted AND persisted with literal spaces.
2140
- * Known tradeoff vs upstream `resolveDshHome`: `~` is NOT expanded here
2141
- * documented as a deliberate difference in the v10 audit; revisit only if a
2142
- * real deployment needs it.
2175
+ * OPT-27 (2026-09, plan D5 accepted): the v10-era "no `~` expansion, no
2176
+ * resolve" divergence from upstream `resolveDshHome` is RETIRED. It became
2177
+ * load-bearing when the skill-catalog shadow made "same tree as the upstream
2178
+ * `USER_DSH_RANK` provider" a hard contract: upstream watches the EXPANDED
2179
+ * absolute `<home>/skills` while this value fed a literal `~/x` (a directory
2180
+ * named `~` under the host CWD) or a CWD-relative path — split-brain skill
2181
+ * trees, preset installs the platform never reads, doctor probes of a
2182
+ * directory nothing serves. Behavior now matches upstream:
2183
+ * `resolve(expandHomePath(selected))`. Only `~`-prefixed and RELATIVE
2184
+ * DSH_HOME values change landing spot; absolute homes are byte-identical.
2143
2185
  */
2144
2186
  function evolutionRoot(env = process.env) {
2145
2187
  const home = env.DSH_HOME?.trim();
2146
- return home ? home : join(homedir(), ".dsh");
2188
+ const selected = home ? home : join(homedir(), ".dsh");
2189
+ const expanded = selected === "~" ? homedir() : selected.startsWith("~/") || selected.startsWith("~\\") ? join(homedir(), selected.slice(2)) : selected;
2190
+ return isAbsolute(expanded) ? expanded : resolve(expanded);
2147
2191
  }
2148
2192
  /** Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
2149
2193
  * state (reports, activity store, feedback file, state-domain data). */
@@ -2516,7 +2560,9 @@ function scanMemoryThreats(text, maxScanChars = 65536, options = NO_SCAN_OPTIONS
2516
2560
  function scanContentThreats(text, maxScanChars = 65536, options = NO_SCAN_OPTIONS) {
2517
2561
  const { blocked, findings } = evaluateThreat(text, "strict", maxScanChars, options);
2518
2562
  if (!blocked) return null;
2519
- return `Blocked by security scan (${findings[0]?.label ?? "unknown"}). This content appears to contain potentially malicious instructions.${THREAT_EXEMPTION_HINT}`;
2563
+ const pattern = findings.find((f) => f.category !== "unicode_obfuscation");
2564
+ if (pattern) return `Blocked by security scan (${pattern.label}). This content appears to contain potentially malicious instructions.${THREAT_EXEMPTION_HINT}`;
2565
+ return `Blocked by security scan: invisible or potentially malicious Unicode detected. This content appears to contain potentially malicious instructions.${THREAT_EXEMPTION_HINT}`;
2520
2566
  }
2521
2567
  /**
2522
2568
  * WD2 (0.3.56): the shared tail of every user-facing threat block — names the
@@ -3508,7 +3554,7 @@ const SECRET_PATTERNS = [
3508
3554
  const INLINE_ASSIGNMENT_PATTERN = /* @__PURE__ */ new RegExp("(^|[^\\w-])([\\w-]{0,64}[_\\-])?((?:token|api[_-]?key|secret|password|passwd)(?:[_\\-][\\w-]{0,64})?)\\b([\"\\']?[\\t ]*[:=][\\t ]*)([^\\r\\n]+)", "gi");
3509
3555
  const URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:)([^\s/@]+)@/gi;
3510
3556
  const PEM_PRIVATE_KEY_PATTERN = new RegExp(`-----BEGIN\\s+(?:(?:RSA|EC|DSA|OPENSSH|ENCRYPTED|PGP)\\s+)?PRIVATE\\s+KEY(?:\\s+BLOCK)?-----[\\s\\S]*?-----END\\s+(?:(?:RSA|EC|DSA|OPENSSH|ENCRYPTED|PGP)\\s+)?PRIVATE\\s+KEY(?:\\s+BLOCK)?-----`, "g");
3511
- const BLOCK_KEY_ONLY_LINE = /(?:^|\s)([\w-]{0,64}[_\-])?((?:token|api[_-]?key|secret|password|passwd)(?:[_\-][\w-]{0,64})?)\s*:\s*$/i;
3557
+ const BLOCK_KEY_ONLY_LINE = /(?:^|\s)([\w-]{0,64}[_\-])?((?:token|api[_-]?key|secret|password|passwd)(?:[_\-][\w-]{0,64})?)\s*:(?:\r)?$/i;
3512
3558
  /**
3513
3559
  * Mask credential-shaped text before it crosses a session boundary.
3514
3560
  * @param text - the text about to be sent to a model outside this session.
@@ -3525,7 +3571,7 @@ function redactSecrets(text) {
3525
3571
  const line = lines[i];
3526
3572
  if (line === void 0 || !BLOCK_KEY_ONLY_LINE.test(line)) continue;
3527
3573
  const next = lines[i + 1] ?? "";
3528
- const [, indent, value, tail] = /^([ \t]+)(\S.*?)([ \t]*)$/.exec(next) ?? [];
3574
+ const [, indent, value, tail] = /^([ \t]+)(\S.*?)([ \t]*)(?:\r)?$/.exec(next) ?? [];
3529
3575
  if (indent === void 0 || value === void 0) continue;
3530
3576
  if (value.includes("<redacted>")) continue;
3531
3577
  lines[i + 1] = `${indent}<redacted>${tail ?? ""}`;
@@ -3877,6 +3923,32 @@ function findDriftSignal(signals, id) {
3877
3923
  * the default dsh skill-filesystem user root. The plugin only manages skills
3878
3924
  * it created unless a `.hermes-managed` marker opts a skill in. Archival is a
3879
3925
  * move to `.archive/` — never a hard delete.
3926
+ *
3927
+ * ## Concurrency discipline (OPT-09, 2026-09) — read before adding a mutator
3928
+ *
3929
+ * Three primitives, three distinct jobs (they compose, they do not replace
3930
+ * each other):
3931
+ *
3932
+ * 1. **In-process serial queue** (`this.serial`, makeSerialQueue) — orders the
3933
+ * read→plan→commit phases of one skill's mutation against OTHER mutators
3934
+ * in this process. Used by: create/update/patch/setPinned/restructure/
3935
+ * writeSupportFile/removeSupportFile and (whole-mutation) consolidate.
3936
+ * NON-reentrant: a callback must never call a public method that wraps
3937
+ * itself in `this.serial` (archive/restoreFromArchive deliberately do not).
3938
+ * 2. **Per-directory write lock** (io.ts LOCK_*) — cross-process mutual
3939
+ * exclusion plus in-process crash ownership (tickets, takeover). Checked
3940
+ * with `hasWriteLock` before any destructive move (archive/restore/
3941
+ * snapshot); held inside transactIo by byte writers.
3942
+ * 3. **CAS baseline (`expected:`)** — any read whose bytes feed a later write
3943
+ * must either live inside the serial section that commits the write, or
3944
+ * carry its plan-time bytes as `expected` so the commit fails closed on
3945
+ * drift (V8-11 / V24-01). A read outside the serial section WITHOUT a
3946
+ * baseline is a lost-update bug; this file's history is the test suite.
3947
+ *
3948
+ * Known residuals (deliberate, documented at their sites): the archive commit
3949
+ * re-check narrows but does not close the pin race (OPT-06); snapshotAll
3950
+ * re-probes after its copies so a mid-copy writer demotes to `skipped`
3951
+ * (OPT-07); the movers' probe→rename window is owned by the io.ts protocol.
3880
3952
  */
3881
3953
  /** 0.3.16 (S1.13, T-6): the pointer-line prefix written into a body when a
3882
3954
  * section is moved to references/ — single literal, both restructure and
@@ -5644,6 +5716,11 @@ var SkillLibrary = class {
5644
5716
  ok: false,
5645
5717
  message: `Skill "${name}" is being written (write lock present); retry archiving once the write completes.`
5646
5718
  };
5719
+ const commitProtection = await this.deleteProtection(name, options);
5720
+ if (commitProtection) return {
5721
+ ok: false,
5722
+ message: commitProtection === "pinned" ? `Skill "${name}" is pinned and cannot be archived. Remove the \`.pinned\` marker in its directory, then retry.` : `Skill "${name}" is protected (${commitProtection}).`
5723
+ };
5647
5724
  const moveFailure = await this.moveDir(dir, dest);
5648
5725
  if (moveFailure !== void 0) return {
5649
5726
  ok: false,
@@ -5707,74 +5784,74 @@ var SkillLibrary = class {
5707
5784
  ok: false,
5708
5785
  message: `Skill "${targetName}" is protected (${targetProtection}).`
5709
5786
  };
5710
- const referenceWrites = [];
5711
- const parts = [];
5712
- if (mode === "append") for (const source of normalizedSources) {
5713
- const protection = await this.deleteProtection(source);
5714
- if (protection) return {
5715
- ok: false,
5716
- message: `Skill "${source}" is protected (${protection}).`
5717
- };
5718
- const sourceMd = await this.io.readText(join(this.dirOf(source), "SKILL.md"));
5719
- if (!sourceMd) return {
5720
- ok: false,
5721
- message: `Skill "${source}" not found.`
5722
- };
5723
- const parsed = parseFrontmatter(sourceMd);
5724
- if (!parsed) return {
5725
- ok: false,
5726
- message: `Skill "${source}" has no valid frontmatter; refusing to merge.`
5727
- };
5728
- if (await this.countSupportDirs(source) > 0) return {
5729
- ok: false,
5730
- message: `Consolidation rejected: source "${source}" carries support files — use mode:'reference' or archive the whole package instead.`
5731
- };
5732
- const refs = supportRefs(parsed.body);
5733
- if (refs.length > 0) return {
5734
- ok: false,
5735
- message: `Consolidation rejected: source "${source}" body references support files (${refs.join(", ")}) that would be left behind — use mode:'reference' or archive the whole package instead.`
5736
- };
5737
- parts.push(`\n<!-- consolidated from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}`);
5738
- }
5739
- else for (const source of normalizedSources) {
5740
- const protection = await this.deleteProtection(source);
5741
- if (protection) return {
5742
- ok: false,
5743
- message: `Skill "${source}" is protected (${protection}).`
5744
- };
5745
- const sourceMd = await this.io.readText(join(this.dirOf(source), "SKILL.md"));
5746
- if (!sourceMd) return {
5747
- ok: false,
5748
- message: `Skill "${source}" not found.`
5749
- };
5750
- const parsed = parseFrontmatter(sourceMd);
5751
- if (!parsed) return {
5752
- ok: false,
5753
- message: `Skill "${source}" has no valid frontmatter; refusing to demote.`
5754
- };
5755
- const refs = supportRefs(parsed.body);
5756
- if (refs.length > 0) return {
5757
- ok: false,
5758
- message: `Consolidation rejected: source "${source}" body references support files (${refs.join(", ")}) that would be left behind — archive the whole package instead.`
5759
- };
5760
- const target = join(targetDir, "references", `${source}.md`);
5761
- referenceWrites.push({
5762
- target,
5763
- content: `<!-- demoted from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}\n`
5764
- });
5765
- }
5766
- const archived = [];
5767
- try {
5768
- if (!await this.io.readText(join(targetDir, "SKILL.md"))) return {
5769
- ok: false,
5770
- message: `Skill "${targetName}" not found.`
5771
- };
5772
- for (const source of normalizedSources) {
5773
- const result = await this.archive(source, { absorbedInto: targetName });
5774
- if (!result.ok) throw new Error(result.message);
5775
- archived.push(source);
5787
+ return await this.serial(async () => {
5788
+ const referenceWrites = [];
5789
+ const parts = [];
5790
+ if (mode === "append") for (const source of normalizedSources) {
5791
+ const protection = await this.deleteProtection(source);
5792
+ if (protection) return {
5793
+ ok: false,
5794
+ message: `Skill "${source}" is protected (${protection}).`
5795
+ };
5796
+ const sourceMd = await this.io.readText(join(this.dirOf(source), "SKILL.md"));
5797
+ if (!sourceMd) return {
5798
+ ok: false,
5799
+ message: `Skill "${source}" not found.`
5800
+ };
5801
+ const parsed = parseFrontmatter(sourceMd);
5802
+ if (!parsed) return {
5803
+ ok: false,
5804
+ message: `Skill "${source}" has no valid frontmatter; refusing to merge.`
5805
+ };
5806
+ if (await this.countSupportDirs(source) > 0) return {
5807
+ ok: false,
5808
+ message: `Consolidation rejected: source "${source}" carries support files — use mode:'reference' or archive the whole package instead.`
5809
+ };
5810
+ const refs = supportRefs(parsed.body);
5811
+ if (refs.length > 0) return {
5812
+ ok: false,
5813
+ message: `Consolidation rejected: source "${source}" body references support files (${refs.join(", ")}) that would be left behind — use mode:'reference' or archive the whole package instead.`
5814
+ };
5815
+ parts.push(`\n<!-- consolidated from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}`);
5816
+ }
5817
+ else for (const source of normalizedSources) {
5818
+ const protection = await this.deleteProtection(source);
5819
+ if (protection) return {
5820
+ ok: false,
5821
+ message: `Skill "${source}" is protected (${protection}).`
5822
+ };
5823
+ const sourceMd = await this.io.readText(join(this.dirOf(source), "SKILL.md"));
5824
+ if (!sourceMd) return {
5825
+ ok: false,
5826
+ message: `Skill "${source}" not found.`
5827
+ };
5828
+ const parsed = parseFrontmatter(sourceMd);
5829
+ if (!parsed) return {
5830
+ ok: false,
5831
+ message: `Skill "${source}" has no valid frontmatter; refusing to demote.`
5832
+ };
5833
+ const refs = supportRefs(parsed.body);
5834
+ if (refs.length > 0) return {
5835
+ ok: false,
5836
+ message: `Consolidation rejected: source "${source}" body references support files (${refs.join(", ")}) that would be left behind — archive the whole package instead.`
5837
+ };
5838
+ const target = join(targetDir, "references", `${source}.md`);
5839
+ referenceWrites.push({
5840
+ target,
5841
+ content: `<!-- demoted from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}\n`
5842
+ });
5776
5843
  }
5777
- const result = await this.serial(async () => {
5844
+ const archived = [];
5845
+ try {
5846
+ if (!await this.io.readText(join(targetDir, "SKILL.md"))) return {
5847
+ ok: false,
5848
+ message: `Skill "${targetName}" not found.`
5849
+ };
5850
+ for (const source of normalizedSources) {
5851
+ const result = await this.archive(source, { absorbedInto: targetName });
5852
+ if (!result.ok) throw new Error(result.message);
5853
+ archived.push(source);
5854
+ }
5778
5855
  const freshTargetMd = await this.io.readText(join(targetDir, "SKILL.md"));
5779
5856
  if (!freshTargetMd) return {
5780
5857
  ok: false,
@@ -5816,7 +5893,7 @@ var SkillLibrary = class {
5816
5893
  expected: freshTargetMd
5817
5894
  });
5818
5895
  }
5819
- return await this.applyTreeChange({
5896
+ const result = await this.applyTreeChange({
5820
5897
  name: targetName,
5821
5898
  origin,
5822
5899
  protection: "write",
@@ -5825,30 +5902,30 @@ var SkillLibrary = class {
5825
5902
  auditSummary: `consolidated ${normalizedSources.join(", ")} (${mode}) into ${targetName}`,
5826
5903
  eventAction: "consolidate"
5827
5904
  });
5828
- });
5829
- if (!result.ok) throw new Error(result.message);
5830
- return {
5831
- ok: true,
5832
- message: `Consolidated ${normalizedSources.join(", ")} into "${targetName}".`,
5833
- path: targetDir
5834
- };
5835
- } catch (error) {
5836
- const reason = error instanceof Error ? error.message : String(error);
5837
- const failedRestores = [];
5838
- for (const source of archived.reverse()) try {
5839
- if (!(await this.restoreFromArchive(source)).ok) failedRestores.push(source);
5840
- } catch {
5841
- failedRestores.push(source);
5905
+ if (!result.ok) throw new Error(result.message);
5906
+ return {
5907
+ ok: true,
5908
+ message: `Consolidated ${normalizedSources.join(", ")} into "${targetName}".`,
5909
+ path: targetDir
5910
+ };
5911
+ } catch (error) {
5912
+ const reason = error instanceof Error ? error.message : String(error);
5913
+ const failedRestores = [];
5914
+ for (const source of archived.reverse()) try {
5915
+ if (!(await this.restoreFromArchive(source)).ok) failedRestores.push(source);
5916
+ } catch {
5917
+ failedRestores.push(source);
5918
+ }
5919
+ if (failedRestores.length > 0) return {
5920
+ ok: false,
5921
+ message: `Consolidation failed (${reason}); rolled back EXCEPT ${failedRestores.join(", ")} — still in .archive, restore them with /evolution skill restore.`
5922
+ };
5923
+ return {
5924
+ ok: false,
5925
+ message: `Consolidation failed and was rolled back: ${reason}`
5926
+ };
5842
5927
  }
5843
- if (failedRestores.length > 0) return {
5844
- ok: false,
5845
- message: `Consolidation failed (${reason}); rolled back EXCEPT ${failedRestores.join(", ")} — still in .archive, restore them with /evolution skill restore.`
5846
- };
5847
- return {
5848
- ok: false,
5849
- message: `Consolidation failed and was rolled back: ${reason}`
5850
- };
5851
- }
5928
+ });
5852
5929
  }
5853
5930
  /**
5854
5931
  * Content-distribution repair (008 batch B, 009-R kernel): move body
@@ -6093,6 +6170,14 @@ var SkillLibrary = class {
6093
6170
  ok: false,
6094
6171
  message: await this.io.exists(join(dest, "SKILL.md")) ? `Skill "${name}" already exists in the active root; refusing to overwrite.` : `Skill directory "${name}" already exists in the active root but carries no SKILL.md; remove or repair it before restoring.`
6095
6172
  };
6173
+ let caseVariant;
6174
+ try {
6175
+ caseVariant = (await this.io.list(this.root)).find((entry) => entry !== name && entry.toLowerCase() === name.toLowerCase());
6176
+ } catch {}
6177
+ if (caseVariant !== void 0) return {
6178
+ ok: false,
6179
+ message: `Skill "${caseVariant}" (same name, different letter case) already exists in the active root; restoring "${name}" beside it would create ambiguous duplicates — restore as "${caseVariant}" or remove the variant first.`
6180
+ };
6096
6181
  const archiveRoot = join(this.root, ".archive");
6097
6182
  let entries;
6098
6183
  try {
@@ -6345,6 +6430,13 @@ var SkillLibrary = class {
6345
6430
  await this.io.copy(this.dirOf(name), join(dest, name));
6346
6431
  }))).find((result) => result.status === "rejected");
6347
6432
  if (copyFailure) throw copyFailure.reason;
6433
+ const suspect = [];
6434
+ for (const name of copyable) if (await this.hasWriteLock(this.dirOf(name))) suspect.push(name);
6435
+ if (suspect.length > 0) {
6436
+ for (const name of suspect) console.warn(`skill-store: snapshot demoted "${name}" to skipped — a write lock appeared while its copy ran; the copied bytes are suspect`);
6437
+ for (const name of suspect) copyable.splice(copyable.indexOf(name), 1);
6438
+ skipped.push(...suspect);
6439
+ }
6348
6440
  const sidecars = [];
6349
6441
  for (const sidecar of [usageFile(this.root), suppressedFile(this.root)]) if (await this.io.exists(sidecar)) {
6350
6442
  const name = basename(sidecar);
@@ -6614,4 +6706,4 @@ var SkillLibrary = class {
6614
6706
  }
6615
6707
  };
6616
6708
  //#endregion
6617
- export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, EMPTY_LOCK_TAKEOVER_MS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, LOCK_BODY_RE, LOCK_SUFFIX, LOCK_TEAR_TAKEOVER_MS, LOW_QUALITY_THRESHOLD, LostWriteLock, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, PATTERN_OVERLAP, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, THREAT_EXEMPTION_HINT, advanceReview, allowRowCollisions, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, clampedNumber, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, decideTakeover, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionEventPayloadIssue, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterCatalogInvalid, getRecord, isCommittedWarning, isProcessAlive, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, makeSerialQueue, markAgentCreated, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseLockBody, pendingSelfCleanup, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renameWithRetry, renderCuratorReportMarkdown, resolveOrigins, resolveRootConfig, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, validateRestructureTarget, verifyPromptBundle, writeDurableTmp, yamlPlainScalarNeedsQuotes };
6709
+ export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, EMPTY_LOCK_TAKEOVER_MS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, LOCK_BODY_RE, LOCK_SUFFIX, LOCK_TEAR_TAKEOVER_MS, LOW_QUALITY_THRESHOLD, LostWriteLock, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, PATTERN_OVERLAP, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, THREAT_EXEMPTION_HINT, advanceReview, allowRowCollisions, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, clampedNumber, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, decideTakeover, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionEventPayloadIssue, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterCatalogInvalid, getRecord, isCommittedWarning, isProcessAlive, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, makeSerialQueue, markAgentCreated, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseLockBody, pendingSelfCleanup, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renameWithRetry, renderCuratorReportMarkdown, resolveOrigins, resolveRootConfig, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, validateRestructureTarget, verifyPromptBundle, writeDurableTmp, yamlPlainScalarNeedsQuotes };
@@ -19,6 +19,22 @@
19
19
  * threshold, which are intentionally left where they are used.
20
20
  * @module @lmzhen/dsh-evolution-core
21
21
  */
22
+ /**
23
+ * Required argument names per `skill_manage` action — the SINGLE SOURCE read
24
+ * by the tool's argument gate (tool-skill-manage executeCore) and the plan
25
+ * validator (evolution-plan-validator), so the two can never drift.
26
+ * OPT-05 (2026-09): the plan validator used to accept a `write_file`/
27
+ * `remove_file` op without `file_path` while the executor required it — the
28
+ * staged write then failed at EVERY approve until rejected.
29
+ * Rows here are the op-level requirements only: `delete` additionally
30
+ * requires `absorbed_into` at the PLAN layer (review passes may only delete
31
+ * into an umbrella) and `pin`/`unpin` are tool-only actions — each consumer
32
+ * adds its own extras on top of this table. An empty-string argument is NOT
33
+ * caught here (the tool's gate deliberately lets it reach the library for a
34
+ * more specific remedy message); the validator adds its own `.trim()`
35
+ * emptiness checks for payload fields.
36
+ */
37
+ export declare const SKILL_ACTION_REQUIRED_FIELDS: Readonly<Record<string, readonly string[]>>;
22
38
  /** Skill frontmatter `name` validated for the file name (lowercase + hyphen).
23
39
  * 计划 B-4 (v18): tightened to the UPSTREAM `SKILL_NAME` shape
24
40
  * (`/^[a-z0-9]+(?:-[a-z0-9]+)*$/`, packages/skill/skill/src/index.ts:20). The
@@ -7,6 +7,23 @@
7
7
  * the host auto-assembles to register this package's no-op invariant);
8
8
  * consumers import named exports from the package root so published npm
9
9
  * bundles never depend on source subpaths.
10
+ *
11
+ * ## Layer map (OPT-28, 2026-09) — locate code by LAYER, not by directory
12
+ *
13
+ * This one physical package carries THREE architecture layers of the family;
14
+ * when adding or looking for something, go by the export's layer:
15
+ *
16
+ * - **Cross-cutting basics** — `state-store.ts` (env roots — the single
17
+ * source of DSH-home semantics), `serial.ts`, `numeric.ts`, `constants.ts`,
18
+ * `mutations.ts`, `events.ts`, `gates.ts`.
19
+ * - **Security primitives** — `threats.ts` (content threat scanner),
20
+ * `redact.ts` (credential masking at model boundaries). Consumers:
21
+ * evolution-policy/threat, both stores, review, maintenance.
22
+ * - **Core domain stores/logic** — `skill-store.ts` (skill tree engine +
23
+ * IO-seam consumer), `memory-store.ts`, `usage.ts`, `curator.ts`,
24
+ * `quality.ts`, `signals.ts`, `drift-signals.ts`, `skill-health.ts`,
25
+ * `preset-composition.ts`, `prompts.ts`, `learn-prompt.ts`,
26
+ * `evolution-events.ts`, `io.ts` (the ctx.evolutionIo seam itself).
10
27
  * @module @lmzhen/dsh-evolution-core
11
28
  */
12
29
  export * from './curator.ts';
@@ -5,6 +5,32 @@
5
5
  * the default dsh skill-filesystem user root. The plugin only manages skills
6
6
  * it created unless a `.hermes-managed` marker opts a skill in. Archival is a
7
7
  * move to `.archive/` — never a hard delete.
8
+ *
9
+ * ## Concurrency discipline (OPT-09, 2026-09) — read before adding a mutator
10
+ *
11
+ * Three primitives, three distinct jobs (they compose, they do not replace
12
+ * each other):
13
+ *
14
+ * 1. **In-process serial queue** (`this.serial`, makeSerialQueue) — orders the
15
+ * read→plan→commit phases of one skill's mutation against OTHER mutators
16
+ * in this process. Used by: create/update/patch/setPinned/restructure/
17
+ * writeSupportFile/removeSupportFile and (whole-mutation) consolidate.
18
+ * NON-reentrant: a callback must never call a public method that wraps
19
+ * itself in `this.serial` (archive/restoreFromArchive deliberately do not).
20
+ * 2. **Per-directory write lock** (io.ts LOCK_*) — cross-process mutual
21
+ * exclusion plus in-process crash ownership (tickets, takeover). Checked
22
+ * with `hasWriteLock` before any destructive move (archive/restore/
23
+ * snapshot); held inside transactIo by byte writers.
24
+ * 3. **CAS baseline (`expected:`)** — any read whose bytes feed a later write
25
+ * must either live inside the serial section that commits the write, or
26
+ * carry its plan-time bytes as `expected` so the commit fails closed on
27
+ * drift (V8-11 / V24-01). A read outside the serial section WITHOUT a
28
+ * baseline is a lost-update bug; this file's history is the test suite.
29
+ *
30
+ * Known residuals (deliberate, documented at their sites): the archive commit
31
+ * re-check narrows but does not close the pin race (OPT-06); snapshotAll
32
+ * re-probes after its copies so a mid-copy writer demotes to `skipped`
33
+ * (OPT-07); the movers' probe→rename window is owned by the io.ts protocol.
8
34
  */
9
35
  import { transactIo, type EvolutionIoLike } from './io.ts';
10
36
  import { type MutationRecord } from './mutations.ts';
@@ -20,9 +20,16 @@
20
20
  * C-11: the adoption test and the RETURNED value now come from the
21
21
  * SAME trimmed source — the old form tested `trim()` but returned the raw
22
22
  * value, so `DSH_HOME=" /x "` was accepted AND persisted with literal spaces.
23
- * Known tradeoff vs upstream `resolveDshHome`: `~` is NOT expanded here
24
- * documented as a deliberate difference in the v10 audit; revisit only if a
25
- * real deployment needs it.
23
+ * OPT-27 (2026-09, plan D5 accepted): the v10-era "no `~` expansion, no
24
+ * resolve" divergence from upstream `resolveDshHome` is RETIRED. It became
25
+ * load-bearing when the skill-catalog shadow made "same tree as the upstream
26
+ * `USER_DSH_RANK` provider" a hard contract: upstream watches the EXPANDED
27
+ * absolute `<home>/skills` while this value fed a literal `~/x` (a directory
28
+ * named `~` under the host CWD) or a CWD-relative path — split-brain skill
29
+ * trees, preset installs the platform never reads, doctor probes of a
30
+ * directory nothing serves. Behavior now matches upstream:
31
+ * `resolve(expandHomePath(selected))`. Only `~`-prefixed and RELATIVE
32
+ * DSH_HOME values change landing spot; absolute homes are byte-identical.
26
33
  */
27
34
  export declare function evolutionRoot(env?: NodeJS.ProcessEnv): string;
28
35
  /** Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-core",
3
3
  "description": "Shared stores, prompts, signals and lifecycle logic for the dsh-evolution plugin family (community build)",
4
- "version": "0.3.71",
4
+ "version": "0.3.73",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },