dsh-continual-evolve 0.2.0 → 0.4.0

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 (66) hide show
  1. package/README.md +83 -371
  2. package/README.zh.md +84 -235
  3. package/lib/apply.js +8 -2
  4. package/lib/approval.d.ts +6 -0
  5. package/lib/approval.js +9 -1
  6. package/lib/auto.d.ts +55 -4
  7. package/lib/auto.js +61 -5
  8. package/lib/benchmark-command.d.ts +9 -0
  9. package/lib/benchmark-command.js +333 -0
  10. package/lib/benchmark.d.ts +70 -0
  11. package/lib/benchmark.js +107 -1
  12. package/lib/command.d.ts +3 -0
  13. package/lib/command.js +62 -441
  14. package/lib/evaluate.d.ts +7 -0
  15. package/lib/evaluate.js +22 -7
  16. package/lib/evolve-event.d.ts +38 -0
  17. package/lib/evolve-event.js +49 -0
  18. package/lib/failures.d.ts +39 -0
  19. package/lib/failures.js +170 -0
  20. package/lib/fate.d.ts +5 -2
  21. package/lib/fate.js +13 -8
  22. package/lib/goal-command.d.ts +7 -0
  23. package/lib/goal-command.js +37 -0
  24. package/lib/index.d.ts +51 -25
  25. package/lib/index.js +33 -1
  26. package/lib/inject.d.ts +24 -1
  27. package/lib/inject.js +93 -5
  28. package/lib/llm-text.d.ts +30 -0
  29. package/lib/llm-text.js +49 -0
  30. package/lib/mount-command.d.ts +10 -0
  31. package/lib/mount-command.js +48 -0
  32. package/lib/plan.js +5 -0
  33. package/lib/planner.d.ts +1 -1
  34. package/lib/planner.js +13 -39
  35. package/lib/promotion.d.ts +62 -0
  36. package/lib/promotion.js +102 -0
  37. package/lib/render.d.ts +1 -3
  38. package/lib/render.js +0 -4
  39. package/lib/review.d.ts +4 -1
  40. package/lib/review.js +10 -38
  41. package/lib/rollback.d.ts +1 -3
  42. package/lib/rollback.js +0 -8
  43. package/lib/score.d.ts +15 -0
  44. package/lib/score.js +74 -5
  45. package/lib/service.d.ts +2 -2
  46. package/lib/service.js +7 -3
  47. package/lib/skill-render.d.ts +23 -0
  48. package/lib/skill-render.js +68 -0
  49. package/lib/skill.d.ts +2 -5
  50. package/lib/skill.js +2 -29
  51. package/lib/skillquality.d.ts +1 -2
  52. package/lib/skillquality.js +2 -2
  53. package/lib/state.js +6 -1
  54. package/lib/store.d.ts +1 -3
  55. package/lib/store.js +0 -7
  56. package/lib/tool.js +22 -1
  57. package/lib/types.d.ts +8 -0
  58. package/lib/usage.d.ts +45 -0
  59. package/lib/usage.js +115 -0
  60. package/lib/validate.d.ts +12 -2
  61. package/lib/validate.js +26 -1
  62. package/lib/wrapup-command.d.ts +9 -0
  63. package/lib/wrapup-command.js +212 -0
  64. package/lib/wrapup.d.ts +29 -15
  65. package/lib/wrapup.js +69 -42
  66. package/package.json +10 -8
package/lib/wrapup.js CHANGED
@@ -1,7 +1,10 @@
1
- import { BlockAssembler, createUserMessage, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
2
1
  import { ARCHIVED_AT_KEY, PROMOTED_AT_KEY, PROMOTED_TO_KEY, SOURCE_SEQS_KEY, SOURCE_SESSION_KEY, SOURCED_FROM_KEY, isArchived } from "./types.js";
3
2
  import { extractJsonObject } from "./plan.js";
4
3
  import { compactText } from "./render.js";
4
+ import { streamText } from "./llm-text.js";
5
+ import { getUsageCount, loadUsage } from "./usage.js";
6
+ import { recencyScore } from "./inject.js";
7
+ import { DEFAULT_PROMOTION_POLICY, mostSimilarGlobalEntry, projectScopedReason } from "./promotion.js";
5
8
  export function candidateKey(kind, id) {
6
9
  return `${kind}:${id}`;
7
10
  }
@@ -71,7 +74,11 @@ export function globalHintsFor(globalState, kind, entry) {
71
74
  * `coveredGlobally` flag so the assessor never wastes a promote on a topic
72
75
  * the global store already owns.
73
76
  */
74
- export function listLocalCandidates(state, globalState) {
77
+ /** Staleness threshold: entries with recency below this AND zero usage are stale. */
78
+ const STALE_RECENCY_THRESHOLD = 0.1;
79
+ export function listLocalCandidates(state, globalState, baseDir) {
80
+ const usage = baseDir ? loadUsage(baseDir) : undefined;
81
+ const now = Date.now();
75
82
  const candidates = [];
76
83
  for (const kind of Object.keys(state.entries)) {
77
84
  for (const entry of Object.values(state.entries[kind])) {
@@ -81,6 +88,8 @@ export function listLocalCandidates(state, globalState) {
81
88
  continue;
82
89
  if (typeof entry.metadata[PROMOTED_TO_KEY] === "string")
83
90
  continue;
91
+ const injectionCount = usage ? getUsageCount(usage, kind, entry.id) : 0;
92
+ const stale = injectionCount === 0 && recencyScore(entry, now) < STALE_RECENCY_THRESHOLD;
84
93
  candidates.push({
85
94
  kind,
86
95
  id: entry.id,
@@ -91,6 +100,8 @@ export function listLocalCandidates(state, globalState) {
91
100
  metadata: entry.metadata,
92
101
  coveredGlobally: globalCoverageDetected(globalState, kind, entry),
93
102
  globalHints: globalHintsFor(globalState, kind, entry),
103
+ injectionCount,
104
+ stale,
94
105
  });
95
106
  }
96
107
  }
@@ -148,9 +159,16 @@ export function parseWrapupAssessment(text, candidates) {
148
159
  * Apply-time deterministic guard: re-check every promote verdict against the
149
160
  * global store right before it lands. The LLM classification may be stale
150
161
  * (a gate ran while assessing) or wrong; this ensures a promote never writes
151
- * a duplicate global entry. Pure and unit-tested.
162
+ * a duplicate, project-scoped, or too-thin global entry. Pure and unit-tested.
163
+ *
164
+ * Guards (2026-08-22 promotion policy):
165
+ * - audited candidate list + title coverage (pre-existing),
166
+ * - project-scoped content markers (absolute paths / session ids) — the
167
+ * global store is shared across projects and must stay portable,
168
+ * - thin content below the policy floor (framing outweighs the fact),
169
+ * - near-duplicate of an existing global entry by content overlap.
152
170
  */
153
- export function filterPromotable(items, globalState, candidates) {
171
+ export function filterPromotable(items, globalState, candidates, policy = DEFAULT_PROMOTION_POLICY) {
154
172
  const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
155
173
  const promotable = [];
156
174
  const skipped = [];
@@ -166,6 +184,26 @@ export function filterPromotable(items, globalState, candidates) {
166
184
  skipped.push({ key: item.key, reason: "already covered globally" });
167
185
  continue;
168
186
  }
187
+ const scoped = projectScopedReason(`${candidate.title}\n${candidate.content}`, policy);
188
+ if (scoped) {
189
+ skipped.push({ key: item.key, reason: scoped });
190
+ continue;
191
+ }
192
+ if (candidate.content.length < policy.minPromoteChars) {
193
+ skipped.push({
194
+ key: item.key,
195
+ reason: `too thin to promote (${candidate.content.length} < ${policy.minPromoteChars} chars) — keep local or merge`,
196
+ });
197
+ continue;
198
+ }
199
+ const similar = mostSimilarGlobalEntry(globalState, candidate.kind, candidate.title, candidate.content, policy);
200
+ if (similar) {
201
+ skipped.push({
202
+ key: item.key,
203
+ reason: `near-duplicate of global ${candidate.kind}:${similar.id} "${similar.title}" (overlap ${similar.score.toFixed(2)}) — update that entry instead`,
204
+ });
205
+ continue;
206
+ }
169
207
  promotable.push(item);
170
208
  }
171
209
  return { promotable, skipped };
@@ -213,16 +251,27 @@ export function splitArchiveGuards(items, candidates) {
213
251
  }
214
252
  /**
215
253
  * Apply-time guard for a split promotion (archive + promote sub-object):
216
- * the cleaned title must not duplicate a topic already covered globally. A
217
- * duplicate split is dropped (the entry still archives plain) rather than
218
- * half-promoting a redundancy.
254
+ * the cleaned payload must pass the same promotion policy as a whole
255
+ * promote no global coverage duplicate, no project-scoped content, not
256
+ * too thin, no near-duplicate global entry. A blocked split is dropped (the
257
+ * entry still archives plain) rather than half-promoting a redundancy.
219
258
  */
220
- export function splitPromoteBlocked(item, globalState, kind) {
259
+ export function splitPromoteBlocked(item, globalState, kind, policy = DEFAULT_PROMOTION_POLICY) {
221
260
  if (!item.promote)
222
261
  return "no split payload";
223
262
  if (globalCoverageDetected(globalState, kind, { id: "", title: item.promote.title })) {
224
263
  return "split promotion duplicates a globally covered topic";
225
264
  }
265
+ const scoped = projectScopedReason(`${item.promote.title}\n${item.promote.content}`, policy);
266
+ if (scoped)
267
+ return `split promotion is ${scoped}`;
268
+ if (item.promote.content.length < policy.minPromoteChars) {
269
+ return `split promotion too thin (${item.promote.content.length} < ${policy.minPromoteChars} chars)`;
270
+ }
271
+ const similar = mostSimilarGlobalEntry(globalState, kind, item.promote.title, item.promote.content, policy);
272
+ if (similar) {
273
+ return `split promotion near-duplicates global ${kind}:${similar.id} "${similar.title}" (overlap ${similar.score.toFixed(2)})`;
274
+ }
226
275
  return undefined;
227
276
  }
228
277
  /**
@@ -342,12 +391,17 @@ listed entry exactly once:
342
391
  procedure or skill. Future sessions would benefit from seeing it.
343
392
  - "archive" — the content is session-specific task progress, one-off noise,
344
393
  superseded or obsolete, or already covered by the global store (note
345
- "covered globally" in the reason).
394
+ "covered globally" in the reason), or stale (old + never injected — note
395
+ "stale (injectionCount=0, recency low)" in the reason).
346
396
  - "keep" — still actively useful to this session, or genuinely uncertain.
347
397
 
348
398
  Rules:
349
399
  - When an entry is marked "covered globally" in the listing, prefer "archive"
350
400
  or "keep" over "promote" — promoting a duplicate gains nothing.
401
+ - When an entry is marked "stale" (injectionCount=0 and low recency), prefer
402
+ "archive" — the entry has never been used and is old, so it is unlikely to
403
+ be needed again. Only "keep" if the content is clearly valuable despite low
404
+ usage (e.g. a safety policy that rarely triggers but is critical).
351
405
  - Do not promote local task state, work-in-progress notes, or content tied to
352
406
  one session's ephemeral details.
353
407
  - Skills: only "promote" a skill entry that is a genuinely reusable procedure
@@ -387,10 +441,11 @@ export async function assessLocalEntries(ctx, agent, candidates, options = {}) {
387
441
  .map((candidate) => {
388
442
  const key = candidateKey(candidate.kind, candidate.id);
389
443
  const covered = candidate.coveredGlobally ? " (covered globally)" : "";
444
+ const stale = candidate.stale ? ` (stale: injectionCount=${candidate.injectionCount}, recency low)` : "";
390
445
  const hints = candidate.globalHints.length > 0
391
446
  ? ` | global≈${candidate.globalHints.map((hint) => hint.id + ":" + hint.title).join(", ")}`
392
447
  : "";
393
- return `- ${key} [${candidate.path}, v${candidate.version}] "${candidate.title}"${covered}${hints}: ${compactText(candidate.content, 220)}`;
448
+ return `- ${key} [${candidate.path}, v${candidate.version}] "${candidate.title}"${covered}${stale}${hints}: ${compactText(candidate.content, 220)}`;
394
449
  })
395
450
  .join("\n");
396
451
  const userPrompt = [
@@ -398,42 +453,14 @@ export async function assessLocalEntries(ctx, agent, candidates, options = {}) {
398
453
  `<local_entries>\n${candidateText}\n</local_entries>`,
399
454
  "Return only JSON. Every item must reference one of the keys above.",
400
455
  ].join("\n\n");
401
- const assembler = new BlockAssembler();
402
- for await (const chunk of ctx.llm.stream({
456
+ const text = await streamText(ctx, {
403
457
  provider: agent.options.provider,
404
458
  model: agent.options.model,
405
459
  system: WRAPUP_ASSESS_SYSTEM_PROMPT,
406
- messages: [
407
- createUserMessage({
408
- content: [{ type: "text", text: userPrompt }],
409
- source: { kind: "plugin", plugin: "dsh-continual-evolve" },
410
- }),
411
- ],
412
- // Force non-reasoning output so the budget lands in the JSON verdicts.
413
- reasoningEffort: ReasoningEffortId("off"),
460
+ prompt: userPrompt,
414
461
  maxTokens: options.maxOutputTokens ?? 4096,
415
- ...(options.signal ? { signal: options.signal } : {}),
416
- })) {
417
- assembler.push(chunk);
418
- }
419
- const finish = assembler.finish;
420
- if (finish.kind === "error") {
421
- throw new Error(`evolve: wrap-up assessor call failed: ${finish.failure?.message ?? "unknown"}`);
422
- }
423
- if (finish.kind === "aborted") {
424
- throw new Error("evolve: wrap-up assessor call aborted");
425
- }
426
- if (finish.kind === "max-tokens") {
427
- throw new Error("evolve: wrap-up assessor output budget exhausted (max-tokens)");
428
- }
429
- const text = assembler
430
- .blocks()
431
- .filter((block) => block.type === "text")
432
- .map((block) => block.text)
433
- .join("\n");
434
- if (text.length === 0) {
435
- throw new Error("evolve: wrap-up assessor produced no text");
436
- }
462
+ signal: options.signal,
463
+ });
437
464
  return parseWrapupAssessment(text, candidates);
438
465
  }
439
466
  //# sourceMappingURL=wrapup.js.map
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "dsh-continual-evolve",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Continual self-evolution plugin for DeepSeek Harness: versioned, auditable, rollback-safe harness state (prompt notes, memories, skills, subagent specs) refined from session trajectories.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/ZK-Andy/dsh-continual-evolve.git"
8
+ "url": "git+https://github.com/ZK-Andy/dsh-continual-evolve.git"
9
9
  },
10
10
  "homepage": "https://github.com/ZK-Andy/dsh-continual-evolve",
11
11
  "bugs": {
@@ -50,6 +50,7 @@
50
50
  "dev": "tsc -p tsconfig.json --watch",
51
51
  "typecheck": "tsc -p tsconfig.json --noEmit",
52
52
  "test": "vitest run",
53
+ "test:coverage": "vitest run --coverage",
53
54
  "test:watch": "vitest",
54
55
  "lint": "oxlint src test",
55
56
  "clean": "rm -rf lib"
@@ -62,14 +63,15 @@
62
63
  },
63
64
  "devDependencies": {
64
65
  "@deepseek-ai/cordis": "^4.0.1",
65
- "@deepseek-ai/dsh-agent": "0.1.0-rc.6",
66
- "@deepseek-ai/dsh-commands": "0.1.0-rc.6",
67
- "@deepseek-ai/dsh-home-paths": "0.1.0-rc.6",
68
- "@deepseek-ai/dsh-llm": "0.1.0-rc.6",
69
- "@deepseek-ai/dsh-system-prompt": "0.1.0-rc.6",
70
- "@deepseek-ai/dsh-tools": "0.1.0-rc.6",
66
+ "@deepseek-ai/dsh-agent": "0.1.1-rc.2",
67
+ "@deepseek-ai/dsh-commands": "0.1.1-rc.2",
68
+ "@deepseek-ai/dsh-home-paths": "0.1.1-rc.2",
69
+ "@deepseek-ai/dsh-llm": "0.1.1-rc.2",
70
+ "@deepseek-ai/dsh-system-prompt": "0.1.1-rc.2",
71
+ "@deepseek-ai/dsh-tools": "0.1.1-rc.2",
71
72
  "@deepseek-ai/schemastery": "^3.18.1",
72
73
  "@types/node": "^22.10.0",
74
+ "@vitest/coverage-v8": "^3.2.0",
73
75
  "oxlint": "^0.16.0",
74
76
  "typescript": "^5.9.0",
75
77
  "vitest": "^3.2.0"