@lmzhen/dsh-evolution-review 0.1.0-rc.4 → 0.1.0-rc.41
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 +7 -1
- package/lib/index.js +225 -43
- package/lib/types/index.d.ts +30 -1
- package/lib/types/redact.d.ts +14 -0
- package/package.json +7 -7
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
|
-
-
|
|
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
|
-
import {
|
|
4
|
-
import { PROMPT_BUNDLE, advanceReview, foldTurn, reviewPrompt, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
|
|
3
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
4
|
+
import { COMPLETION_SKILL_REVIEW_PROMPT, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_USER_CHAR_LIMIT, PROMPT_BUNDLE, SkillLibrary, advanceReview, evolutionIoAdapter, 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,23 +42,24 @@ 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(
|
|
17
|
-
skillInterval: z.number().default(
|
|
18
|
-
reviewToolAllow: z.array(z.string()).default([
|
|
19
|
-
"skill",
|
|
20
|
-
"skill_search",
|
|
21
|
-
"skill_load"
|
|
22
|
-
]),
|
|
45
|
+
memoryInterval: z.number().default(DEFAULT_REVIEW_MEMORY_INTERVAL),
|
|
46
|
+
skillInterval: z.number().default(DEFAULT_REVIEW_SKILL_INTERVAL),
|
|
47
|
+
reviewToolAllow: z.array(z.string()).default(["skill"]),
|
|
23
48
|
reviewTimeoutMs: z.number().default(12e4),
|
|
24
49
|
executionTimeoutMs: z.number().default(3e4),
|
|
25
50
|
reviewContextMessages: z.number().default(60),
|
|
26
51
|
reviewMessageChars: z.number().default(2e3),
|
|
27
|
-
reviewMaxDepth: z.number().default(0)
|
|
52
|
+
reviewMaxDepth: z.number().default(0),
|
|
53
|
+
reviewProvider: z.string(),
|
|
54
|
+
skillReviewTrigger: z.string().default(DEFAULT_SKILL_REVIEW_TRIGGER),
|
|
55
|
+
skillReviewCompletionMinToolCalls: z.number().default(DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS)
|
|
28
56
|
});
|
|
29
57
|
function apply(ctx, rawConfig) {
|
|
30
58
|
if (!verifyPromptBundle(PROMPT_BUNDLE)) throw new Error("dsh-evolution prompt bundle integrity check failed; refusing to schedule review work");
|
|
31
59
|
const config = rawConfig;
|
|
32
60
|
const turnStarts = /* @__PURE__ */ new Map();
|
|
61
|
+
const cumulativeToolCalls = /* @__PURE__ */ new Map();
|
|
62
|
+
const completionInjected = /* @__PURE__ */ new Set();
|
|
33
63
|
const policy = () => ctx.get("evolutionPolicy")?.get();
|
|
34
64
|
ctx.on("session/event", (session, event) => {
|
|
35
65
|
if (event.type === "turn/start") turnStarts.set(session.id, session.seq - 1);
|
|
@@ -58,17 +88,50 @@ function apply(ctx, rawConfig) {
|
|
|
58
88
|
substantiveMinAgentChars: snapshot?.substantiveMinAgentChars ?? 500
|
|
59
89
|
});
|
|
60
90
|
await stateService?.saveReviewState(session.id, state);
|
|
61
|
-
|
|
62
|
-
|
|
91
|
+
const cumulative = (cumulativeToolCalls.get(session.id) ?? 0) + signal.toolCalls;
|
|
92
|
+
cumulativeToolCalls.set(session.id, cumulative);
|
|
93
|
+
if (kind) {
|
|
94
|
+
session.append("evolution/review-scheduled", {
|
|
95
|
+
kind,
|
|
96
|
+
toolCalls: signal.toolCalls,
|
|
97
|
+
userChars: signal.userChars,
|
|
98
|
+
assistantChars: signal.assistantChars
|
|
99
|
+
});
|
|
100
|
+
if (!await trySubagentReview(session, agent, kind, signal)) agent.inject(createUserMessage({
|
|
101
|
+
content: [{
|
|
102
|
+
type: "text",
|
|
103
|
+
text: reviewPrompt(kind)
|
|
104
|
+
}],
|
|
105
|
+
source: {
|
|
106
|
+
kind: "plugin",
|
|
107
|
+
plugin: "dsh-evolution-review",
|
|
108
|
+
form: "notice",
|
|
109
|
+
summary: "auto-review"
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const trigger = config.skillReviewTrigger;
|
|
115
|
+
if (trigger !== "completion" && trigger !== "both") return;
|
|
116
|
+
if (completionInjected.has(session.id)) return;
|
|
117
|
+
if (!shouldCompletionReview(event.data.reason, cumulative, config.skillReviewCompletionMinToolCalls)) return;
|
|
118
|
+
completionInjected.add(session.id);
|
|
119
|
+
session.append("evolution/review-scheduled", {
|
|
120
|
+
kind: "skill",
|
|
121
|
+
toolCalls: signal.toolCalls,
|
|
122
|
+
userChars: signal.userChars,
|
|
123
|
+
assistantChars: signal.assistantChars
|
|
124
|
+
});
|
|
125
|
+
agent.inject(createUserMessage({
|
|
63
126
|
content: [{
|
|
64
127
|
type: "text",
|
|
65
|
-
text:
|
|
128
|
+
text: COMPLETION_SKILL_REVIEW_PROMPT
|
|
66
129
|
}],
|
|
67
130
|
source: {
|
|
68
131
|
kind: "plugin",
|
|
69
132
|
plugin: "dsh-evolution-review",
|
|
70
133
|
form: "notice",
|
|
71
|
-
summary: "
|
|
134
|
+
summary: "completion review"
|
|
72
135
|
}
|
|
73
136
|
}));
|
|
74
137
|
}
|
|
@@ -79,7 +142,9 @@ function apply(ctx, rawConfig) {
|
|
|
79
142
|
try {
|
|
80
143
|
const routingPolicy = ctx.get("evolutionPolicy");
|
|
81
144
|
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);
|
|
145
|
+
const reviewText = redactReviewSecrets(buildReviewRequest(session, kind, signal, config.reviewContextMessages, config.reviewMessageChars));
|
|
146
|
+
const agentOptions = { model };
|
|
147
|
+
if (config.reviewProvider) agentOptions.provider = config.reviewProvider;
|
|
83
148
|
const run = await subagents.start("spawn", {
|
|
84
149
|
label: "dsh-evolution-review",
|
|
85
150
|
prompt: [{
|
|
@@ -89,10 +154,7 @@ function apply(ctx, rawConfig) {
|
|
|
89
154
|
parent: agent,
|
|
90
155
|
signal: AbortSignal.timeout(config.reviewTimeoutMs),
|
|
91
156
|
maxDepth: config.reviewMaxDepth,
|
|
92
|
-
agentOptions
|
|
93
|
-
provider: "deepseek-official",
|
|
94
|
-
model
|
|
95
|
-
},
|
|
157
|
+
agentOptions,
|
|
96
158
|
persona: reviewPrompt(kind),
|
|
97
159
|
toolFilter: { allow: [...config.reviewToolAllow] },
|
|
98
160
|
outputSchema: {
|
|
@@ -111,6 +173,7 @@ function apply(ctx, rawConfig) {
|
|
|
111
173
|
}
|
|
112
174
|
}
|
|
113
175
|
});
|
|
176
|
+
const childReads = run.localAgent ? collectReadSkillNames(run.localAgent.session) : /* @__PURE__ */ new Set();
|
|
114
177
|
const result = await run.result;
|
|
115
178
|
await run.dispose();
|
|
116
179
|
if (!result.structured) return true;
|
|
@@ -119,20 +182,23 @@ function apply(ctx, rawConfig) {
|
|
|
119
182
|
const policyFingerprint = fingerprintPolicy(snapshot);
|
|
120
183
|
const validation = validateEvolutionPlan(plan, {
|
|
121
184
|
sessionSeq: session.seq - 1,
|
|
122
|
-
maxOpsPerPlan: snapshot?.maxOpsPerPlan ??
|
|
185
|
+
maxOpsPerPlan: snapshot?.maxOpsPerPlan ?? DEFAULT_MAX_OPS_PER_PLAN,
|
|
123
186
|
protectedSkillNames: new Set(snapshot?.protectedSkillNames ?? []),
|
|
124
|
-
maxMemoryChars: snapshot?.memoryChars ??
|
|
125
|
-
maxUserChars: snapshot?.userChars ??
|
|
126
|
-
maxSkillContentChars: snapshot?.skillContentChars ??
|
|
187
|
+
maxMemoryChars: snapshot?.memoryChars ?? DEFAULT_MEMORY_CHAR_LIMIT,
|
|
188
|
+
maxUserChars: snapshot?.userChars ?? DEFAULT_USER_CHAR_LIMIT,
|
|
189
|
+
maxSkillContentChars: snapshot?.skillContentChars ?? DEFAULT_SKILL_CONTENT_CHARS
|
|
127
190
|
});
|
|
128
|
-
const
|
|
129
|
-
const
|
|
191
|
+
const acceptedSkillOps = validation.accepted.skillOps ?? [];
|
|
192
|
+
const skippedUnread = filterUnreadSkillOps(acceptedSkillOps, new Set([...collectReadSkillNames(session), ...childReads]));
|
|
193
|
+
validation.accepted.skillOps = acceptedSkillOps;
|
|
194
|
+
const actions = await executePlan(validation.accepted);
|
|
195
|
+
const evidenceQuotes = [...validation.accepted.memoryOps ?? [], ...acceptedSkillOps].reduce((total, op) => total + (Array.isArray(op.evidence) ? op.evidence.length : 0), 0);
|
|
130
196
|
session.append("evolution/plan-applied", {
|
|
131
197
|
planId: randomUUID(),
|
|
132
198
|
policyFingerprint,
|
|
133
199
|
memoryApplied: actions.filter((action) => action.startsWith("Memory")).length,
|
|
134
200
|
skillApplied: actions.filter((action) => action.startsWith("Skill ")).length,
|
|
135
|
-
rejectedOps: validation.rejected.length,
|
|
201
|
+
rejectedOps: validation.rejected.length + skippedUnread,
|
|
136
202
|
evidenceQuotes,
|
|
137
203
|
estimatedInputChars: reviewText.length
|
|
138
204
|
});
|
|
@@ -149,11 +215,12 @@ function apply(ctx, rawConfig) {
|
|
|
149
215
|
}
|
|
150
216
|
}));
|
|
151
217
|
return true;
|
|
152
|
-
} catch {
|
|
218
|
+
} catch (error) {
|
|
219
|
+
ctx.logger.warn(`dsh-evolution-review: subagent review failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
153
220
|
return false;
|
|
154
221
|
}
|
|
155
222
|
}
|
|
156
|
-
async function executePlan(plan
|
|
223
|
+
async function executePlan(plan) {
|
|
157
224
|
const memory = ctx.get("memory");
|
|
158
225
|
const approval = ctx.get("evolutionApproval");
|
|
159
226
|
const actions = [];
|
|
@@ -173,10 +240,11 @@ function apply(ctx, rawConfig) {
|
|
|
173
240
|
...op,
|
|
174
241
|
evidence: op.evidence
|
|
175
242
|
};
|
|
176
|
-
|
|
243
|
+
const runnerArgs = {
|
|
177
244
|
operation: args,
|
|
178
245
|
origin: "background_review"
|
|
179
|
-
}
|
|
246
|
+
};
|
|
247
|
+
if ((approval ? await runApproved("skill", `skill ${op.action ?? "patch"} ${op.name}`, runnerArgs, runnerArgs) : await executeSkillDirect(args))?.ok) actions.push(`Skill ${op.name} ${op.action ?? "patch"}`);
|
|
180
248
|
}
|
|
181
249
|
return actions;
|
|
182
250
|
async function runApproved(kind, summary, stored, runnerArgs) {
|
|
@@ -191,28 +259,124 @@ function apply(ctx, rawConfig) {
|
|
|
191
259
|
ok: false,
|
|
192
260
|
message: decision.message
|
|
193
261
|
};
|
|
262
|
+
if (approval.isEnabled === false) return await runnerDirect(kind, runnerArgs);
|
|
194
263
|
return await approval.run(kind, runnerArgs);
|
|
195
264
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
265
|
+
/** Direct execution for the approval-disabled case (parallel to executeSkillDirect). */
|
|
266
|
+
async function runnerDirect(kind, args) {
|
|
267
|
+
if (kind === "memory") {
|
|
268
|
+
const memory = ctx.get("memory");
|
|
269
|
+
const op = args;
|
|
270
|
+
if (!memory?.applyBatch) return void 0;
|
|
271
|
+
return await memory.applyBatch(op.target === "user" ? "user" : "memory", [{
|
|
272
|
+
action: op.action ?? "add",
|
|
273
|
+
facts: op.facts ?? op.content,
|
|
274
|
+
old_text: op.old_text
|
|
275
|
+
}]);
|
|
276
|
+
}
|
|
277
|
+
const wrapped = args ?? {};
|
|
278
|
+
if (!wrapped.operation) return void 0;
|
|
279
|
+
return await executeSkillDirect(wrapped.operation);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Approval-disabled path: execute the skill op through SkillLibrary with an
|
|
283
|
+
* EXPLICIT background_review origin. Going through ctx.tools.execute would
|
|
284
|
+
* make tool-skill-manage infer origin from the parent agent's header
|
|
285
|
+
* (not 'subagent'), silently escaping the .hermes-managed marker and the
|
|
286
|
+
* pinned write guard.
|
|
287
|
+
*/
|
|
288
|
+
async function executeSkillDirect(skillArgs) {
|
|
289
|
+
const io = ctx.get("evolutionIo");
|
|
290
|
+
if (!io) return {
|
|
291
|
+
ok: false,
|
|
292
|
+
message: "evolution-io service not mounted"
|
|
293
|
+
};
|
|
294
|
+
const library = new SkillLibrary(void 0, evolutionIoAdapter(() => io.provider()));
|
|
295
|
+
const op = skillArgs;
|
|
296
|
+
const name = op.name ?? "";
|
|
297
|
+
const origin = "background_review";
|
|
298
|
+
if (op.action === "create") {
|
|
299
|
+
const created = await library.create(name, op.content ?? "", origin);
|
|
300
|
+
if (created.ok) await ctx.get("skillUsage")?.markAgentCreated?.(name);
|
|
301
|
+
return created;
|
|
302
|
+
}
|
|
303
|
+
if (op.action === "edit" || op.action === "update") return await library.update(name, op.content ?? "", origin);
|
|
304
|
+
if (op.action === "patch") return await library.patch(name, op.old_string ?? "", op.new_string ?? "", op.file_path ?? "", false, origin);
|
|
305
|
+
if (op.action === "delete") {
|
|
306
|
+
const into = (op.absorbed_into ?? "").trim();
|
|
307
|
+
if (!into || !await library.read(into)) return {
|
|
308
|
+
ok: false,
|
|
309
|
+
message: "delete requires an existing absorbed_into target"
|
|
310
|
+
};
|
|
311
|
+
const archived = await library.archive(name, { absorbedInto: into });
|
|
312
|
+
if (archived.ok) await ctx.get("skillUsage")?.markArchived?.(name);
|
|
313
|
+
return archived;
|
|
314
|
+
}
|
|
315
|
+
if (op.action === "write_file") return await library.writeSupportFile(name, op.file_path ?? "", op.file_content ?? op.content ?? "", origin);
|
|
316
|
+
if (op.action === "remove_file") return await library.removeSupportFile(name, op.file_path ?? "", origin);
|
|
317
|
+
return {
|
|
204
318
|
ok: false,
|
|
205
|
-
message: "
|
|
206
|
-
} : {
|
|
207
|
-
ok: true,
|
|
208
|
-
message: "skill_manage executed"
|
|
319
|
+
message: `Unknown skill action "${op.action ?? ""}"`
|
|
209
320
|
};
|
|
210
321
|
}
|
|
211
322
|
}
|
|
212
323
|
ctx.effect(() => () => {
|
|
213
324
|
turnStarts.clear();
|
|
325
|
+
cumulativeToolCalls.clear();
|
|
326
|
+
completionInjected.clear();
|
|
214
327
|
}, "dsh-evolution-review.cleanup");
|
|
215
328
|
}
|
|
329
|
+
/** Completion-channel decision: task finished normally AND the session is proven long. */
|
|
330
|
+
function shouldCompletionReview(reason, sessionToolCalls, minToolCalls) {
|
|
331
|
+
return reason?.kind === "completed" && sessionToolCalls >= minToolCalls;
|
|
332
|
+
}
|
|
333
|
+
/** Skill names this session loaded (read-before-write source for the background review). */
|
|
334
|
+
function collectReadSkillNames(session) {
|
|
335
|
+
const names = /* @__PURE__ */ new Set();
|
|
336
|
+
for (const event of session.events) {
|
|
337
|
+
if (event.type !== "tool/call") continue;
|
|
338
|
+
if (event.data.name !== "skill" && event.data.name !== "skill_load") continue;
|
|
339
|
+
const raw = event.data.arguments;
|
|
340
|
+
let parsed = {};
|
|
341
|
+
if (typeof raw === "string") try {
|
|
342
|
+
parsed = JSON.parse(raw);
|
|
343
|
+
} catch {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
else parsed = raw ?? {};
|
|
347
|
+
const name = typeof parsed.name === "string" ? parsed.name : typeof parsed.skill === "string" ? parsed.skill : "";
|
|
348
|
+
if (name) names.add(name);
|
|
349
|
+
}
|
|
350
|
+
return names;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Drop mutating ops whose target was not read this session, in place.
|
|
354
|
+
* Create is exempt (no read required to author a new skill). Covers the same
|
|
355
|
+
* mutating surface Hermes guards (edit/patch/write_file/remove_file), so a
|
|
356
|
+
* background review cannot blind-touch support files or edits of skills it
|
|
357
|
+
* never loaded. Returns the count of dropped ops so the plan event can report
|
|
358
|
+
* them as rejected.
|
|
359
|
+
*/
|
|
360
|
+
function filterUnreadSkillOps(ops, readNames) {
|
|
361
|
+
const READ_REQUIRED = [
|
|
362
|
+
"edit",
|
|
363
|
+
"update",
|
|
364
|
+
"patch",
|
|
365
|
+
"delete",
|
|
366
|
+
"write_file",
|
|
367
|
+
"remove_file"
|
|
368
|
+
];
|
|
369
|
+
let dropped = 0;
|
|
370
|
+
for (let index = ops.length - 1; index >= 0; index -= 1) {
|
|
371
|
+
const op = ops[index];
|
|
372
|
+
if (!op) continue;
|
|
373
|
+
if (op.action !== void 0 && READ_REQUIRED.includes(op.action) && op.name && !readNames.has(op.name)) {
|
|
374
|
+
ops.splice(index, 1);
|
|
375
|
+
dropped += 1;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return dropped;
|
|
379
|
+
}
|
|
216
380
|
function fingerprintPolicy(snapshot) {
|
|
217
381
|
try {
|
|
218
382
|
return createHash("sha256").update(JSON.stringify(snapshot)).digest("hex").slice(0, 12);
|
|
@@ -227,13 +391,31 @@ function buildReviewRequest(session, kind, signal, maxMessages, maxMessageChars)
|
|
|
227
391
|
const text = message.content.map((block) => block.type === "text" ? block.text : "").join(" ").trim();
|
|
228
392
|
if (text) messages.push(`${message.role.toUpperCase()}: ${text.slice(0, maxMessageChars)}`);
|
|
229
393
|
}
|
|
394
|
+
const toolLines = [];
|
|
395
|
+
const events = session.events;
|
|
396
|
+
for (let index = events.length - 1; index >= 0 && toolLines.length < 12; index -= 1) {
|
|
397
|
+
const event = events[index];
|
|
398
|
+
if (event?.type === "tool/call") {
|
|
399
|
+
const data = event.data;
|
|
400
|
+
const argsRaw = typeof data?.arguments === "string" ? data.arguments : JSON.stringify(data?.arguments ?? {});
|
|
401
|
+
toolLines.push(`[call] ${data?.name ?? "?"} ${argsRaw.slice(0, 500)}`);
|
|
402
|
+
} else if (event?.type === "tool/result") {
|
|
403
|
+
const data = event.data;
|
|
404
|
+
const output = typeof data?.output === "string" ? data.output : "";
|
|
405
|
+
const failure = data?.error ? " [ERROR]" : "";
|
|
406
|
+
toolLines.push(`[result]${failure} ${output.slice(0, 500)}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
toolLines.reverse();
|
|
230
410
|
return [
|
|
231
411
|
`Review kind: ${kind}`,
|
|
232
412
|
`Signals: ${signal.toolCalls} tool calls, ${signal.userChars} user chars, ${signal.assistantChars} assistant chars.`,
|
|
413
|
+
`Recent tool activity (${toolLines.length}):`,
|
|
414
|
+
...toolLines,
|
|
233
415
|
"Return ONLY the structured JSON plan. Evidence is mandatory for every op.",
|
|
234
416
|
"",
|
|
235
417
|
...messages
|
|
236
418
|
].join("\n");
|
|
237
419
|
}
|
|
238
420
|
//#endregion
|
|
239
|
-
export { Config, apply, inject, name };
|
|
421
|
+
export { Config, apply, collectReadSkillNames, filterUnreadSkillOps, inject, name, shouldCompletionReview };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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 {
|
|
@@ -11,14 +12,42 @@ export interface Config {
|
|
|
11
12
|
reviewMode?: string;
|
|
12
13
|
memoryInterval?: number;
|
|
13
14
|
skillInterval?: number;
|
|
14
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Tools the one-shot review subagent may use. Only actually-existing tools
|
|
17
|
+
* may be listed (the DSH tool catalog has `skill`; the `skill_search` /
|
|
18
|
+
* `skill_load` discovery pair does not exist on this platform).
|
|
19
|
+
*/
|
|
15
20
|
reviewToolAllow?: string[];
|
|
16
21
|
reviewTimeoutMs?: number;
|
|
17
22
|
executionTimeoutMs?: number;
|
|
18
23
|
reviewContextMessages?: number;
|
|
19
24
|
reviewMessageChars?: number;
|
|
20
25
|
reviewMaxDepth?: number;
|
|
26
|
+
/** LLM provider for review subagents. Omit to inherit the deployment default route. */
|
|
27
|
+
reviewProvider?: string;
|
|
28
|
+
/** Skill-review trigger: cadence (interval) | completion (once after a proven-long task) | both. */
|
|
29
|
+
skillReviewTrigger?: string;
|
|
30
|
+
/** Cumulative session tool calls before a session counts as proven-long for the completion channel. */
|
|
31
|
+
skillReviewCompletionMinToolCalls?: number;
|
|
21
32
|
}
|
|
22
33
|
export declare const Config: z<Config>;
|
|
23
34
|
export declare function apply(ctx: Context, rawConfig: Config): void;
|
|
35
|
+
/** Completion-channel decision: task finished normally AND the session is proven long. */
|
|
36
|
+
export declare function shouldCompletionReview(reason: {
|
|
37
|
+
kind?: string;
|
|
38
|
+
} | undefined, sessionToolCalls: number, minToolCalls: number): boolean;
|
|
39
|
+
/** Skill names this session loaded (read-before-write source for the background review). */
|
|
40
|
+
export declare function collectReadSkillNames(session: Session): Set<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Drop mutating ops whose target was not read this session, in place.
|
|
43
|
+
* Create is exempt (no read required to author a new skill). Covers the same
|
|
44
|
+
* mutating surface Hermes guards (edit/patch/write_file/remove_file), so a
|
|
45
|
+
* background review cannot blind-touch support files or edits of skills it
|
|
46
|
+
* never loaded. Returns the count of dropped ops so the plan event can report
|
|
47
|
+
* them as rejected.
|
|
48
|
+
*/
|
|
49
|
+
export declare function filterUnreadSkillOps(ops: Array<{
|
|
50
|
+
action?: string;
|
|
51
|
+
name?: string;
|
|
52
|
+
}>, readNames: ReadonlySet<string>): number;
|
|
24
53
|
//# 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.
|
|
4
|
+
"version": "0.1.0-rc.41",
|
|
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.
|
|
37
|
-
"@lmzhen/dsh-evolution-plan-validator": "^0.1.0-rc.
|
|
36
|
+
"@lmzhen/dsh-evolution-core": "^0.1.0-rc.41",
|
|
37
|
+
"@lmzhen/dsh-evolution-plan-validator": "^0.1.0-rc.41"
|
|
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.
|
|
46
|
+
"@lmzhen/dsh-evolution-state": "^0.1.0-rc.41"
|
|
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.
|
|
56
|
-
"@lmzhen/dsh-evolution-plan-validator": "^0.1.0-rc.
|
|
57
|
-
"@lmzhen/dsh-evolution-state": "^0.1.0-rc.
|
|
55
|
+
"@lmzhen/dsh-evolution-core": "^0.1.0-rc.41",
|
|
56
|
+
"@lmzhen/dsh-evolution-plan-validator": "^0.1.0-rc.41",
|
|
57
|
+
"@lmzhen/dsh-evolution-state": "^0.1.0-rc.41"
|
|
58
58
|
}
|
|
59
59
|
}
|