dsh-continual-evolve 0.10.6 → 0.12.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 (126) hide show
  1. package/README.md +35 -122
  2. package/README.zh.md +26 -97
  3. package/cordis.patch.yml +2 -2
  4. package/lib/index.d.ts +44 -241
  5. package/lib/index.js +30 -253
  6. package/lib/memory-guide.d.ts +13 -14
  7. package/lib/memory-guide.js +32 -21
  8. package/lib/memory-section.d.ts +77 -0
  9. package/lib/memory-section.js +189 -0
  10. package/package.json +2 -17
  11. package/lib/apply.d.ts +0 -22
  12. package/lib/apply.js +0 -157
  13. package/lib/approval.d.ts +0 -72
  14. package/lib/approval.js +0 -127
  15. package/lib/auto.d.ts +0 -236
  16. package/lib/auto.js +0 -773
  17. package/lib/autocase.d.ts +0 -38
  18. package/lib/autocase.js +0 -76
  19. package/lib/benchmark-command.d.ts +0 -9
  20. package/lib/benchmark-command.js +0 -357
  21. package/lib/benchmark.d.ts +0 -151
  22. package/lib/benchmark.js +0 -250
  23. package/lib/command.d.ts +0 -49
  24. package/lib/command.js +0 -568
  25. package/lib/consolidate.d.ts +0 -90
  26. package/lib/consolidate.js +0 -180
  27. package/lib/copy.d.ts +0 -74
  28. package/lib/copy.js +0 -160
  29. package/lib/declines.d.ts +0 -101
  30. package/lib/declines.js +0 -175
  31. package/lib/evaluate.d.ts +0 -74
  32. package/lib/evaluate.js +0 -271
  33. package/lib/evolve-event.d.ts +0 -40
  34. package/lib/evolve-event.js +0 -59
  35. package/lib/failures.d.ts +0 -42
  36. package/lib/failures.js +0 -171
  37. package/lib/fate.d.ts +0 -130
  38. package/lib/fate.js +0 -350
  39. package/lib/goal-command.d.ts +0 -7
  40. package/lib/goal-command.js +0 -37
  41. package/lib/goal.d.ts +0 -72
  42. package/lib/goal.js +0 -72
  43. package/lib/inject.d.ts +0 -196
  44. package/lib/inject.js +0 -412
  45. package/lib/llm-text.d.ts +0 -104
  46. package/lib/llm-text.js +0 -140
  47. package/lib/logfile.d.ts +0 -71
  48. package/lib/logfile.js +0 -159
  49. package/lib/memory-agent.d.ts +0 -129
  50. package/lib/memory-agent.js +0 -568
  51. package/lib/memory-benchmark.d.ts +0 -76
  52. package/lib/memory-benchmark.js +0 -170
  53. package/lib/memory-index.d.ts +0 -114
  54. package/lib/memory-index.js +0 -197
  55. package/lib/message-source.d.ts +0 -23
  56. package/lib/message-source.js +0 -12
  57. package/lib/mount-command.d.ts +0 -10
  58. package/lib/mount-command.js +0 -48
  59. package/lib/mount.d.ts +0 -42
  60. package/lib/mount.js +0 -212
  61. package/lib/notify.d.ts +0 -59
  62. package/lib/notify.js +0 -97
  63. package/lib/plan.d.ts +0 -16
  64. package/lib/plan.js +0 -126
  65. package/lib/planner.d.ts +0 -61
  66. package/lib/planner.js +0 -144
  67. package/lib/pool.d.ts +0 -7
  68. package/lib/pool.js +0 -25
  69. package/lib/prefix-cache.d.ts +0 -78
  70. package/lib/prefix-cache.js +0 -168
  71. package/lib/project.d.ts +0 -28
  72. package/lib/project.js +0 -66
  73. package/lib/projection.d.ts +0 -22
  74. package/lib/projection.js +0 -112
  75. package/lib/promotion.d.ts +0 -94
  76. package/lib/promotion.js +0 -171
  77. package/lib/recall.d.ts +0 -85
  78. package/lib/recall.js +0 -142
  79. package/lib/record-language.d.ts +0 -85
  80. package/lib/record-language.js +0 -152
  81. package/lib/render.d.ts +0 -13
  82. package/lib/render.js +0 -80
  83. package/lib/review-scheduler.d.ts +0 -38
  84. package/lib/review-scheduler.js +0 -191
  85. package/lib/review.d.ts +0 -60
  86. package/lib/review.js +0 -145
  87. package/lib/rollback.d.ts +0 -9
  88. package/lib/rollback.js +0 -68
  89. package/lib/rubric.d.ts +0 -29
  90. package/lib/rubric.js +0 -119
  91. package/lib/runtime.d.ts +0 -24
  92. package/lib/runtime.js +0 -87
  93. package/lib/score.d.ts +0 -64
  94. package/lib/score.js +0 -191
  95. package/lib/search.d.ts +0 -83
  96. package/lib/search.js +0 -136
  97. package/lib/service.d.ts +0 -49
  98. package/lib/service.js +0 -180
  99. package/lib/skill-render.d.ts +0 -23
  100. package/lib/skill-render.js +0 -69
  101. package/lib/skill.d.ts +0 -15
  102. package/lib/skill.js +0 -80
  103. package/lib/skillquality.d.ts +0 -85
  104. package/lib/skillquality.js +0 -320
  105. package/lib/source.d.ts +0 -30
  106. package/lib/source.js +0 -43
  107. package/lib/state.d.ts +0 -26
  108. package/lib/state.js +0 -151
  109. package/lib/store.d.ts +0 -62
  110. package/lib/store.js +0 -164
  111. package/lib/token-usage.d.ts +0 -68
  112. package/lib/token-usage.js +0 -211
  113. package/lib/tool.d.ts +0 -34
  114. package/lib/tool.js +0 -320
  115. package/lib/turn-snapshot.d.ts +0 -72
  116. package/lib/turn-snapshot.js +0 -238
  117. package/lib/types.d.ts +0 -208
  118. package/lib/types.js +0 -127
  119. package/lib/usage.d.ts +0 -36
  120. package/lib/usage.js +0 -98
  121. package/lib/validate.d.ts +0 -28
  122. package/lib/validate.js +0 -181
  123. package/lib/wrapup-command.d.ts +0 -10
  124. package/lib/wrapup-command.js +0 -243
  125. package/lib/wrapup.d.ts +0 -260
  126. package/lib/wrapup.js +0 -539
package/lib/usage.js DELETED
@@ -1,98 +0,0 @@
1
- /**
2
- * Entry usage tracking (gap B1): records how many times each entry has been
3
- * injected into system prompts. The counts are durable (persisted to disk)
4
- * and exposed in `evolve_list` and the gate's archive-candidate reporting,
5
- * so "zero-usage stale entries" can be surfaced for cleanup.
6
- *
7
- * Storage: `<baseDir>/evolve/usage.json` — a flat JSON object mapping
8
- * `kind:id` to an integer count. Reads are tolerant of missing/corrupt files;
9
- * writes are atomic (tmp + rename).
10
- */
11
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
12
- import { join } from "node:path";
13
- const USAGE_FILE = "usage.json";
14
- function usagePath(baseDir) {
15
- return join(baseDir, "evolve", USAGE_FILE);
16
- }
17
- /**
18
- * Load the usage store from disk; returns an empty store when absent or
19
- * corrupt. Accepts BOTH on-disk shapes:
20
- * - legacy (≤0.3.x): a flat `{ "kind:id": count }` map,
21
- * - v2: `{ version: 2, counts, lastSession }` with per-session dedup.
22
- */
23
- export function loadUsage(baseDir) {
24
- const path = usagePath(baseDir);
25
- try {
26
- if (!existsSync(path))
27
- return { counts: {}, lastSession: {} };
28
- const raw = JSON.parse(readFileSync(path, "utf8"));
29
- if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
30
- const record = raw;
31
- if (record["version"] === 2 && typeof record["counts"] === "object" && record["counts"] !== null) {
32
- return {
33
- counts: record["counts"],
34
- lastSession: typeof record["lastSession"] === "object" && record["lastSession"] !== null
35
- ? record["lastSession"]
36
- : {},
37
- };
38
- }
39
- // Legacy flat map: every key is a count.
40
- return { counts: raw, lastSession: {} };
41
- }
42
- return { counts: {}, lastSession: {} };
43
- }
44
- catch {
45
- return { counts: {}, lastSession: {} };
46
- }
47
- }
48
- /** Persist the usage store atomically (always the v2 shape). */
49
- export function saveUsage(baseDir, store) {
50
- const dir = join(baseDir, "evolve");
51
- mkdirSync(dir, { recursive: true });
52
- const path = usagePath(baseDir);
53
- const tmp = `${path}.${process.pid}.tmp`;
54
- writeFileSync(tmp, `${JSON.stringify({ version: 2, counts: store.counts, lastSession: store.lastSession ?? {} }, null, 2)}\n`, "utf8");
55
- renameSync(tmp, path);
56
- }
57
- /** Build the usage key for an entry. */
58
- export function usageKey(kind, id) {
59
- return `${kind}:${id}`;
60
- }
61
- /**
62
- * Increment injection counts for the entries that were actually injected.
63
- * Called after `entriesSectionText` renders the injected block. Keys not
64
- * present in the store are initialized to 1.
65
- *
66
- * Session dedup (2026-08-22): with a sessionId, each key counts AT MOST
67
- * ONCE per session — the old per-build counting produced meaningless
68
- * numbers (one entry hit 2311× in a week) and hid the real "how many
69
- * sessions found this useful" signal that staleness decay needs. Without
70
- * a sessionId the call degrades to legacy always-increment behavior.
71
- */
72
- export function recordInjection(baseDir, injectedKeys, sessionId) {
73
- if (injectedKeys.length === 0)
74
- return;
75
- const store = loadUsage(baseDir);
76
- let dirty = false;
77
- for (const key of injectedKeys) {
78
- if (sessionId !== undefined && store.lastSession?.[key] === sessionId) {
79
- continue;
80
- }
81
- store.counts[key] = (store.counts[key] ?? 0) + 1;
82
- if (store.lastSession && sessionId !== undefined) {
83
- store.lastSession[key] = sessionId;
84
- }
85
- dirty = true;
86
- }
87
- if (dirty) {
88
- saveUsage(baseDir, store);
89
- }
90
- }
91
- /**
92
- * Get the injection count for a specific entry. Returns 0 when the entry
93
- * has never been injected (absent from the store).
94
- */
95
- export function getUsageCount(store, kind, id) {
96
- return store.counts[usageKey(kind, id)] ?? 0;
97
- }
98
- //# sourceMappingURL=usage.js.map
package/lib/validate.d.ts DELETED
@@ -1,28 +0,0 @@
1
- /**
2
- * Code-enforced validation of model-proposed edits. Every rule here exists
3
- * because the proposal is untrusted input: enum membership, immutability of
4
- * the base system prompt, required fields per action, and the executable
5
- * contract skill entries must carry.
6
- */
7
- import type { HarnessEntry, HarnessScope, RefinementEdit } from "./types.js";
8
- export declare const BASE_SYSTEM_PROMPT_ID = "base_system_prompt";
9
- /**
10
- * Gap C2: mechanical check that an edit's declared blast radius is coherent
11
- * with the scope it targets. A session-scoped edit claiming "general" would
12
- * silently read like a cross-project tactical rule; a global edit claiming
13
- * "session" would contradict its persistence. Absent blastRadius is NOT
14
- * rejected (pre-C2 data and manual edits stay compatible) — the planner is
15
- * instructed to always declare it, and this rule catches what it declares
16
- * incoherently.
17
- */
18
- export declare function validateBlastRadiusScope(scope: HarnessScope, blastRadius: "general" | "project" | "session"): string | undefined;
19
- /**
20
- * Returns a human-readable failure reason, or undefined when the edit passes.
21
- *
22
- * `before` (the entry the edit targets, absent for creates/unknown ids) lets
23
- * update rules distinguish "carrying the persisted value" from "changing it"
24
- * — e.g. a rollback inverse re-carries the stored skill_kind, which must
25
- * pass, while a genuine executable↔guidance switch must not.
26
- */
27
- export declare function validateEdit(edit: RefinementEdit, computedId: string | undefined, scope?: HarnessScope, before?: HarnessEntry): string | undefined;
28
- //# sourceMappingURL=validate.d.ts.map
package/lib/validate.js DELETED
@@ -1,181 +0,0 @@
1
- import { MEMORY_TYPE_KEY, isMemoryType } from "./types.js";
2
- import { validateSkillEntryContent } from "./skillquality.js";
3
- const ACTIONS = new Set(["create", "update", "delete", "archive"]);
4
- const KINDS = new Set(["prompt", "memory", "skill", "subagent"]);
5
- export const BASE_SYSTEM_PROMPT_ID = "base_system_prompt";
6
- /**
7
- * Gap C2: mechanical check that an edit's declared blast radius is coherent
8
- * with the scope it targets. A session-scoped edit claiming "general" would
9
- * silently read like a cross-project tactical rule; a global edit claiming
10
- * "session" would contradict its persistence. Absent blastRadius is NOT
11
- * rejected (pre-C2 data and manual edits stay compatible) — the planner is
12
- * instructed to always declare it, and this rule catches what it declares
13
- * incoherently.
14
- */
15
- export function validateBlastRadiusScope(scope, blastRadius) {
16
- if (scope === "local" && blastRadius === "general") {
17
- return "local-scope edit must declare blastRadius \"session\" or \"project\" (\"general\" would claim a cross-project rule)";
18
- }
19
- if (scope === "global" && blastRadius === "session") {
20
- return "global-scope edit must declare blastRadius \"general\" or \"project\" (\"session\" contradicts cross-session persistence)";
21
- }
22
- if (scope === "project" && blastRadius === "session") {
23
- return "project-scope edit must declare blastRadius \"project\" or \"general\" (\"session\" contradicts cross-session persistence)";
24
- }
25
- return undefined;
26
- }
27
- /**
28
- * Returns a human-readable failure reason, or undefined when the edit passes.
29
- *
30
- * `before` (the entry the edit targets, absent for creates/unknown ids) lets
31
- * update rules distinguish "carrying the persisted value" from "changing it"
32
- * — e.g. a rollback inverse re-carries the stored skill_kind, which must
33
- * pass, while a genuine executable↔guidance switch must not.
34
- */
35
- export function validateEdit(edit, computedId, scope, before) {
36
- if (!ACTIONS.has(edit.action)) {
37
- return `unsupported action ${String(edit.action)}`;
38
- }
39
- if (!KINDS.has(edit.kind)) {
40
- return `unsupported kind ${String(edit.kind)}`;
41
- }
42
- if (edit.kind === "prompt" && (edit.id === BASE_SYSTEM_PROMPT_ID || computedId === BASE_SYSTEM_PROMPT_ID)) {
43
- return "base system prompt is not editable";
44
- }
45
- if (edit.action !== "create" && !edit.id) {
46
- return `${edit.action} requires id`;
47
- }
48
- // Gap C2: blast-radius/scope coherence is a mechanical property of the
49
- // edit payload itself — checked for every action, not only create/update.
50
- if (scope && edit.blastRadius !== undefined) {
51
- const blastError = validateBlastRadiusScope(scope, edit.blastRadius);
52
- if (blastError)
53
- return blastError;
54
- }
55
- // Archive only names an existing entry: no title/content payload, and the
56
- // base system prompt stays immutable under every action.
57
- if (edit.action === "archive") {
58
- return undefined;
59
- }
60
- // Create needs the full payload (nothing to fall back to). Update may
61
- // carry any subset — apply merges with `?? before` — but must carry at
62
- // least one change (review audit 2026-08-28 B3/B5: the old blanket
63
- // "update requires title and content" broke every partial-update path:
64
- // skill archive/demote, consolidate, and the evolve_update tool contract
65
- // all construct payload-subset updates).
66
- if (edit.action === "create" && (!edit.title || !edit.content)) {
67
- return "create requires title and content";
68
- }
69
- if (edit.action === "update" &&
70
- edit.title === undefined &&
71
- edit.content === undefined &&
72
- edit.path === undefined &&
73
- edit.metadata === undefined &&
74
- edit.reference === undefined &&
75
- edit.arguments === undefined) {
76
- return "update carries no changes";
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
- }
104
- if (edit.action === "create" && edit.kind === "skill") {
105
- // Guidance skills are SKILL.md documents: no python reference (a
106
- // reference on a guidance skill would be an invented contract) and
107
- // no arguments contract. Executable skills keep the full contract.
108
- if (edit.skill_kind === "guidance") {
109
- if (edit.reference !== undefined && Object.keys(edit.reference).length > 0) {
110
- return "guidance skill must not carry a python reference (it is a SKILL.md document, not an executable)";
111
- }
112
- if (edit.arguments !== undefined && Object.keys(edit.arguments).length > 0) {
113
- return "guidance skill must not carry an arguments contract (only executable skills declare inputs)";
114
- }
115
- }
116
- else {
117
- const contractError = validateSkillContract(edit);
118
- if (contractError)
119
- return contractError;
120
- }
121
- // The entry body materializes as a SKILL.md under generated
122
- // frontmatter; content-level mechanics (no shadowing `---`, no
123
- // escaping resource refs) are code-enforced so a bad body never
124
- // reaches the store (mirrors skill-creator's validate-frontmatter).
125
- const contentProblems = validateSkillEntryContent(edit.content ?? "");
126
- if (contentProblems.length > 0) {
127
- return contentProblems.join("; ");
128
- }
129
- }
130
- if (edit.action === "update" && edit.kind === "skill") {
131
- // skill_kind is immutable on update: a switch without the matching
132
- // contract pair would leave an invalid entry (guidance carrying a
133
- // python reference, or an executable with none). Re-carrying the
134
- // persisted value (rollback inverses do) passes. Recreate the entry
135
- // to change the kind.
136
- if (edit.skill_kind !== undefined && before !== undefined && before.skill_kind !== edit.skill_kind) {
137
- return "update cannot change skill_kind — delete and recreate the entry instead";
138
- }
139
- // A carried non-empty reference replaces the contract wholesale, so it
140
- // must be a complete executable contract on its own. Empty references
141
- // (guidance skills) and absent payloads pass through — apply keeps
142
- // the persisted values.
143
- if (edit.reference !== undefined && Object.keys(edit.reference).length > 0) {
144
- const contractError = validateSkillContract(edit);
145
- if (contractError)
146
- return contractError;
147
- }
148
- if (edit.content !== undefined) {
149
- const contentProblems = validateSkillEntryContent(edit.content);
150
- if (contentProblems.length > 0) {
151
- return contentProblems.join("; ");
152
- }
153
- }
154
- }
155
- return undefined;
156
- }
157
- function validateSkillContract(edit) {
158
- if (edit.arguments === undefined) {
159
- return "create/update skill requires arguments";
160
- }
161
- const reference = edit.reference;
162
- if (!reference || typeof reference !== "object") {
163
- return "create/update skill requires python reference";
164
- }
165
- const ref = reference;
166
- if (ref.type !== "python") {
167
- return "create/update skill reference.type must be python";
168
- }
169
- const hasImport = (typeof ref.import === "string" && ref.import.length > 0) ||
170
- (typeof ref.python_import === "string" && ref.python_import.length > 0);
171
- const hasCallable = (typeof ref.callable === "string" && ref.callable.length > 0) ||
172
- (typeof ref.call_pattern === "string" && ref.call_pattern.length > 0);
173
- if (!hasImport) {
174
- return "create/update skill requires python import";
175
- }
176
- if (!hasCallable) {
177
- return "create/update skill requires callable or call_pattern";
178
- }
179
- return undefined;
180
- }
181
- //# sourceMappingURL=validate.js.map
@@ -1,10 +0,0 @@
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
- import { type PromotionPolicy } from "./promotion.js";
8
- import { type RecordLanguagePreference } from "./record-language.js";
9
- export declare function executeWrapupCommand(ctx: Context, engine: EvolutionEngine, invocation: CommandInvocation, policy?: PromotionPolicy, recordLanguage?: RecordLanguagePreference): Promise<CommandResult>;
10
- //# sourceMappingURL=wrapup-command.d.ts.map
@@ -1,243 +0,0 @@
1
- import { questionServiceOf, requireGlobalApproval } from "./approval.js";
2
- import { wrapupArchiveCopy } from "./copy.js";
3
- import { assessLocalEntries, candidateKey, filterPromotable, listLocalCandidates, splitArchiveGuards, splitPromoteBlocked, splitPromoteProposals, valenceStampProposal, wholePromoteProposals } from "./wrapup.js";
4
- import { DEFAULT_PROMOTION_POLICY } from "./promotion.js";
5
- import { normalizeRecordLanguage, resolveRecordLanguage } from "./record-language.js";
6
- import { recentUserText } from "./inject.js";
7
- function success(text) {
8
- return { kind: "success", text };
9
- }
10
- export async function executeWrapupCommand(ctx, engine, invocation, policy = DEFAULT_PROMOTION_POLICY, recordLanguage) {
11
- const sessionId = invocation.agent.id;
12
- const localState = engine.load("local", sessionId);
13
- const globalState = engine.load("global", undefined);
14
- const candidates = listLocalCandidates(localState, globalState, engine.baseDir);
15
- if (candidates.length === 0) {
16
- return success(`(nothing to wrap up: ${sessionId}'s local store has no active, un-promoted entries — use /evolve list to inspect it)`);
17
- }
18
- // 1. Classify: the model judges each audited candidate's fate.
19
- const assessment = await assessLocalEntries(ctx, invocation.agent, candidates, {
20
- signal: invocation.signal,
21
- tokenUsage: {
22
- baseDir: engine.baseDir,
23
- sessionId,
24
- retain: engine.retention.tokenUsage,
25
- onError: (cause) => ctx.logger("continual-evolve").warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
26
- },
27
- tokenUsagePhase: "wrapup",
28
- });
29
- const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
30
- // 2. Partition by action. Deterministic guards re-check the LIVE global
31
- // store right before anything lands (state may have changed mid-call).
32
- const { promotable, skipped } = filterPromotable(assessment.items, globalState, candidates, policy);
33
- const promoteItems = promotable.filter((item) => item.verdict === "promote");
34
- const archiveItems = assessment.items.filter((item) => item.verdict === "archive");
35
- // Split promotion (A-form): archive a mixed entry but promote ONLY the
36
- // cleaned durable part the model extracted. Guarded the same way as whole
37
- // promotes — a split that would duplicate a globally covered topic is
38
- // dropped and the entry archives plain.
39
- const splitItems = [];
40
- const splitSkipped = [];
41
- for (const item of archiveItems) {
42
- if (!item.promote)
43
- continue;
44
- const candidate = byKey.get(item.key);
45
- if (!candidate) {
46
- splitSkipped.push({ key: item.key, reason: "not in the audited candidate list" });
47
- continue;
48
- }
49
- const blocked = splitPromoteBlocked(item, globalState, candidate.kind, policy, candidate.metadata);
50
- if (blocked) {
51
- splitSkipped.push({ key: item.key, reason: blocked });
52
- continue;
53
- }
54
- splitItems.push({ item, candidate });
55
- }
56
- // Plain archives (no split payload): the symmetric guard — an archive that
57
- // is NOT globally covered AND was distilled from real user messages must
58
- // not proceed silently.
59
- const plainArchives = archiveItems.filter((item) => !item.promote);
60
- const { silent: silentArchives, review: reviewArchives } = splitArchiveGuards(plainArchives, candidates);
61
- const keepItems = assessment.items.filter((item) => item.verdict === "keep");
62
- // 3. Report the assessment before touching anything.
63
- const lines = [
64
- `wrapup assessment (${sessionId}): ${candidates.length} candidates${candidates.some((c) => c.coveredGlobally) ? `, ${candidates.filter((c) => c.coveredGlobally).length} covered globally` : ""}`,
65
- `${assessment.rationale}`,
66
- ];
67
- for (const [heading, items] of [
68
- ["PROMOTE (to global)", promoteItems],
69
- ["SPLIT (archive + promote durable part)", splitItems.map((split) => split.item)],
70
- ["ARCHIVE", silentArchives],
71
- ["ARCHIVE (needs review)", reviewArchives],
72
- ["KEEP", keepItems],
73
- ]) {
74
- lines.push(`${heading}: ${items.length}`);
75
- for (const item of items) {
76
- const candidate = byKey.get(item.key);
77
- const title = candidate ? candidate.title : item.key;
78
- const splitNote = item.promote ? ` → 拆出提升「${item.promote.title}」` : "";
79
- lines.push(`- ${item.key} "${title}"${splitNote} — ${item.reason}`);
80
- }
81
- }
82
- for (const skip of skipped) {
83
- lines.push(`- promote skipped: ${skip.key} — ${skip.reason}`);
84
- }
85
- for (const skip of splitSkipped) {
86
- lines.push(`- split skipped: ${skip.key} — ${skip.reason}`);
87
- }
88
- lines.push("");
89
- const applied = [];
90
- // 4. Global writes: governed resource — ONE human approval gate covers
91
- // every create (whole promotes AND split promotions). On approval:
92
- // - whole promote → create global copy + stamp local promotedTo+archivedAt;
93
- // - split → create the cleaned durable part + archive the original with
94
- // promotedTo. On rejection: whole promotes are not written, and each
95
- // split's original STILL archives plain (its snapshot half deserves
96
- // the archive; the durable half is reported for manual handling).
97
- const wholeCreates = promoteItems.map((item) => ({ item, candidate: byKey.get(item.key) }));
98
- const splitCreates = splitItems;
99
- const allCreates = new Set([...wholeCreates.map((c) => c.item.key), ...splitCreates.map((c) => c.item.key)]);
100
- if (allCreates.size > 0) {
101
- const what = `wrapup 将写入跨会话 global store(共 ${allCreates.size} 条:${promoteItems.length} 条整条提升 + ${splitItems.length} 条拆解提升):\n${[
102
- ...promoteItems.map((item) => `- 整条提升 ${item.key} "${byKey.get(item.key)?.title ?? item.key}"`),
103
- ...splitItems.map((split) => `- 拆解提升 ${split.item.key} → 清洗「${split.item.promote?.title}」(原条目随之归档)`),
104
- ].join("\n")}`;
105
- let promoteAllowed = true;
106
- try {
107
- await requireGlobalApproval(ctx, invocation.agent, invocation.signal, what, recordLanguage);
108
- }
109
- catch (cause) {
110
- promoteAllowed = false;
111
- const message = `global 写入未批准 — 整条提升与拆解提升均未写入 (${cause instanceof Error ? cause.message : String(cause)})`;
112
- applied.push(message);
113
- lines.push(message);
114
- }
115
- if (promoteAllowed) {
116
- // Whole promotes: create global entry, retire the local copy.
117
- // Shared proposal builders keep the wrap-up command and the gate's
118
- // local-fate dimension writing IDENTICAL edits.
119
- for (const { item, candidate } of wholeCreates) {
120
- if (!candidate)
121
- continue;
122
- const proposals = wholePromoteProposals(item, candidate, sessionId);
123
- const globalResult = engine.apply("global", undefined, proposals.global, { scope: "global" });
124
- const createdId = globalResult.appliedEdits.find((edit) => edit.applied)?.id ?? candidate.id;
125
- const localResult = engine.apply("local", sessionId, proposals.localStamp(createdId), {
126
- scope: "local",
127
- baselineState: localState,
128
- });
129
- applied.push(`promoted ${item.key} → global:${createdId} (${globalResult.id}; local stamped ${localResult.id})`);
130
- }
131
- // Split promotions: create the cleaned durable part, retire the
132
- // original local entry (its snapshot half is archived along).
133
- for (const { item, candidate } of splitCreates) {
134
- if (!item.promote)
135
- continue;
136
- const proposals = splitPromoteProposals(item, candidate, sessionId);
137
- const globalResult = engine.apply("global", undefined, proposals.global, { scope: "global" });
138
- const createdId = globalResult.appliedEdits.find((edit) => edit.applied)?.id ?? candidate.id;
139
- const localResult = engine.apply("local", sessionId, proposals.localStamp(createdId), {
140
- scope: "local",
141
- baselineState: localState,
142
- });
143
- applied.push(`split ${item.key}: promoted cleaned part → global:${createdId} (${globalResult.id}); original archived (${localResult.id})`);
144
- }
145
- }
146
- else {
147
- // Rejected: whole promotes stay un-written; each split's original
148
- // still archives plain (reported, data restorable).
149
- for (const { item, candidate } of splitCreates) {
150
- if (!candidate)
151
- continue;
152
- const result = engine.apply("local", sessionId, {
153
- summary: `wrapup: split promotion not approved — archive original ${item.key} plain`,
154
- rationale: item.reason,
155
- expectedOutcome: `The original leaves injection; the cleaned part was NOT written (reported for manual handling).`,
156
- edits: [{ action: "archive", kind: candidate.kind, id: candidate.id }],
157
- }, { scope: "local", baselineState: localState });
158
- applied.push(`split ${item.key}: promotion not approved — original archived plain (${result.id})`);
159
- }
160
- }
161
- }
162
- // 5. Silent archives: deterministic local action (hidden from injection,
163
- // data kept restorable) — covered topics and operational entries need no
164
- // confirmation, matching the original behavior.
165
- for (const item of silentArchives) {
166
- const candidate = byKey.get(item.key);
167
- if (!candidate)
168
- continue;
169
- const result = engine.apply("local", sessionId, {
170
- summary: `wrapup: archive local ${item.key} — ${item.reason}`,
171
- rationale: item.reason,
172
- expectedOutcome: `The entry stops being injected but stays restorable.`,
173
- edits: [{ action: "archive", kind: candidate.kind, id: candidate.id }],
174
- }, { scope: "local", baselineState: localState });
175
- applied.push(`archived ${item.key} (${result.id})`);
176
- }
177
- // 6. Review archives (symmetric guard): not covered globally + distilled
178
- // from real user messages — the user decides before this content is
179
- // hidden from future sessions. No question service → conservative keep.
180
- const userQuestions = questionServiceOf(ctx);
181
- for (const item of reviewArchives) {
182
- const candidate = byKey.get(item.key);
183
- if (!candidate)
184
- continue;
185
- if (!userQuestions) {
186
- applied.push(`kept ${item.key} — archive pending user confirmation (no question service)`);
187
- continue;
188
- }
189
- const questionId = "evolve-wrapup-archive-review";
190
- let archiveConfirmed = false;
191
- try {
192
- const copy = wrapupArchiveCopy(candidate.title, normalizeRecordLanguage(recordLanguage) ??
193
- resolveRecordLanguage({ ctx, trajectoryText: recentUserText(invocation.agent) }));
194
- const answer = await userQuestions.ask({
195
- questions: [
196
- {
197
- id: questionId,
198
- question: copy.question,
199
- options: copy.options,
200
- },
201
- ],
202
- agent: invocation.agent,
203
- signal: invocation.signal,
204
- });
205
- const selected = answer.answers?.find((entry) => entry.id === questionId)?.selected ?? [];
206
- archiveConfirmed = selected.includes("归档") || selected.includes("Archive");
207
- }
208
- catch {
209
- archiveConfirmed = false;
210
- }
211
- if (archiveConfirmed) {
212
- const result = engine.apply("local", sessionId, {
213
- summary: `wrapup: archive local ${item.key} (user-confirmed) — ${item.reason}`,
214
- rationale: item.reason,
215
- expectedOutcome: `The entry stops being injected but stays restorable.`,
216
- edits: [{ action: "archive", kind: candidate.kind, id: candidate.id }],
217
- }, { scope: "local", baselineState: localState });
218
- applied.push(`archived ${item.key} (user-confirmed, ${result.id})`);
219
- }
220
- else {
221
- applied.push(`kept ${item.key} — user declined the archive`);
222
- }
223
- }
224
- // 7. Valence stamps (P1 效价反馈): keep-verdict entries the assessor marked
225
- // contradicted get their negative counter bumped — the entry stays live
226
- // but sinks in injection ranking and the next assessment prefers
227
- // archiving it. Deterministic local action, no approval needed.
228
- for (const item of keepItems) {
229
- if (!item.contradicted)
230
- continue;
231
- const candidate = byKey.get(item.key);
232
- if (!candidate)
233
- continue;
234
- const proposal = valenceStampProposal(item, candidate);
235
- if (!proposal)
236
- continue;
237
- const result = engine.apply("local", sessionId, proposal, { scope: "local", baselineState: localState });
238
- applied.push(`contradicted stamp ${item.key} → valenceNegative=${candidate.negativeCount + 1} (${result.id})`);
239
- }
240
- lines.push(...(applied.length > 0 ? applied : ["(no changes applied — all entries kept)"]));
241
- return success(lines.join("\n"));
242
- }
243
- //# sourceMappingURL=wrapup-command.js.map