@lmzhen/dsh-evolution-review 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.
Files changed (2) hide show
  1. package/lib/index.js +59 -41
  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) {
@@ -379,6 +376,7 @@ function apply(ctx, rawConfig = {}) {
379
376
  const agentOptions = { model };
380
377
  if (config.reviewProvider) agentOptions.provider = config.reviewProvider;
381
378
  const preRunHashes = await treeSkillHashes();
379
+ const sessionSeqAtPlanTime = session.seq - 1;
382
380
  const run = await subagents.start("spawn", {
383
381
  label: "dsh-evolution-review",
384
382
  prompt: [{
@@ -401,14 +399,17 @@ function apply(ctx, rawConfig = {}) {
401
399
  } catch (emitError) {
402
400
  ctx.logger.warn(`dsh-evolution-review: review-error emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
403
401
  }
404
- ctx.logger.warn("dsh-evolution-review: review subagent returned no structured plan");
402
+ const failureShape = result;
403
+ const stopDetail = typeof failureShape.stopReason === "string" ? failureShape.stopReason : void 0;
404
+ const diagDetail = typeof failureShape.diagnostic === "string" ? failureShape.diagnostic : void 0;
405
+ ctx.logger.warn(`dsh-evolution-review: review subagent returned no structured plan${stopDetail !== void 0 ? ` (stopReason=${stopDetail}${diagDetail !== void 0 ? `; diagnostic=${diagDetail}` : ""})` : ""}`);
405
406
  return false;
406
407
  }
407
408
  const childReads = run.localAgent ? collectReadSkillNames(run.localAgent.session) : /* @__PURE__ */ new Set();
408
409
  const plan = result.structured;
409
410
  const policyFingerprint = fingerprintPolicy(snapshot);
410
411
  const validation = validateEvolutionPlan(plan, {
411
- sessionSeq: session.seq - 1,
412
+ sessionSeq: sessionSeqAtPlanTime,
412
413
  maxOpsPerPlan: snapshot?.maxOpsPerPlan ?? DEFAULT_MAX_OPS_PER_PLAN,
413
414
  protectedSkillNames: new Set(snapshot?.protectedSkillNames ?? []),
414
415
  maxMemoryChars: snapshot?.memoryChars ?? DEFAULT_MEMORY_CHAR_LIMIT,
@@ -559,8 +560,9 @@ function apply(ctx, rawConfig = {}) {
559
560
  ...op,
560
561
  evidence: op.evidence
561
562
  };
563
+ let stageCurrent = null;
562
564
  if ((args.action === "update" || args.action === "edit") && hashLibrary && typeof args.name === "string" && args.name !== "") {
563
- const stageCurrent = await hashLibrary.read(args.name).catch(() => null);
565
+ stageCurrent = await hashLibrary.read(args.name).catch(() => null);
564
566
  if (stageCurrent !== null) args.staged_from_sha256 = contentHash(stageCurrent);
565
567
  }
566
568
  if ((args.action === "write_file" || args.action === "remove_file") && hashLibrary && typeof args.name === "string" && args.name !== "" && typeof args.file_path === "string" && args.file_path !== "") {
@@ -568,13 +570,13 @@ function apply(ctx, rawConfig = {}) {
568
570
  if (stageFile !== void 0) args.staged_from_sha256 = stageFile === null ? "absent" : contentHash(stageFile);
569
571
  }
570
572
  const opName = typeof args.name === "string" ? args.name : "";
571
- const hashChecked = (args.action === "update" || args.action === "edit") && opName !== "" && preRunHashes?.has(opName) === true;
573
+ const hashChecked = (args.action === "update" || args.action === "edit" || args.action === "patch") && opName !== "" && preRunHashes?.has(opName) === true;
572
574
  if (hashChecked) {
573
- const live = await hashLibrary?.read(opName).catch(() => null) ?? null;
575
+ const live = args.action === "patch" ? hashLibrary ? await hashLibrary.read(opName).catch(() => null) : null : stageCurrent;
574
576
  const preRun = preRunHashes.get(opName);
575
577
  if (live === null || preRun !== void 0 && contentHash(live) !== preRun) {
576
578
  ok = false;
577
- failedOps.push(`skill ${args.action} ${args.name}: the skill changed while this review ran — update refused as stale; produce a fresh plan`);
579
+ failedOps.push(`skill ${args.action} ${args.name}: the skill changed while this review ran — ${args.action === "patch" ? "patch" : "update"} refused as stale; produce a fresh plan`);
578
580
  continue;
579
581
  }
580
582
  }
@@ -679,32 +681,14 @@ function apply(ctx, rawConfig = {}) {
679
681
  ok: false,
680
682
  message: `Skill "${name}" is protected by the current policy (protectedSkillNames); autonomous writes are refused.`
681
683
  };
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
684
  if (op.action === "create") {
694
685
  const created = await library.create(name, op.content ?? "", origin);
695
686
  if (created.ok) await ctx.get("skillUsage")?.markAgentCreated?.(name);
696
687
  return created;
697
688
  }
698
689
  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);
690
+ const updated = await library.update(name, op.content ?? "", origin, anchorOf(op));
691
+ if (updated.stale === true) return staleRefusal(updated, name, op.file_path);
708
692
  if (updated.ok) await ctx.get("skillUsage")?.record?.(name, "patch").catch(() => {});
709
693
  return updated;
710
694
  }
@@ -724,17 +708,14 @@ function apply(ctx, rawConfig = {}) {
724
708
  return archived;
725
709
  }
726
710
  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
- };
711
+ const anchor = anchorOf(op);
712
+ if (op.action === "write_file") {
713
+ const written = await library.writeSupportFile(name, op.file_path ?? "", op.file_content ?? "", origin, anchor);
714
+ if (written.stale === true) return staleRefusal(written, name, op.file_path);
715
+ return written;
735
716
  }
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);
717
+ const removedSupport = await library.removeSupportFile(name, op.file_path ?? "", origin, anchor);
718
+ if (removedSupport.stale === true) return staleRefusal(removedSupport, name, op.file_path);
738
719
  if (removedSupport.ok) await ctx.get("skillUsage")?.record?.(name, "patch").catch(() => {});
739
720
  return removedSupport;
740
721
  }
@@ -771,6 +752,43 @@ function shouldCompletionReview(reason, sessionToolCalls, minToolCalls) {
771
752
  * `skill_search` discovery pair, so that branch was dead. `skill_manage` has no
772
753
  * per-skill read action (its `list`/`review` are whole-library), so a specific
773
754
  * skill read through it cannot be tracked — see README Known Limitations. */
755
+ /**
756
+ * v35 C9: the staging anchor of one accepted plan op, in the shape the library
757
+ * verifies against the bytes it reads under the write's own lock. The `absent`
758
+ * sentinel is the same one the tool's staging attaches for a support file that
759
+ * did not exist when the plan was produced.
760
+ * The staged field rides OUTSIDE the plan schema (the staging channel attaches
761
+ * it to the op payload), so it is read defensively rather than typed on SkillOp.
762
+ * @param op - one accepted plan op.
763
+ * @returns the anchor, or `undefined` when the op carries none.
764
+ */
765
+ function anchorOf(op) {
766
+ const expected = op.staged_from_sha256;
767
+ if (typeof expected !== "string" || expected === "") return void 0;
768
+ return expected === "absent" ? { absent: true } : { sha256: expected };
769
+ }
770
+ /**
771
+ * Translate the library's anchor refusal into this channel's staging wording.
772
+ * The library reports WHICH drift it saw (drift / missing / unverifiable); the
773
+ * vocabulary ("this plan was produced", "produce a fresh plan") is the review
774
+ * channel's. A body op whose skill vanished keeps the historical not-found
775
+ * report: the former lock-free check only refused on a DRIFTED body, so a
776
+ * deleted target fell through to the write's own not-found result.
777
+ * @param result - the library's stale result.
778
+ * @param name - the skill the op targets.
779
+ * @param filePath - the support-file path for file ops; omitted for body ops.
780
+ * @returns the refusal to return from the direct path.
781
+ */
782
+ function staleRefusal(result, name, filePath) {
783
+ if (filePath === void 0 && result.anchor === "missing") return {
784
+ ok: false,
785
+ message: `Skill "${name}" not found.`
786
+ };
787
+ return {
788
+ ok: false,
789
+ 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.`
790
+ };
791
+ }
774
792
  function collectReadSkillNames(session) {
775
793
  const callNames = /* @__PURE__ */ new Map();
776
794
  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.72",
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.72",
35
+ "@lmzhen/dsh-evolution-core": "^0.3.72",
36
+ "@lmzhen/dsh-evolution-plan-validator": "^0.3.72"
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.72",
46
+ "@lmzhen/dsh-evolution-policy": "^0.3.72"
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.72",
58
+ "@lmzhen/dsh-evolution-core": "^0.3.72",
59
+ "@lmzhen/dsh-evolution-curator": "^0.3.72",
60
+ "@lmzhen/dsh-evolution-plan-validator": "^0.3.72",
61
+ "@lmzhen/dsh-evolution-state": "^0.3.72"
62
62
  }
63
63
  }