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.
- package/README.md +172 -17
- package/README.zh.md +87 -10
- package/lib/apply.js +3 -1
- package/lib/approval.d.ts +25 -0
- package/lib/approval.js +9 -1
- package/lib/auto.d.ts +99 -5
- package/lib/auto.js +165 -6
- package/lib/benchmark-command.d.ts +9 -0
- package/lib/benchmark-command.js +331 -0
- package/lib/benchmark.d.ts +84 -0
- package/lib/benchmark.js +107 -1
- package/lib/command.js +33 -221
- package/lib/evaluate.d.ts +43 -7
- package/lib/evaluate.js +172 -43
- package/lib/evolve-event.d.ts +38 -0
- package/lib/evolve-event.js +49 -0
- package/lib/failures.d.ts +39 -0
- package/lib/failures.js +170 -0
- package/lib/fate.d.ts +128 -0
- package/lib/fate.js +342 -0
- package/lib/goal-command.d.ts +7 -0
- package/lib/goal-command.js +37 -0
- package/lib/index.d.ts +51 -21
- package/lib/index.js +32 -2
- package/lib/inject.d.ts +8 -0
- package/lib/inject.js +51 -4
- package/lib/llm-text.d.ts +30 -0
- package/lib/llm-text.js +49 -0
- package/lib/mount-command.d.ts +10 -0
- package/lib/mount-command.js +48 -0
- package/lib/mount.js +5 -0
- package/lib/plan.js +5 -0
- package/lib/planner.d.ts +8 -1
- package/lib/planner.js +40 -39
- package/lib/render.d.ts +1 -3
- package/lib/render.js +2 -5
- package/lib/review.d.ts +5 -2
- package/lib/review.js +27 -38
- package/lib/rollback.d.ts +1 -3
- package/lib/rollback.js +0 -8
- package/lib/score.d.ts +37 -4
- package/lib/score.js +120 -10
- package/lib/service.d.ts +2 -2
- package/lib/service.js +5 -2
- package/lib/skill-render.d.ts +15 -0
- package/lib/skill-render.js +30 -0
- package/lib/skill.d.ts +12 -7
- package/lib/skill.js +36 -31
- package/lib/skillquality.d.ts +80 -0
- package/lib/skillquality.js +311 -0
- package/lib/store.d.ts +1 -3
- package/lib/store.js +0 -7
- package/lib/tool.js +28 -4
- package/lib/types.d.ts +39 -0
- package/lib/types.js +19 -0
- package/lib/usage.d.ts +32 -0
- package/lib/usage.js +84 -0
- package/lib/validate.d.ts +12 -2
- package/lib/validate.js +51 -2
- package/lib/wrapup-command.d.ts +8 -0
- package/lib/wrapup-command.js +211 -0
- package/lib/wrapup.d.ts +215 -0
- package/lib/wrapup.js +427 -0
- package/package.json +8 -8
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill-quality integration: makes the DSH skill quality standard — carried
|
|
3
|
+
* by the author-distilled skills skill-creator / skill-audit (distilled
|
|
4
|
+
* from the official deepseek-harness 11 skills, facts verified against
|
|
5
|
+
* deepseek-harness 47f9438) — usable INSIDE the self-evolution loop.
|
|
6
|
+
*
|
|
7
|
+
* The planner and review gate are raw `ctx.llm` calls — they do not live in
|
|
8
|
+
* an agent session, so they cannot load skills through the `skill` tool.
|
|
9
|
+
* The skill-creator / skill-audit skills stay the single source of truth on
|
|
10
|
+
* disk; this module only:
|
|
11
|
+
*
|
|
12
|
+
* 1. reads the template facts at runtime
|
|
13
|
+
* (`<skillsRoot>/skill-creator/references/template.md`, 85 lines) and
|
|
14
|
+
* hands them to the planner as a `<skill_quality_standard>` block —
|
|
15
|
+
* the on-disk template wins, a built-in distilled guide is the fallback
|
|
16
|
+
* for installs without these skills;
|
|
17
|
+
* 2. code-enforces the mechanical frontmatter rules of
|
|
18
|
+
* `skill-creator/scripts/validate-frontmatter.mjs` (the platform would
|
|
19
|
+
* IGNORE a file that fails them), so a skill entry can never materialize
|
|
20
|
+
* a SKILL.md the platform refuses to load.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { renderSkillMarkdown } from "./skill-render.js";
|
|
25
|
+
/** Skill-name regex the platform enforces (skill-filesystem). */
|
|
26
|
+
const NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
27
|
+
const TRUE_WORDS = new Set(["true", "yes", "on", "1"]);
|
|
28
|
+
const FALSE_WORDS = new Set(["false", "no", "off", "0"]);
|
|
29
|
+
const LEGACY_KEYS = ["disableModelInvocation", "modelInvocable", "userInvocable"];
|
|
30
|
+
const CANONICAL_KEYS = {
|
|
31
|
+
disableModelInvocation: "disable-model-invocation",
|
|
32
|
+
modelInvocable: "disable-model-invocation",
|
|
33
|
+
userInvocable: "user-invocable",
|
|
34
|
+
};
|
|
35
|
+
/** Relative location of the skill-creator template facts. */
|
|
36
|
+
export const SKILL_CREATOR_TEMPLATE_REL = join("skill-creator", "references", "template.md");
|
|
37
|
+
/**
|
|
38
|
+
* Read the skill-creator template facts
|
|
39
|
+
* (`<skillsRoot>/skill-creator/references/template.md`; facts distilled
|
|
40
|
+
* from the official deepseek-harness skills). Returns null when the skills
|
|
41
|
+
* are not installed — callers fall back to the builtin distilled guide.
|
|
42
|
+
* Reading is a runtime reference, never a copy: template updates in the
|
|
43
|
+
* skill are picked up automatically.
|
|
44
|
+
*/
|
|
45
|
+
export function readSkillCreatorTemplate(skillsRoot) {
|
|
46
|
+
const path = join(skillsRoot, SKILL_CREATOR_TEMPLATE_REL);
|
|
47
|
+
try {
|
|
48
|
+
if (!existsSync(path))
|
|
49
|
+
return null;
|
|
50
|
+
const text = readFileSync(path, "utf8");
|
|
51
|
+
return text.trim().length > 0 ? text : null;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Builtin distilled skill-quality guide (fallback when the skill-creator
|
|
59
|
+
* template is not installed). Condenses the template facts — frontmatter
|
|
60
|
+
* schema, the 7 structural features, paragraph skeleton, and the
|
|
61
|
+
* no-duplication / real-trigger rules — so a planner still authors skills
|
|
62
|
+
* to the standard on installs without the skill-creator / skill-audit
|
|
63
|
+
* skills.
|
|
64
|
+
*/
|
|
65
|
+
export const BUILTIN_SKILL_QUALITY_GUIDE = `DSH skill quality standard (distilled by the author from the official deepseek-harness 11 skills; the full facts live in <skillsRoot>/skill-creator/references/template.md when installed):
|
|
66
|
+
|
|
67
|
+
Frontmatter schema (platform-enforced; violations make the platform IGNORE the whole file):
|
|
68
|
+
- name: required, kebab-case only (^[a-z0-9]+(?:-[a-z0-9]+)*$)
|
|
69
|
+
- description: required, non-empty; write "use when / do not use when" routing so the model can select it correctly
|
|
70
|
+
- invocation booleans accept true/false/yes/no/on/off/1/0; legacy camelCase keys (disableModelInvocation / modelInvocable / userInvocable) are rejected
|
|
71
|
+
- whenToUse (optional): non-empty string; metadata (optional): object
|
|
72
|
+
|
|
73
|
+
The 7 structural features of the official deepseek-harness skills:
|
|
74
|
+
1. Frontmatter is routing metadata, not a summary (description = when to use / when not to use)
|
|
75
|
+
2. Opens with a boundary declaration (guidance, not a script; mechanical flow skills may omit the disclaimer)
|
|
76
|
+
3. Prerequisites + exclusions: explicit required input, stop when missing (report the required input and stop), excluded scenarios
|
|
77
|
+
4. Layered information: Sources of truth (link only, do not re-summarize) -> numbered blocking requirements -> manual checks -> verification commands -> report format; all executable, no slogans
|
|
78
|
+
5. Skill interlinks: reference a single source of truth instead of duplicating it
|
|
79
|
+
6. Verifiable completion criteria: explicit verification commands and report format
|
|
80
|
+
7. Real use + iteration: a real trigger scenario must exist; calibration conclusions distill into references/
|
|
81
|
+
|
|
82
|
+
Paragraph skeleton (writing order): frontmatter -> H1 + boundary declaration -> Sources of truth -> numbered requirements / workflow (full commands) -> exclusions / stop conditions -> verification and report.
|
|
83
|
+
|
|
84
|
+
Creation rules: only create a skill for a REAL trigger scenario (who, in what real task, what signal) grounded in the trajectory — never invent one to pad the store; do not duplicate the official 11 skills or existing entries; skill bodies should be a SKILL.md document (this is what materializes under <skillsRoot>/<kebab-name>/SKILL.md).`;
|
|
85
|
+
/**
|
|
86
|
+
* The quality guide handed to the planner: the skill-creator template facts
|
|
87
|
+
* when the skills are installed, otherwise the builtin distilled guide.
|
|
88
|
+
* Never throws — a missing/unreadable template degrades to the builtin.
|
|
89
|
+
*/
|
|
90
|
+
export function skillQualityGuide(skillsRoot) {
|
|
91
|
+
if (skillsRoot) {
|
|
92
|
+
const template = readSkillCreatorTemplate(skillsRoot);
|
|
93
|
+
if (template !== null) {
|
|
94
|
+
return {
|
|
95
|
+
source: "template",
|
|
96
|
+
text: `The skill-creator template facts (distilled from the official deepseek-harness 11 skills, verified against deepseek-harness 47f9438; single source of truth, read from <skillsRoot>/skill-creator/references/template.md):\n\n${template}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { source: "builtin", text: BUILTIN_SKILL_QUALITY_GUIDE };
|
|
101
|
+
}
|
|
102
|
+
/** Split frontmatter out of a raw SKILL.md. Returns { yaml, body } or null when delimiters are missing. */
|
|
103
|
+
export function splitFrontmatter(raw) {
|
|
104
|
+
const lines = raw.split(/\r?\n/);
|
|
105
|
+
if (lines[0] !== "---")
|
|
106
|
+
return null;
|
|
107
|
+
const close = lines.indexOf("---", 1);
|
|
108
|
+
if (close < 0)
|
|
109
|
+
return null;
|
|
110
|
+
return { yaml: lines.slice(1, close).join("\n"), body: lines.slice(close + 1).join("\n") };
|
|
111
|
+
}
|
|
112
|
+
/** Parse one scalar in the YAML subset the platform's schema keys use. */
|
|
113
|
+
function parseScalar(raw, lineNo) {
|
|
114
|
+
const value = raw.trim();
|
|
115
|
+
if (value === "")
|
|
116
|
+
return "";
|
|
117
|
+
if (value.startsWith("'")) {
|
|
118
|
+
if (!value.endsWith("'"))
|
|
119
|
+
throw new Error(`unterminated single-quoted scalar at line ${lineNo}`);
|
|
120
|
+
return value.slice(1, -1).replace(/''/g, "'");
|
|
121
|
+
}
|
|
122
|
+
if (value.startsWith('"')) {
|
|
123
|
+
if (!value.endsWith('"'))
|
|
124
|
+
throw new Error(`unterminated double-quoted scalar at line ${lineNo}`);
|
|
125
|
+
return value
|
|
126
|
+
.slice(1, -1)
|
|
127
|
+
.replace(/\\n/g, "\n")
|
|
128
|
+
.replace(/\\t/g, "\t")
|
|
129
|
+
.replace(/\\"/g, '"')
|
|
130
|
+
.replace(/\\\\/g, "\\");
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
/** Minimal YAML-subset parser for the flat schema the platform reads (mirrors validate-frontmatter.mjs). */
|
|
135
|
+
function parseMiniYaml(text) {
|
|
136
|
+
const data = {};
|
|
137
|
+
const lines = text.split("\n");
|
|
138
|
+
let i = 0;
|
|
139
|
+
while (i < lines.length) {
|
|
140
|
+
const line = lines[i] ?? "";
|
|
141
|
+
const lineNo = i + 1;
|
|
142
|
+
i += 1;
|
|
143
|
+
const trimmed = line.trim();
|
|
144
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
145
|
+
continue;
|
|
146
|
+
if (line.length - line.trimStart().length > 0) {
|
|
147
|
+
throw new Error(`unsupported indented construct at line ${lineNo}: ${trimmed}`);
|
|
148
|
+
}
|
|
149
|
+
const match = /^([A-Za-z0-9_-]+):(?:\s+(.*))?$/.exec(trimmed);
|
|
150
|
+
if (!match)
|
|
151
|
+
throw new Error(`unparseable line ${lineNo}: ${trimmed}`);
|
|
152
|
+
const key = match[1] ?? "";
|
|
153
|
+
let value = match[2] ?? "";
|
|
154
|
+
if (value === "|" || value === ">") {
|
|
155
|
+
const block = [];
|
|
156
|
+
while (i < lines.length && (lines[i] ?? "").trim() !== "" && (lines[i] ?? "").startsWith(" ")) {
|
|
157
|
+
block.push(lines[i] ?? "");
|
|
158
|
+
i += 1;
|
|
159
|
+
}
|
|
160
|
+
data[key] = block.join("\n");
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (value === "" && i < lines.length && (lines[i] ?? "").startsWith(" ") && (lines[i] ?? "").trim() !== "") {
|
|
164
|
+
const nested = {};
|
|
165
|
+
while (i < lines.length && (lines[i] ?? "").trim() !== "" && (lines[i] ?? "").startsWith(" ")) {
|
|
166
|
+
const nm = /^([A-Za-z0-9_-]+):(?:\s+(.*))?$/.exec((lines[i] ?? "").trim());
|
|
167
|
+
if (!nm)
|
|
168
|
+
throw new Error(`unparseable nested line ${i + 1}: ${(lines[i] ?? "").trim()}`);
|
|
169
|
+
nested[nm[1] ?? ""] = parseScalar(nm[2] ?? "", i + 1);
|
|
170
|
+
i += 1;
|
|
171
|
+
}
|
|
172
|
+
data[key] = nested;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
data[key] = parseScalar(value, lineNo);
|
|
176
|
+
}
|
|
177
|
+
return data;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Mechanical frontmatter validation of a rendered SKILL.md, mirroring
|
|
181
|
+
* `skill-creator/scripts/validate-frontmatter.mjs` (and the platform's
|
|
182
|
+
* skill-filesystem rules): delimiter structure, name kebab-case, non-empty
|
|
183
|
+
* description, invocation-boolean spellings, legacy camelCase key rejection,
|
|
184
|
+
* whenToUse/metadata types. Returns human-readable problems; an empty array
|
|
185
|
+
* means the file would load.
|
|
186
|
+
*/
|
|
187
|
+
export function validateRenderedSkillMarkdown(markdown) {
|
|
188
|
+
const problems = [];
|
|
189
|
+
const split = splitFrontmatter(markdown);
|
|
190
|
+
if (!split) {
|
|
191
|
+
return [
|
|
192
|
+
"missing YAML frontmatter (first line `---` with a closing `---`) — platform would IGNORE this file",
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
let data;
|
|
196
|
+
try {
|
|
197
|
+
data = parseMiniYaml(split.yaml);
|
|
198
|
+
}
|
|
199
|
+
catch (cause) {
|
|
200
|
+
return [`invalid YAML frontmatter: ${cause instanceof Error ? cause.message : String(cause)} — platform would IGNORE this file`];
|
|
201
|
+
}
|
|
202
|
+
const name = typeof data["name"] === "string" && data["name"].length > 0 ? data["name"] : undefined;
|
|
203
|
+
if (name === undefined) {
|
|
204
|
+
problems.push("frontmatter requires non-empty `name` — platform would IGNORE this file");
|
|
205
|
+
}
|
|
206
|
+
else if (!NAME_RE.test(name)) {
|
|
207
|
+
problems.push(`invalid skill name "${name}" (must match ${NAME_RE}) — platform would IGNORE this file`);
|
|
208
|
+
}
|
|
209
|
+
const description = typeof data["description"] === "string" && data["description"].length > 0 ? data["description"] : undefined;
|
|
210
|
+
if (description === undefined) {
|
|
211
|
+
problems.push("frontmatter requires non-empty `description` — platform would IGNORE this file");
|
|
212
|
+
}
|
|
213
|
+
for (const legacy of LEGACY_KEYS) {
|
|
214
|
+
if (Object.hasOwn(data, legacy)) {
|
|
215
|
+
problems.push(`legacy key "${legacy}" is unsupported; use "${CANONICAL_KEYS[legacy]}" — platform would IGNORE this file`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
for (const key of ["disable-model-invocation", "user-invocable"]) {
|
|
219
|
+
if (!Object.hasOwn(data, key))
|
|
220
|
+
continue;
|
|
221
|
+
const value = data[key];
|
|
222
|
+
if (typeof value === "boolean")
|
|
223
|
+
continue;
|
|
224
|
+
const word = String(value).toLowerCase();
|
|
225
|
+
if (TRUE_WORDS.has(word) || FALSE_WORDS.has(word))
|
|
226
|
+
continue;
|
|
227
|
+
problems.push(`frontmatter field "${key}" must be a boolean (accepted: true/false/yes/no/on/off/1/0), got ${JSON.stringify(value)} — platform would IGNORE this file`);
|
|
228
|
+
}
|
|
229
|
+
if (Object.hasOwn(data, "whenToUse") && !(typeof data["whenToUse"] === "string" && data["whenToUse"].length > 0)) {
|
|
230
|
+
problems.push("`whenToUse` must be a non-empty string when present");
|
|
231
|
+
}
|
|
232
|
+
if (Object.hasOwn(data, "metadata") && (typeof data["metadata"] !== "object" || data["metadata"] === null || Array.isArray(data["metadata"]))) {
|
|
233
|
+
problems.push("`metadata` must be an object when present");
|
|
234
|
+
}
|
|
235
|
+
return problems;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Mechanical validation of a skill entry's raw `content` (the SKILL.md body
|
|
239
|
+
* that materializes under the generated frontmatter). Code-enforced at
|
|
240
|
+
* apply time so a bad entry never reaches the store:
|
|
241
|
+
* - empty content is rejected;
|
|
242
|
+
* - content must not open with a `---` block: the materializer generates
|
|
243
|
+
* its own frontmatter, and a second frontmatter in the body would be
|
|
244
|
+
* parsed instead of the generated one (the platform reads the FIRST
|
|
245
|
+
* closing `---`), so the file could be ignored or routed wrongly;
|
|
246
|
+
* - resource references (`references/…`, `scripts/…`) must be skill-local
|
|
247
|
+
* relative paths — parent-relative (`../`) or absolute targets escape the
|
|
248
|
+
* skill directory and are rejected.
|
|
249
|
+
* Returns human-readable problems; an empty array means the content is
|
|
250
|
+
* mechanically acceptable.
|
|
251
|
+
*/
|
|
252
|
+
export function validateSkillEntryContent(content) {
|
|
253
|
+
const problems = [];
|
|
254
|
+
const trimmed = content.trim();
|
|
255
|
+
if (trimmed.length === 0) {
|
|
256
|
+
problems.push("skill content is empty");
|
|
257
|
+
return problems;
|
|
258
|
+
}
|
|
259
|
+
if (trimmed.startsWith("---")) {
|
|
260
|
+
problems.push("skill content must not start with a `---` frontmatter block (the materializer generates frontmatter from id/title; a body-level `---` would shadow it and the platform could IGNORE the file)");
|
|
261
|
+
}
|
|
262
|
+
for (const match of trimmed.matchAll(/(?<![\w])(references|scripts)\/[^\s)]+/g)) {
|
|
263
|
+
const ref = match[0] ?? "";
|
|
264
|
+
if (ref.startsWith("../") || ref.includes("/../") || ref.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(ref)) {
|
|
265
|
+
problems.push(`skill content resource reference escapes the skill directory: ${ref}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return problems;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Validate the FULL rendered SKILL.md of an entry (generated frontmatter +
|
|
272
|
+
* body) — the exact bytes that materialize on disk. Used as the final
|
|
273
|
+
* code-enforced line after materialization; problems here mean the platform
|
|
274
|
+
* would refuse to load the file.
|
|
275
|
+
*/
|
|
276
|
+
export function validateRenderedSkill(entry) {
|
|
277
|
+
return validateRenderedSkillMarkdown(renderSkillMarkdown(entry));
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Resource references (`references/…`, `scripts/…`) found in a skill body —
|
|
281
|
+
* the same scanning policy as validate-frontmatter.mjs: markdown link
|
|
282
|
+
* targets starting with the category, plus backticked/prose paths carrying
|
|
283
|
+
* a filename extension. Used after materialization to warn about dangling
|
|
284
|
+
* references (a body referencing a resource the entry never ships).
|
|
285
|
+
*/
|
|
286
|
+
export function skillResourceRefs(content) {
|
|
287
|
+
const refs = new Set();
|
|
288
|
+
for (const match of content.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)) {
|
|
289
|
+
const target = ((match[1] ?? "").trim().split(/\s+/)[0] ?? "").trim();
|
|
290
|
+
if (/^(references|scripts)\/[\w./-]+$/.test(target) && !target.startsWith("../")) {
|
|
291
|
+
refs.add(target);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const stripped = content.replace(/\[[^\]]*\]\([^)]*\)/g, "");
|
|
295
|
+
for (const match of stripped.matchAll(/(?<![\w])(references|scripts)\/[\w./-]+\.\w+/g)) {
|
|
296
|
+
const path = match[0] ?? "";
|
|
297
|
+
// Cross-skill interlinks (`../skill-creator/...`) resolve against the
|
|
298
|
+
// sibling skill's directory, not this one — skip references whose
|
|
299
|
+
// prose prefix walks up a directory (mirrors validate-frontmatter.mjs).
|
|
300
|
+
let cursor = (match.index ?? 0) - 1;
|
|
301
|
+
while (cursor >= 0 && /[\w./-]/.test(stripped[cursor] ?? ""))
|
|
302
|
+
cursor -= 1;
|
|
303
|
+
if (!stripped.slice(cursor + 1, match.index ?? 0).includes("..")) {
|
|
304
|
+
refs.add(path);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return [...refs];
|
|
308
|
+
}
|
|
309
|
+
/** Kebab-case name under which the entry materializes (exported for diagnostics). */
|
|
310
|
+
export { skillNameOf } from "./skill-render.js";
|
|
311
|
+
//# sourceMappingURL=skillquality.js.map
|
package/lib/store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HarnessScope,
|
|
1
|
+
import type { HarnessScope, RefinementResult } from "./types.js";
|
|
2
2
|
export declare const EVOLVE_DIR = "evolve";
|
|
3
3
|
export interface StorePaths {
|
|
4
4
|
/** Directory holding harness_state.json. */
|
|
@@ -15,6 +15,4 @@ export declare function snapshotBefore(paths: StorePaths, refinementId: string):
|
|
|
15
15
|
export declare function appendResult(paths: StorePaths, result: RefinementResult): void;
|
|
16
16
|
/** Read the applied results history; malformed lines are skipped, never fatal. */
|
|
17
17
|
export declare function loadResults(paths: StorePaths): RefinementResult[];
|
|
18
|
-
/** Load a state file into memory, returning empty state when absent. */
|
|
19
|
-
export declare function loadStateFile(paths: StorePaths): HarnessState;
|
|
20
18
|
//# sourceMappingURL=store.d.ts.map
|
package/lib/store.js
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
|
-
import { emptyHarnessState } from "./types.js";
|
|
18
17
|
import { stateFilePath } from "./state.js";
|
|
19
18
|
export const EVOLVE_DIR = "evolve";
|
|
20
19
|
export function storePaths(baseDir, scope, sessionId) {
|
|
@@ -65,10 +64,4 @@ export function loadResults(paths) {
|
|
|
65
64
|
function isResult(data) {
|
|
66
65
|
return typeof data === "object" && data !== null && "id" in data && "appliedEdits" in data;
|
|
67
66
|
}
|
|
68
|
-
/** Load a state file into memory, returning empty state when absent. */
|
|
69
|
-
export function loadStateFile(paths) {
|
|
70
|
-
return existsSync(stateFilePath(paths.stateDir))
|
|
71
|
-
? JSON.parse(readFileSync(stateFilePath(paths.stateDir), "utf8"))
|
|
72
|
-
: emptyHarnessState();
|
|
73
|
-
}
|
|
74
67
|
//# sourceMappingURL=store.js.map
|
package/lib/tool.js
CHANGED
|
@@ -2,6 +2,8 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
|
2
2
|
import { formatHarnessStateForPrompt } from "./render.js";
|
|
3
3
|
import { requireGlobalApproval } from "./approval.js";
|
|
4
4
|
import { entrySourceOf } from "./source.js";
|
|
5
|
+
import { getUsageCount, loadUsage } from "./usage.js";
|
|
6
|
+
import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
|
|
5
7
|
const SCOPES = ["local", "global"];
|
|
6
8
|
/** Accept both the boolean tool parameter (`global: true`) and the string form. */
|
|
7
9
|
export function scopeOf(value, fallback) {
|
|
@@ -32,19 +34,35 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
32
34
|
execute: async (args, exec) => {
|
|
33
35
|
const scope = scopeOf(args.scope, "local");
|
|
34
36
|
const state = engine.load(scope, sessionIdOf(exec));
|
|
35
|
-
|
|
37
|
+
const text = formatHarnessStateForPrompt(state);
|
|
38
|
+
// Append injection usage counts (gap B1).
|
|
39
|
+
const usage = loadUsage(engine.baseDir);
|
|
40
|
+
const usageLines = [];
|
|
41
|
+
for (const kind of Object.keys(state.entries)) {
|
|
42
|
+
for (const entry of Object.values(state.entries[kind])) {
|
|
43
|
+
const count = getUsageCount(usage, kind, entry.id);
|
|
44
|
+
if (count > 0) {
|
|
45
|
+
usageLines.push(`${kind}:${entry.id} — injected ${count}×`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (usageLines.length > 0) {
|
|
50
|
+
return textResult(`${text}\n\n# Injection Usage\n${usageLines.join("\n")}`);
|
|
51
|
+
}
|
|
52
|
+
return textResult(text);
|
|
36
53
|
},
|
|
37
54
|
}));
|
|
38
55
|
ctx.tools.register(defineTool({
|
|
39
56
|
name: "evolve_add",
|
|
40
|
-
description: "Create one harness entry (prompt/memory/skill/subagent).
|
|
57
|
+
description: "Create one harness entry (prompt/memory/skill/subagent). Executable skills require reference {type:python, import, callable} and an arguments contract; guidance skills (skill_kind=guidance) are SKILL.md documents — recurring multi-step workflows — and must NOT carry a reference. Snapshot, version, and history are handled automatically.",
|
|
41
58
|
parameters: {
|
|
42
59
|
kind: { type: "string", enum: ["prompt", "memory", "skill", "subagent"], required: true, description: "Entry kind." },
|
|
43
60
|
title: { type: "string", required: true, description: "Stable title." },
|
|
44
61
|
content: { type: "string", required: true, description: "Entry body." },
|
|
45
62
|
path: { type: "string", description: "Optional grouping path." },
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
skill_kind: { type: "string", enum: ["executable", "guidance"], description: "For skills: executable (python reference, default) or guidance (SKILL.md document, no reference)." },
|
|
64
|
+
reference: { type: "object", additionalProperties: true, description: "For executable skills: {type:'python', import, callable}." },
|
|
65
|
+
arguments: { type: "object", additionalProperties: true, description: "For executable skills: accepted input contract." },
|
|
48
66
|
global: { type: "boolean", description: "Set true to write the cross-session store (requires human approval; only for durable, reusable lessons)." },
|
|
49
67
|
},
|
|
50
68
|
output: {
|
|
@@ -64,6 +82,8 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
64
82
|
};
|
|
65
83
|
if (args.path !== undefined)
|
|
66
84
|
edit.path = args.path;
|
|
85
|
+
if (args.skill_kind !== undefined)
|
|
86
|
+
edit.skill_kind = args.skill_kind;
|
|
67
87
|
if (args.reference !== undefined)
|
|
68
88
|
edit.reference = args.reference;
|
|
69
89
|
if (args.arguments !== undefined)
|
|
@@ -151,6 +171,10 @@ function applyEditsText(engine, scope, sessionId, edits, agent) {
|
|
|
151
171
|
: { scope });
|
|
152
172
|
const applied = result.appliedEdits.filter((e) => e.applied);
|
|
153
173
|
const failed = result.appliedEdits.filter((e) => !e.applied);
|
|
174
|
+
// Gap C4: emit structured evolve_complete event for third-party consumers.
|
|
175
|
+
if (applied.length > 0 && sessionId) {
|
|
176
|
+
emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, "manual_tool", sessionId));
|
|
177
|
+
}
|
|
154
178
|
const lines = [`refinement ${result.id}: ${applied.length} applied, ${failed.length} failed`];
|
|
155
179
|
for (const e of applied) {
|
|
156
180
|
lines.push(`- ${e.action} ${e.kind}:${e.id} (v${(e.after?.version ?? e.before?.version) ?? "?"})`);
|
package/lib/types.d.ts
CHANGED
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
*/
|
|
11
11
|
/** What a harness entry can be. */
|
|
12
12
|
export type RefinementKind = "prompt" | "memory" | "skill" | "subagent";
|
|
13
|
+
/**
|
|
14
|
+
* Skill-entry form: `executable` skills carry a python reference contract
|
|
15
|
+
* and can be hot-mounted as tools; `guidance` skills are SKILL.md documents
|
|
16
|
+
* (no python reference) that materialize as discoverable skills for the
|
|
17
|
+
* `skill` tool — the form for recurring multi-step workflows. Absent means
|
|
18
|
+
* `executable` (backwards compatible with pre-guidance stores).
|
|
19
|
+
*/
|
|
20
|
+
export type SkillKind = "executable" | "guidance";
|
|
13
21
|
/** How an entry changes. */
|
|
14
22
|
export type RefinementAction = "create" | "update" | "delete" | "archive";
|
|
15
23
|
/** Where an entry lives: session-scoped or cross-session. */
|
|
@@ -30,6 +38,25 @@ export declare const SOURCE_SEQS_KEY = "sourceSeqs";
|
|
|
30
38
|
* the entry can be restored (unarchive) or rolled back like any other edit.
|
|
31
39
|
*/
|
|
32
40
|
export declare const ARCHIVED_AT_KEY = "archivedAt";
|
|
41
|
+
/**
|
|
42
|
+
* Metadata key stamped on a LOCAL entry that was promoted to the global
|
|
43
|
+
* store by a session wrap-up: the id of the global entry it became. Present
|
|
44
|
+
* means the entry's lifecycle is finished — it must not be offered for
|
|
45
|
+
* promotion again (the global copy is the live one, the local copy is a
|
|
46
|
+
* restorable trace).
|
|
47
|
+
*/
|
|
48
|
+
export declare const PROMOTED_TO_KEY = "promotedTo";
|
|
49
|
+
/**
|
|
50
|
+
* Metadata key recording when a local entry was promoted to the global
|
|
51
|
+
* store (companion of {@link PROMOTED_TO_KEY}).
|
|
52
|
+
*/
|
|
53
|
+
export declare const PROMOTED_AT_KEY = "promotedAt";
|
|
54
|
+
/**
|
|
55
|
+
* Metadata key stamped on a GLOBAL entry created by a session wrap-up
|
|
56
|
+
* promotion: `<sessionId>:<localEntryId>` — the反向 provenance link from the
|
|
57
|
+
* cross-session copy back to the session it was distilled from.
|
|
58
|
+
*/
|
|
59
|
+
export declare const SOURCED_FROM_KEY = "sourcedFromLocal";
|
|
33
60
|
/**
|
|
34
61
|
* True when the entry is archived (hidden from injection, restorable).
|
|
35
62
|
* Absent or empty archivedAt means the entry is active.
|
|
@@ -58,6 +85,8 @@ export interface HarnessEntry {
|
|
|
58
85
|
reference: Record<string, unknown>;
|
|
59
86
|
/** Skill entries declare their accepted inputs here. */
|
|
60
87
|
arguments: Record<string, unknown>;
|
|
88
|
+
/** Skill form: "executable" (default) or "guidance" (SKILL.md document). */
|
|
89
|
+
skill_kind?: SkillKind;
|
|
61
90
|
metadata: Record<string, unknown>;
|
|
62
91
|
source: "evolve";
|
|
63
92
|
created_at: string;
|
|
@@ -91,8 +120,18 @@ export interface RefinementEdit {
|
|
|
91
120
|
path?: string;
|
|
92
121
|
reference?: Record<string, unknown>;
|
|
93
122
|
arguments?: Record<string, unknown>;
|
|
123
|
+
/** Skill form: "guidance" for SKILL.md document skills; absent = executable. */
|
|
124
|
+
skill_kind?: SkillKind;
|
|
94
125
|
metadata?: Record<string, unknown>;
|
|
95
126
|
reason?: string;
|
|
127
|
+
/**
|
|
128
|
+
* Gap C2: blast-radius annotation — how broadly this edit applies.
|
|
129
|
+
* Values: "general" (cross-project tactical), "project" (single project),
|
|
130
|
+
* "session" (one-off session-specific). The review gate checks that
|
|
131
|
+
* local-scope edits are "session" or "project" and global-scope edits
|
|
132
|
+
* are "general" or "project".
|
|
133
|
+
*/
|
|
134
|
+
blastRadius?: "general" | "project" | "session";
|
|
96
135
|
}
|
|
97
136
|
/** The structured output of a planning pass. */
|
|
98
137
|
export interface RefinementProposal {
|
package/lib/types.js
CHANGED
|
@@ -24,6 +24,25 @@ export const SOURCE_SEQS_KEY = "sourceSeqs";
|
|
|
24
24
|
* the entry can be restored (unarchive) or rolled back like any other edit.
|
|
25
25
|
*/
|
|
26
26
|
export const ARCHIVED_AT_KEY = "archivedAt";
|
|
27
|
+
/**
|
|
28
|
+
* Metadata key stamped on a LOCAL entry that was promoted to the global
|
|
29
|
+
* store by a session wrap-up: the id of the global entry it became. Present
|
|
30
|
+
* means the entry's lifecycle is finished — it must not be offered for
|
|
31
|
+
* promotion again (the global copy is the live one, the local copy is a
|
|
32
|
+
* restorable trace).
|
|
33
|
+
*/
|
|
34
|
+
export const PROMOTED_TO_KEY = "promotedTo";
|
|
35
|
+
/**
|
|
36
|
+
* Metadata key recording when a local entry was promoted to the global
|
|
37
|
+
* store (companion of {@link PROMOTED_TO_KEY}).
|
|
38
|
+
*/
|
|
39
|
+
export const PROMOTED_AT_KEY = "promotedAt";
|
|
40
|
+
/**
|
|
41
|
+
* Metadata key stamped on a GLOBAL entry created by a session wrap-up
|
|
42
|
+
* promotion: `<sessionId>:<localEntryId>` — the反向 provenance link from the
|
|
43
|
+
* cross-session copy back to the session it was distilled from.
|
|
44
|
+
*/
|
|
45
|
+
export const SOURCED_FROM_KEY = "sourcedFromLocal";
|
|
27
46
|
/**
|
|
28
47
|
* True when the entry is archived (hidden from injection, restorable).
|
|
29
48
|
* Absent or empty archivedAt means the entry is active.
|
package/lib/usage.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { HarnessState, RefinementKind } from "./types.js";
|
|
2
|
+
export interface UsageStore {
|
|
3
|
+
/** Injection count per entry key (`kind:id`). */
|
|
4
|
+
counts: Record<string, number>;
|
|
5
|
+
}
|
|
6
|
+
/** Load the usage store from disk; returns an empty store when absent or corrupt. */
|
|
7
|
+
export declare function loadUsage(baseDir: string): UsageStore;
|
|
8
|
+
/** Persist the usage store atomically. */
|
|
9
|
+
export declare function saveUsage(baseDir: string, store: UsageStore): void;
|
|
10
|
+
/** Build the usage key for an entry. */
|
|
11
|
+
export declare function usageKey(kind: RefinementKind, id: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Increment injection counts for the entries that were actually injected.
|
|
14
|
+
* Called after `entriesSectionText` renders the injected block. Keys not
|
|
15
|
+
* present in the store are initialized to 1; existing keys are incremented.
|
|
16
|
+
*/
|
|
17
|
+
export declare function recordInjection(baseDir: string, injectedKeys: string[]): void;
|
|
18
|
+
/**
|
|
19
|
+
* Get the injection count for a specific entry. Returns 0 when the entry
|
|
20
|
+
* has never been injected (absent from the store).
|
|
21
|
+
*/
|
|
22
|
+
export declare function getUsageCount(store: UsageStore, kind: RefinementKind, id: string): number;
|
|
23
|
+
/**
|
|
24
|
+
* Find entries with zero injection usage. Returns `{kind, id, title}` for
|
|
25
|
+
* each entry that has never been injected — prime candidates for archival.
|
|
26
|
+
*/
|
|
27
|
+
export declare function zeroUsageEntries(state: HarnessState, store: UsageStore): {
|
|
28
|
+
kind: RefinementKind;
|
|
29
|
+
id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
}[];
|
|
32
|
+
//# sourceMappingURL=usage.d.ts.map
|
package/lib/usage.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
/** Load the usage store from disk; returns an empty store when absent or corrupt. */
|
|
18
|
+
export function loadUsage(baseDir) {
|
|
19
|
+
const path = usagePath(baseDir);
|
|
20
|
+
try {
|
|
21
|
+
if (!existsSync(path))
|
|
22
|
+
return { counts: {} };
|
|
23
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
24
|
+
if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
|
|
25
|
+
return { counts: raw };
|
|
26
|
+
}
|
|
27
|
+
return { counts: {} };
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return { counts: {} };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Persist the usage store atomically. */
|
|
34
|
+
export function saveUsage(baseDir, store) {
|
|
35
|
+
const dir = join(baseDir, "evolve");
|
|
36
|
+
mkdirSync(dir, { recursive: true });
|
|
37
|
+
const path = usagePath(baseDir);
|
|
38
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
39
|
+
writeFileSync(tmp, `${JSON.stringify(store.counts, null, 2)}\n`, "utf8");
|
|
40
|
+
renameSync(tmp, path);
|
|
41
|
+
}
|
|
42
|
+
/** Build the usage key for an entry. */
|
|
43
|
+
export function usageKey(kind, id) {
|
|
44
|
+
return `${kind}:${id}`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Increment injection counts for the entries that were actually injected.
|
|
48
|
+
* Called after `entriesSectionText` renders the injected block. Keys not
|
|
49
|
+
* present in the store are initialized to 1; existing keys are incremented.
|
|
50
|
+
*/
|
|
51
|
+
export function recordInjection(baseDir, injectedKeys) {
|
|
52
|
+
if (injectedKeys.length === 0)
|
|
53
|
+
return;
|
|
54
|
+
const store = loadUsage(baseDir);
|
|
55
|
+
for (const key of injectedKeys) {
|
|
56
|
+
store.counts[key] = (store.counts[key] ?? 0) + 1;
|
|
57
|
+
}
|
|
58
|
+
saveUsage(baseDir, store);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get the injection count for a specific entry. Returns 0 when the entry
|
|
62
|
+
* has never been injected (absent from the store).
|
|
63
|
+
*/
|
|
64
|
+
export function getUsageCount(store, kind, id) {
|
|
65
|
+
return store.counts[usageKey(kind, id)] ?? 0;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Find entries with zero injection usage. Returns `{kind, id, title}` for
|
|
69
|
+
* each entry that has never been injected — prime candidates for archival.
|
|
70
|
+
*/
|
|
71
|
+
export function zeroUsageEntries(state, store) {
|
|
72
|
+
const results = [];
|
|
73
|
+
for (const kind of Object.keys(state.entries)) {
|
|
74
|
+
for (const entry of Object.values(state.entries[kind])) {
|
|
75
|
+
if (entry.scope !== "local")
|
|
76
|
+
continue;
|
|
77
|
+
if (getUsageCount(store, kind, entry.id) === 0) {
|
|
78
|
+
results.push({ kind, id: entry.id, title: entry.title });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return results;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=usage.js.map
|
package/lib/validate.d.ts
CHANGED
|
@@ -4,8 +4,18 @@
|
|
|
4
4
|
* the base system prompt, required fields per action, and the executable
|
|
5
5
|
* contract skill entries must carry.
|
|
6
6
|
*/
|
|
7
|
-
import type { RefinementEdit } from "./types.js";
|
|
7
|
+
import type { HarnessScope, RefinementEdit } from "./types.js";
|
|
8
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;
|
|
9
19
|
/** Returns a human-readable failure reason, or undefined when the edit passes. */
|
|
10
|
-
export declare function validateEdit(edit: RefinementEdit, computedId: string | undefined): string | undefined;
|
|
20
|
+
export declare function validateEdit(edit: RefinementEdit, computedId: string | undefined, scope?: HarnessScope): string | undefined;
|
|
11
21
|
//# sourceMappingURL=validate.d.ts.map
|