@lmzhen/dsh-evolution-review 0.1.0-rc.13 → 0.1.0-rc.15

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 { createHash, randomUUID } from "node:crypto";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { CallId, createUserMessage } from "@deepseek-ai/dsh-llm";
4
- import { DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, PROMPT_BUNDLE, advanceReview, foldTurn, reviewPrompt, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
4
+ import { COMPLETION_SKILL_REVIEW_PROMPT, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, PROMPT_BUNDLE, advanceReview, foldTurn, reviewPrompt, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
5
5
  import { validateEvolutionPlan } from "@lmzhen/dsh-evolution-plan-validator";
6
6
  //#region lib/types/redact.js
7
7
  /**
@@ -54,12 +54,16 @@ const Config = z.object({
54
54
  reviewContextMessages: z.number().default(60),
55
55
  reviewMessageChars: z.number().default(2e3),
56
56
  reviewMaxDepth: z.number().default(0),
57
- reviewProvider: z.string()
57
+ reviewProvider: z.string(),
58
+ skillReviewTrigger: z.string().default(DEFAULT_SKILL_REVIEW_TRIGGER),
59
+ skillReviewCompletionMinToolCalls: z.number().default(DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS)
58
60
  });
59
61
  function apply(ctx, rawConfig) {
60
62
  if (!verifyPromptBundle(PROMPT_BUNDLE)) throw new Error("dsh-evolution prompt bundle integrity check failed; refusing to schedule review work");
61
63
  const config = rawConfig;
62
64
  const turnStarts = /* @__PURE__ */ new Map();
65
+ const cumulativeToolCalls = /* @__PURE__ */ new Map();
66
+ const completionInjected = /* @__PURE__ */ new Set();
63
67
  const policy = () => ctx.get("evolutionPolicy")?.get();
64
68
  ctx.on("session/event", (session, event) => {
65
69
  if (event.type === "turn/start") turnStarts.set(session.id, session.seq - 1);
@@ -88,17 +92,38 @@ function apply(ctx, rawConfig) {
88
92
  substantiveMinAgentChars: snapshot?.substantiveMinAgentChars ?? 500
89
93
  });
90
94
  await stateService?.saveReviewState(session.id, state);
91
- if (!kind) return;
92
- if (!await trySubagentReview(session, agent, kind, signal)) agent.inject(createUserMessage({
95
+ if (kind) {
96
+ if (!await trySubagentReview(session, agent, kind, signal)) agent.inject(createUserMessage({
97
+ content: [{
98
+ type: "text",
99
+ text: reviewPrompt(kind)
100
+ }],
101
+ source: {
102
+ kind: "plugin",
103
+ plugin: "dsh-evolution-review",
104
+ form: "notice",
105
+ summary: "auto-review"
106
+ }
107
+ }));
108
+ return;
109
+ }
110
+ const trigger = config.skillReviewTrigger;
111
+ if (trigger !== "completion" && trigger !== "both") return;
112
+ if (completionInjected.has(session.id)) return;
113
+ const cumulative = (cumulativeToolCalls.get(session.id) ?? 0) + signal.toolCalls;
114
+ cumulativeToolCalls.set(session.id, cumulative);
115
+ if (!shouldCompletionReview(event.data.reason, cumulative, config.skillReviewCompletionMinToolCalls)) return;
116
+ completionInjected.add(session.id);
117
+ agent.inject(createUserMessage({
93
118
  content: [{
94
119
  type: "text",
95
- text: reviewPrompt(kind)
120
+ text: COMPLETION_SKILL_REVIEW_PROMPT
96
121
  }],
97
122
  source: {
98
123
  kind: "plugin",
99
124
  plugin: "dsh-evolution-review",
100
125
  form: "notice",
101
- summary: "auto-review"
126
+ summary: "completion review"
102
127
  }
103
128
  }));
104
129
  }
@@ -154,14 +179,17 @@ function apply(ctx, rawConfig) {
154
179
  maxUserChars: snapshot?.userChars ?? 1375,
155
180
  maxSkillContentChars: snapshot?.skillContentChars ?? 1e5
156
181
  });
182
+ const acceptedSkillOps = validation.accepted.skillOps ?? [];
183
+ const skippedUnread = filterUnreadSkillOps(acceptedSkillOps, collectReadSkillNames(session));
184
+ validation.accepted.skillOps = acceptedSkillOps;
157
185
  const actions = await executePlan(validation.accepted, agent);
158
- const evidenceQuotes = [...validation.accepted.memoryOps ?? [], ...validation.accepted.skillOps ?? []].reduce((total, op) => total + (Array.isArray(op.evidence) ? op.evidence.length : 0), 0);
186
+ const evidenceQuotes = [...validation.accepted.memoryOps ?? [], ...acceptedSkillOps].reduce((total, op) => total + (Array.isArray(op.evidence) ? op.evidence.length : 0), 0);
159
187
  session.append("evolution/plan-applied", {
160
188
  planId: randomUUID(),
161
189
  policyFingerprint,
162
190
  memoryApplied: actions.filter((action) => action.startsWith("Memory")).length,
163
191
  skillApplied: actions.filter((action) => action.startsWith("Skill ")).length,
164
- rejectedOps: validation.rejected.length,
192
+ rejectedOps: validation.rejected.length + skippedUnread,
165
193
  evidenceQuotes,
166
194
  estimatedInputChars: reviewText.length
167
195
  });
@@ -203,10 +231,11 @@ function apply(ctx, rawConfig) {
203
231
  ...op,
204
232
  evidence: op.evidence
205
233
  };
206
- if ((approval ? await runApproved("skill", `skill ${op.action ?? "patch"} ${op.name}`, {
234
+ const runnerArgs = {
207
235
  operation: args,
208
236
  origin: "background_review"
209
- }, args) : await executeSkillTool(parent, args))?.ok) actions.push(`Skill ${op.name} ${op.action ?? "patch"}`);
237
+ };
238
+ if ((approval ? await runApproved("skill", `skill ${op.action ?? "patch"} ${op.name}`, runnerArgs, runnerArgs) : await executeSkillTool(parent, args))?.ok) actions.push(`Skill ${op.name} ${op.action ?? "patch"}`);
210
239
  }
211
240
  return actions;
212
241
  async function runApproved(kind, summary, stored, runnerArgs) {
@@ -241,8 +270,50 @@ function apply(ctx, rawConfig) {
241
270
  }
242
271
  ctx.effect(() => () => {
243
272
  turnStarts.clear();
273
+ cumulativeToolCalls.clear();
274
+ completionInjected.clear();
244
275
  }, "dsh-evolution-review.cleanup");
245
276
  }
277
+ /** Completion-channel decision: task finished normally AND the session is proven long. */
278
+ function shouldCompletionReview(reason, sessionToolCalls, minToolCalls) {
279
+ return reason?.kind === "completed" && sessionToolCalls >= minToolCalls;
280
+ }
281
+ /** Skill names this session loaded (read-before-write source for the background review). */
282
+ function collectReadSkillNames(session) {
283
+ const names = /* @__PURE__ */ new Set();
284
+ for (const event of session.events) {
285
+ if (event.type !== "tool/call") continue;
286
+ if (event.data.name !== "skill" && event.data.name !== "skill_load") continue;
287
+ const raw = event.data.arguments;
288
+ let parsed = {};
289
+ if (typeof raw === "string") try {
290
+ parsed = JSON.parse(raw);
291
+ } catch {
292
+ continue;
293
+ }
294
+ else parsed = raw ?? {};
295
+ const name = typeof parsed.name === "string" ? parsed.name : typeof parsed.skill === "string" ? parsed.skill : "";
296
+ if (name) names.add(name);
297
+ }
298
+ return names;
299
+ }
300
+ /**
301
+ * Drop patch/update ops whose target was not read this session, in place.
302
+ * Create is exempt (no read required to author a new skill). Returns the count
303
+ * of dropped ops so the plan event can report them as rejected.
304
+ */
305
+ function filterUnreadSkillOps(ops, readNames) {
306
+ let dropped = 0;
307
+ for (let index = ops.length - 1; index >= 0; index -= 1) {
308
+ const op = ops[index];
309
+ if (!op) continue;
310
+ if ((op.action === "patch" || op.action === "update") && op.name && !readNames.has(op.name)) {
311
+ ops.splice(index, 1);
312
+ dropped += 1;
313
+ }
314
+ }
315
+ return dropped;
316
+ }
246
317
  function fingerprintPolicy(snapshot) {
247
318
  try {
248
319
  return createHash("sha256").update(JSON.stringify(snapshot)).digest("hex").slice(0, 12);
@@ -266,4 +337,4 @@ function buildReviewRequest(session, kind, signal, maxMessages, maxMessageChars)
266
337
  ].join("\n");
267
338
  }
268
339
  //#endregion
269
- export { Config, apply, inject, name };
340
+ export { Config, apply, collectReadSkillNames, filterUnreadSkillOps, inject, name, shouldCompletionReview };
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import type { Context } from '@deepseek-ai/cordis';
6
6
  import z from '@deepseek-ai/schemastery';
7
+ import type { Session } from '@deepseek-ai/dsh-session';
7
8
  export declare const name = "evolution-review";
8
9
  export declare const inject: string[];
9
10
  export interface Config {
@@ -20,7 +21,26 @@ export interface Config {
20
21
  reviewMaxDepth?: number;
21
22
  /** LLM provider for review subagents. Omit to inherit the deployment default route. */
22
23
  reviewProvider?: string;
24
+ /** Skill-review trigger: cadence (interval) | completion (once after a proven-long task) | both. */
25
+ skillReviewTrigger?: string;
26
+ /** Cumulative session tool calls before a session counts as proven-long for the completion channel. */
27
+ skillReviewCompletionMinToolCalls?: number;
23
28
  }
24
29
  export declare const Config: z<Config>;
25
30
  export declare function apply(ctx: Context, rawConfig: Config): void;
31
+ /** Completion-channel decision: task finished normally AND the session is proven long. */
32
+ export declare function shouldCompletionReview(reason: {
33
+ kind?: string;
34
+ } | undefined, sessionToolCalls: number, minToolCalls: number): boolean;
35
+ /** Skill names this session loaded (read-before-write source for the background review). */
36
+ export declare function collectReadSkillNames(session: Session): Set<string>;
37
+ /**
38
+ * Drop patch/update ops whose target was not read this session, in place.
39
+ * Create is exempt (no read required to author a new skill). Returns the count
40
+ * of dropped ops so the plan event can report them as rejected.
41
+ */
42
+ export declare function filterUnreadSkillOps(ops: Array<{
43
+ action?: string;
44
+ name?: string;
45
+ }>, readNames: ReadonlySet<string>): number;
26
46
  //# sourceMappingURL=index.d.ts.map
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.1.0-rc.13",
4
+ "version": "0.1.0-rc.15",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -33,8 +33,8 @@
33
33
  "license": "MIT",
34
34
  "dependencies": {
35
35
  "@deepseek-ai/schemastery": "^3.18.1",
36
- "@lmzhen/dsh-evolution-core": "^0.1.0-rc.13",
37
- "@lmzhen/dsh-evolution-plan-validator": "^0.1.0-rc.13"
36
+ "@lmzhen/dsh-evolution-core": "^0.1.0-rc.15",
37
+ "@lmzhen/dsh-evolution-plan-validator": "^0.1.0-rc.15"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@deepseek-ai/cordis": "^4.0.1",
@@ -43,7 +43,7 @@
43
43
  "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
44
44
  "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
45
45
  "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
46
- "@lmzhen/dsh-evolution-state": "^0.1.0-rc.13"
46
+ "@lmzhen/dsh-evolution-state": "^0.1.0-rc.15"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
@@ -52,8 +52,8 @@
52
52
  "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
53
53
  "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
54
54
  "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
55
- "@lmzhen/dsh-evolution-core": "^0.1.0-rc.13",
56
- "@lmzhen/dsh-evolution-plan-validator": "^0.1.0-rc.13",
57
- "@lmzhen/dsh-evolution-state": "^0.1.0-rc.13"
55
+ "@lmzhen/dsh-evolution-core": "^0.1.0-rc.15",
56
+ "@lmzhen/dsh-evolution-plan-validator": "^0.1.0-rc.15",
57
+ "@lmzhen/dsh-evolution-state": "^0.1.0-rc.15"
58
58
  }
59
59
  }