dsh-continual-evolve 0.10.5 → 0.11.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 (69) hide show
  1. package/README.md +25 -31
  2. package/README.zh.md +18 -21
  3. package/lib/approval.d.ts +19 -6
  4. package/lib/approval.js +73 -12
  5. package/lib/audit-log.d.ts +19 -0
  6. package/lib/audit-log.js +52 -0
  7. package/lib/auto.js +1 -0
  8. package/lib/benchmark-command.d.ts +1 -1
  9. package/lib/benchmark-command.js +1 -1
  10. package/lib/command-util.d.ts +70 -0
  11. package/lib/command-util.js +123 -0
  12. package/lib/command.d.ts +7 -40
  13. package/lib/command.js +25 -459
  14. package/lib/copy.d.ts +4 -41
  15. package/lib/copy.js +0 -102
  16. package/lib/declines.d.ts +2 -0
  17. package/lib/declines.js +4 -0
  18. package/lib/evolve-event.js +2 -1
  19. package/lib/extraction-phase.d.ts +132 -0
  20. package/lib/extraction-phase.js +236 -0
  21. package/lib/index.d.ts +26 -42
  22. package/lib/index.js +24 -34
  23. package/lib/inject.d.ts +13 -119
  24. package/lib/inject.js +12 -260
  25. package/lib/injection-caps.d.ts +4 -0
  26. package/lib/injection-caps.js +4 -0
  27. package/lib/injection-directory.d.ts +29 -0
  28. package/lib/injection-directory.js +78 -0
  29. package/lib/injection-rank.d.ts +85 -0
  30. package/lib/injection-rank.js +161 -0
  31. package/lib/listener.d.ts +45 -0
  32. package/lib/listener.js +199 -0
  33. package/lib/maintenance-commands.d.ts +23 -0
  34. package/lib/maintenance-commands.js +136 -0
  35. package/lib/memory-agent.d.ts +7 -41
  36. package/lib/memory-agent.js +8 -276
  37. package/lib/memory-apply.d.ts +44 -0
  38. package/lib/memory-apply.js +202 -0
  39. package/lib/memory-manifest.d.ts +16 -0
  40. package/lib/memory-manifest.js +70 -0
  41. package/lib/mount-command.js +1 -1
  42. package/lib/notify.d.ts +9 -20
  43. package/lib/notify.js +0 -40
  44. package/lib/plan-command.d.ts +12 -0
  45. package/lib/plan-command.js +38 -0
  46. package/lib/prefix-cache.d.ts +2 -1
  47. package/lib/review-scheduler.d.ts +1 -1
  48. package/lib/review-scheduler.js +2 -10
  49. package/lib/runtime-commands.d.ts +14 -0
  50. package/lib/runtime-commands.js +115 -0
  51. package/lib/service.d.ts +16 -2
  52. package/lib/service.js +74 -1
  53. package/lib/store.d.ts +1 -2
  54. package/lib/store.js +2 -2
  55. package/lib/tool.d.ts +7 -0
  56. package/lib/tool.js +4 -4
  57. package/lib/turn-snapshot.d.ts +7 -1
  58. package/lib/turn-snapshot.js +14 -0
  59. package/lib/types.d.ts +10 -0
  60. package/lib/types.js +6 -0
  61. package/lib/wrapup-candidates.d.ts +127 -0
  62. package/lib/wrapup-candidates.js +161 -0
  63. package/lib/wrapup-command.d.ts +2 -1
  64. package/lib/wrapup-command.js +7 -4
  65. package/lib/wrapup-proposals.d.ts +101 -0
  66. package/lib/wrapup-proposals.js +281 -0
  67. package/lib/wrapup.d.ts +8 -212
  68. package/lib/wrapup.js +4 -438
  69. package/package.json +1 -1
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Wrap-up proposal builders: turn assessed verdicts into deterministic,
3
+ * evidence-carrying refinement proposals — whole promotes, split promotes,
4
+ * archive reviews, and valence stamps. Pure logic over the candidate types
5
+ * in `wrapup-candidates.ts`; the user approves, the engine applies.
6
+ */
7
+ import type { HarnessState, RefinementKind, RefinementProposal } from "./types.js";
8
+ import { type PromotionPolicy } from "./promotion.js";
9
+ import { type WrapupCandidate, type WrapupItem } from "./wrapup-candidates.js";
10
+ export interface PromotableSplit {
11
+ /** Items that may be promoted: classified promote AND not covered globally. */
12
+ promotable: WrapupItem[];
13
+ /** Items classified promote but blocked by the deterministic guard, with why. */
14
+ skipped: {
15
+ key: string;
16
+ reason: string;
17
+ }[];
18
+ }
19
+ /**
20
+ * Apply-time deterministic guard: re-check every promote verdict against the
21
+ * global store right before it lands. The LLM classification may be stale
22
+ * (a gate ran while assessing) or wrong; this ensures a promote never writes
23
+ * a duplicate, project-scoped, or too-thin global entry. Pure and unit-tested.
24
+ *
25
+ * Guards (2026-08-22 promotion policy):
26
+ * - audited candidate list + title coverage (pre-existing),
27
+ * - project-scoped content markers (absolute paths / session ids) — the
28
+ * global store is shared across projects and must stay portable,
29
+ * - thin content below the policy floor (framing outweighs the fact),
30
+ * - near-duplicate of an existing global entry by content overlap.
31
+ */
32
+ export declare function filterPromotable(items: readonly WrapupItem[], globalState: HarnessState, candidates: readonly WrapupCandidate[], policy?: PromotionPolicy): PromotableSplit;
33
+ export interface ArchiveReviewSplit {
34
+ /** Archives that may proceed silently: topic already covered, no real
35
+ * distillation source, or the archive half of an already-approved split. */
36
+ silent: WrapupItem[];
37
+ /**
38
+ * Archives that would bury possibly-reusable content: not covered
39
+ * globally AND distilled from real user messages (sourceSeqs present).
40
+ * These MAY NOT archive silently — the command must get user
41
+ * confirmation first (the symmetric guard to filterPromotable: it stops
42
+ * over-archiving, not just over-writing).
43
+ */
44
+ review: WrapupItem[];
45
+ }
46
+ /**
47
+ * The symmetric archive guard. `filterPromotable` is one-directional: it
48
+ * stops the model from WRITING duplicate global entries, but nothing stopped
49
+ * an unfounded ARCHIVE from hiding content that was actually only local.
50
+ * Guard criteria: an archive needs user confirmation when it is NOT covered
51
+ * globally AND the entry carries a real distillation source (sourceSeqs /
52
+ * sourceSession — i.e. it was distilled from actual user messages, so it
53
+ * may hold reusable value). Operational/empty entries archive silently as
54
+ * before. Split archives (archive + promote sub-object) skip this check:
55
+ * their promotion already crosses a human approval gate, so the archive is
56
+ * the completion of an approved action, not a silent burial.
57
+ */
58
+ export declare function needsArchiveReview(item: WrapupItem, candidate: WrapupCandidate): boolean;
59
+ /** Partition archive items into silent vs review-required (see needsArchiveReview). */
60
+ export declare function splitArchiveGuards(items: readonly WrapupItem[], candidates: readonly WrapupCandidate[]): ArchiveReviewSplit;
61
+ /**
62
+ * Apply-time guard for a split promotion (archive + promote sub-object):
63
+ * the cleaned payload must pass the same promotion policy as a whole
64
+ * promote — no global coverage duplicate, no project-scoped content, not
65
+ * too thin, no near-duplicate global entry. A blocked split is dropped (the
66
+ * entry still archives plain) rather than half-promoting a redundancy.
67
+ */
68
+ export declare function splitPromoteBlocked(item: WrapupItem, globalState: HarnessState, kind: RefinementKind, policy?: PromotionPolicy, candidateMetadata?: Record<string, unknown>): string | undefined;
69
+ /**
70
+ * Shared proposal builders for a WHOLE promotion — used by both the
71
+ * `/evolve wrapup` command and the gate's local-fate dimension so the two
72
+ * paths apply IDENTICAL edits (global create + local retirement stamp).
73
+ *
74
+ * The local stamp is a factory: the `promotedTo` id is only known after the
75
+ * global create lands (validation may slugify the id), so the caller applies
76
+ * the global proposal first and stamps the local copy with the created id.
77
+ */
78
+ export declare function wholePromoteProposals(item: WrapupItem, candidate: WrapupCandidate, sessionId: string): {
79
+ global: RefinementProposal;
80
+ localStamp: (createdId: string) => RefinementProposal;
81
+ };
82
+ /**
83
+ * Shared proposal builders for a SPLIT promotion (A-form): archive a mixed
84
+ * local entry but promote ONLY the cleaned durable part the model extracted.
85
+ * Same usage contract as {@link wholePromoteProposals}: apply the global
86
+ * create, then stamp the original local entry with the created id.
87
+ */
88
+ export declare function splitPromoteProposals(item: WrapupItem, candidate: WrapupCandidate, sessionId: string): {
89
+ global: RefinementProposal;
90
+ localStamp: (createdId: string) => RefinementProposal;
91
+ };
92
+ /**
93
+ * Valence stamp (P1 效价反馈): a keep-verdict item the assessor marked
94
+ * contradicted gets its negative-valence counter bumped — the entry stays
95
+ * live but sinks in injection ranking and the next assessment prefers
96
+ * archiving it. Promotes pass untouched (the human-approved global copy is
97
+ * the live truth); archives don't need the stamp (they already leave
98
+ * injection). Undefined when nothing applies.
99
+ */
100
+ export declare function valenceStampProposal(item: WrapupItem, candidate: WrapupCandidate): RefinementProposal | undefined;
101
+ //# sourceMappingURL=wrapup-proposals.d.ts.map
@@ -0,0 +1,281 @@
1
+ import { ARCHIVED_AT_KEY, PROMOTED_AT_KEY, PROMOTED_TO_KEY, SOURCE_SEQS_KEY, SOURCE_SESSION_KEY, SOURCED_FROM_KEY, VALENCE_NEGATIVE_KEY } from "./types.js";
2
+ import { MEMORY_TYPE_KEY, isMemoryType } from "./types.js";
3
+ import { DEFAULT_PROMOTION_POLICY, mostSimilarGlobalEntry, projectScopedReason, secretLeakReason } from "./promotion.js";
4
+ import { globalCoverageDetected } from "./wrapup-candidates.js";
5
+ import { candidateKey } from "./wrapup-candidates.js";
6
+ /**
7
+ * Apply-time deterministic guard: re-check every promote verdict against the
8
+ * global store right before it lands. The LLM classification may be stale
9
+ * (a gate ran while assessing) or wrong; this ensures a promote never writes
10
+ * a duplicate, project-scoped, or too-thin global entry. Pure and unit-tested.
11
+ *
12
+ * Guards (2026-08-22 promotion policy):
13
+ * - audited candidate list + title coverage (pre-existing),
14
+ * - project-scoped content markers (absolute paths / session ids) — the
15
+ * global store is shared across projects and must stay portable,
16
+ * - thin content below the policy floor (framing outweighs the fact),
17
+ * - near-duplicate of an existing global entry by content overlap.
18
+ */
19
+ export function filterPromotable(items, globalState, candidates, policy = DEFAULT_PROMOTION_POLICY) {
20
+ const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
21
+ const promotable = [];
22
+ const skipped = [];
23
+ for (const item of items) {
24
+ if (item.verdict !== "promote")
25
+ continue;
26
+ const candidate = byKey.get(item.key);
27
+ if (!candidate) {
28
+ skipped.push({ key: item.key, reason: "not in the audited candidate list" });
29
+ continue;
30
+ }
31
+ if (candidate.coveredGlobally || globalCoverageDetected(globalState, candidate.kind, candidate)) {
32
+ skipped.push({ key: item.key, reason: "already covered globally" });
33
+ continue;
34
+ }
35
+ if (candidate.kind === "memory" && !isMemoryType(candidate.metadata[MEMORY_TYPE_KEY])) {
36
+ skipped.push({
37
+ key: item.key,
38
+ 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`,
39
+ });
40
+ continue;
41
+ }
42
+ const scoped = projectScopedReason(`${candidate.title}\n${candidate.content}`, policy);
43
+ if (scoped) {
44
+ skipped.push({ key: item.key, reason: scoped });
45
+ continue;
46
+ }
47
+ // Metadata screens too: wholePromoteProposals copies candidate.metadata
48
+ // verbatim into the global entry, so a credential planted in local
49
+ // metadata must not ride the promotion path into the shared store
50
+ // (review audit 2026-08-28 B2).
51
+ const secret = secretLeakReason([candidate.title, candidate.content, JSON.stringify(candidate.metadata ?? {})].join("\n"));
52
+ if (secret) {
53
+ skipped.push({ key: item.key, reason: secret });
54
+ continue;
55
+ }
56
+ if (candidate.content.length < policy.minPromoteChars) {
57
+ skipped.push({
58
+ key: item.key,
59
+ reason: `too thin to promote (${candidate.content.length} < ${policy.minPromoteChars} chars) — keep local or merge`,
60
+ });
61
+ continue;
62
+ }
63
+ const similar = mostSimilarGlobalEntry(globalState, candidate.kind, candidate.title, candidate.content, policy);
64
+ if (similar) {
65
+ skipped.push({
66
+ key: item.key,
67
+ reason: `near-duplicate of global ${candidate.kind}:${similar.id} "${similar.title}" (overlap ${similar.score.toFixed(2)}) — update that entry instead`,
68
+ });
69
+ continue;
70
+ }
71
+ promotable.push(item);
72
+ }
73
+ return { promotable, skipped };
74
+ }
75
+ /**
76
+ * The symmetric archive guard. `filterPromotable` is one-directional: it
77
+ * stops the model from WRITING duplicate global entries, but nothing stopped
78
+ * an unfounded ARCHIVE from hiding content that was actually only local.
79
+ * Guard criteria: an archive needs user confirmation when it is NOT covered
80
+ * globally AND the entry carries a real distillation source (sourceSeqs /
81
+ * sourceSession — i.e. it was distilled from actual user messages, so it
82
+ * may hold reusable value). Operational/empty entries archive silently as
83
+ * before. Split archives (archive + promote sub-object) skip this check:
84
+ * their promotion already crosses a human approval gate, so the archive is
85
+ * the completion of an approved action, not a silent burial.
86
+ */
87
+ export function needsArchiveReview(item, candidate) {
88
+ if (item.verdict !== "archive")
89
+ return false;
90
+ if (item.promote)
91
+ return false;
92
+ if (candidate.coveredGlobally)
93
+ return false;
94
+ const seqs = candidate.metadata[SOURCE_SEQS_KEY];
95
+ const session = candidate.metadata[SOURCE_SESSION_KEY];
96
+ return (Array.isArray(seqs) && seqs.length > 0) || (typeof session === "string" && session.length > 0);
97
+ }
98
+ /** Partition archive items into silent vs review-required (see needsArchiveReview). */
99
+ export function splitArchiveGuards(items, candidates) {
100
+ const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
101
+ const silent = [];
102
+ const review = [];
103
+ for (const item of items) {
104
+ if (item.verdict !== "archive")
105
+ continue;
106
+ const candidate = byKey.get(item.key);
107
+ if (candidate && needsArchiveReview(item, candidate)) {
108
+ review.push(item);
109
+ }
110
+ else {
111
+ silent.push(item);
112
+ }
113
+ }
114
+ return { silent, review };
115
+ }
116
+ /**
117
+ * Apply-time guard for a split promotion (archive + promote sub-object):
118
+ * the cleaned payload must pass the same promotion policy as a whole
119
+ * promote — no global coverage duplicate, no project-scoped content, not
120
+ * too thin, no near-duplicate global entry. A blocked split is dropped (the
121
+ * entry still archives plain) rather than half-promoting a redundancy.
122
+ */
123
+ export function splitPromoteBlocked(item, globalState, kind, policy = DEFAULT_PROMOTION_POLICY, candidateMetadata) {
124
+ if (!item.promote)
125
+ return "no split payload";
126
+ if (kind === "memory" && !isMemoryType(candidateMetadata?.[MEMORY_TYPE_KEY])) {
127
+ return `split promotion needs the source entry classified (metadata.${MEMORY_TYPE_KEY} one of user|feedback|project|reference) — update it first`;
128
+ }
129
+ if (globalCoverageDetected(globalState, kind, { id: "", title: item.promote.title })) {
130
+ return "split promotion duplicates a globally covered topic";
131
+ }
132
+ const scoped = projectScopedReason(`${item.promote.title}\n${item.promote.content}`, policy);
133
+ if (scoped)
134
+ return `split promotion is ${scoped}`;
135
+ const secret = secretLeakReason([item.promote.title, item.promote.content].join("\n"));
136
+ if (secret)
137
+ return `split promotion blocked: ${secret}`;
138
+ if (item.promote.content.length < policy.minPromoteChars) {
139
+ return `split promotion too thin (${item.promote.content.length} < ${policy.minPromoteChars} chars)`;
140
+ }
141
+ const similar = mostSimilarGlobalEntry(globalState, kind, item.promote.title, item.promote.content, policy);
142
+ if (similar) {
143
+ return `split promotion near-duplicates global ${kind}:${similar.id} "${similar.title}" (overlap ${similar.score.toFixed(2)})`;
144
+ }
145
+ return undefined;
146
+ }
147
+ /**
148
+ * Shared proposal builders for a WHOLE promotion — used by both the
149
+ * `/evolve wrapup` command and the gate's local-fate dimension so the two
150
+ * paths apply IDENTICAL edits (global create + local retirement stamp).
151
+ *
152
+ * The local stamp is a factory: the `promotedTo` id is only known after the
153
+ * global create lands (validation may slugify the id), so the caller applies
154
+ * the global proposal first and stamps the local copy with the created id.
155
+ */
156
+ export function wholePromoteProposals(item, candidate, sessionId) {
157
+ const now = new Date().toISOString();
158
+ return {
159
+ global: {
160
+ summary: `wrapup: promote local ${item.key} to the global store`,
161
+ rationale: item.reason,
162
+ expectedOutcome: `The entry is now visible to every session via the global store (sourcedFromLocal=${sessionId}:${candidate.id}).`,
163
+ edits: [
164
+ {
165
+ action: "create",
166
+ kind: candidate.kind,
167
+ id: candidate.id,
168
+ title: candidate.title,
169
+ content: candidate.content,
170
+ path: candidate.path,
171
+ metadata: {
172
+ ...candidate.metadata,
173
+ [SOURCED_FROM_KEY]: `${sessionId}:${candidate.id}`,
174
+ [PROMOTED_AT_KEY]: now,
175
+ },
176
+ },
177
+ ],
178
+ },
179
+ localStamp: (createdId) => ({
180
+ summary: `wrapup: stamp local ${item.key} as promoted to ${createdId} and retire it from injection`,
181
+ rationale: item.reason,
182
+ expectedOutcome: `The local copy keeps its data but stops being injected; the global copy is the live one.`,
183
+ edits: [
184
+ {
185
+ action: "update",
186
+ kind: candidate.kind,
187
+ id: candidate.id,
188
+ title: candidate.title,
189
+ content: candidate.content,
190
+ metadata: {
191
+ ...candidate.metadata,
192
+ [PROMOTED_TO_KEY]: createdId,
193
+ [PROMOTED_AT_KEY]: now,
194
+ [ARCHIVED_AT_KEY]: now,
195
+ },
196
+ },
197
+ ],
198
+ }),
199
+ };
200
+ }
201
+ /**
202
+ * Shared proposal builders for a SPLIT promotion (A-form): archive a mixed
203
+ * local entry but promote ONLY the cleaned durable part the model extracted.
204
+ * Same usage contract as {@link wholePromoteProposals}: apply the global
205
+ * create, then stamp the original local entry with the created id.
206
+ */
207
+ export function splitPromoteProposals(item, candidate, sessionId) {
208
+ if (!item.promote)
209
+ throw new Error("split promote proposals require a promote payload");
210
+ const now = new Date().toISOString();
211
+ return {
212
+ global: {
213
+ summary: `wrapup: split — promote cleaned part of ${item.key} to the global store`,
214
+ rationale: item.reason,
215
+ expectedOutcome: `Only the durable part becomes visible globally; the snapshot half stays archived with the original.`,
216
+ edits: [
217
+ {
218
+ action: "create",
219
+ kind: candidate.kind,
220
+ id: candidate.id,
221
+ title: item.promote.title,
222
+ content: item.promote.content,
223
+ path: candidate.path,
224
+ metadata: {
225
+ ...candidate.metadata,
226
+ [SOURCED_FROM_KEY]: `${sessionId}:${candidate.id}`,
227
+ [PROMOTED_AT_KEY]: now,
228
+ },
229
+ },
230
+ ],
231
+ },
232
+ localStamp: (createdId) => ({
233
+ summary: `wrapup: split — archive original ${item.key}, stamped as promoted to ${createdId}`,
234
+ rationale: item.reason,
235
+ expectedOutcome: `The original leaves injection (data kept, restorable); the cleaned global copy is the live one.`,
236
+ edits: [
237
+ {
238
+ action: "update",
239
+ kind: candidate.kind,
240
+ id: candidate.id,
241
+ title: candidate.title,
242
+ content: candidate.content,
243
+ metadata: {
244
+ ...candidate.metadata,
245
+ [PROMOTED_TO_KEY]: createdId,
246
+ [PROMOTED_AT_KEY]: now,
247
+ [ARCHIVED_AT_KEY]: now,
248
+ },
249
+ },
250
+ ],
251
+ }),
252
+ };
253
+ }
254
+ /**
255
+ * Valence stamp (P1 效价反馈): a keep-verdict item the assessor marked
256
+ * contradicted gets its negative-valence counter bumped — the entry stays
257
+ * live but sinks in injection ranking and the next assessment prefers
258
+ * archiving it. Promotes pass untouched (the human-approved global copy is
259
+ * the live truth); archives don't need the stamp (they already leave
260
+ * injection). Undefined when nothing applies.
261
+ */
262
+ export function valenceStampProposal(item, candidate) {
263
+ if (item.verdict !== "keep" || !item.contradicted)
264
+ return undefined;
265
+ return {
266
+ summary: `wrapup: stamp contradicted valence on ${item.key} (${candidate.negativeCount + 1})`,
267
+ rationale: item.reason,
268
+ expectedOutcome: "The entry's negative-valence counter rises; injection downranks it and the next assessment prefers archiving it.",
269
+ edits: [
270
+ {
271
+ action: "update",
272
+ kind: candidate.kind,
273
+ id: candidate.id,
274
+ title: candidate.title,
275
+ content: candidate.content,
276
+ metadata: { ...candidate.metadata, [VALENCE_NEGATIVE_KEY]: candidate.negativeCount + 1 },
277
+ },
278
+ ],
279
+ };
280
+ }
281
+ //# sourceMappingURL=wrapup-proposals.js.map
package/lib/wrapup.d.ts CHANGED
@@ -17,223 +17,19 @@
17
17
  * classifies, the user approves, the code applies deterministically. The
18
18
  * apply-side guard (`filterPromotable`) re-checks global coverage at apply
19
19
  * time so a stale classification can never write a duplicate global entry.
20
+ *
21
+ * Split (2026-10-03 refactor): the candidate domain lives in
22
+ * `wrapup-candidates.ts`, the proposal builders in `wrapup-proposals.ts`;
23
+ * this module owns the bounded LLM assessment call. Moved symbols are
24
+ * re-exported here so the public surface stays on one module.
20
25
  */
21
26
  import type { Context } from "@deepseek-ai/cordis";
22
27
  import type { Agent } from "@deepseek-ai/dsh-agent";
23
- import type { HarnessEntry, HarnessState, RefinementKind, RefinementProposal } from "./types.js";
24
28
  import { type TokenUsageTarget } from "./token-usage.js";
25
29
  import { type RecordLanguage } from "./record-language.js";
26
- import { type PromotionPolicy } from "./promotion.js";
27
- /** What should happen to one local entry at session end. */
28
- export type WrapupVerdict = "promote" | "archive" | "keep";
29
- /** A classified local entry: `key` matches one audited candidate exactly. */
30
- export interface WrapupItem {
31
- /** `kind:id` of the candidate this verdict refers to. */
32
- key: string;
33
- verdict: WrapupVerdict;
34
- reason: string;
35
- /**
36
- * Optional split-promotion payload (verdict "archive" only): the entry is
37
- * archived as a whole, but a CLEANED cross-session-reusable part is
38
- * offered for promotion — the durable fact distilled out of the mixed
39
- * entry, with the ephemeral snapshot left behind in the archive.
40
- */
41
- promote?: {
42
- title: string;
43
- content: string;
44
- };
45
- /**
46
- * Valence signal (P1 效价反馈): the assessor found trajectory or global
47
- * evidence contradicting this entry's content (user correction, overridden
48
- * fact). Strictly parsed — only an explicit JSON `true` sets it. Keep
49
- * verdicts with this flag get a negative-valence stamp; the assessor is
50
- * instructed to prefer "archive" for contradicted entries.
51
- */
52
- contradicted?: boolean;
53
- }
54
- /** A real global entry worth showing the assessor for the same topic. */
55
- export interface GlobalHint {
56
- id: string;
57
- title: string;
58
- }
59
- /** The model's full classification of a session's local entries. */
60
- export interface WrapupAssessment {
61
- items: WrapupItem[];
62
- rationale: string;
63
- }
64
- /** A local entry offered for assessment, plus its deterministic audit flags. */
65
- export interface WrapupCandidate {
66
- kind: RefinementKind;
67
- id: string;
68
- title: string;
69
- content: string;
70
- path: string;
71
- version: number;
72
- metadata: Record<string, unknown>;
73
- /**
74
- * True when the global store already covers this topic by a STRONG
75
- * signal: a title that normalizes equal to, or (beyond a length floor)
76
- * contains, the candidate's title. Collisions on id alone with a wildly
77
- * different title are deliberately NOT coverage — see {@link globalHintsFor}.
78
- */
79
- coveredGlobally: boolean;
80
- /**
81
- * Actual global entries that touch the same topic (same id, equal
82
- * normalized title, or title overlap). Shown to the assessor so it judges
83
- * against real titles instead of a bare boolean; a bare same-id collision
84
- * shows up here precisely so the model can tell whether the global copy
85
- * really covers the local content.
86
- */
87
- globalHints: GlobalHint[];
88
- /**
89
- * Injection usage count (gap B1): how many times this entry was included
90
- * in a system-prompt assembly. Zero means the entry was never used — a
91
- * strong staleness signal the assessor can weigh.
92
- */
93
- injectionCount: number;
94
- /**
95
- * Staleness flag (gap B2): true when the entry has both zero injection
96
- * usage AND a recency score below the staleness threshold (old + unused).
97
- * The assessor is instructed to prefer "archive" for stale entries.
98
- */
99
- stale: boolean;
100
- /**
101
- * Negative-valence counter (P1 效价反馈): how many prior assessments marked
102
- * this entry contradicted (metadata {@link VALENCE_NEGATIVE_KEY}). Shown to
103
- * the assessor so it can prefer "archive"; also demotes the entry in
104
- * injection ranking (inject.ts rankEntries).
105
- */
106
- negativeCount: number;
107
- }
108
- export declare function candidateKey(kind: RefinementKind, id: string): string;
109
- /**
110
- * Deterministic global-coverage check (STRONG signal): the global store
111
- * already covers a topic when it holds a title that normalizes equal to, or
112
- * (beyond a length floor) contains, the candidate's normalized title.
113
- * Archived global entries count too — the topic was already judged
114
- * cross-session; a local duplicate would only re-sediment it.
115
- *
116
- * The bare same-id case is deliberately NOT coverage: ids are slugs derived
117
- * from titles, so a real collision is usually caught by the title check
118
- * below. A same-id entry with a wildly different title is a weak signal — the
119
- * caller routes it through {@link globalHintsFor} for the assessor to judge
120
- * against the actual global title (real case: local `memory` "用户产品愿景与
121
- * 收入需求(本会话)" vs global `memory` "用户画像(持续更新)").
122
- */
123
- export declare function globalCoverageDetected(globalState: HarnessState, kind: RefinementKind, entry: Pick<HarnessEntry, "id" | "title">): boolean;
124
- /**
125
- * The actual global entries that touch the same topic as a local candidate:
126
- * same id (regardless of title — the weak collision signal that is NOT
127
- * coverage on its own), equal normalized title, or title overlap. The raw ids
128
- * and titles let the assessor judge enrichment against real global content
129
- * (does the global copy already hold what the local one adds?) rather than a
130
- * bare boolean. Bounded: a handful of best matches, never the whole store.
131
- */
132
- export declare function globalHintsFor(globalState: HarnessState, kind: RefinementKind, entry: Pick<HarnessEntry, "id" | "title">): GlobalHint[];
133
- export declare function listLocalCandidates(state: HarnessState, globalState: HarnessState, baseDir?: string): WrapupCandidate[];
134
- /**
135
- * Parse and validate the model's assessment JSON. Defense is mechanical:
136
- * keys outside the candidate list are dropped, verdicts outside the enum
137
- * collapse to "keep", and candidates the model omitted default to "keep" —
138
- * a malformed reply can never change an entry's fate by itself.
139
- *
140
- * Split promotion (verdict "archive" with a `promote` sub-object): the
141
- * sub-object is accepted ONLY on archive verdicts and ONLY when both cleaned
142
- * title and content are non-empty strings — a dropped/malformed sub-object
143
- * silently degrades to a plain archive (the entry is never half-promoted).
144
- */
145
- export declare function parseWrapupAssessment(text: string, candidates: readonly WrapupCandidate[]): WrapupAssessment;
146
- export interface PromotableSplit {
147
- /** Items that may be promoted: classified promote AND not covered globally. */
148
- promotable: WrapupItem[];
149
- /** Items classified promote but blocked by the deterministic guard, with why. */
150
- skipped: {
151
- key: string;
152
- reason: string;
153
- }[];
154
- }
155
- /**
156
- * Apply-time deterministic guard: re-check every promote verdict against the
157
- * global store right before it lands. The LLM classification may be stale
158
- * (a gate ran while assessing) or wrong; this ensures a promote never writes
159
- * a duplicate, project-scoped, or too-thin global entry. Pure and unit-tested.
160
- *
161
- * Guards (2026-08-22 promotion policy):
162
- * - audited candidate list + title coverage (pre-existing),
163
- * - project-scoped content markers (absolute paths / session ids) — the
164
- * global store is shared across projects and must stay portable,
165
- * - thin content below the policy floor (framing outweighs the fact),
166
- * - near-duplicate of an existing global entry by content overlap.
167
- */
168
- export declare function filterPromotable(items: readonly WrapupItem[], globalState: HarnessState, candidates: readonly WrapupCandidate[], policy?: PromotionPolicy): PromotableSplit;
169
- export interface ArchiveReviewSplit {
170
- /** Archives that may proceed silently: topic already covered, no real
171
- * distillation source, or the archive half of an already-approved split. */
172
- silent: WrapupItem[];
173
- /**
174
- * Archives that would bury possibly-reusable content: not covered
175
- * globally AND distilled from real user messages (sourceSeqs present).
176
- * These MAY NOT archive silently — the command must get user
177
- * confirmation first (the symmetric guard to filterPromotable: it stops
178
- * over-archiving, not just over-writing).
179
- */
180
- review: WrapupItem[];
181
- }
182
- /**
183
- * The symmetric archive guard. `filterPromotable` is one-directional: it
184
- * stops the model from WRITING duplicate global entries, but nothing stopped
185
- * an unfounded ARCHIVE from hiding content that was actually only local.
186
- * Guard criteria: an archive needs user confirmation when it is NOT covered
187
- * globally AND the entry carries a real distillation source (sourceSeqs /
188
- * sourceSession — i.e. it was distilled from actual user messages, so it
189
- * may hold reusable value). Operational/empty entries archive silently as
190
- * before. Split archives (archive + promote sub-object) skip this check:
191
- * their promotion already crosses a human approval gate, so the archive is
192
- * the completion of an approved action, not a silent burial.
193
- */
194
- export declare function needsArchiveReview(item: WrapupItem, candidate: WrapupCandidate): boolean;
195
- /** Partition archive items into silent vs review-required (see needsArchiveReview). */
196
- export declare function splitArchiveGuards(items: readonly WrapupItem[], candidates: readonly WrapupCandidate[]): ArchiveReviewSplit;
197
- /**
198
- * Apply-time guard for a split promotion (archive + promote sub-object):
199
- * the cleaned payload must pass the same promotion policy as a whole
200
- * promote — no global coverage duplicate, no project-scoped content, not
201
- * too thin, no near-duplicate global entry. A blocked split is dropped (the
202
- * entry still archives plain) rather than half-promoting a redundancy.
203
- */
204
- export declare function splitPromoteBlocked(item: WrapupItem, globalState: HarnessState, kind: RefinementKind, policy?: PromotionPolicy, candidateMetadata?: Record<string, unknown>): string | undefined;
205
- /**
206
- * Shared proposal builders for a WHOLE promotion — used by both the
207
- * `/evolve wrapup` command and the gate's local-fate dimension so the two
208
- * paths apply IDENTICAL edits (global create + local retirement stamp).
209
- *
210
- * The local stamp is a factory: the `promotedTo` id is only known after the
211
- * global create lands (validation may slugify the id), so the caller applies
212
- * the global proposal first and stamps the local copy with the created id.
213
- */
214
- export declare function wholePromoteProposals(item: WrapupItem, candidate: WrapupCandidate, sessionId: string): {
215
- global: RefinementProposal;
216
- localStamp: (createdId: string) => RefinementProposal;
217
- };
218
- /**
219
- * Shared proposal builders for a SPLIT promotion (A-form): archive a mixed
220
- * local entry but promote ONLY the cleaned durable part the model extracted.
221
- * Same usage contract as {@link wholePromoteProposals}: apply the global
222
- * create, then stamp the original local entry with the created id.
223
- */
224
- export declare function splitPromoteProposals(item: WrapupItem, candidate: WrapupCandidate, sessionId: string): {
225
- global: RefinementProposal;
226
- localStamp: (createdId: string) => RefinementProposal;
227
- };
228
- /**
229
- * Valence stamp (P1 效价反馈): a keep-verdict item the assessor marked
230
- * contradicted gets its negative-valence counter bumped — the entry stays
231
- * live but sinks in injection ranking and the next assessment prefers
232
- * archiving it. Promotes pass untouched (the human-approved global copy is
233
- * the live truth); archives don't need the stamp (they already leave
234
- * injection). Undefined when nothing applies.
235
- */
236
- export declare function valenceStampProposal(item: WrapupItem, candidate: WrapupCandidate): RefinementProposal | undefined;
30
+ import { type WrapupAssessment, type WrapupCandidate } from "./wrapup-candidates.js";
31
+ export * from "./wrapup-candidates.js";
32
+ export * from "./wrapup-proposals.js";
237
33
  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\".";
238
34
  export interface AssessOptions {
239
35
  /** Output token budget for the assessment call. */