dsh-continual-evolve 0.1.1 → 0.3.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 (64) hide show
  1. package/README.md +172 -17
  2. package/README.zh.md +87 -10
  3. package/lib/apply.js +3 -1
  4. package/lib/approval.d.ts +25 -0
  5. package/lib/approval.js +9 -1
  6. package/lib/auto.d.ts +99 -5
  7. package/lib/auto.js +165 -6
  8. package/lib/benchmark-command.d.ts +9 -0
  9. package/lib/benchmark-command.js +331 -0
  10. package/lib/benchmark.d.ts +84 -0
  11. package/lib/benchmark.js +107 -1
  12. package/lib/command.js +33 -221
  13. package/lib/evaluate.d.ts +43 -7
  14. package/lib/evaluate.js +172 -43
  15. package/lib/evolve-event.d.ts +38 -0
  16. package/lib/evolve-event.js +49 -0
  17. package/lib/failures.d.ts +39 -0
  18. package/lib/failures.js +170 -0
  19. package/lib/fate.d.ts +128 -0
  20. package/lib/fate.js +342 -0
  21. package/lib/goal-command.d.ts +7 -0
  22. package/lib/goal-command.js +37 -0
  23. package/lib/index.d.ts +51 -21
  24. package/lib/index.js +32 -2
  25. package/lib/inject.d.ts +8 -0
  26. package/lib/inject.js +51 -4
  27. package/lib/llm-text.d.ts +30 -0
  28. package/lib/llm-text.js +49 -0
  29. package/lib/mount-command.d.ts +10 -0
  30. package/lib/mount-command.js +48 -0
  31. package/lib/mount.js +5 -0
  32. package/lib/plan.js +5 -0
  33. package/lib/planner.d.ts +8 -1
  34. package/lib/planner.js +40 -39
  35. package/lib/render.d.ts +1 -3
  36. package/lib/render.js +2 -5
  37. package/lib/review.d.ts +5 -2
  38. package/lib/review.js +27 -38
  39. package/lib/rollback.d.ts +1 -3
  40. package/lib/rollback.js +0 -8
  41. package/lib/score.d.ts +37 -4
  42. package/lib/score.js +120 -10
  43. package/lib/service.d.ts +2 -2
  44. package/lib/service.js +5 -2
  45. package/lib/skill-render.d.ts +15 -0
  46. package/lib/skill-render.js +30 -0
  47. package/lib/skill.d.ts +12 -7
  48. package/lib/skill.js +36 -31
  49. package/lib/skillquality.d.ts +80 -0
  50. package/lib/skillquality.js +311 -0
  51. package/lib/store.d.ts +1 -3
  52. package/lib/store.js +0 -7
  53. package/lib/tool.js +28 -4
  54. package/lib/types.d.ts +39 -0
  55. package/lib/types.js +19 -0
  56. package/lib/usage.d.ts +32 -0
  57. package/lib/usage.js +84 -0
  58. package/lib/validate.d.ts +12 -2
  59. package/lib/validate.js +51 -2
  60. package/lib/wrapup-command.d.ts +8 -0
  61. package/lib/wrapup-command.js +211 -0
  62. package/lib/wrapup.d.ts +215 -0
  63. package/lib/wrapup.js +427 -0
  64. package/package.json +8 -8
package/lib/validate.js CHANGED
@@ -1,8 +1,27 @@
1
+ import { validateSkillEntryContent } from "./skillquality.js";
1
2
  const ACTIONS = new Set(["create", "update", "delete", "archive"]);
2
3
  const KINDS = new Set(["prompt", "memory", "skill", "subagent"]);
3
4
  export const BASE_SYSTEM_PROMPT_ID = "base_system_prompt";
5
+ /**
6
+ * Gap C2: mechanical check that an edit's declared blast radius is coherent
7
+ * with the scope it targets. A session-scoped edit claiming "general" would
8
+ * silently read like a cross-project tactical rule; a global edit claiming
9
+ * "session" would contradict its persistence. Absent blastRadius is NOT
10
+ * rejected (pre-C2 data and manual edits stay compatible) — the planner is
11
+ * instructed to always declare it, and this rule catches what it declares
12
+ * incoherently.
13
+ */
14
+ export function validateBlastRadiusScope(scope, blastRadius) {
15
+ if (scope === "local" && blastRadius === "general") {
16
+ return "local-scope edit must declare blastRadius \"session\" or \"project\" (\"general\" would claim a cross-project rule)";
17
+ }
18
+ if (scope === "global" && blastRadius === "session") {
19
+ return "global-scope edit must declare blastRadius \"general\" or \"project\" (\"session\" contradicts cross-session persistence)";
20
+ }
21
+ return undefined;
22
+ }
4
23
  /** Returns a human-readable failure reason, or undefined when the edit passes. */
5
- export function validateEdit(edit, computedId) {
24
+ export function validateEdit(edit, computedId, scope) {
6
25
  if (!ACTIONS.has(edit.action)) {
7
26
  return `unsupported action ${String(edit.action)}`;
8
27
  }
@@ -15,6 +34,13 @@ export function validateEdit(edit, computedId) {
15
34
  if (edit.action !== "create" && !edit.id) {
16
35
  return `${edit.action} requires id`;
17
36
  }
37
+ // Gap C2: blast-radius/scope coherence is a mechanical property of the
38
+ // edit payload itself — checked for every action, not only create/update.
39
+ if (scope && edit.blastRadius !== undefined) {
40
+ const blastError = validateBlastRadiusScope(scope, edit.blastRadius);
41
+ if (blastError)
42
+ return blastError;
43
+ }
18
44
  // Archive only names an existing entry: no title/content payload, and the
19
45
  // base system prompt stays immutable under every action.
20
46
  if (edit.action === "archive") {
@@ -24,7 +50,30 @@ export function validateEdit(edit, computedId) {
24
50
  return `${edit.action} requires title and content`;
25
51
  }
26
52
  if (edit.action !== "delete" && edit.kind === "skill") {
27
- return validateSkillContract(edit);
53
+ // Guidance skills are SKILL.md documents: no python reference (a
54
+ // reference on a guidance skill would be an invented contract) and
55
+ // no arguments contract. Executable skills keep the full contract.
56
+ if (edit.skill_kind === "guidance") {
57
+ if (edit.reference !== undefined && Object.keys(edit.reference).length > 0) {
58
+ return "guidance skill must not carry a python reference (it is a SKILL.md document, not an executable)";
59
+ }
60
+ if (edit.arguments !== undefined && Object.keys(edit.arguments).length > 0) {
61
+ return "guidance skill must not carry an arguments contract (only executable skills declare inputs)";
62
+ }
63
+ }
64
+ else {
65
+ const contractError = validateSkillContract(edit);
66
+ if (contractError)
67
+ return contractError;
68
+ }
69
+ // The entry body materializes as a SKILL.md under generated
70
+ // frontmatter; content-level mechanics (no shadowing `---`, no
71
+ // escaping resource refs) are code-enforced so a bad body never
72
+ // reaches the store (mirrors skill-creator's validate-frontmatter).
73
+ const contentProblems = validateSkillEntryContent(edit.content ?? "");
74
+ if (contentProblems.length > 0) {
75
+ return contentProblems.join("; ");
76
+ }
28
77
  }
29
78
  return undefined;
30
79
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The `/evolve wrapup` subcommand handler. Extracted from command.ts (P2-2).
3
+ */
4
+ import type { Context } from "@deepseek-ai/cordis";
5
+ import type { CommandInvocation, CommandResult } from "@deepseek-ai/dsh-commands";
6
+ import type { EvolutionEngine } from "./service.js";
7
+ export declare function executeWrapupCommand(ctx: Context, engine: EvolutionEngine, invocation: CommandInvocation): Promise<CommandResult>;
8
+ //# sourceMappingURL=wrapup-command.d.ts.map
@@ -0,0 +1,211 @@
1
+ import { questionServiceOf, requireGlobalApproval } from "./approval.js";
2
+ import { assessLocalEntries, candidateKey, filterPromotable, listLocalCandidates, splitArchiveGuards, splitPromoteBlocked, splitPromoteProposals, wholePromoteProposals } from "./wrapup.js";
3
+ function success(text) {
4
+ return { kind: "success", text };
5
+ }
6
+ export async function executeWrapupCommand(ctx, engine, invocation) {
7
+ const sessionId = invocation.agent.id;
8
+ const localState = engine.load("local", sessionId);
9
+ const globalState = engine.load("global", undefined);
10
+ const candidates = listLocalCandidates(localState, globalState, engine.baseDir);
11
+ if (candidates.length === 0) {
12
+ return success(`(nothing to wrap up: ${sessionId}'s local store has no active, un-promoted entries — use /evolve list to inspect it)`);
13
+ }
14
+ // 1. Classify: the model judges each audited candidate's fate.
15
+ const assessment = await assessLocalEntries(ctx, invocation.agent, candidates, { signal: invocation.signal });
16
+ const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
17
+ // 2. Partition by action. Deterministic guards re-check the LIVE global
18
+ // store right before anything lands (state may have changed mid-call).
19
+ const { promotable, skipped } = filterPromotable(assessment.items, globalState, candidates);
20
+ const promoteItems = promotable.filter((item) => item.verdict === "promote");
21
+ const archiveItems = assessment.items.filter((item) => item.verdict === "archive");
22
+ // Split promotion (A-form): archive a mixed entry but promote ONLY the
23
+ // cleaned durable part the model extracted. Guarded the same way as whole
24
+ // promotes — a split that would duplicate a globally covered topic is
25
+ // dropped and the entry archives plain.
26
+ const splitItems = [];
27
+ const splitSkipped = [];
28
+ for (const item of archiveItems) {
29
+ if (!item.promote)
30
+ continue;
31
+ const candidate = byKey.get(item.key);
32
+ if (!candidate) {
33
+ splitSkipped.push({ key: item.key, reason: "not in the audited candidate list" });
34
+ continue;
35
+ }
36
+ const blocked = splitPromoteBlocked(item, globalState, candidate.kind);
37
+ if (blocked) {
38
+ splitSkipped.push({ key: item.key, reason: blocked });
39
+ continue;
40
+ }
41
+ splitItems.push({ item, candidate });
42
+ }
43
+ // Plain archives (no split payload): the symmetric guard — an archive that
44
+ // is NOT globally covered AND was distilled from real user messages must
45
+ // not proceed silently.
46
+ const plainArchives = archiveItems.filter((item) => !item.promote);
47
+ const { silent: silentArchives, review: reviewArchives } = splitArchiveGuards(plainArchives, candidates);
48
+ const keepItems = assessment.items.filter((item) => item.verdict === "keep");
49
+ // 3. Report the assessment before touching anything.
50
+ const lines = [
51
+ `wrapup assessment (${sessionId}): ${candidates.length} candidates${candidates.some((c) => c.coveredGlobally) ? `, ${candidates.filter((c) => c.coveredGlobally).length} covered globally` : ""}`,
52
+ `${assessment.rationale}`,
53
+ ];
54
+ for (const [heading, items] of [
55
+ ["PROMOTE (to global)", promoteItems],
56
+ ["SPLIT (archive + promote durable part)", splitItems.map((split) => split.item)],
57
+ ["ARCHIVE", silentArchives],
58
+ ["ARCHIVE (needs review)", reviewArchives],
59
+ ["KEEP", keepItems],
60
+ ]) {
61
+ lines.push(`${heading}: ${items.length}`);
62
+ for (const item of items) {
63
+ const candidate = byKey.get(item.key);
64
+ const title = candidate ? candidate.title : item.key;
65
+ const splitNote = item.promote ? ` → 拆出提升「${item.promote.title}」` : "";
66
+ lines.push(`- ${item.key} "${title}"${splitNote} — ${item.reason}`);
67
+ }
68
+ }
69
+ for (const skip of skipped) {
70
+ lines.push(`- promote skipped: ${skip.key} — ${skip.reason}`);
71
+ }
72
+ for (const skip of splitSkipped) {
73
+ lines.push(`- split skipped: ${skip.key} — ${skip.reason}`);
74
+ }
75
+ lines.push("");
76
+ const applied = [];
77
+ // 4. Global writes: governed resource — ONE human approval gate covers
78
+ // every create (whole promotes AND split promotions). On approval:
79
+ // - whole promote → create global copy + stamp local promotedTo+archivedAt;
80
+ // - split → create the cleaned durable part + archive the original with
81
+ // promotedTo. On rejection: whole promotes are not written, and each
82
+ // split's original STILL archives plain (its snapshot half deserves
83
+ // the archive; the durable half is reported for manual handling).
84
+ const wholeCreates = promoteItems.map((item) => ({ item, candidate: byKey.get(item.key) }));
85
+ const splitCreates = splitItems;
86
+ const allCreates = new Set([...wholeCreates.map((c) => c.item.key), ...splitCreates.map((c) => c.item.key)]);
87
+ if (allCreates.size > 0) {
88
+ const what = `wrapup 将写入跨会话 global store(共 ${allCreates.size} 条:${promoteItems.length} 条整条提升 + ${splitItems.length} 条拆解提升):\n${[
89
+ ...promoteItems.map((item) => `- 整条提升 ${item.key} "${byKey.get(item.key)?.title ?? item.key}"`),
90
+ ...splitItems.map((split) => `- 拆解提升 ${split.item.key} → 清洗「${split.item.promote?.title}」(原条目随之归档)`),
91
+ ].join("\n")}`;
92
+ let promoteAllowed = true;
93
+ try {
94
+ await requireGlobalApproval(ctx, invocation.agent, invocation.signal, what);
95
+ }
96
+ catch (cause) {
97
+ promoteAllowed = false;
98
+ const message = `global 写入未批准 — 整条提升与拆解提升均未写入 (${cause instanceof Error ? cause.message : String(cause)})`;
99
+ applied.push(message);
100
+ lines.push(message);
101
+ }
102
+ if (promoteAllowed) {
103
+ // Whole promotes: create global entry, retire the local copy.
104
+ // Shared proposal builders keep the wrap-up command and the gate's
105
+ // local-fate dimension writing IDENTICAL edits.
106
+ for (const { item, candidate } of wholeCreates) {
107
+ if (!candidate)
108
+ continue;
109
+ const proposals = wholePromoteProposals(item, candidate, sessionId);
110
+ const globalResult = engine.apply("global", undefined, proposals.global, { scope: "global" });
111
+ const createdId = globalResult.appliedEdits.find((edit) => edit.applied)?.id ?? candidate.id;
112
+ const localResult = engine.apply("local", sessionId, proposals.localStamp(createdId), {
113
+ scope: "local",
114
+ baselineState: localState,
115
+ });
116
+ applied.push(`promoted ${item.key} → global:${createdId} (${globalResult.id}; local stamped ${localResult.id})`);
117
+ }
118
+ // Split promotions: create the cleaned durable part, retire the
119
+ // original local entry (its snapshot half is archived along).
120
+ for (const { item, candidate } of splitCreates) {
121
+ if (!item.promote)
122
+ continue;
123
+ const proposals = splitPromoteProposals(item, candidate, sessionId);
124
+ const globalResult = engine.apply("global", undefined, proposals.global, { scope: "global" });
125
+ const createdId = globalResult.appliedEdits.find((edit) => edit.applied)?.id ?? candidate.id;
126
+ const localResult = engine.apply("local", sessionId, proposals.localStamp(createdId), {
127
+ scope: "local",
128
+ baselineState: localState,
129
+ });
130
+ applied.push(`split ${item.key}: promoted cleaned part → global:${createdId} (${globalResult.id}); original archived (${localResult.id})`);
131
+ }
132
+ }
133
+ else {
134
+ // Rejected: whole promotes stay un-written; each split's original
135
+ // still archives plain (reported, data restorable).
136
+ for (const { item, candidate } of splitCreates) {
137
+ if (!candidate)
138
+ continue;
139
+ const result = engine.apply("local", sessionId, {
140
+ summary: `wrapup: split promotion not approved — archive original ${item.key} plain`,
141
+ rationale: item.reason,
142
+ expectedOutcome: `The original leaves injection; the cleaned part was NOT written (reported for manual handling).`,
143
+ edits: [{ action: "archive", kind: candidate.kind, id: candidate.id }],
144
+ }, { scope: "local", baselineState: localState });
145
+ applied.push(`split ${item.key}: promotion not approved — original archived plain (${result.id})`);
146
+ }
147
+ }
148
+ }
149
+ // 5. Silent archives: deterministic local action (hidden from injection,
150
+ // data kept restorable) — covered topics and operational entries need no
151
+ // confirmation, matching the original behavior.
152
+ for (const item of silentArchives) {
153
+ const candidate = byKey.get(item.key);
154
+ if (!candidate)
155
+ continue;
156
+ const result = engine.apply("local", sessionId, {
157
+ summary: `wrapup: archive local ${item.key} — ${item.reason}`,
158
+ rationale: item.reason,
159
+ expectedOutcome: `The entry stops being injected but stays restorable.`,
160
+ edits: [{ action: "archive", kind: candidate.kind, id: candidate.id }],
161
+ }, { scope: "local", baselineState: localState });
162
+ applied.push(`archived ${item.key} (${result.id})`);
163
+ }
164
+ // 6. Review archives (symmetric guard): not covered globally + distilled
165
+ // from real user messages — the user decides before this content is
166
+ // hidden from future sessions. No question service → conservative keep.
167
+ const userQuestions = questionServiceOf(ctx);
168
+ for (const item of reviewArchives) {
169
+ const candidate = byKey.get(item.key);
170
+ if (!candidate)
171
+ continue;
172
+ if (!userQuestions) {
173
+ applied.push(`kept ${item.key} — archive pending user confirmation (no question service)`);
174
+ continue;
175
+ }
176
+ const questionId = "evolve-wrapup-archive-review";
177
+ let archiveConfirmed = false;
178
+ try {
179
+ const answer = await userQuestions.ask({
180
+ questions: [
181
+ {
182
+ id: questionId,
183
+ question: `wrapup:条目「${candidate.title}」未被全局覆盖且源自真实对话,直接归档会隐藏它(数据保留、可恢复)。确认归档?`,
184
+ options: [{ label: "归档" }, { label: "保留" }],
185
+ },
186
+ ],
187
+ agent: invocation.agent,
188
+ signal: invocation.signal,
189
+ });
190
+ archiveConfirmed = answer.answers?.find((entry) => entry.id === questionId)?.selected?.includes("归档") ?? false;
191
+ }
192
+ catch {
193
+ archiveConfirmed = false;
194
+ }
195
+ if (archiveConfirmed) {
196
+ const result = engine.apply("local", sessionId, {
197
+ summary: `wrapup: archive local ${item.key} (user-confirmed) — ${item.reason}`,
198
+ rationale: item.reason,
199
+ expectedOutcome: `The entry stops being injected but stays restorable.`,
200
+ edits: [{ action: "archive", kind: candidate.kind, id: candidate.id }],
201
+ }, { scope: "local", baselineState: localState });
202
+ applied.push(`archived ${item.key} (user-confirmed, ${result.id})`);
203
+ }
204
+ else {
205
+ applied.push(`kept ${item.key} — user declined the archive`);
206
+ }
207
+ }
208
+ lines.push(...(applied.length > 0 ? applied : ["(no changes applied — all entries kept)"]));
209
+ return success(lines.join("\n"));
210
+ }
211
+ //# sourceMappingURL=wrapup-command.js.map
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Session wrap-up: the lifecycle exit for a session's local harness entries.
3
+ *
4
+ * When a session ends, its local entries default to orphans: a later session
5
+ * (not on the parentSession chain) never sees them, and nothing promotes or
6
+ * archives them — the exploration results effectively "die" with the session.
7
+ * Wrap-up gives those entries a real exit:
8
+ *
9
+ * - cross-session-reusable content is classified `promote` and moved into the
10
+ * global store (through the human approval gate — global is a governed
11
+ * resource, exactly like skill proposals);
12
+ * - session-specific / superseded / already-covered content is classified
13
+ * `archive` (hidden from injection, data stays restorable, rollbackable);
14
+ * - everything else is kept.
15
+ *
16
+ * Division of labor is deliberate: the mechanical audit proposes, the LLM
17
+ * classifies, the user approves, the code applies deterministically. The
18
+ * apply-side guard (`filterPromotable`) re-checks global coverage at apply
19
+ * time so a stale classification can never write a duplicate global entry.
20
+ */
21
+ import type { Context } from "@deepseek-ai/cordis";
22
+ import type { Agent } from "@deepseek-ai/dsh-agent";
23
+ import type { HarnessEntry, HarnessState, RefinementKind, RefinementProposal } from "./types.js";
24
+ /** What should happen to one local entry at session end. */
25
+ export type WrapupVerdict = "promote" | "archive" | "keep";
26
+ /** A classified local entry: `key` matches one audited candidate exactly. */
27
+ export interface WrapupItem {
28
+ /** `kind:id` of the candidate this verdict refers to. */
29
+ key: string;
30
+ verdict: WrapupVerdict;
31
+ reason: string;
32
+ /**
33
+ * Optional split-promotion payload (verdict "archive" only): the entry is
34
+ * archived as a whole, but a CLEANED cross-session-reusable part is
35
+ * offered for promotion — the durable fact distilled out of the mixed
36
+ * entry, with the ephemeral snapshot left behind in the archive.
37
+ */
38
+ promote?: {
39
+ title: string;
40
+ content: string;
41
+ };
42
+ }
43
+ /** A real global entry worth showing the assessor for the same topic. */
44
+ export interface GlobalHint {
45
+ id: string;
46
+ title: string;
47
+ }
48
+ /** The model's full classification of a session's local entries. */
49
+ export interface WrapupAssessment {
50
+ items: WrapupItem[];
51
+ rationale: string;
52
+ }
53
+ /** A local entry offered for assessment, plus its deterministic audit flags. */
54
+ export interface WrapupCandidate {
55
+ kind: RefinementKind;
56
+ id: string;
57
+ title: string;
58
+ content: string;
59
+ path: string;
60
+ version: number;
61
+ metadata: Record<string, unknown>;
62
+ /**
63
+ * True when the global store already covers this topic by a STRONG
64
+ * signal: a title that normalizes equal to, or (beyond a length floor)
65
+ * contains, the candidate's title. Collisions on id alone with a wildly
66
+ * different title are deliberately NOT coverage — see {@link globalHintsFor}.
67
+ */
68
+ coveredGlobally: boolean;
69
+ /**
70
+ * Actual global entries that touch the same topic (same id, equal
71
+ * normalized title, or title overlap). Shown to the assessor so it judges
72
+ * against real titles instead of a bare boolean; a bare same-id collision
73
+ * shows up here precisely so the model can tell whether the global copy
74
+ * really covers the local content.
75
+ */
76
+ globalHints: GlobalHint[];
77
+ /**
78
+ * Injection usage count (gap B1): how many times this entry was included
79
+ * in a system-prompt assembly. Zero means the entry was never used — a
80
+ * strong staleness signal the assessor can weigh.
81
+ */
82
+ injectionCount: number;
83
+ /**
84
+ * Staleness flag (gap B2): true when the entry has both zero injection
85
+ * usage AND a recency score below the staleness threshold (old + unused).
86
+ * The assessor is instructed to prefer "archive" for stale entries.
87
+ */
88
+ stale: boolean;
89
+ }
90
+ export declare function candidateKey(kind: RefinementKind, id: string): string;
91
+ /**
92
+ * Deterministic global-coverage check (STRONG signal): the global store
93
+ * already covers a topic when it holds a title that normalizes equal to, or
94
+ * (beyond a length floor) contains, the candidate's normalized title.
95
+ * Archived global entries count too — the topic was already judged
96
+ * cross-session; a local duplicate would only re-sediment it.
97
+ *
98
+ * The bare same-id case is deliberately NOT coverage: ids are slugs derived
99
+ * from titles, so a real collision is usually caught by the title check
100
+ * below. A same-id entry with a wildly different title is a weak signal — the
101
+ * caller routes it through {@link globalHintsFor} for the assessor to judge
102
+ * against the actual global title (real case: local `memory` "用户产品愿景与
103
+ * 收入需求(本会话)" vs global `memory` "用户画像(持续更新)").
104
+ */
105
+ export declare function globalCoverageDetected(globalState: HarnessState, kind: RefinementKind, entry: Pick<HarnessEntry, "id" | "title">): boolean;
106
+ /**
107
+ * The actual global entries that touch the same topic as a local candidate:
108
+ * same id (regardless of title — the weak collision signal that is NOT
109
+ * coverage on its own), equal normalized title, or title overlap. The raw ids
110
+ * and titles let the assessor judge enrichment against real global content
111
+ * (does the global copy already hold what the local one adds?) rather than a
112
+ * bare boolean. Bounded: a handful of best matches, never the whole store.
113
+ */
114
+ export declare function globalHintsFor(globalState: HarnessState, kind: RefinementKind, entry: Pick<HarnessEntry, "id" | "title">): GlobalHint[];
115
+ export declare function listLocalCandidates(state: HarnessState, globalState: HarnessState, baseDir?: string): WrapupCandidate[];
116
+ /**
117
+ * Parse and validate the model's assessment JSON. Defense is mechanical:
118
+ * keys outside the candidate list are dropped, verdicts outside the enum
119
+ * collapse to "keep", and candidates the model omitted default to "keep" —
120
+ * a malformed reply can never change an entry's fate by itself.
121
+ *
122
+ * Split promotion (verdict "archive" with a `promote` sub-object): the
123
+ * sub-object is accepted ONLY on archive verdicts and ONLY when both cleaned
124
+ * title and content are non-empty strings — a dropped/malformed sub-object
125
+ * silently degrades to a plain archive (the entry is never half-promoted).
126
+ */
127
+ export declare function parseWrapupAssessment(text: string, candidates: readonly WrapupCandidate[]): WrapupAssessment;
128
+ export interface PromotableSplit {
129
+ /** Items that may be promoted: classified promote AND not covered globally. */
130
+ promotable: WrapupItem[];
131
+ /** Items classified promote but blocked by the deterministic guard, with why. */
132
+ skipped: {
133
+ key: string;
134
+ reason: string;
135
+ }[];
136
+ }
137
+ /**
138
+ * Apply-time deterministic guard: re-check every promote verdict against the
139
+ * global store right before it lands. The LLM classification may be stale
140
+ * (a gate ran while assessing) or wrong; this ensures a promote never writes
141
+ * a duplicate global entry. Pure and unit-tested.
142
+ */
143
+ export declare function filterPromotable(items: readonly WrapupItem[], globalState: HarnessState, candidates: readonly WrapupCandidate[]): PromotableSplit;
144
+ export interface ArchiveReviewSplit {
145
+ /** Archives that may proceed silently: topic already covered, no real
146
+ * distillation source, or the archive half of an already-approved split. */
147
+ silent: WrapupItem[];
148
+ /**
149
+ * Archives that would bury possibly-reusable content: not covered
150
+ * globally AND distilled from real user messages (sourceSeqs present).
151
+ * These MAY NOT archive silently — the command must get user
152
+ * confirmation first (the symmetric guard to filterPromotable: it stops
153
+ * over-archiving, not just over-writing).
154
+ */
155
+ review: WrapupItem[];
156
+ }
157
+ /**
158
+ * The symmetric archive guard. `filterPromotable` is one-directional: it
159
+ * stops the model from WRITING duplicate global entries, but nothing stopped
160
+ * an unfounded ARCHIVE from hiding content that was actually only local.
161
+ * Guard criteria: an archive needs user confirmation when it is NOT covered
162
+ * globally AND the entry carries a real distillation source (sourceSeqs /
163
+ * sourceSession — i.e. it was distilled from actual user messages, so it
164
+ * may hold reusable value). Operational/empty entries archive silently as
165
+ * before. Split archives (archive + promote sub-object) skip this check:
166
+ * their promotion already crosses a human approval gate, so the archive is
167
+ * the completion of an approved action, not a silent burial.
168
+ */
169
+ export declare function needsArchiveReview(item: WrapupItem, candidate: WrapupCandidate): boolean;
170
+ /** Partition archive items into silent vs review-required (see needsArchiveReview). */
171
+ export declare function splitArchiveGuards(items: readonly WrapupItem[], candidates: readonly WrapupCandidate[]): ArchiveReviewSplit;
172
+ /**
173
+ * Apply-time guard for a split promotion (archive + promote sub-object):
174
+ * the cleaned title must not duplicate a topic already covered globally. A
175
+ * duplicate split is dropped (the entry still archives plain) rather than
176
+ * half-promoting a redundancy.
177
+ */
178
+ export declare function splitPromoteBlocked(item: WrapupItem, globalState: HarnessState, kind: RefinementKind): string | undefined;
179
+ /**
180
+ * Shared proposal builders for a WHOLE promotion — used by both the
181
+ * `/evolve wrapup` command and the gate's local-fate dimension so the two
182
+ * paths apply IDENTICAL edits (global create + local retirement stamp).
183
+ *
184
+ * The local stamp is a factory: the `promotedTo` id is only known after the
185
+ * global create lands (validation may slugify the id), so the caller applies
186
+ * the global proposal first and stamps the local copy with the created id.
187
+ */
188
+ export declare function wholePromoteProposals(item: WrapupItem, candidate: WrapupCandidate, sessionId: string): {
189
+ global: RefinementProposal;
190
+ localStamp: (createdId: string) => RefinementProposal;
191
+ };
192
+ /**
193
+ * Shared proposal builders for a SPLIT promotion (A-form): archive a mixed
194
+ * local entry but promote ONLY the cleaned durable part the model extracted.
195
+ * Same usage contract as {@link wholePromoteProposals}: apply the global
196
+ * create, then stamp the original local entry with the created id.
197
+ */
198
+ export declare function splitPromoteProposals(item: WrapupItem, candidate: WrapupCandidate, sessionId: string): {
199
+ global: RefinementProposal;
200
+ localStamp: (createdId: string) => RefinementProposal;
201
+ };
202
+ export declare const WRAPUP_ASSESS_SYSTEM_PROMPT = "You are the /evolve session wrap-up assessor.\n\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).\n- \"keep\" \u2014 still actively useful to this session, or genuinely uncertain.\n\nRules:\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\",\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\".";
203
+ export interface AssessOptions {
204
+ /** Output token budget for the assessment call. */
205
+ maxOutputTokens?: number;
206
+ /** Abort signal forwarded to the model call. */
207
+ signal?: AbortSignal;
208
+ }
209
+ /**
210
+ * Ask the model to classify the audited local candidates. Routes through the
211
+ * calling agent's own provider/model (same model the session runs on), with
212
+ * reasoning disabled so the output budget goes to the JSON verdicts.
213
+ */
214
+ export declare function assessLocalEntries(ctx: Context, agent: Agent, candidates: readonly WrapupCandidate[], options?: AssessOptions): Promise<WrapupAssessment>;
215
+ //# sourceMappingURL=wrapup.d.ts.map