dsh-continual-evolve 0.11.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -115
- package/README.zh.md +33 -93
- package/client/client.js +253 -0
- package/cordis.patch.yml +2 -2
- package/lib/card-routes.d.ts +32 -0
- package/lib/card-routes.js +92 -0
- package/lib/index.d.ts +65 -217
- package/lib/index.js +74 -234
- package/lib/known-workspaces.d.ts +22 -0
- package/lib/known-workspaces.js +42 -0
- package/lib/memory-guide.d.ts +13 -14
- package/lib/memory-guide.js +34 -21
- package/lib/memory-section.d.ts +77 -0
- package/lib/memory-section.js +189 -0
- package/lib/memory-snapshot.d.ts +54 -0
- package/lib/memory-snapshot.js +125 -0
- package/package.json +8 -17
- package/lib/apply.d.ts +0 -22
- package/lib/apply.js +0 -157
- package/lib/approval.d.ts +0 -73
- package/lib/approval.js +0 -127
- package/lib/audit-log.d.ts +0 -19
- package/lib/audit-log.js +0 -52
- package/lib/auto.d.ts +0 -236
- package/lib/auto.js +0 -773
- package/lib/autocase.d.ts +0 -38
- package/lib/autocase.js +0 -76
- package/lib/benchmark-command.d.ts +0 -9
- package/lib/benchmark-command.js +0 -357
- package/lib/benchmark.d.ts +0 -151
- package/lib/benchmark.js +0 -250
- package/lib/command-util.d.ts +0 -70
- package/lib/command-util.js +0 -123
- package/lib/command.d.ts +0 -13
- package/lib/command.js +0 -134
- package/lib/consolidate.d.ts +0 -90
- package/lib/consolidate.js +0 -180
- package/lib/copy.d.ts +0 -37
- package/lib/copy.js +0 -58
- package/lib/declines.d.ts +0 -103
- package/lib/declines.js +0 -179
- package/lib/evaluate.d.ts +0 -74
- package/lib/evaluate.js +0 -271
- package/lib/evolve-event.d.ts +0 -40
- package/lib/evolve-event.js +0 -60
- package/lib/extraction-phase.d.ts +0 -132
- package/lib/extraction-phase.js +0 -236
- package/lib/failures.d.ts +0 -42
- package/lib/failures.js +0 -171
- package/lib/fate.d.ts +0 -130
- package/lib/fate.js +0 -350
- package/lib/goal-command.d.ts +0 -7
- package/lib/goal-command.js +0 -37
- package/lib/goal.d.ts +0 -72
- package/lib/goal.js +0 -72
- package/lib/inject.d.ts +0 -90
- package/lib/inject.js +0 -164
- package/lib/injection-caps.d.ts +0 -4
- package/lib/injection-caps.js +0 -4
- package/lib/injection-directory.d.ts +0 -29
- package/lib/injection-directory.js +0 -78
- package/lib/injection-rank.d.ts +0 -85
- package/lib/injection-rank.js +0 -161
- package/lib/listener.d.ts +0 -45
- package/lib/listener.js +0 -199
- package/lib/llm-text.d.ts +0 -104
- package/lib/llm-text.js +0 -140
- package/lib/logfile.d.ts +0 -71
- package/lib/logfile.js +0 -159
- package/lib/maintenance-commands.d.ts +0 -23
- package/lib/maintenance-commands.js +0 -136
- package/lib/memory-agent.d.ts +0 -93
- package/lib/memory-agent.js +0 -300
- package/lib/memory-apply.d.ts +0 -44
- package/lib/memory-apply.js +0 -202
- package/lib/memory-benchmark.d.ts +0 -76
- package/lib/memory-benchmark.js +0 -170
- package/lib/memory-index.d.ts +0 -114
- package/lib/memory-index.js +0 -197
- package/lib/memory-manifest.d.ts +0 -16
- package/lib/memory-manifest.js +0 -70
- package/lib/message-source.d.ts +0 -23
- package/lib/message-source.js +0 -12
- package/lib/mount-command.d.ts +0 -10
- package/lib/mount-command.js +0 -48
- package/lib/mount.d.ts +0 -42
- package/lib/mount.js +0 -212
- package/lib/notify.d.ts +0 -48
- package/lib/notify.js +0 -57
- package/lib/plan-command.d.ts +0 -12
- package/lib/plan-command.js +0 -38
- package/lib/plan.d.ts +0 -16
- package/lib/plan.js +0 -126
- package/lib/planner.d.ts +0 -61
- package/lib/planner.js +0 -144
- package/lib/pool.d.ts +0 -7
- package/lib/pool.js +0 -25
- package/lib/prefix-cache.d.ts +0 -79
- package/lib/prefix-cache.js +0 -168
- package/lib/project.d.ts +0 -28
- package/lib/project.js +0 -66
- package/lib/projection.d.ts +0 -22
- package/lib/projection.js +0 -112
- package/lib/promotion.d.ts +0 -94
- package/lib/promotion.js +0 -171
- package/lib/recall.d.ts +0 -85
- package/lib/recall.js +0 -142
- package/lib/record-language.d.ts +0 -85
- package/lib/record-language.js +0 -152
- package/lib/render.d.ts +0 -13
- package/lib/render.js +0 -80
- package/lib/review-scheduler.d.ts +0 -38
- package/lib/review-scheduler.js +0 -183
- package/lib/review.d.ts +0 -60
- package/lib/review.js +0 -145
- package/lib/rollback.d.ts +0 -9
- package/lib/rollback.js +0 -68
- package/lib/rubric.d.ts +0 -29
- package/lib/rubric.js +0 -119
- package/lib/runtime-commands.d.ts +0 -14
- package/lib/runtime-commands.js +0 -115
- package/lib/runtime.d.ts +0 -24
- package/lib/runtime.js +0 -87
- package/lib/score.d.ts +0 -64
- package/lib/score.js +0 -191
- package/lib/search.d.ts +0 -83
- package/lib/search.js +0 -136
- package/lib/service.d.ts +0 -63
- package/lib/service.js +0 -253
- package/lib/skill-render.d.ts +0 -23
- package/lib/skill-render.js +0 -69
- package/lib/skill.d.ts +0 -15
- package/lib/skill.js +0 -80
- package/lib/skillquality.d.ts +0 -85
- package/lib/skillquality.js +0 -320
- package/lib/source.d.ts +0 -30
- package/lib/source.js +0 -43
- package/lib/state.d.ts +0 -26
- package/lib/state.js +0 -151
- package/lib/store.d.ts +0 -61
- package/lib/store.js +0 -164
- package/lib/token-usage.d.ts +0 -68
- package/lib/token-usage.js +0 -211
- package/lib/tool.d.ts +0 -34
- package/lib/tool.js +0 -320
- package/lib/turn-snapshot.d.ts +0 -78
- package/lib/turn-snapshot.js +0 -252
- package/lib/types.d.ts +0 -218
- package/lib/types.js +0 -133
- package/lib/usage.d.ts +0 -36
- package/lib/usage.js +0 -98
- package/lib/validate.d.ts +0 -28
- package/lib/validate.js +0 -181
- package/lib/wrapup-candidates.d.ts +0 -127
- package/lib/wrapup-candidates.js +0 -161
- package/lib/wrapup-command.d.ts +0 -10
- package/lib/wrapup-command.js +0 -243
- package/lib/wrapup-proposals.d.ts +0 -101
- package/lib/wrapup-proposals.js +0 -281
- package/lib/wrapup.d.ts +0 -56
- package/lib/wrapup.js +0 -105
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The whole runtime of the plugin after the 2026-10-04 ZCode-alignment
|
|
3
|
+
* teardown: the session-start memory section.
|
|
4
|
+
*
|
|
5
|
+
* The store is plain markdown inside the workspace — `<cwd>/.evolve/memory/`
|
|
6
|
+
* with a `MEMORY.md` index and one file per fact — so the model reads and
|
|
7
|
+
* writes it with its native file tools (DSH fences only out-of-workspace
|
|
8
|
+
* writes; dot-directories inside the workspace pass both ways). This module
|
|
9
|
+
* hands out the absolute path, injects the index content, and wraps the
|
|
10
|
+
* when-to-save guide around it. There are no tools, no scopes, and no
|
|
11
|
+
* governance: the files are the store, and a bad memory is a visible file
|
|
12
|
+
* the user deletes.
|
|
13
|
+
*
|
|
14
|
+
* Session-freeze contract (the prompt-cache guard): the section is computed
|
|
15
|
+
* once per agent id and reused byte-for-byte afterwards, so the system
|
|
16
|
+
* prompt stays stable within a session and cache reads keep hitting. A
|
|
17
|
+
* memory written mid-session becomes visible in the NEXT session; the model
|
|
18
|
+
* can always read the directory directly for the immediate need.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
22
|
+
import { MEMORY_GUIDE_RULES, memoryGuideIntro } from "./memory-guide.js";
|
|
23
|
+
/** Name of the injected section (unique — duplicate names throw upstream). */
|
|
24
|
+
export const MEMORY_SECTION_NAME = "evolve:memory-index";
|
|
25
|
+
/** Default prompt-section order (empty 1–499 slot; upstream named slots start at 500). */
|
|
26
|
+
export const DEFAULT_MEMORY_SECTION_ORDER = 400;
|
|
27
|
+
/** Default hard character budget for the injected index. */
|
|
28
|
+
export const DEFAULT_MAX_CHARS = 6000;
|
|
29
|
+
/** Sessions whose frozen section text is kept before LRU eviction. */
|
|
30
|
+
export const DEFAULT_FROZEN_SESSIONS = 32;
|
|
31
|
+
const MEMORY_DIR_SEGMENTS = [".evolve", "memory"];
|
|
32
|
+
const INDEX_FILE = "MEMORY.md";
|
|
33
|
+
/** Starter index written on first use; the model maintains every line after. */
|
|
34
|
+
const STARTER_INDEX = "# 记忆索引\n\n<!-- 一行一条:- [标题](文件名.md) — 一句话相关性钩子;写新记忆后在此追加一行 -->\n";
|
|
35
|
+
/** Appended to the workspace .gitignore on bootstrap (git workspaces only). */
|
|
36
|
+
const GITIGNORE_ENTRY = "# workspace memory (dsh-continual-evolve)\n.evolve/\n";
|
|
37
|
+
/** The assembling agent's validated absolute cwd, or undefined when absent. */
|
|
38
|
+
export function cwdOf(agent) {
|
|
39
|
+
try {
|
|
40
|
+
const header = agent?.session?.header;
|
|
41
|
+
const raw = header?.cwd ?? header?.meta?.cwd;
|
|
42
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const cwd = raw.trim();
|
|
46
|
+
if (!isAbsolute(cwd)) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return resolve(cwd);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Absolute memory directory for a workspace root. */
|
|
56
|
+
export function memoryDirFor(cwd) {
|
|
57
|
+
return join(resolve(cwd), ...MEMORY_DIR_SEGMENTS);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Ensure `.evolve/` is ignored when the workspace is a git repository, so
|
|
61
|
+
* the memory store never shows up in git status. Best-effort hygiene: a
|
|
62
|
+
* non-git workspace (no `.git` entry at the cwd) is left untouched — no
|
|
63
|
+
* `.gitignore` is created — and any write failure is swallowed.
|
|
64
|
+
*/
|
|
65
|
+
export function ensureGitIgnored(cwd) {
|
|
66
|
+
try {
|
|
67
|
+
if (!existsSync(join(cwd, ".git"))) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const gitignore = join(cwd, ".gitignore");
|
|
71
|
+
const existing = existsSync(gitignore) ? readFileSync(gitignore, "utf8") : "";
|
|
72
|
+
if (existing.split("\n").some((line) => line.trim() === ".evolve/")) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
76
|
+
writeFileSync(gitignore, `${existing}${prefix}${GITIGNORE_ENTRY}`, "utf8");
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Never blocks the store; worst case is a visible .evolve/ in git status.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Ensure the store exists (creating the directory and a starter index on
|
|
84
|
+
* first use), then return the trimmed index text. Any filesystem failure
|
|
85
|
+
* degrades to "" — injection must never break an assembly.
|
|
86
|
+
*/
|
|
87
|
+
export function readMemoryIndex(dir, bootstrap = true) {
|
|
88
|
+
try {
|
|
89
|
+
if (bootstrap && !existsSync(dir)) {
|
|
90
|
+
mkdirSync(dir, { recursive: true });
|
|
91
|
+
writeFileSync(join(dir, INDEX_FILE), STARTER_INDEX, "utf8");
|
|
92
|
+
ensureGitIgnored(resolve(dir, "..", ".."));
|
|
93
|
+
}
|
|
94
|
+
return readFileSync(join(dir, INDEX_FILE), "utf8").trim();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Whole-line-preserving truncation of the index to `maxChars` characters.
|
|
102
|
+
* Only index rows that fit whole are shown — a partial markdown row would
|
|
103
|
+
* corrupt the line the model reads. When not even one row fits, `text` is
|
|
104
|
+
* "" and `dropped` carries the full length.
|
|
105
|
+
*/
|
|
106
|
+
export function fitIndex(index, maxChars) {
|
|
107
|
+
const budget = Math.max(0, maxChars);
|
|
108
|
+
if (index.length <= budget) {
|
|
109
|
+
return { text: index, dropped: 0 };
|
|
110
|
+
}
|
|
111
|
+
const cut = index.slice(0, budget);
|
|
112
|
+
const lastLine = cut.lastIndexOf("\n");
|
|
113
|
+
if (lastLine <= 0) {
|
|
114
|
+
return { text: "", dropped: index.length };
|
|
115
|
+
}
|
|
116
|
+
const text = cut.slice(0, lastLine);
|
|
117
|
+
return { text, dropped: index.length - text.length };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Compose the full memory section for one assembling agent: the guide
|
|
121
|
+
* wrapped around the budget-bounded index, with the absolute store path
|
|
122
|
+
* handed out so the model can read and write the files directly. With the
|
|
123
|
+
* guide off and an empty index the result is "" — the prompt renderer then
|
|
124
|
+
* drops the section, so an empty workspace costs zero tokens.
|
|
125
|
+
*/
|
|
126
|
+
export function memorySectionText(agent, opts) {
|
|
127
|
+
const guide = opts?.guide ?? true;
|
|
128
|
+
const maxChars = opts?.maxChars ?? DEFAULT_MAX_CHARS;
|
|
129
|
+
const cwd = cwdOf(agent);
|
|
130
|
+
if (!cwd) {
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
const dir = memoryDirFor(cwd);
|
|
134
|
+
const index = readMemoryIndex(dir, opts?.bootstrap ?? true);
|
|
135
|
+
const { text, dropped } = fitIndex(index, maxChars);
|
|
136
|
+
const parts = [];
|
|
137
|
+
if (index.length === 0) {
|
|
138
|
+
parts.push("<memories>\n(暂无记忆)\n</memories>");
|
|
139
|
+
}
|
|
140
|
+
else if (text.length > 0) {
|
|
141
|
+
parts.push(`<memories>\n${text}\n</memories>`);
|
|
142
|
+
if (dropped > 0) {
|
|
143
|
+
parts.push(`(索引超出 ${maxChars} 字符预算,已截断——其余记忆直接读取 \`${dir}/\` 目录)`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
parts.push(`(索引超出 ${maxChars} 字符预算,一行都放不下——记忆直接读取 \`${dir}/\` 目录)`);
|
|
148
|
+
}
|
|
149
|
+
if (!guide) {
|
|
150
|
+
return index.length === 0 ? "" : parts.join("\n\n");
|
|
151
|
+
}
|
|
152
|
+
return [memoryGuideIntro(dir), ...parts, MEMORY_GUIDE_RULES].join("\n\n");
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Freeze the section text per agent id. The frozen value is what keeps the
|
|
156
|
+
* system prompt stable across a session's assemblies (prompt-cache guard);
|
|
157
|
+
* bounded by `maxSessions` with insertion-order (LRU) eviction, so a
|
|
158
|
+
* long-lived process cannot grow without limit.
|
|
159
|
+
*/
|
|
160
|
+
export function createFrozenMemorySection(maxSessions = DEFAULT_FROZEN_SESSIONS) {
|
|
161
|
+
const limit = Math.max(1, maxSessions);
|
|
162
|
+
const frozen = new Map();
|
|
163
|
+
return {
|
|
164
|
+
textFor(agent, build) {
|
|
165
|
+
const id = agent?.id;
|
|
166
|
+
const key = typeof id === "string" && id.length > 0 ? id : undefined;
|
|
167
|
+
if (key === undefined) {
|
|
168
|
+
return build(agent);
|
|
169
|
+
}
|
|
170
|
+
const cached = frozen.get(key);
|
|
171
|
+
if (cached !== undefined) {
|
|
172
|
+
return cached;
|
|
173
|
+
}
|
|
174
|
+
const text = build(agent);
|
|
175
|
+
frozen.set(key, text);
|
|
176
|
+
while (frozen.size > limit) {
|
|
177
|
+
const oldest = frozen.keys().next();
|
|
178
|
+
if (oldest.done === true) {
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
frozen.delete(oldest.value);
|
|
182
|
+
}
|
|
183
|
+
return text;
|
|
184
|
+
},
|
|
185
|
+
size: () => frozen.size,
|
|
186
|
+
clear: () => frozen.clear(),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=memory-section.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** The index file name (same store contract as the injection path). */
|
|
2
|
+
export declare const MEMORY_SNAPSHOT_INDEX_FILE = "MEMORY.md";
|
|
3
|
+
/** One memory file with its frontmatter fields ("" when absent). */
|
|
4
|
+
export interface MemoryFileInfo {
|
|
5
|
+
file: string;
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
type: string;
|
|
9
|
+
}
|
|
10
|
+
/** One parsed index row: `- [title](file.md) — hook`. */
|
|
11
|
+
export interface MemoryIndexRow {
|
|
12
|
+
title: string;
|
|
13
|
+
file: string;
|
|
14
|
+
}
|
|
15
|
+
/** The full read-only snapshot the card renders for one workspace root. */
|
|
16
|
+
export interface MemorySnapshot {
|
|
17
|
+
root: string;
|
|
18
|
+
memoryDir: string;
|
|
19
|
+
exists: boolean;
|
|
20
|
+
fileCount: number;
|
|
21
|
+
indexEntryCount: number;
|
|
22
|
+
files: MemoryFileInfo[];
|
|
23
|
+
/** Index rows whose referenced file no longer exists on disk. */
|
|
24
|
+
missingFiles: string[];
|
|
25
|
+
/** .md files on disk that no index row references. */
|
|
26
|
+
unindexedFiles: string[];
|
|
27
|
+
/** Set when the store exists but could not be read; null otherwise. */
|
|
28
|
+
readError: string | null;
|
|
29
|
+
}
|
|
30
|
+
/** Minimal frontmatter fields the card shows. */
|
|
31
|
+
export interface MemoryFrontmatter {
|
|
32
|
+
name: string;
|
|
33
|
+
description: string;
|
|
34
|
+
type: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parse `- [title](file.md) …` rows out of an index text. Anything else
|
|
38
|
+
* (headings, the bootstrap hint comment, blank lines) is ignored.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseMemoryIndexRows(indexText: string): MemoryIndexRow[];
|
|
41
|
+
/**
|
|
42
|
+
* Read the card-relevant frontmatter fields (`name`, `description`, `type`)
|
|
43
|
+
* from a leading `---` block. A missing or malformed block yields "" fields —
|
|
44
|
+
* the body text remains the source of truth the user opens in the editor.
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseMemoryFrontmatter(text: string): MemoryFrontmatter;
|
|
47
|
+
/**
|
|
48
|
+
* The read-only memory snapshot for one workspace root. A missing store
|
|
49
|
+
* reports `exists: false` (the card shows the empty state); a store that
|
|
50
|
+
* exists but fails to read reports `readError` instead of throwing — the
|
|
51
|
+
* card must observe, never break.
|
|
52
|
+
*/
|
|
53
|
+
export declare function memorySnapshot(root: string): MemorySnapshot;
|
|
54
|
+
//# sourceMappingURL=memory-snapshot.d.ts.map
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only projection of one workspace memory store for the plugin
|
|
3
|
+
* management card. Everything here is computed from disk at call time — the
|
|
4
|
+
* card is a viewer, never a second store: no caching, no writes, no bootstrap
|
|
5
|
+
* (unlike the injection path, a missing store is reported, not created).
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
import { memoryDirFor } from "./memory-section.js";
|
|
10
|
+
/** The index file name (same store contract as the injection path). */
|
|
11
|
+
export const MEMORY_SNAPSHOT_INDEX_FILE = "MEMORY.md";
|
|
12
|
+
/**
|
|
13
|
+
* Parse `- [title](file.md) …` rows out of an index text. Anything else
|
|
14
|
+
* (headings, the bootstrap hint comment, blank lines) is ignored.
|
|
15
|
+
*/
|
|
16
|
+
export function parseMemoryIndexRows(indexText) {
|
|
17
|
+
const rows = [];
|
|
18
|
+
for (const line of indexText.split("\n")) {
|
|
19
|
+
const match = /^-\s+\[(.+?)\]\(([^()\s]+\.md)\)/.exec(line.trim());
|
|
20
|
+
if (match?.[1] !== undefined && match?.[2] !== undefined) {
|
|
21
|
+
rows.push({ title: match[1], file: match[2] });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return rows;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Read the card-relevant frontmatter fields (`name`, `description`, `type`)
|
|
28
|
+
* from a leading `---` block. A missing or malformed block yields "" fields —
|
|
29
|
+
* the body text remains the source of truth the user opens in the editor.
|
|
30
|
+
*/
|
|
31
|
+
export function parseMemoryFrontmatter(text) {
|
|
32
|
+
const fields = { name: "", description: "", type: "" };
|
|
33
|
+
if (!text.startsWith("---")) {
|
|
34
|
+
return fields;
|
|
35
|
+
}
|
|
36
|
+
const blockEnd = text.indexOf("\n---", 3);
|
|
37
|
+
if (blockEnd < 0) {
|
|
38
|
+
return fields;
|
|
39
|
+
}
|
|
40
|
+
for (const line of text.slice(3, blockEnd).split("\n")) {
|
|
41
|
+
const match = /^(name|description|type):\s*(.*)$/.exec(line.trim());
|
|
42
|
+
if (match?.[1] === undefined || match?.[2] === undefined) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const value = match[2].trim().replace(/^["']|["']$/g, "");
|
|
46
|
+
if (match[1] === "name") {
|
|
47
|
+
fields.name = value;
|
|
48
|
+
}
|
|
49
|
+
else if (match[1] === "description") {
|
|
50
|
+
fields.description = value;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
fields.type = value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return fields;
|
|
57
|
+
}
|
|
58
|
+
/** Store files the card inspects: every .md except the index, sorted. */
|
|
59
|
+
function listStoreFiles(memoryDir) {
|
|
60
|
+
return readdirSync(memoryDir)
|
|
61
|
+
.filter((file) => file.endsWith(".md") && file !== MEMORY_SNAPSHOT_INDEX_FILE)
|
|
62
|
+
.sort();
|
|
63
|
+
}
|
|
64
|
+
/** Single read pass over the store; throws on any filesystem failure. */
|
|
65
|
+
function readStoreState(memoryDir) {
|
|
66
|
+
const indexText = readFileSync(join(memoryDir, MEMORY_SNAPSHOT_INDEX_FILE), "utf8");
|
|
67
|
+
const diskFiles = listStoreFiles(memoryDir);
|
|
68
|
+
const files = diskFiles.map((file) => {
|
|
69
|
+
const frontmatter = parseMemoryFrontmatter(readFileSync(join(memoryDir, file), "utf8"));
|
|
70
|
+
return { file, name: frontmatter.name, description: frontmatter.description, type: frontmatter.type };
|
|
71
|
+
});
|
|
72
|
+
return { indexRows: parseMemoryIndexRows(indexText), diskFiles, files };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The read-only memory snapshot for one workspace root. A missing store
|
|
76
|
+
* reports `exists: false` (the card shows the empty state); a store that
|
|
77
|
+
* exists but fails to read reports `readError` instead of throwing — the
|
|
78
|
+
* card must observe, never break.
|
|
79
|
+
*/
|
|
80
|
+
export function memorySnapshot(root) {
|
|
81
|
+
const workspaceRoot = resolve(root);
|
|
82
|
+
const memoryDir = memoryDirFor(workspaceRoot);
|
|
83
|
+
const absent = {
|
|
84
|
+
root: workspaceRoot,
|
|
85
|
+
memoryDir,
|
|
86
|
+
exists: false,
|
|
87
|
+
fileCount: 0,
|
|
88
|
+
indexEntryCount: 0,
|
|
89
|
+
files: [],
|
|
90
|
+
missingFiles: [],
|
|
91
|
+
unindexedFiles: [],
|
|
92
|
+
readError: null,
|
|
93
|
+
};
|
|
94
|
+
if (!existsSync(memoryDir)) {
|
|
95
|
+
return absent;
|
|
96
|
+
}
|
|
97
|
+
let state;
|
|
98
|
+
try {
|
|
99
|
+
state = readStoreState(memoryDir);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
// Filesystem read failure degrades to an error field: the card shows
|
|
103
|
+
// what went wrong instead of taking the host down with it.
|
|
104
|
+
return {
|
|
105
|
+
...absent,
|
|
106
|
+
exists: true,
|
|
107
|
+
readError: error instanceof Error ? error.message : String(error),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const referenced = new Set(state.indexRows.map((row) => row.file));
|
|
111
|
+
return {
|
|
112
|
+
root: workspaceRoot,
|
|
113
|
+
memoryDir,
|
|
114
|
+
exists: true,
|
|
115
|
+
fileCount: state.diskFiles.length,
|
|
116
|
+
indexEntryCount: state.indexRows.length,
|
|
117
|
+
files: state.files,
|
|
118
|
+
missingFiles: state.indexRows
|
|
119
|
+
.map((row) => row.file)
|
|
120
|
+
.filter((file) => !state.diskFiles.includes(file)),
|
|
121
|
+
unindexedFiles: state.diskFiles.filter((file) => !referenced.has(file)),
|
|
122
|
+
readError: null,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=memory-snapshot.js.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-continual-evolve",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.12.1",
|
|
4
|
+
"description": "ZCode-style workspace memory for DeepSeek Harness: a per-workspace markdown store (<workspace>/.evolve/memory/) with session-start index injection — the model reads and writes the files with its native tools.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -19,15 +19,13 @@
|
|
|
19
19
|
"types": "./lib/index.d.ts",
|
|
20
20
|
"default": "./lib/index.js"
|
|
21
21
|
},
|
|
22
|
-
"./
|
|
23
|
-
"types": "./lib/types.d.ts",
|
|
24
|
-
"default": "./lib/types.js"
|
|
25
|
-
},
|
|
22
|
+
"./client": "./client/client.js",
|
|
26
23
|
"./package.json": "./package.json"
|
|
27
24
|
},
|
|
28
25
|
"files": [
|
|
29
26
|
"lib/**/*.js",
|
|
30
27
|
"lib/**/*.d.ts",
|
|
28
|
+
"client/client.js",
|
|
31
29
|
"cordis.patch.yml",
|
|
32
30
|
"README.md",
|
|
33
31
|
"LICENSE"
|
|
@@ -35,6 +33,10 @@
|
|
|
35
33
|
"dsh": {
|
|
36
34
|
"bundle": {
|
|
37
35
|
"patch": "./cordis.patch.yml"
|
|
36
|
+
},
|
|
37
|
+
"client": {
|
|
38
|
+
"inject": [],
|
|
39
|
+
"platform": "web"
|
|
38
40
|
}
|
|
39
41
|
},
|
|
40
42
|
"engines": {
|
|
@@ -51,7 +53,6 @@
|
|
|
51
53
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.scripts.json --noEmit",
|
|
52
54
|
"test": "vitest run",
|
|
53
55
|
"test:coverage": "vitest run --coverage",
|
|
54
|
-
"coverage:gaps": "tsx scripts/coverage-gaps.ts",
|
|
55
56
|
"test:watch": "vitest",
|
|
56
57
|
"lint": "oxlint src test",
|
|
57
58
|
"clean": "rm -rf lib",
|
|
@@ -60,19 +61,9 @@
|
|
|
60
61
|
"check:governance": "tsx scripts/verify-governance.ts"
|
|
61
62
|
},
|
|
62
63
|
"peerDependencies": {
|
|
63
|
-
"@deepseek-ai/cordis": "^4.0.1",
|
|
64
|
-
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.7-rc.1 || ^0.2.0-rc.1",
|
|
65
|
-
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.7-rc.1 || ^0.2.0-rc.1",
|
|
66
|
-
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.7-rc.1 || ^0.2.0-rc.1",
|
|
67
64
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
68
65
|
},
|
|
69
66
|
"devDependencies": {
|
|
70
|
-
"@deepseek-ai/cordis": "4.0.4",
|
|
71
|
-
"@deepseek-ai/dsh-agent": "0.2.0-rc.2",
|
|
72
|
-
"@deepseek-ai/dsh-commands": "0.2.0-rc.2",
|
|
73
|
-
"@deepseek-ai/dsh-home-paths": "0.2.0-rc.2",
|
|
74
|
-
"@deepseek-ai/dsh-llm": "0.2.0-rc.2",
|
|
75
|
-
"@deepseek-ai/dsh-tools": "0.2.0-rc.2",
|
|
76
67
|
"@deepseek-ai/schemastery": "3.18.4",
|
|
77
68
|
"@types/node": "^22.10.0",
|
|
78
69
|
"@vitest/coverage-v8": "^3.2.0",
|
package/lib/apply.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The apply pass: turn a validated proposal into state changes with full
|
|
3
|
-
* per-edit accounting. Every failure is recorded per edit — a bad edit never
|
|
4
|
-
* invalidates the whole proposal — and optimistic-concurrency checks reject
|
|
5
|
-
* edits whose target entry changed while planning was in flight.
|
|
6
|
-
*/
|
|
7
|
-
import type { EntrySource, HarnessScope, HarnessState, RefinementProposal, RefinementResult } from "./types.js";
|
|
8
|
-
export interface ApplyOptions {
|
|
9
|
-
id: string;
|
|
10
|
-
scope?: HarnessScope;
|
|
11
|
-
rollbackOf?: string;
|
|
12
|
-
/** State captured before planning; used to reject conflicting edits. */
|
|
13
|
-
baselineState?: HarnessState;
|
|
14
|
-
/**
|
|
15
|
-
* Trajectory citation stamped into newly created entries' metadata
|
|
16
|
-
* (sourceSession + sourceSeqs); updates keep whatever the entry already
|
|
17
|
-
* carries. Omitted entirely when the caller cannot determine it.
|
|
18
|
-
*/
|
|
19
|
-
source?: EntrySource;
|
|
20
|
-
}
|
|
21
|
-
export declare function applyRefinementProposal(state: HarnessState, proposal: RefinementProposal, options: ApplyOptions): RefinementResult;
|
|
22
|
-
//# sourceMappingURL=apply.d.ts.map
|
package/lib/apply.js
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { SOURCE_SESSION_KEY, SOURCE_SEQS_KEY, ARCHIVED_AT_KEY, cloneEntry, isArchived, slug } from "./types.js";
|
|
2
|
-
import { entryChangedSince } from "./state.js";
|
|
3
|
-
import { validateEdit } from "./validate.js";
|
|
4
|
-
export function applyRefinementProposal(state, proposal, options) {
|
|
5
|
-
const appliedEdits = [];
|
|
6
|
-
const touched = new Set();
|
|
7
|
-
const now = new Date().toISOString();
|
|
8
|
-
for (const edit of proposal.edits) {
|
|
9
|
-
// Store-prefix hygiene: planner edits are written against the MERGED
|
|
10
|
-
// view, where colliding project/local ids carry a `project:` /
|
|
11
|
-
// `local:` / `global:` prefix. A CREATE must never bake that view
|
|
12
|
-
// prefix into a permanent id (observed: global entries literally
|
|
13
|
-
// named "local:handoff_todo_…").
|
|
14
|
-
// Non-create edits address existing entries: the literal id wins —
|
|
15
|
-
// legacy stores contain entries whose literal id carries a colon
|
|
16
|
-
// prefix, and stripping unconditionally would resolve them to a
|
|
17
|
-
// sibling that does not exist ("entry not found", forcing file-level
|
|
18
|
-
// surgery). Only when the literal id is absent does the stripped
|
|
19
|
-
// form serve as the merged-view fallback.
|
|
20
|
-
// (Review audit 2026-08-28 S7 origin; literal-first fix 2026-09-24.)
|
|
21
|
-
const strippedId = edit.id?.replace(/^(?:local|project|global):/, "");
|
|
22
|
-
const requestedId = edit.action === "create"
|
|
23
|
-
? strippedId
|
|
24
|
-
: edit.id !== undefined && state.entries[edit.kind]?.[edit.id] !== undefined
|
|
25
|
-
? edit.id
|
|
26
|
-
: edit.id && strippedId;
|
|
27
|
-
const computedId = requestedId ?? (edit.action === "create" ? slug(edit.title ?? edit.kind, edit.kind) : undefined);
|
|
28
|
-
const id = computedId ?? "";
|
|
29
|
-
// Unknown kinds fail per-edit — a malformed proposal must never crash
|
|
30
|
-
// the whole pass (review audit 2026-08-28 S1).
|
|
31
|
-
const records = state.entries[edit.kind];
|
|
32
|
-
if (!records) {
|
|
33
|
-
appliedEdits.push({ ...edit, id, applied: false, error: `unsupported kind ${String(edit.kind)}` });
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
const before = cloneEntry(records[id]);
|
|
37
|
-
const entryKey = `${edit.kind}:${id}`;
|
|
38
|
-
if (options.baselineState && !touched.has(entryKey) && entryChangedSince(options.baselineState, state, edit.kind, id)) {
|
|
39
|
-
appliedEdits.push({
|
|
40
|
-
...edit,
|
|
41
|
-
id,
|
|
42
|
-
...(before ? { before } : {}),
|
|
43
|
-
applied: false,
|
|
44
|
-
error: "entry changed during planning",
|
|
45
|
-
});
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
// Validation sees the current entry so update rules can distinguish
|
|
49
|
-
// "carrying the persisted value" from "changing it" (skill_kind).
|
|
50
|
-
const validationError = validateEdit(edit, computedId, options.scope, before);
|
|
51
|
-
if (validationError) {
|
|
52
|
-
appliedEdits.push({ ...edit, id, applied: false, error: validationError });
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
if (edit.action === "delete") {
|
|
56
|
-
if (!before) {
|
|
57
|
-
appliedEdits.push({ ...edit, id, applied: false, error: "entry not found" });
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
delete records[id];
|
|
61
|
-
touched.add(entryKey);
|
|
62
|
-
appliedEdits.push({ ...edit, id, before, applied: true });
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
if (edit.action === "archive") {
|
|
66
|
-
// Archive hides an entry from injection but never deletes it:
|
|
67
|
-
// metadata.archivedAt is stamped through the normal apply path, so
|
|
68
|
-
// the edit gets a before/after snapshot, a version bump, and a
|
|
69
|
-
// rollback inverse like any other edit (restoring the snapshot
|
|
70
|
-
// clears the stamp). Idempotency is explicit: re-archiving an
|
|
71
|
-
// already-archived entry is an error, not a silent no-op.
|
|
72
|
-
if (!before) {
|
|
73
|
-
appliedEdits.push({ ...edit, id, applied: false, error: "entry not found" });
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
if (isArchived(before)) {
|
|
77
|
-
appliedEdits.push({ ...edit, id, before, applied: false, error: "entry already archived" });
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
const after = {
|
|
81
|
-
...before,
|
|
82
|
-
metadata: { ...before.metadata, [ARCHIVED_AT_KEY]: now },
|
|
83
|
-
updated_at: now,
|
|
84
|
-
version: before.version + 1,
|
|
85
|
-
};
|
|
86
|
-
records[id] = after;
|
|
87
|
-
touched.add(entryKey);
|
|
88
|
-
appliedEdits.push({ ...edit, id, before, after: cloneEntry(after) ?? after, applied: true });
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (edit.action === "create" && before) {
|
|
92
|
-
appliedEdits.push({ ...edit, id, before, applied: false, error: "entry already exists" });
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
if (edit.action === "update" && !before) {
|
|
96
|
-
appliedEdits.push({ ...edit, id, applied: false, error: "entry not found" });
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
// Trajectory citation: stamped on create (the distillation moment),
|
|
100
|
-
// never re-stamped on update (the entry keeps its original source).
|
|
101
|
-
// Model-supplied metadata wins on key collision.
|
|
102
|
-
const sourceMetadata = !before && options.source
|
|
103
|
-
? {
|
|
104
|
-
[SOURCE_SESSION_KEY]: options.source.sessionId,
|
|
105
|
-
...(options.source.seqs && options.source.seqs.length > 0
|
|
106
|
-
? { [SOURCE_SEQS_KEY]: options.source.seqs }
|
|
107
|
-
: {}),
|
|
108
|
-
}
|
|
109
|
-
: {};
|
|
110
|
-
const skillKind = edit.skill_kind ?? before?.skill_kind;
|
|
111
|
-
const after = {
|
|
112
|
-
id,
|
|
113
|
-
kind: edit.kind,
|
|
114
|
-
title: edit.title ?? before?.title ?? id,
|
|
115
|
-
content: edit.content ?? before?.content ?? "",
|
|
116
|
-
path: edit.path ?? before?.path ?? "general",
|
|
117
|
-
scope: before?.scope ?? options.scope ?? "local",
|
|
118
|
-
reference: edit.reference ?? before?.reference ?? {},
|
|
119
|
-
arguments: edit.arguments ?? before?.arguments ?? {},
|
|
120
|
-
...(skillKind !== undefined ? { skill_kind: skillKind } : {}),
|
|
121
|
-
metadata: { ...sourceMetadata, ...(edit.metadata ?? before?.metadata ?? {}) },
|
|
122
|
-
source: "evolve",
|
|
123
|
-
created_at: before?.created_at ?? now,
|
|
124
|
-
updated_at: now,
|
|
125
|
-
version: before ? before.version + 1 : 1,
|
|
126
|
-
};
|
|
127
|
-
records[id] = after;
|
|
128
|
-
touched.add(entryKey);
|
|
129
|
-
appliedEdits.push({
|
|
130
|
-
...edit,
|
|
131
|
-
id,
|
|
132
|
-
...(before ? { before } : {}),
|
|
133
|
-
after: cloneEntry(after) ?? after,
|
|
134
|
-
applied: true,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
const changes = appliedEdits.filter((edit) => edit.applied).map((edit) => `${edit.action} ${edit.kind}:${edit.id}`);
|
|
138
|
-
state.refinements.push({
|
|
139
|
-
id: options.id,
|
|
140
|
-
trigger: proposal.summary,
|
|
141
|
-
changes,
|
|
142
|
-
evidence: proposal.rationale,
|
|
143
|
-
outcome: proposal.expectedOutcome,
|
|
144
|
-
created_at: now,
|
|
145
|
-
});
|
|
146
|
-
return {
|
|
147
|
-
id: options.id,
|
|
148
|
-
summary: proposal.summary,
|
|
149
|
-
rationale: proposal.rationale,
|
|
150
|
-
expectedOutcome: proposal.expectedOutcome,
|
|
151
|
-
appliedEdits,
|
|
152
|
-
harnessStatePath: "",
|
|
153
|
-
...(options.rollbackOf ? { rollbackOf: options.rollbackOf } : {}),
|
|
154
|
-
...(options.scope ? { scope: options.scope } : {}),
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
//# sourceMappingURL=apply.js.map
|