@lmzhen/dsh-evolution-review 0.3.70 → 0.3.71

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 +51 -37
  2. package/package.json +11 -11
package/lib/index.js CHANGED
@@ -342,13 +342,10 @@ function apply(ctx, rawConfig = {}) {
342
342
  const io = ctx.get("evolutionIo");
343
343
  if (!io) return hashes;
344
344
  const library = new SkillLibrary(resolveSkillsRoot({ root: rootConfig.root }), evolutionIoAdapter(() => io.provider()));
345
- for (const summary of await library.list().catch((error) => {
345
+ for (const summary of await library.list({ withContent: true }).catch((error) => {
346
346
  ctx.logger.warn(`dsh-evolution-review: skill tree scan failed (${error instanceof Error ? error.message : String(error)}) — pre-run staleness hashes are unavailable; full-content updates will not be drift-checked this review`);
347
347
  return [];
348
- })) {
349
- const text = await library.read(summary.name).catch(() => null);
350
- if (text !== null) hashes.set(summary.name, contentHash(text));
351
- }
348
+ })) if (summary.content !== void 0) hashes.set(summary.name, contentHash(summary.content));
352
349
  return hashes;
353
350
  }
354
351
  async function trySubagentReview(session, agent, kind, signal) {
@@ -559,8 +556,9 @@ function apply(ctx, rawConfig = {}) {
559
556
  ...op,
560
557
  evidence: op.evidence
561
558
  };
559
+ let stageCurrent = null;
562
560
  if ((args.action === "update" || args.action === "edit") && hashLibrary && typeof args.name === "string" && args.name !== "") {
563
- const stageCurrent = await hashLibrary.read(args.name).catch(() => null);
561
+ stageCurrent = await hashLibrary.read(args.name).catch(() => null);
564
562
  if (stageCurrent !== null) args.staged_from_sha256 = contentHash(stageCurrent);
565
563
  }
566
564
  if ((args.action === "write_file" || args.action === "remove_file") && hashLibrary && typeof args.name === "string" && args.name !== "" && typeof args.file_path === "string" && args.file_path !== "") {
@@ -570,7 +568,7 @@ function apply(ctx, rawConfig = {}) {
570
568
  const opName = typeof args.name === "string" ? args.name : "";
571
569
  const hashChecked = (args.action === "update" || args.action === "edit") && opName !== "" && preRunHashes?.has(opName) === true;
572
570
  if (hashChecked) {
573
- const live = await hashLibrary?.read(opName).catch(() => null) ?? null;
571
+ const live = stageCurrent;
574
572
  const preRun = preRunHashes.get(opName);
575
573
  if (live === null || preRun !== void 0 && contentHash(live) !== preRun) {
576
574
  ok = false;
@@ -679,32 +677,14 @@ function apply(ctx, rawConfig = {}) {
679
677
  ok: false,
680
678
  message: `Skill "${name}" is protected by the current policy (protectedSkillNames); autonomous writes are refused.`
681
679
  };
682
- if (op.action === "write_file" || op.action === "remove_file") {
683
- const expected = op.staged_from_sha256;
684
- if (typeof expected === "string" && expected !== "") {
685
- const current = await library.readSupportFile(name, op.file_path ?? "").catch(() => void 0);
686
- const actual = current === void 0 || current === null ? "absent" : contentHash(current);
687
- if (current === void 0 || actual !== expected) return {
688
- ok: false,
689
- message: `Support file "${op.file_path ?? ""}" of "${name}" changed since this plan was produced — the staged file operation was refused as stale. Re-read the skill tree and produce a fresh plan.`
690
- };
691
- }
692
- }
693
680
  if (op.action === "create") {
694
681
  const created = await library.create(name, op.content ?? "", origin);
695
682
  if (created.ok) await ctx.get("skillUsage")?.markAgentCreated?.(name);
696
683
  return created;
697
684
  }
698
685
  if (op.action === "edit" || op.action === "update") {
699
- const expected = op.staged_from_sha256;
700
- if (typeof expected === "string" && expected !== "") {
701
- const current = await library.read(name).catch(() => null);
702
- if (current !== null && contentHash(current) !== expected) return {
703
- ok: false,
704
- message: `Skill "${name}" changed since this plan was produced — the full-content update was refused as stale. Re-read the skill and produce a fresh plan.`
705
- };
706
- }
707
- const updated = await library.update(name, op.content ?? "", origin);
686
+ const updated = await library.update(name, op.content ?? "", origin, anchorOf(op));
687
+ if (updated.stale === true) return staleRefusal(updated, name, op.file_path);
708
688
  if (updated.ok) await ctx.get("skillUsage")?.record?.(name, "patch").catch(() => {});
709
689
  return updated;
710
690
  }
@@ -724,17 +704,14 @@ function apply(ctx, rawConfig = {}) {
724
704
  return archived;
725
705
  }
726
706
  if (op.action === "write_file" || op.action === "remove_file") {
727
- const expected = op.staged_from_sha256;
728
- if (typeof expected === "string" && expected !== "") {
729
- const current = await library.readSupportFile(name, op.file_path ?? "").catch(() => void 0);
730
- const actual = current === void 0 || current === null ? "absent" : contentHash(current);
731
- if (current === void 0 || actual !== expected) return {
732
- ok: false,
733
- message: `Support file "${op.file_path ?? ""}" of "${name}" changed since this plan was produced — the staged file operation was refused as stale. Re-read the skill tree and produce a fresh plan.`
734
- };
707
+ const anchor = anchorOf(op);
708
+ if (op.action === "write_file") {
709
+ const written = await library.writeSupportFile(name, op.file_path ?? "", op.file_content ?? "", origin, anchor);
710
+ if (written.stale === true) return staleRefusal(written, name, op.file_path);
711
+ return written;
735
712
  }
736
- if (op.action === "write_file") return await library.writeSupportFile(name, op.file_path ?? "", op.file_content ?? "", origin);
737
- const removedSupport = await library.removeSupportFile(name, op.file_path ?? "", origin);
713
+ const removedSupport = await library.removeSupportFile(name, op.file_path ?? "", origin, anchor);
714
+ if (removedSupport.stale === true) return staleRefusal(removedSupport, name, op.file_path);
738
715
  if (removedSupport.ok) await ctx.get("skillUsage")?.record?.(name, "patch").catch(() => {});
739
716
  return removedSupport;
740
717
  }
@@ -771,6 +748,43 @@ function shouldCompletionReview(reason, sessionToolCalls, minToolCalls) {
771
748
  * `skill_search` discovery pair, so that branch was dead. `skill_manage` has no
772
749
  * per-skill read action (its `list`/`review` are whole-library), so a specific
773
750
  * skill read through it cannot be tracked — see README Known Limitations. */
751
+ /**
752
+ * v35 C9: the staging anchor of one accepted plan op, in the shape the library
753
+ * verifies against the bytes it reads under the write's own lock. The `absent`
754
+ * sentinel is the same one the tool's staging attaches for a support file that
755
+ * did not exist when the plan was produced.
756
+ * The staged field rides OUTSIDE the plan schema (the staging channel attaches
757
+ * it to the op payload), so it is read defensively rather than typed on SkillOp.
758
+ * @param op - one accepted plan op.
759
+ * @returns the anchor, or `undefined` when the op carries none.
760
+ */
761
+ function anchorOf(op) {
762
+ const expected = op.staged_from_sha256;
763
+ if (typeof expected !== "string" || expected === "") return void 0;
764
+ return expected === "absent" ? { absent: true } : { sha256: expected };
765
+ }
766
+ /**
767
+ * Translate the library's anchor refusal into this channel's staging wording.
768
+ * The library reports WHICH drift it saw (drift / missing / unverifiable); the
769
+ * vocabulary ("this plan was produced", "produce a fresh plan") is the review
770
+ * channel's. A body op whose skill vanished keeps the historical not-found
771
+ * report: the former lock-free check only refused on a DRIFTED body, so a
772
+ * deleted target fell through to the write's own not-found result.
773
+ * @param result - the library's stale result.
774
+ * @param name - the skill the op targets.
775
+ * @param filePath - the support-file path for file ops; omitted for body ops.
776
+ * @returns the refusal to return from the direct path.
777
+ */
778
+ function staleRefusal(result, name, filePath) {
779
+ if (filePath === void 0 && result.anchor === "missing") return {
780
+ ok: false,
781
+ message: `Skill "${name}" not found.`
782
+ };
783
+ return {
784
+ ok: false,
785
+ message: filePath === void 0 ? `Skill "${name}" changed since this plan was produced — the full-content update was refused as stale. Re-read the skill and produce a fresh plan.` : `Support file "${filePath}" of "${name}" changed since this plan was produced — the staged file operation was refused as stale. Re-read the skill tree and produce a fresh plan.`
786
+ };
787
+ }
774
788
  function collectReadSkillNames(session) {
775
789
  const callNames = /* @__PURE__ */ new Map();
776
790
  const okCallIds = /* @__PURE__ */ new Set();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-review",
3
3
  "description": "Background review orchestration (community build)",
4
- "version": "0.3.70",
4
+ "version": "0.3.71",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -31,9 +31,9 @@
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",
36
- "@lmzhen/dsh-evolution-plan-validator": "^0.3.70"
34
+ "@lmzhen/dsh-evolution-approval": "^0.3.71",
35
+ "@lmzhen/dsh-evolution-core": "^0.3.71",
36
+ "@lmzhen/dsh-evolution-plan-validator": "^0.3.71"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@deepseek-ai/cordis": "^4.0.1",
@@ -42,8 +42,8 @@
42
42
  "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
43
43
  "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
44
44
  "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
45
- "@lmzhen/dsh-evolution-state": "^0.3.70",
46
- "@lmzhen/dsh-evolution-policy": "^0.3.70"
45
+ "@lmzhen/dsh-evolution-state": "^0.3.71",
46
+ "@lmzhen/dsh-evolution-policy": "^0.3.71"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@deepseek-ai/dsh-agent": "^0.1.5-rc.2",
@@ -54,10 +54,10 @@
54
54
  "@deepseek-ai/dsh-session-persistence": "^0.1.5-rc.2",
55
55
  "@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.5-rc.2",
56
56
  "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
57
- "@lmzhen/dsh-evolution-approval": "^0.3.70",
58
- "@lmzhen/dsh-evolution-core": "^0.3.70",
59
- "@lmzhen/dsh-evolution-curator": "^0.3.70",
60
- "@lmzhen/dsh-evolution-plan-validator": "^0.3.70",
61
- "@lmzhen/dsh-evolution-state": "^0.3.70"
57
+ "@lmzhen/dsh-evolution-approval": "^0.3.71",
58
+ "@lmzhen/dsh-evolution-core": "^0.3.71",
59
+ "@lmzhen/dsh-evolution-curator": "^0.3.71",
60
+ "@lmzhen/dsh-evolution-plan-validator": "^0.3.71",
61
+ "@lmzhen/dsh-evolution-state": "^0.3.71"
62
62
  }
63
63
  }