dsh-continual-evolve 0.6.1 → 0.7.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.
package/lib/tool.js CHANGED
@@ -1,31 +1,68 @@
1
1
  import { defineTool } from "@deepseek-ai/dsh-tools";
2
+ import { MEMORY_TYPE_KEY } from "./types.js";
3
+ import { projectKeyOf } from "./project.js";
2
4
  import { formatHarnessStateForPrompt } from "./render.js";
3
5
  import { requireGlobalApproval } from "./approval.js";
4
6
  import { CONFLICT_WARN_SCORE, buildConflictNotice, mostSimilarEntry } from "./promotion.js";
5
7
  import { entrySourceOf } from "./source.js";
6
8
  import { getUsageCount, loadUsage } from "./usage.js";
7
9
  import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
8
- const SCOPES = ["local", "global"];
9
- /** Accept both the boolean tool parameter (`global: true`) and the string form. */
10
+ const SCOPES = ["local", "project", "global"];
11
+ /**
12
+ * Accept the string form (`scope: "project"`) with the legacy boolean tool
13
+ * parameter (`global: true`) as fallback. `scope` wins when both are given.
14
+ */
10
15
  export function scopeOf(value, fallback) {
11
- return value === "global" || value === true ? "global" : fallback;
16
+ if (value === "global" || value === true) {
17
+ return "global";
18
+ }
19
+ if (value === "project") {
20
+ return "project";
21
+ }
22
+ return fallback;
12
23
  }
13
24
  /** The calling agent's session id; tools always run inside an agent scope. */
14
25
  function sessionIdOf(exec) {
15
26
  return exec.agent?.id;
16
27
  }
28
+ /**
29
+ * The store id a scope reads/writes: session id for local, derived project
30
+ * key for project, undefined for global. Throws for project when the
31
+ * session cwd is unavailable — fail loud instead of writing local-by-mistake.
32
+ */
33
+ function storeIdFor(scope, exec) {
34
+ if (scope === "local") {
35
+ return sessionIdOf(exec);
36
+ }
37
+ if (scope === "project") {
38
+ const key = projectKeyOf(exec.agent);
39
+ if (!key) {
40
+ throw new Error("project scope needs the session cwd (unavailable for this agent) — use local or global instead");
41
+ }
42
+ return key;
43
+ }
44
+ return undefined;
45
+ }
46
+ /** Human-approval gate shared by global and project writes (both cross-session). */
47
+ function needsApproval(scope) {
48
+ return scope === "global" || scope === "project";
49
+ }
50
+ /** Store label used in approval prompts. */
51
+ function storeLabel(scope) {
52
+ return scope === "project" ? "本项目跨会话 store" : "跨会话全局 store";
53
+ }
17
54
  function textResult(text) {
18
55
  return { text };
19
56
  }
20
57
  export function registerEvolveTools(ctx, engine, opts) {
21
58
  ctx.tools.register(defineTool({
22
59
  name: "evolve_list",
23
- description: "List the continual harness state (prompt notes, memories, skills, subagent specs) for the current session (local) or across sessions (global).",
60
+ description: "List the continual harness state (prompt notes, memories, skills, subagent specs) for the current session (local), the current project (project), or across projects (global).",
24
61
  parameters: {
25
62
  scope: {
26
63
  type: "string",
27
64
  enum: SCOPES,
28
- description: "Which store to list: 'local' (default) or 'global'.",
65
+ description: "Which store to list: 'local' (default), 'project', or 'global'.",
29
66
  },
30
67
  },
31
68
  output: {
@@ -34,7 +71,7 @@ export function registerEvolveTools(ctx, engine, opts) {
34
71
  },
35
72
  execute: async (args, exec) => {
36
73
  const scope = scopeOf(args.scope, "local");
37
- const state = engine.load(scope, sessionIdOf(exec));
74
+ const state = engine.load(scope, storeIdFor(scope, exec));
38
75
  const text = formatHarnessStateForPrompt(state);
39
76
  // Append injection usage counts (gap B1).
40
77
  const usage = loadUsage(engine.baseDir);
@@ -64,6 +101,8 @@ export function registerEvolveTools(ctx, engine, opts) {
64
101
  skill_kind: { type: "string", enum: ["executable", "guidance"], description: "For skills: executable (python reference, default) or guidance (SKILL.md document, no reference)." },
65
102
  reference: { type: "object", additionalProperties: true, description: "For executable skills: {type:'python', import, callable}." },
66
103
  arguments: { type: "object", additionalProperties: true, description: "For executable skills: accepted input contract." },
104
+ memoryType: { type: "string", enum: ["user", "feedback", "project", "reference"], description: "Required for memory: one fact per entry (feedback/project must carry Why + How to apply)." },
105
+ scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
67
106
  global: { type: "boolean", description: "Set true to write the cross-session store (requires human approval; only for durable, reusable lessons)." },
68
107
  },
69
108
  output: {
@@ -71,15 +110,18 @@ export function registerEvolveTools(ctx, engine, opts) {
71
110
  render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
72
111
  },
73
112
  execute: async (args, exec) => {
74
- const scope = scopeOf(args.global, "local");
75
- if (scope === "global" && opts.requireGlobalApproval) {
113
+ const scope = scopeOf(args.scope ?? args.global, "local");
114
+ // Resolve first: a project scope without a session cwd fails
115
+ // here, before any human approval question is asked.
116
+ const storeId = storeIdFor(scope, exec);
117
+ if (needsApproval(scope) && opts.requireGlobalApproval) {
76
118
  // Informed approval: surface a similarity hit against the
77
- // existing global store BEFORE the human decides — the
78
- // engine's write-time guard still has the final say.
79
- const globalState = engine.load("global", undefined);
80
- const hit = mostSimilarEntry(Object.values(globalState.entries[args.kind]), args.title ?? "", args.content ?? "", CONFLICT_WARN_SCORE);
119
+ // target store BEFORE the human decides — the engine's
120
+ // write-time guard still has the final say.
121
+ const targetState = engine.load(scope, storeId);
122
+ const hit = mostSimilarEntry(Object.values(targetState.entries[args.kind]), args.title ?? "", args.content ?? "", CONFLICT_WARN_SCORE);
81
123
  const conflictNote = hit ? ` ⚠️ ${buildConflictNotice(hit)}——建议改用 evolve_update` : "";
82
- await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_add ${args.kind} "${args.title}" → 跨会话全局 store${conflictNote}`);
124
+ await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_add ${args.kind} "${args.title}" → ${storeLabel(scope)}${conflictNote}`);
83
125
  }
84
126
  const edit = {
85
127
  action: "create",
@@ -95,7 +137,10 @@ export function registerEvolveTools(ctx, engine, opts) {
95
137
  edit.reference = args.reference;
96
138
  if (args.arguments !== undefined)
97
139
  edit.arguments = args.arguments;
98
- return textResult(applyEditsText(engine, scope, sessionIdOf(exec), [edit], exec.agent));
140
+ if (args.kind === "memory" && args.memoryType !== undefined) {
141
+ edit.metadata = { [MEMORY_TYPE_KEY]: args.memoryType };
142
+ }
143
+ return textResult(applyEditsText(engine, scope, storeId, [edit], exec.agent, sessionIdOf(exec)));
99
144
  },
100
145
  }));
101
146
  ctx.tools.register(defineTool({
@@ -106,6 +151,8 @@ export function registerEvolveTools(ctx, engine, opts) {
106
151
  id: { type: "string", required: true, description: "Existing entry id." },
107
152
  title: { type: "string" },
108
153
  content: { type: "string" },
154
+ memoryType: { type: "string", enum: ["user", "feedback", "project", "reference"], description: "For memory: (re)classify the entry's recall type." },
155
+ scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
109
156
  global: { type: "boolean", description: "Set true to edit the cross-session store (requires human approval)." },
110
157
  },
111
158
  output: {
@@ -113,16 +160,19 @@ export function registerEvolveTools(ctx, engine, opts) {
113
160
  render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
114
161
  },
115
162
  execute: async (args, exec) => {
116
- const scope = scopeOf(args.global, "local");
117
- if (scope === "global" && opts.requireGlobalApproval) {
118
- await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_update ${args.kind}:${args.id} → 跨会话全局 store`);
163
+ const scope = scopeOf(args.scope ?? args.global, "local");
164
+ const storeId = storeIdFor(scope, exec);
165
+ if (needsApproval(scope) && opts.requireGlobalApproval) {
166
+ await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_update ${args.kind}:${args.id} → ${storeLabel(scope)}`);
119
167
  }
120
168
  const edit = { action: "update", kind: args.kind, id: args.id };
121
169
  if (args.title !== undefined)
122
170
  edit.title = args.title;
123
171
  if (args.content !== undefined)
124
172
  edit.content = args.content;
125
- return textResult(applyEditsText(engine, scope, sessionIdOf(exec), [edit], exec.agent));
173
+ if (args.memoryType !== undefined)
174
+ edit.metadata = { [MEMORY_TYPE_KEY]: args.memoryType };
175
+ return textResult(applyEditsText(engine, scope, storeId, [edit], exec.agent, sessionIdOf(exec)));
126
176
  },
127
177
  }));
128
178
  ctx.tools.register(defineTool({
@@ -131,6 +181,7 @@ export function registerEvolveTools(ctx, engine, opts) {
131
181
  parameters: {
132
182
  kind: { type: "string", enum: ["prompt", "memory", "skill", "subagent"], required: true },
133
183
  id: { type: "string", required: true },
184
+ scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
134
185
  global: { type: "boolean", description: "Set true to edit the cross-session store (requires human approval)." },
135
186
  },
136
187
  output: {
@@ -138,12 +189,13 @@ export function registerEvolveTools(ctx, engine, opts) {
138
189
  render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
139
190
  },
140
191
  execute: async (args, exec) => {
141
- const scope = scopeOf(args.global, "local");
142
- if (scope === "global" && opts.requireGlobalApproval) {
143
- await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_delete ${args.kind}:${args.id} → 跨会话全局 store`);
192
+ const scope = scopeOf(args.scope ?? args.global, "local");
193
+ const storeId = storeIdFor(scope, exec);
194
+ if (needsApproval(scope) && opts.requireGlobalApproval) {
195
+ await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_delete ${args.kind}:${args.id} → ${storeLabel(scope)}`);
144
196
  }
145
197
  const edit = { action: "delete", kind: args.kind, id: args.id };
146
- return textResult(applyEditsText(engine, scope, sessionIdOf(exec), [edit], exec.agent));
198
+ return textResult(applyEditsText(engine, scope, storeId, [edit], exec.agent, sessionIdOf(exec)));
147
199
  },
148
200
  }));
149
201
  ctx.tools.register(defineTool({
@@ -151,6 +203,7 @@ export function registerEvolveTools(ctx, engine, opts) {
151
203
  description: "Deterministically revert a previous refinement by its id (from evolve_list history or the /evolve command).",
152
204
  parameters: {
153
205
  refinementId: { type: "string", required: true, description: "The refinement id to roll back." },
206
+ scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
154
207
  global: { type: "boolean", description: "Set true to roll back a cross-session refinement." },
155
208
  },
156
209
  output: {
@@ -158,14 +211,14 @@ export function registerEvolveTools(ctx, engine, opts) {
158
211
  render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
159
212
  },
160
213
  execute: async (args, exec) => {
161
- const scope = scopeOf(args.global, "local");
162
- const result = engine.rollback(scope, sessionIdOf(exec), args.refinementId);
214
+ const scope = scopeOf(args.scope ?? args.global, "local");
215
+ const result = engine.rollback(scope, storeIdFor(scope, exec), args.refinementId);
163
216
  return textResult(`Rolled back ${result.rollbackOf ?? result.id}: ${result.appliedEdits.filter((e) => e.applied).length} edit(s) reverted.`);
164
217
  },
165
218
  }));
166
219
  }
167
- function applyEditsText(engine, scope, sessionId, edits, agent) {
168
- const result = engine.apply(scope, sessionId, {
220
+ function applyEditsText(engine, scope, storeId, edits, agent, eventSessionId) {
221
+ const result = engine.apply(scope, storeId, {
169
222
  summary: "Direct tool edit",
170
223
  rationale: "Model-invoked single edit via evolve_* tool.",
171
224
  expectedOutcome: "Entry is created, updated, or deleted as requested.",
@@ -173,14 +226,16 @@ function applyEditsText(engine, scope, sessionId, edits, agent) {
173
226
  }, agent
174
227
  ? {
175
228
  scope,
176
- ...(entrySourceOf(agent, sessionId) ? { source: entrySourceOf(agent, sessionId) } : {}),
229
+ ...(entrySourceOf(agent, eventSessionId ?? storeId) ? { source: entrySourceOf(agent, eventSessionId ?? storeId) } : {}),
177
230
  }
178
231
  : { scope });
179
232
  const applied = result.appliedEdits.filter((e) => e.applied);
180
233
  const failed = result.appliedEdits.filter((e) => !e.applied);
181
234
  // Gap C4: emit structured evolve_complete event for third-party consumers.
182
- if (applied.length > 0 && sessionId) {
183
- emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, "manual_tool", sessionId));
235
+ // The event keys on the live session (not the project store key).
236
+ const eventSession = eventSessionId ?? storeId;
237
+ if (applied.length > 0 && eventSession) {
238
+ emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, "manual_tool", eventSession));
184
239
  }
185
240
  const lines = [`refinement ${result.id}: ${applied.length} applied, ${failed.length} failed`];
186
241
  for (const e of applied) {
package/lib/types.d.ts CHANGED
@@ -20,8 +20,20 @@ export type RefinementKind = "prompt" | "memory" | "skill" | "subagent";
20
20
  export type SkillKind = "executable" | "guidance";
21
21
  /** How an entry changes. */
22
22
  export type RefinementAction = "create" | "update" | "delete" | "archive";
23
- /** Where an entry lives: session-scoped or cross-session. */
24
- export type HarnessScope = "local" | "global";
23
+ /** Where an entry lives: session staging, one project, or cross-project. */
24
+ export type HarnessScope = "local" | "project" | "global";
25
+ /**
26
+ * Memory-entry shape discipline (one fact per entry, typed like ZCode's
27
+ * memory frontmatter): every `memory` create must stamp one of these in
28
+ * `metadata[MEMORY_TYPE_KEY]` — the recall hook the directory shows.
29
+ */
30
+ export declare const MEMORY_TYPE_KEY = "memoryType";
31
+ /** Closed memory-type set: user | feedback | project | reference. */
32
+ export declare const MEMORY_TYPES: readonly ["user", "feedback", "project", "reference"];
33
+ /** One of {@link MEMORY_TYPES}. */
34
+ export type MemoryRecallType = (typeof MEMORY_TYPES)[number];
35
+ /** True when the value is a closed-set memory type. */
36
+ export declare function isMemoryType(value: unknown): value is MemoryRecallType;
25
37
  /**
26
38
  * Metadata key recording which session an entry's content was distilled from
27
39
  * (trajectory citation; see {@link EntrySource}).
package/lib/types.js CHANGED
@@ -8,6 +8,18 @@
8
8
  * code here is an original implementation, written for the DeepSeek Harness
9
9
  * plugin surface.
10
10
  */
11
+ /**
12
+ * Memory-entry shape discipline (one fact per entry, typed like ZCode's
13
+ * memory frontmatter): every `memory` create must stamp one of these in
14
+ * `metadata[MEMORY_TYPE_KEY]` — the recall hook the directory shows.
15
+ */
16
+ export const MEMORY_TYPE_KEY = "memoryType";
17
+ /** Closed memory-type set: user | feedback | project | reference. */
18
+ export const MEMORY_TYPES = ["user", "feedback", "project", "reference"];
19
+ /** True when the value is a closed-set memory type. */
20
+ export function isMemoryType(value) {
21
+ return typeof value === "string" && MEMORY_TYPES.includes(value);
22
+ }
11
23
  /**
12
24
  * Metadata key recording which session an entry's content was distilled from
13
25
  * (trajectory citation; see {@link EntrySource}).
package/lib/validate.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { MEMORY_TYPE_KEY, isMemoryType } from "./types.js";
1
2
  import { validateSkillEntryContent } from "./skillquality.js";
2
3
  const ACTIONS = new Set(["create", "update", "delete", "archive"]);
3
4
  const KINDS = new Set(["prompt", "memory", "skill", "subagent"]);
@@ -18,6 +19,9 @@ export function validateBlastRadiusScope(scope, blastRadius) {
18
19
  if (scope === "global" && blastRadius === "session") {
19
20
  return "global-scope edit must declare blastRadius \"general\" or \"project\" (\"session\" contradicts cross-session persistence)";
20
21
  }
22
+ if (scope === "project" && blastRadius === "session") {
23
+ return "project-scope edit must declare blastRadius \"project\" or \"general\" (\"session\" contradicts cross-session persistence)";
24
+ }
21
25
  return undefined;
22
26
  }
23
27
  /**
@@ -71,6 +75,32 @@ export function validateEdit(edit, computedId, scope, before) {
71
75
  edit.arguments === undefined) {
72
76
  return "update carries no changes";
73
77
  }
78
+ if (edit.kind === "memory" && (edit.action === "create" || edit.action === "update")) {
79
+ // One-fact typed memories: every memory create declares its recall
80
+ // type; feedback/project (pitfalls, conventions) additionally carry
81
+ // the Why + How-to-apply structure so the fact is reusable, not just
82
+ // recorded. Updates are partial — only carried fields are checked,
83
+ // resolved against the persisted entry.
84
+ const carriedType = edit.metadata?.[MEMORY_TYPE_KEY];
85
+ if (edit.action === "create" && !isMemoryType(carriedType)) {
86
+ return `create memory requires metadata.${MEMORY_TYPE_KEY} one of user|feedback|project|reference (one fact per entry; update the existing entry instead of adding a duplicate)`;
87
+ }
88
+ if (carriedType !== undefined && !isMemoryType(carriedType)) {
89
+ return `invalid metadata.${MEMORY_TYPE_KEY} ${JSON.stringify(carriedType)} (one of user|feedback|project|reference)`;
90
+ }
91
+ const effectiveType = isMemoryType(carriedType)
92
+ ? carriedType
93
+ : edit.action === "update"
94
+ ? before?.metadata?.[MEMORY_TYPE_KEY]
95
+ : undefined;
96
+ if ((effectiveType === "feedback" || effectiveType === "project") &&
97
+ (edit.action === "create" || edit.content !== undefined)) {
98
+ const content = edit.content ?? "";
99
+ if (!/why/i.test(content) || !/how/i.test(content)) {
100
+ return `${effectiveType} memory content must carry a Why and a How to apply section (the fact plus why it matters and how to apply it next time)`;
101
+ }
102
+ }
103
+ }
74
104
  if (edit.action === "create" && edit.kind === "skill") {
75
105
  // Guidance skills are SKILL.md documents: no python reference (a
76
106
  // reference on a guidance skill would be an invented contract) and
@@ -34,7 +34,7 @@ export async function executeWrapupCommand(ctx, engine, invocation, policy = DEF
34
34
  splitSkipped.push({ key: item.key, reason: "not in the audited candidate list" });
35
35
  continue;
36
36
  }
37
- const blocked = splitPromoteBlocked(item, globalState, candidate.kind, policy);
37
+ const blocked = splitPromoteBlocked(item, globalState, candidate.kind, policy, candidate.metadata);
38
38
  if (blocked) {
39
39
  splitSkipped.push({ key: item.key, reason: blocked });
40
40
  continue;
package/lib/wrapup.d.ts CHANGED
@@ -199,7 +199,7 @@ export declare function splitArchiveGuards(items: readonly WrapupItem[], candida
199
199
  * too thin, no near-duplicate global entry. A blocked split is dropped (the
200
200
  * entry still archives plain) rather than half-promoting a redundancy.
201
201
  */
202
- export declare function splitPromoteBlocked(item: WrapupItem, globalState: HarnessState, kind: RefinementKind, policy?: PromotionPolicy): string | undefined;
202
+ export declare function splitPromoteBlocked(item: WrapupItem, globalState: HarnessState, kind: RefinementKind, policy?: PromotionPolicy, candidateMetadata?: Record<string, unknown>): string | undefined;
203
203
  /**
204
204
  * Shared proposal builders for a WHOLE promotion — used by both the
205
205
  * `/evolve wrapup` command and the gate's local-fate dimension so the two
@@ -232,7 +232,7 @@ export declare function splitPromoteProposals(item: WrapupItem, candidate: Wrapu
232
232
  * injection). Undefined when nothing applies.
233
233
  */
234
234
  export declare function valenceStampProposal(item: WrapupItem, candidate: WrapupCandidate): RefinementProposal | undefined;
235
- export declare const WRAPUP_ASSESS_SYSTEM_PROMPT = "You are the /evolve session wrap-up assessor.\nA session is ending and its local harness entries need a fate. Classify each\nlisted entry exactly once:\n\n- \"promote\" \u2014 the content is a stable, durable, CROSS-SESSION reusable lesson:\n a durable user preference, a project-level fact or convention, a reusable\n procedure or skill. Future sessions would benefit from seeing it.\n- \"archive\" \u2014 the content is session-specific task progress, one-off noise,\n superseded or obsolete, or already covered by the global store (note\n \"covered globally\" in the reason), or stale (old + never injected \u2014 note\n \"stale (injectionCount=0, recency low)\" in the reason), or CONTRADICTED \u2014\n newer evidence in the trajectory or the global store disproves or overrides\n the entry's content (note what contradicts it in the reason).\n- \"keep\" \u2014 still actively useful to this session, or genuinely uncertain.\n\nRules:\n- CONTRADICTED: when evidence contradicts an entry, set \"contradicted\": true\n on that item (besides the verdict). Entries marked \"(contradicted N\u00D7\n before)\" were contradicted in earlier assessments \u2014 prefer \"archive\" for\n them unless you have concrete evidence the content is valid again.\n- When an entry is marked \"covered globally\" in the listing, prefer \"archive\"\n or \"keep\" over \"promote\" \u2014 promoting a duplicate gains nothing.\n- When an entry is marked \"stale\" (injectionCount=0 and low recency), prefer\n \"archive\" \u2014 the entry has never been used and is old, so it is unlikely to\n be needed again. Only \"keep\" if the content is clearly valuable despite low\n usage (e.g. a safety policy that rarely triggers but is critical).\n- Do not promote local task state, work-in-progress notes, or content tied to\n one session's ephemeral details.\n- Skills: only \"promote\" a skill entry that is a genuinely reusable procedure\n meeting the DSH skill quality standard; one-off workflows are \"archive\" or\n \"keep\".\n- SPLIT PROMOTION: when an entry mixes a stable, cross-session-reusable part\n WITH session-specific snapshot details, do NOT promote it whole. Instead\n give verdict \"archive\" WITH a \"promote\" sub-object holding a CLEANED\n version of only the durable part (a stable title + the persistent facts,\n stripped of dates/states/one-off figures). Ephemeral snapshot content stays\n out of the sub-object \u2014 it is left behind in the archive. A sub-object is\n only meaningful on \"archive\" verdicts.\n\nReturn JSON only:\n{\n \"rationale\": \"one or two sentences\",\n \"items\": [\n {\"key\": \"memory:foo\", \"verdict\": \"promote|archive|keep\", \"reason\": \"why\"},\n {\"key\": \"memory:bar\", \"verdict\": \"archive\", \"reason\": \"why\", \"contradicted\": true,\n \"promote\": {\"title\": \"cleaned stable title\", \"content\": \"cleaned durable part only\"}}\n ]\n}\nOnly keys from the provided list are allowed; any entry you omit defaults to \"keep\".";
235
+ export declare const WRAPUP_ASSESS_SYSTEM_PROMPT = "You are the /evolve session wrap-up assessor.\nA session is ending and its local harness entries need a fate. Classify each\nlisted entry exactly once:\n\n- \"promote\" \u2014 the content is a stable, durable, CROSS-SESSION reusable lesson:\n a durable user preference, a project-level fact or convention, a reusable\n procedure or skill. Future sessions would benefit from seeing it.\n- \"archive\" \u2014 the content is session-specific task progress, one-off noise,\n superseded or obsolete, or already covered by the global store (note\n \"covered globally\" in the reason), or stale (old + never injected \u2014 note\n \"stale (injectionCount=0, recency low)\" in the reason), or CONTRADICTED \u2014\n newer evidence in the trajectory or the global store disproves or overrides\n the entry's content (note what contradicts it in the reason).\n- \"keep\" \u2014 still actively useful to this session, or genuinely uncertain.\n\nRules:\n- CONTRADICTED: when evidence contradicts an entry, set \"contradicted\": true\n on that item (besides the verdict). Entries marked \"(contradicted N\u00D7\n before)\" were contradicted in earlier assessments \u2014 prefer \"archive\" for\n them unless you have concrete evidence the content is valid again.\n- When an entry is marked \"covered globally\" in the listing, prefer \"archive\"\n or \"keep\" over \"promote\" \u2014 promoting a duplicate gains nothing.\n- When an entry is marked \"stale\" (injectionCount=0 and low recency), prefer\n \"archive\" \u2014 the entry has never been used and is old, so it is unlikely to\n be needed again. Only \"keep\" if the content is clearly valuable despite low\n usage (e.g. a safety policy that rarely triggers but is critical).\n- Do not promote local task state, work-in-progress notes, or content tied to\n one session's ephemeral details.\n- Memory entries carry a recall type (user/feedback/project/reference): a\n memory WITHOUT one cannot be promoted (the guard skips it) \u2014 verdict such\n entries \"keep\" (or \"archive\" when they qualify) rather than \"promote\".\n- Skills: only \"promote\" a skill entry that is a genuinely reusable procedure\n meeting the DSH skill quality standard; one-off workflows are \"archive\" or\n \"keep\".\n- SPLIT PROMOTION: when an entry mixes a stable, cross-session-reusable part\n WITH session-specific snapshot details, do NOT promote it whole. Instead\n give verdict \"archive\" WITH a \"promote\" sub-object holding a CLEANED\n version of only the durable part (a stable title + the persistent facts,\n stripped of dates/states/one-off figures). Ephemeral snapshot content stays\n out of the sub-object \u2014 it is left behind in the archive. A sub-object is\n only meaningful on \"archive\" verdicts.\n\nReturn JSON only:\n{\n \"rationale\": \"one or two sentences\",\n \"items\": [\n {\"key\": \"memory:foo\", \"verdict\": \"promote|archive|keep\", \"reason\": \"why\"},\n {\"key\": \"memory:bar\", \"verdict\": \"archive\", \"reason\": \"why\", \"contradicted\": true,\n \"promote\": {\"title\": \"cleaned stable title\", \"content\": \"cleaned durable part only\"}}\n ]\n}\nOnly keys from the provided list are allowed; any entry you omit defaults to \"keep\".";
236
236
  export interface AssessOptions {
237
237
  /** Output token budget for the assessment call. */
238
238
  maxOutputTokens?: number;
package/lib/wrapup.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ARCHIVED_AT_KEY, PROMOTED_AT_KEY, PROMOTED_TO_KEY, SOURCE_SEQS_KEY, SOURCE_SESSION_KEY, SOURCED_FROM_KEY, VALENCE_NEGATIVE_KEY, isArchived } from "./types.js";
2
+ import { MEMORY_TYPE_KEY, isMemoryType } from "./types.js";
2
3
  import { extractJsonObject } from "./plan.js";
3
4
  import { compactText } from "./render.js";
4
5
  import { streamText } from "./llm-text.js";
@@ -190,6 +191,13 @@ export function filterPromotable(items, globalState, candidates, policy = DEFAUL
190
191
  skipped.push({ key: item.key, reason: "already covered globally" });
191
192
  continue;
192
193
  }
194
+ if (candidate.kind === "memory" && !isMemoryType(candidate.metadata[MEMORY_TYPE_KEY])) {
195
+ skipped.push({
196
+ key: item.key,
197
+ reason: `memory has no recall type (metadata.${MEMORY_TYPE_KEY} one of user|feedback|project|reference) — update the local entry to classify it first, then promote`,
198
+ });
199
+ continue;
200
+ }
193
201
  const scoped = projectScopedReason(`${candidate.title}\n${candidate.content}`, policy);
194
202
  if (scoped) {
195
203
  skipped.push({ key: item.key, reason: scoped });
@@ -271,9 +279,12 @@ export function splitArchiveGuards(items, candidates) {
271
279
  * too thin, no near-duplicate global entry. A blocked split is dropped (the
272
280
  * entry still archives plain) rather than half-promoting a redundancy.
273
281
  */
274
- export function splitPromoteBlocked(item, globalState, kind, policy = DEFAULT_PROMOTION_POLICY) {
282
+ export function splitPromoteBlocked(item, globalState, kind, policy = DEFAULT_PROMOTION_POLICY, candidateMetadata) {
275
283
  if (!item.promote)
276
284
  return "no split payload";
285
+ if (kind === "memory" && !isMemoryType(candidateMetadata?.[MEMORY_TYPE_KEY])) {
286
+ return `split promotion needs the source entry classified (metadata.${MEMORY_TYPE_KEY} one of user|feedback|project|reference) — update it first`;
287
+ }
277
288
  if (globalCoverageDetected(globalState, kind, { id: "", title: item.promote.title })) {
278
289
  return "split promotion duplicates a globally covered topic";
279
290
  }
@@ -454,6 +465,9 @@ Rules:
454
465
  usage (e.g. a safety policy that rarely triggers but is critical).
455
466
  - Do not promote local task state, work-in-progress notes, or content tied to
456
467
  one session's ephemeral details.
468
+ - Memory entries carry a recall type (user/feedback/project/reference): a
469
+ memory WITHOUT one cannot be promoted (the guard skips it) — verdict such
470
+ entries "keep" (or "archive" when they qualify) rather than "promote".
457
471
  - Skills: only "promote" a skill entry that is a genuinely reusable procedure
458
472
  meeting the DSH skill quality standard; one-off workflows are "archive" or
459
473
  "keep".
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-continual-evolve",
3
- "version": "0.6.1",
3
+ "version": "0.7.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": {
@@ -57,18 +57,18 @@
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@deepseek-ai/cordis": "^4.0.1",
60
- "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.7-alpha.1",
61
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.7-alpha.1",
62
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.7-alpha.1",
60
+ "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.7-rc.1",
61
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.7-rc.1",
62
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.7-rc.1",
63
63
  "@deepseek-ai/schemastery": "^3.18.1"
64
64
  },
65
65
  "devDependencies": {
66
- "@deepseek-ai/cordis": "4.0.3",
67
- "@deepseek-ai/dsh-agent": "0.1.7-alpha.1",
68
- "@deepseek-ai/dsh-commands": "0.1.7-alpha.1",
69
- "@deepseek-ai/dsh-home-paths": "0.1.7-alpha.1",
70
- "@deepseek-ai/dsh-llm": "0.1.7-alpha.1",
71
- "@deepseek-ai/dsh-tools": "0.1.7-alpha.1",
66
+ "@deepseek-ai/cordis": "4.0.4",
67
+ "@deepseek-ai/dsh-agent": "0.1.7-rc.1",
68
+ "@deepseek-ai/dsh-commands": "0.1.7-rc.1",
69
+ "@deepseek-ai/dsh-home-paths": "0.1.7-rc.1",
70
+ "@deepseek-ai/dsh-llm": "0.1.7-rc.1",
71
+ "@deepseek-ai/dsh-tools": "0.1.7-rc.1",
72
72
  "@deepseek-ai/schemastery": "3.18.3",
73
73
  "@types/node": "^22.10.0",
74
74
  "@vitest/coverage-v8": "^3.2.0",