@lmzhen/dsh-evolution-review 0.1.0-rc.2 → 0.1.0-rc.20

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
@@ -21,4 +21,10 @@ Independent of request-prefix construction. This package does not alter the asse
21
21
  ## Known Limitations and Deferred Work
22
22
 
23
23
 
24
- - - Review subagents inherit the host preset; Anchored Standard deployments rely on the default `skill_search`/`skill_load` allow-list.
24
+ - Review subagents inherit the host preset; Anchored Standard deployments rely on the default `skill_search`/`skill_load` allow-list.
25
+ - Review subagents run as `spawn` children on the deployment default preset rather than inheriting the parent agent's composition (`fork`): a fork child is always promoted by the Anchored Standard bootstrap and its narrowed resident catalog would drop the plain `skill` tool from the review allow-list.
26
+ - The review request text is redacted for credential-shaped patterns before it reaches the subagent, but redaction is pattern-based and best-effort, not a security boundary.
27
+
28
+ ## Configuration
29
+
30
+ `reviewProvider` selects the LLM provider for review subagents. When omitted, the subagent inherits the deployment default route instead of a hardcoded provider name. Model selection stays on the policy (`memoryReviewModel` / `skillReviewModel`).
package/lib/index.js CHANGED
@@ -1,8 +1,37 @@
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 { 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
+ //#region lib/types/redact.js
7
+ /**
8
+ * Review-input redaction. Review/curator subagents are the one place where a
9
+ * cross-session conversation snapshot leaves the owning session's context, so
10
+ * credential-shaped text is masked before it is sent. Redaction is best-effort
11
+ * and conservative: it targets well-known secret shapes and inline
12
+ * assignment patterns, never wholesale content.
13
+ */
14
+ const SECRET_PATTERNS = [
15
+ ["openai-style key", /sk-[A-Za-z0-9_-]{16,}/g],
16
+ ["aws access key", /AKIA[0-9A-Z]{16}/g],
17
+ ["github token", /gh[pousr]_[A-Za-z0-9]{20,}/g],
18
+ ["gitlab token", /glpat-[A-Za-z0-9_-]{16,}/g],
19
+ ["slack token", /xox[baprs]-[A-Za-z0-9-]{10,}/g],
20
+ ["jwt", /eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g],
21
+ ["bearer credential", /Bearer [A-Za-z0-9._~+/=\-]{16,}/g],
22
+ ["inline assignment", /(\b(?:token|api[_-]?key|secret|password|passwd)\b[\s]*[:=][\s]*[\"\x27]?)([A-Z0-9._~+/=\-]{12,})/gi]
23
+ ];
24
+ /**
25
+ * Mask credential-shaped text in a review request.
26
+ * @param text - the assembled review request text.
27
+ * @returns the text with matched secrets replaced by `<redacted>`.
28
+ */
29
+ function redactReviewSecrets(text) {
30
+ let out = text;
31
+ for (const [, pattern] of SECRET_PATTERNS) out = out.replace(pattern, (_match, p1) => p1 === void 0 ? "<redacted>" : `${p1}<redacted>`);
32
+ return out;
33
+ }
34
+ //#endregion
6
35
  //#region lib/types/index.js
7
36
  /**
8
37
  * Background review orchestration: signal gate → one-shot subagent → trusted plan execution.
@@ -13,8 +42,8 @@ const inject = ["agents", "tools"];
13
42
  const Config = z.object({
14
43
  reviewEnabled: z.boolean().default(true),
15
44
  reviewMode: z.string().default("subagent"),
16
- memoryInterval: z.number().default(10),
17
- skillInterval: z.number().default(10),
45
+ memoryInterval: z.number().default(DEFAULT_REVIEW_MEMORY_INTERVAL),
46
+ skillInterval: z.number().default(DEFAULT_REVIEW_SKILL_INTERVAL),
18
47
  reviewToolAllow: z.array(z.string()).default([
19
48
  "skill",
20
49
  "skill_search",
@@ -24,12 +53,17 @@ const Config = z.object({
24
53
  executionTimeoutMs: z.number().default(3e4),
25
54
  reviewContextMessages: z.number().default(60),
26
55
  reviewMessageChars: z.number().default(2e3),
27
- reviewMaxDepth: z.number().default(0)
56
+ reviewMaxDepth: z.number().default(0),
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)
28
60
  });
29
61
  function apply(ctx, rawConfig) {
30
62
  if (!verifyPromptBundle(PROMPT_BUNDLE)) throw new Error("dsh-evolution prompt bundle integrity check failed; refusing to schedule review work");
31
63
  const config = rawConfig;
32
64
  const turnStarts = /* @__PURE__ */ new Map();
65
+ const cumulativeToolCalls = /* @__PURE__ */ new Map();
66
+ const completionInjected = /* @__PURE__ */ new Set();
33
67
  const policy = () => ctx.get("evolutionPolicy")?.get();
34
68
  ctx.on("session/event", (session, event) => {
35
69
  if (event.type === "turn/start") turnStarts.set(session.id, session.seq - 1);
@@ -58,17 +92,50 @@ function apply(ctx, rawConfig) {
58
92
  substantiveMinAgentChars: snapshot?.substantiveMinAgentChars ?? 500
59
93
  });
60
94
  await stateService?.saveReviewState(session.id, state);
61
- if (!kind) return;
62
- if (!await trySubagentReview(session, agent, kind, signal)) agent.inject(createUserMessage({
95
+ if (kind) {
96
+ session.append("evolution/review-scheduled", {
97
+ kind,
98
+ toolCalls: signal.toolCalls,
99
+ userChars: signal.userChars,
100
+ assistantChars: signal.assistantChars
101
+ });
102
+ if (!await trySubagentReview(session, agent, kind, signal)) agent.inject(createUserMessage({
103
+ content: [{
104
+ type: "text",
105
+ text: reviewPrompt(kind)
106
+ }],
107
+ source: {
108
+ kind: "plugin",
109
+ plugin: "dsh-evolution-review",
110
+ form: "notice",
111
+ summary: "auto-review"
112
+ }
113
+ }));
114
+ return;
115
+ }
116
+ const trigger = config.skillReviewTrigger;
117
+ if (trigger !== "completion" && trigger !== "both") return;
118
+ if (completionInjected.has(session.id)) return;
119
+ const cumulative = (cumulativeToolCalls.get(session.id) ?? 0) + signal.toolCalls;
120
+ cumulativeToolCalls.set(session.id, cumulative);
121
+ if (!shouldCompletionReview(event.data.reason, cumulative, config.skillReviewCompletionMinToolCalls)) return;
122
+ completionInjected.add(session.id);
123
+ session.append("evolution/review-scheduled", {
124
+ kind: "skill",
125
+ toolCalls: signal.toolCalls,
126
+ userChars: signal.userChars,
127
+ assistantChars: signal.assistantChars
128
+ });
129
+ agent.inject(createUserMessage({
63
130
  content: [{
64
131
  type: "text",
65
- text: reviewPrompt(kind)
132
+ text: COMPLETION_SKILL_REVIEW_PROMPT
66
133
  }],
67
134
  source: {
68
135
  kind: "plugin",
69
136
  plugin: "dsh-evolution-review",
70
137
  form: "notice",
71
- summary: "auto-review"
138
+ summary: "completion review"
72
139
  }
73
140
  }));
74
141
  }
@@ -79,7 +146,9 @@ function apply(ctx, rawConfig) {
79
146
  try {
80
147
  const routingPolicy = ctx.get("evolutionPolicy");
81
148
  const model = kind === "memory" ? routingPolicy?.get().memoryReviewModel ?? "deepseek-v4-flash" : routingPolicy?.get().skillReviewModel ?? "deepseek-v4-pro";
82
- const reviewText = buildReviewRequest(session, kind, signal, config.reviewContextMessages, config.reviewMessageChars);
149
+ const reviewText = redactReviewSecrets(buildReviewRequest(session, kind, signal, config.reviewContextMessages, config.reviewMessageChars));
150
+ const agentOptions = { model };
151
+ if (config.reviewProvider) agentOptions.provider = config.reviewProvider;
83
152
  const run = await subagents.start("spawn", {
84
153
  label: "dsh-evolution-review",
85
154
  prompt: [{
@@ -89,10 +158,7 @@ function apply(ctx, rawConfig) {
89
158
  parent: agent,
90
159
  signal: AbortSignal.timeout(config.reviewTimeoutMs),
91
160
  maxDepth: config.reviewMaxDepth,
92
- agentOptions: {
93
- provider: "deepseek-official",
94
- model
95
- },
161
+ agentOptions,
96
162
  persona: reviewPrompt(kind),
97
163
  toolFilter: { allow: [...config.reviewToolAllow] },
98
164
  outputSchema: {
@@ -125,14 +191,17 @@ function apply(ctx, rawConfig) {
125
191
  maxUserChars: snapshot?.userChars ?? 1375,
126
192
  maxSkillContentChars: snapshot?.skillContentChars ?? 1e5
127
193
  });
194
+ const acceptedSkillOps = validation.accepted.skillOps ?? [];
195
+ const skippedUnread = filterUnreadSkillOps(acceptedSkillOps, collectReadSkillNames(session));
196
+ validation.accepted.skillOps = acceptedSkillOps;
128
197
  const actions = await executePlan(validation.accepted, agent);
129
- const evidenceQuotes = [...validation.accepted.memoryOps ?? [], ...validation.accepted.skillOps ?? []].reduce((total, op) => total + (Array.isArray(op.evidence) ? op.evidence.length : 0), 0);
198
+ const evidenceQuotes = [...validation.accepted.memoryOps ?? [], ...acceptedSkillOps].reduce((total, op) => total + (Array.isArray(op.evidence) ? op.evidence.length : 0), 0);
130
199
  session.append("evolution/plan-applied", {
131
200
  planId: randomUUID(),
132
201
  policyFingerprint,
133
202
  memoryApplied: actions.filter((action) => action.startsWith("Memory")).length,
134
203
  skillApplied: actions.filter((action) => action.startsWith("Skill ")).length,
135
- rejectedOps: validation.rejected.length,
204
+ rejectedOps: validation.rejected.length + skippedUnread,
136
205
  evidenceQuotes,
137
206
  estimatedInputChars: reviewText.length
138
207
  });
@@ -149,7 +218,8 @@ function apply(ctx, rawConfig) {
149
218
  }
150
219
  }));
151
220
  return true;
152
- } catch {
221
+ } catch (error) {
222
+ ctx.logger.warn(`dsh-evolution-review: subagent review failed: ${error instanceof Error ? error.message : String(error)}`);
153
223
  return false;
154
224
  }
155
225
  }
@@ -173,10 +243,11 @@ function apply(ctx, rawConfig) {
173
243
  ...op,
174
244
  evidence: op.evidence
175
245
  };
176
- if ((approval ? await runApproved("skill", `skill ${op.action ?? "patch"} ${op.name}`, {
246
+ const runnerArgs = {
177
247
  operation: args,
178
248
  origin: "background_review"
179
- }, args) : await executeSkillTool(parent, args))?.ok) actions.push(`Skill ${op.name} ${op.action ?? "patch"}`);
249
+ };
250
+ 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"}`);
180
251
  }
181
252
  return actions;
182
253
  async function runApproved(kind, summary, stored, runnerArgs) {
@@ -211,8 +282,50 @@ function apply(ctx, rawConfig) {
211
282
  }
212
283
  ctx.effect(() => () => {
213
284
  turnStarts.clear();
285
+ cumulativeToolCalls.clear();
286
+ completionInjected.clear();
214
287
  }, "dsh-evolution-review.cleanup");
215
288
  }
289
+ /** Completion-channel decision: task finished normally AND the session is proven long. */
290
+ function shouldCompletionReview(reason, sessionToolCalls, minToolCalls) {
291
+ return reason?.kind === "completed" && sessionToolCalls >= minToolCalls;
292
+ }
293
+ /** Skill names this session loaded (read-before-write source for the background review). */
294
+ function collectReadSkillNames(session) {
295
+ const names = /* @__PURE__ */ new Set();
296
+ for (const event of session.events) {
297
+ if (event.type !== "tool/call") continue;
298
+ if (event.data.name !== "skill" && event.data.name !== "skill_load") continue;
299
+ const raw = event.data.arguments;
300
+ let parsed = {};
301
+ if (typeof raw === "string") try {
302
+ parsed = JSON.parse(raw);
303
+ } catch {
304
+ continue;
305
+ }
306
+ else parsed = raw ?? {};
307
+ const name = typeof parsed.name === "string" ? parsed.name : typeof parsed.skill === "string" ? parsed.skill : "";
308
+ if (name) names.add(name);
309
+ }
310
+ return names;
311
+ }
312
+ /**
313
+ * Drop patch/update ops whose target was not read this session, in place.
314
+ * Create is exempt (no read required to author a new skill). Returns the count
315
+ * of dropped ops so the plan event can report them as rejected.
316
+ */
317
+ function filterUnreadSkillOps(ops, readNames) {
318
+ let dropped = 0;
319
+ for (let index = ops.length - 1; index >= 0; index -= 1) {
320
+ const op = ops[index];
321
+ if (!op) continue;
322
+ if ((op.action === "patch" || op.action === "update") && op.name && !readNames.has(op.name)) {
323
+ ops.splice(index, 1);
324
+ dropped += 1;
325
+ }
326
+ }
327
+ return dropped;
328
+ }
216
329
  function fingerprintPolicy(snapshot) {
217
330
  try {
218
331
  return createHash("sha256").update(JSON.stringify(snapshot)).digest("hex").slice(0, 12);
@@ -236,4 +349,4 @@ function buildReviewRequest(session, kind, signal, maxMessages, maxMessageChars)
236
349
  ].join("\n");
237
350
  }
238
351
  //#endregion
239
- export { Config, apply, inject, name };
352
+ 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 {
@@ -18,7 +19,28 @@ export interface Config {
18
19
  reviewContextMessages?: number;
19
20
  reviewMessageChars?: number;
20
21
  reviewMaxDepth?: number;
22
+ /** LLM provider for review subagents. Omit to inherit the deployment default route. */
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;
21
28
  }
22
29
  export declare const Config: z<Config>;
23
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;
24
46
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Review-input redaction. Review/curator subagents are the one place where a
3
+ * cross-session conversation snapshot leaves the owning session's context, so
4
+ * credential-shaped text is masked before it is sent. Redaction is best-effort
5
+ * and conservative: it targets well-known secret shapes and inline
6
+ * assignment patterns, never wholesale content.
7
+ */
8
+ /**
9
+ * Mask credential-shaped text in a review request.
10
+ * @param text - the assembled review request text.
11
+ * @returns the text with matched secrets replaced by `<redacted>`.
12
+ */
13
+ export declare function redactReviewSecrets(text: string): string;
14
+ //# sourceMappingURL=redact.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.2",
4
+ "version": "0.1.0-rc.20",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -33,7 +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.2"
36
+ "@lmzhen/dsh-evolution-core": "^0.1.0-rc.20",
37
+ "@lmzhen/dsh-evolution-plan-validator": "^0.1.0-rc.20"
37
38
  },
38
39
  "peerDependencies": {
39
40
  "@deepseek-ai/cordis": "^4.0.1",
@@ -42,8 +43,7 @@
42
43
  "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
43
44
  "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
44
45
  "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
45
- "@lmzhen/dsh-evolution-plan-validator": "^0.1.0-rc.2",
46
- "@lmzhen/dsh-evolution-state": "^0.1.0-rc.2"
46
+ "@lmzhen/dsh-evolution-state": "^0.1.0-rc.20"
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.2",
56
- "@lmzhen/dsh-evolution-plan-validator": "^0.1.0-rc.2",
57
- "@lmzhen/dsh-evolution-state": "^0.1.0-rc.2"
55
+ "@lmzhen/dsh-evolution-core": "^0.1.0-rc.20",
56
+ "@lmzhen/dsh-evolution-plan-validator": "^0.1.0-rc.20",
57
+ "@lmzhen/dsh-evolution-state": "^0.1.0-rc.20"
58
58
  }
59
59
  }