dsh-continual-evolve 0.15.3 → 0.15.5
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 +13 -9
- package/README.zh.md +13 -9
- package/client/client.js +181 -16
- package/lib/card-routes.d.ts +2 -0
- package/lib/card-routes.js +66 -1
- package/lib/extraction.d.ts +5 -2
- package/lib/extraction.js +84 -7
- package/lib/index.d.ts +4 -2
- package/lib/index.js +7 -2
- package/lib/memory-snapshot.d.ts +52 -0
- package/lib/memory-snapshot.js +100 -4
- package/lib/skill-rules.d.ts +73 -0
- package/lib/skill-rules.js +158 -0
- package/lib/skill-tools.d.ts +9 -0
- package/lib/skill-tools.js +198 -0
- package/lib/store.d.ts +73 -5
- package/lib/store.js +504 -6
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +1 -1
package/lib/extraction.js
CHANGED
|
@@ -3,11 +3,57 @@ import { EXTRACTION_SYSTEM_PROMPT, extractionPrompt, manifestLineOf, parseExtrac
|
|
|
3
3
|
import { cwdOf } from "./memory-section.js";
|
|
4
4
|
/** Head length of a failed run's raw answer kept in the ledger `files` column. */
|
|
5
5
|
const FAILURE_RAW_MAX = 2048;
|
|
6
|
+
/** Head length of an unexpected error's message/stack trace kept in the ledger `files` column. */
|
|
7
|
+
const UNEXPECTED_ERROR_RAW_MAX = 2048;
|
|
8
|
+
/** Head length of an error message embedded in a ledger `skip_reason`. */
|
|
9
|
+
const ERROR_REASON_MAX = 300;
|
|
10
|
+
/** Split an unexpected throw into a ledger-sized reason head and file body. */
|
|
11
|
+
function errorText(error) {
|
|
12
|
+
const message = error instanceof Error && error.message.length > 0 ? error.message : String(error);
|
|
13
|
+
const stack = error instanceof Error && typeof error.stack === "string" ? error.stack : message;
|
|
14
|
+
return { message, detail: `${message}\n${stack}`.slice(0, UNEXPECTED_ERROR_RAW_MAX) };
|
|
15
|
+
}
|
|
6
16
|
/**
|
|
7
|
-
* Run one extraction pass for a target. Never throws
|
|
8
|
-
* is
|
|
17
|
+
* Run one extraction pass for a target. Never throws unless the ledger
|
|
18
|
+
* itself is unwritable: every terminal path is either a landed application,
|
|
19
|
+
* a ledgered skip, or a ledgered failure — anything that escapes the
|
|
20
|
+
* per-stage handling inside lands a failed `unexpected-error` ledger row
|
|
21
|
+
* here, so no failure ends as a scheduler warn without an account trace.
|
|
9
22
|
*/
|
|
10
23
|
export async function runExtraction(deps, target, opts) {
|
|
24
|
+
const started = Date.now();
|
|
25
|
+
try {
|
|
26
|
+
return await runExtractionInner(deps, target, opts);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const { message, detail } = errorText(error);
|
|
30
|
+
const reason = `unexpected-error: ${message.slice(0, ERROR_REASON_MAX)}`;
|
|
31
|
+
try {
|
|
32
|
+
deps.store.logSkipped({
|
|
33
|
+
sessionId: target.sessionId,
|
|
34
|
+
workspaceId: target.workspaceId,
|
|
35
|
+
trigger: opts.trigger,
|
|
36
|
+
skipReason: reason,
|
|
37
|
+
status: "failed",
|
|
38
|
+
model: target.model,
|
|
39
|
+
durationMs: Date.now() - started,
|
|
40
|
+
files: detail,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch (ledgerError) {
|
|
44
|
+
// The ledger itself is down — nothing left to write to. Chain
|
|
45
|
+
// both errors so the scheduler's last-resort row keeps the
|
|
46
|
+
// triggering failure, not just the ledger fault.
|
|
47
|
+
throw new Error(`extraction failed (${message}); failure ledger write also failed (${errorText(ledgerError).message})`);
|
|
48
|
+
}
|
|
49
|
+
return { status: "failed", skipReason: reason };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The pipeline body: one bounded increment of one session, one LLM call, one
|
|
54
|
+
* gated transactional application, one ledger row, one cursor.
|
|
55
|
+
*/
|
|
56
|
+
async function runExtractionInner(deps, target, opts) {
|
|
11
57
|
const { store } = deps;
|
|
12
58
|
const ledgerBase = {
|
|
13
59
|
sessionId: target.sessionId,
|
|
@@ -37,8 +83,14 @@ export async function runExtraction(deps, target, opts) {
|
|
|
37
83
|
all = Array.isArray(snapshot.events) ? snapshot.events : [];
|
|
38
84
|
}
|
|
39
85
|
catch (error) {
|
|
40
|
-
|
|
41
|
-
|
|
86
|
+
const { message, detail } = errorText(error);
|
|
87
|
+
try {
|
|
88
|
+
store.logSkipped({ ...ledgerBase, skipReason: "surface-read-failed", status: "failed", files: detail });
|
|
89
|
+
}
|
|
90
|
+
catch (ledgerError) {
|
|
91
|
+
throw new Error(`surface read failed (${message}); failure ledger write also failed (${errorText(ledgerError).message})`);
|
|
92
|
+
}
|
|
93
|
+
return { status: "failed", skipReason: `surface read failed: ${message}` };
|
|
42
94
|
}
|
|
43
95
|
const events = eventsAfterCursor(all, cursor === undefined ? undefined : `seq:${cursor}`);
|
|
44
96
|
const slice = evaluateSlice({ events, internalAgent: false });
|
|
@@ -72,8 +124,14 @@ export async function runExtraction(deps, target, opts) {
|
|
|
72
124
|
usage = answer.usage;
|
|
73
125
|
}
|
|
74
126
|
catch (error) {
|
|
75
|
-
|
|
76
|
-
|
|
127
|
+
const { message, detail } = errorText(error);
|
|
128
|
+
try {
|
|
129
|
+
store.logSkipped({ ...ledgerBase, skipReason: "llm-failed", status: "failed", durationMs: Date.now() - started, files: detail });
|
|
130
|
+
}
|
|
131
|
+
catch (ledgerError) {
|
|
132
|
+
throw new Error(`llm call failed (${message}); failure ledger write also failed (${errorText(ledgerError).message})`);
|
|
133
|
+
}
|
|
134
|
+
return { status: "failed", skipReason: `llm call failed: ${message}` };
|
|
77
135
|
}
|
|
78
136
|
const parsed = parseExtractionAnswer(text);
|
|
79
137
|
if (parsed.decision === "error") {
|
|
@@ -213,7 +271,26 @@ export function createExtractionScheduler(host, deps) {
|
|
|
213
271
|
provider: target.provider,
|
|
214
272
|
model: target.model,
|
|
215
273
|
}, { trigger }).catch((error) => {
|
|
216
|
-
|
|
274
|
+
// Last resort: runExtraction ledgers its own unexpected failures,
|
|
275
|
+
// so this fires only when even that failed (the ledger write
|
|
276
|
+
// threw) — retry one ledger row here before the warn, so the
|
|
277
|
+
// failure still has an account trace.
|
|
278
|
+
const { message, detail } = errorText(error);
|
|
279
|
+
try {
|
|
280
|
+
deps.store.logSkipped({
|
|
281
|
+
sessionId: target.sessionId,
|
|
282
|
+
workspaceId: cwdOf(target.agent),
|
|
283
|
+
trigger,
|
|
284
|
+
skipReason: `unexpected-error: ${message.slice(0, ERROR_REASON_MAX)}`,
|
|
285
|
+
status: "failed",
|
|
286
|
+
model: target.model,
|
|
287
|
+
files: detail,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
// The ledger is down — the warn below is the last resort.
|
|
292
|
+
}
|
|
293
|
+
host.logger("continual-evolve").warn(`extraction run failed unexpectedly: ${message}`);
|
|
217
294
|
}).finally(() => {
|
|
218
295
|
slot.running = false;
|
|
219
296
|
if (slot.pending !== null) {
|
package/lib/index.d.ts
CHANGED
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
*
|
|
10
10
|
* - the session-start section (`memory-section.ts`): a store query rendered
|
|
11
11
|
* as the injected index, frozen per session for the prompt cache;
|
|
12
|
-
* - the model tools (`memory-tools.ts`): `memory_write`
|
|
13
|
-
* "记住/忘掉" instructions, `memory_read` for bodies and
|
|
12
|
+
* - the model tools (`memory-tools.ts`, `skill-tools.ts`): `memory_write`
|
|
13
|
+
* for explicit "记住/忘掉" instructions, `memory_read` for bodies and
|
|
14
|
+
* search, plus `skill_write` (project-only creation, manual move to
|
|
15
|
+
* global) and `skill_read` (project shadowing global);
|
|
14
16
|
* - the extraction scheduler (`extraction.ts`): turn-level, per-session
|
|
15
17
|
* slots, single-flight per session; proposals land only through the
|
|
16
18
|
* store's gates;
|
package/lib/index.js
CHANGED
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
*
|
|
10
10
|
* - the session-start section (`memory-section.ts`): a store query rendered
|
|
11
11
|
* as the injected index, frozen per session for the prompt cache;
|
|
12
|
-
* - the model tools (`memory-tools.ts`): `memory_write`
|
|
13
|
-
* "记住/忘掉" instructions, `memory_read` for bodies and
|
|
12
|
+
* - the model tools (`memory-tools.ts`, `skill-tools.ts`): `memory_write`
|
|
13
|
+
* for explicit "记住/忘掉" instructions, `memory_read` for bodies and
|
|
14
|
+
* search, plus `skill_write` (project-only creation, manual move to
|
|
15
|
+
* global) and `skill_read` (project shadowing global);
|
|
14
16
|
* - the extraction scheduler (`extraction.ts`): turn-level, per-session
|
|
15
17
|
* slots, single-flight per session; proposals land only through the
|
|
16
18
|
* store's gates;
|
|
@@ -31,6 +33,7 @@ import { mountCardRoutes } from "./card-routes.js";
|
|
|
31
33
|
import { createExtractionScheduler } from "./extraction.js";
|
|
32
34
|
import { createFrozenMemorySection, cwdOf, DEFAULT_MAX_CHARS, DEFAULT_MEMORY_SECTION_ORDER, MEMORY_SECTION_NAME, memorySectionText, } from "./memory-section.js";
|
|
33
35
|
import { registerMemoryTools } from "./memory-tools.js";
|
|
36
|
+
import { registerSkillTools } from "./skill-tools.js";
|
|
34
37
|
import { importWorkspaceMd } from "./import-md.js";
|
|
35
38
|
import { openMemoryStore } from "./store.js";
|
|
36
39
|
import { newestRegistryPath } from "./workspace-hint.js";
|
|
@@ -80,8 +83,10 @@ function mountToolsWhenAvailable(host, store, state) {
|
|
|
80
83
|
return;
|
|
81
84
|
}
|
|
82
85
|
registerMemoryTools(tools, store);
|
|
86
|
+
registerSkillTools(tools, store);
|
|
83
87
|
state.toolsAvailable = true;
|
|
84
88
|
host.logger("continual-evolve").info("memory_write and memory_read registered");
|
|
89
|
+
host.logger("continual-evolve").info("skill_write and skill_read registered");
|
|
85
90
|
});
|
|
86
91
|
}
|
|
87
92
|
/**
|
package/lib/memory-snapshot.d.ts
CHANGED
|
@@ -63,4 +63,56 @@ export declare function listCardWorkspaces(store: MemoryStore): {
|
|
|
63
63
|
root: string;
|
|
64
64
|
label: string;
|
|
65
65
|
}[];
|
|
66
|
+
/** One skill row the card renders (scope resolved: project shadows global). */
|
|
67
|
+
export interface SkillFileInfo {
|
|
68
|
+
id: string;
|
|
69
|
+
title: string;
|
|
70
|
+
description: string;
|
|
71
|
+
/** `project` or `global` — which partition the row was read from. */
|
|
72
|
+
scope: string;
|
|
73
|
+
/** Row status; quarantined rows render in the patrol section, not here. */
|
|
74
|
+
status: string;
|
|
75
|
+
/** Update time in epoch milliseconds (the card's "updated" line). */
|
|
76
|
+
updatedAt: number;
|
|
77
|
+
}
|
|
78
|
+
/** The full read-only skill snapshot the card renders for one workspace root. */
|
|
79
|
+
export interface SkillSnapshot {
|
|
80
|
+
root: string;
|
|
81
|
+
exists: boolean;
|
|
82
|
+
/** Set when the store query failed; null otherwise. */
|
|
83
|
+
readError: string | null;
|
|
84
|
+
/** Active row count (the number the card's stats line shows). */
|
|
85
|
+
fileCount: number;
|
|
86
|
+
/** Active rows, project first (the list the card renders). */
|
|
87
|
+
files: SkillFileInfo[];
|
|
88
|
+
/** Rows quarantined by patrol — displayed as 巡检异常, never in the list. */
|
|
89
|
+
quarantined: {
|
|
90
|
+
id: string;
|
|
91
|
+
scope: string;
|
|
92
|
+
reason: string;
|
|
93
|
+
}[];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The read-only skill snapshot for one workspace partition. Mirrors the
|
|
97
|
+
* memory snapshot's degrade contract: unknown partitions read `exists:
|
|
98
|
+
* false`, failing queries report `readError` instead of throwing.
|
|
99
|
+
*/
|
|
100
|
+
export declare function skillSnapshot(store: MemoryStore, root: string): SkillSnapshot;
|
|
101
|
+
/** The content endpoint's outcomes for one skill body. */
|
|
102
|
+
export type SkillEntryContent = {
|
|
103
|
+
ok: true;
|
|
104
|
+
id: string;
|
|
105
|
+
scope: string;
|
|
106
|
+
content: string;
|
|
107
|
+
updatedAtMs: number;
|
|
108
|
+
} | {
|
|
109
|
+
ok: false;
|
|
110
|
+
reason: "absent" | "too-large";
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Read one skill's body for the card's content preview — project shadowing
|
|
114
|
+
* global, keyed by id, never by file name. Bodies are schema-capped at 64KB,
|
|
115
|
+
* so `too-large` is dead code kept for the day the preview limit shrinks.
|
|
116
|
+
*/
|
|
117
|
+
export declare function skillEntryContent(store: MemoryStore, root: string, id: string): SkillEntryContent;
|
|
66
118
|
//# sourceMappingURL=memory-snapshot.d.ts.map
|
package/lib/memory-snapshot.js
CHANGED
|
@@ -127,13 +127,109 @@ export function memoryEntryContent(store, root, id) {
|
|
|
127
127
|
/** The workspace rows the card lists: active partitions whose directory survives. */
|
|
128
128
|
export function listCardWorkspaces(store) {
|
|
129
129
|
try {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
const rows = [];
|
|
132
|
+
// One fence for every card route: partitions holding memories or
|
|
133
|
+
// skills. A skills-only workspace lists (its memory snapshot reads
|
|
134
|
+
// empty); global skills ride along with whichever workspace is open.
|
|
135
|
+
for (const workspace of [...store.listWorkspaces(), ...store.listSkillWorkspaces()]) {
|
|
136
|
+
if (workspace.workspaceId === "" || seen.has(workspace.workspaceId)) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
seen.add(workspace.workspaceId);
|
|
140
|
+
if (!existsSync(workspace.workspaceId)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
rows.push({ root: workspace.workspaceId, label: basename(workspace.workspaceId) });
|
|
144
|
+
}
|
|
145
|
+
return rows;
|
|
134
146
|
}
|
|
135
147
|
catch {
|
|
136
148
|
return [];
|
|
137
149
|
}
|
|
138
150
|
}
|
|
151
|
+
/** Project-first merge of one partition's skills with the global scope. */
|
|
152
|
+
function scopedSkills(store, root) {
|
|
153
|
+
const seen = new Set();
|
|
154
|
+
const merged = [];
|
|
155
|
+
for (const record of store.listAllSkills(root)) {
|
|
156
|
+
if (seen.has(record.id)) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
seen.add(record.id);
|
|
160
|
+
merged.push({ record, scope: "project" });
|
|
161
|
+
}
|
|
162
|
+
for (const record of store.listAllSkills("")) {
|
|
163
|
+
if (seen.has(record.id)) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
seen.add(record.id);
|
|
167
|
+
merged.push({ record, scope: "global" });
|
|
168
|
+
}
|
|
169
|
+
return merged;
|
|
170
|
+
}
|
|
171
|
+
function skillInfoOf(entry) {
|
|
172
|
+
return {
|
|
173
|
+
id: entry.record.id,
|
|
174
|
+
title: entry.record.title,
|
|
175
|
+
description: entry.record.description,
|
|
176
|
+
scope: entry.scope,
|
|
177
|
+
status: entry.record.status,
|
|
178
|
+
updatedAt: Date.parse(entry.record.updatedAt) || 0,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The read-only skill snapshot for one workspace partition. Mirrors the
|
|
183
|
+
* memory snapshot's degrade contract: unknown partitions read `exists:
|
|
184
|
+
* false`, failing queries report `readError` instead of throwing.
|
|
185
|
+
*/
|
|
186
|
+
export function skillSnapshot(store, root) {
|
|
187
|
+
const absent = { root, exists: false, readError: null, fileCount: 0, files: [], quarantined: [] };
|
|
188
|
+
let entries;
|
|
189
|
+
try {
|
|
190
|
+
entries = scopedSkills(store, root);
|
|
191
|
+
if (entries.length === 0) {
|
|
192
|
+
return absent;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
return { ...absent, exists: true, readError: error instanceof Error ? error.message : String(error) };
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
root,
|
|
200
|
+
exists: true,
|
|
201
|
+
readError: null,
|
|
202
|
+
fileCount: entries.filter((entry) => entry.record.status === "active").length,
|
|
203
|
+
files: entries.filter((entry) => entry.record.status === "active").map(skillInfoOf),
|
|
204
|
+
quarantined: entries
|
|
205
|
+
.filter((entry) => entry.record.status === "quarantined")
|
|
206
|
+
.map((entry) => ({ id: entry.record.id, scope: entry.scope, reason: quarantineReasonOf(store, entry.record) })),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Read one skill's body for the card's content preview — project shadowing
|
|
211
|
+
* global, keyed by id, never by file name. Bodies are schema-capped at 64KB,
|
|
212
|
+
* so `too-large` is dead code kept for the day the preview limit shrinks.
|
|
213
|
+
*/
|
|
214
|
+
export function skillEntryContent(store, root, id) {
|
|
215
|
+
if (id.length === 0) {
|
|
216
|
+
return { ok: false, reason: "absent" };
|
|
217
|
+
}
|
|
218
|
+
let entries;
|
|
219
|
+
try {
|
|
220
|
+
entries = scopedSkills(store, root);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return { ok: false, reason: "absent" };
|
|
224
|
+
}
|
|
225
|
+
const hit = entries.find((entry) => entry.record.id === id);
|
|
226
|
+
if (hit === undefined) {
|
|
227
|
+
return { ok: false, reason: "absent" };
|
|
228
|
+
}
|
|
229
|
+
/* v8 ignore next 3 */
|
|
230
|
+
if (hit.record.body.length > MEMORY_FILE_PREVIEW_LIMIT) {
|
|
231
|
+
return { ok: false, reason: "too-large" };
|
|
232
|
+
}
|
|
233
|
+
return { ok: true, id: hit.record.id, scope: hit.scope, content: hit.record.body, updatedAtMs: Date.parse(hit.record.updatedAt) || 0 };
|
|
234
|
+
}
|
|
139
235
|
//# sourceMappingURL=memory-snapshot.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { isValidMemoryId, slugifyId } from "./memory-rules.js";
|
|
2
|
+
export { isValidMemoryId as isValidSkillId, slugifyId as slugifySkillId };
|
|
3
|
+
/** Skill scopes: project rows live in their workspace partition, global rows in `""`. */
|
|
4
|
+
export declare const SKILL_SCOPES: readonly ["project", "global"];
|
|
5
|
+
export type SkillScope = (typeof SKILL_SCOPES)[number];
|
|
6
|
+
/** The global partition id (shared with the memory store's global state rows). */
|
|
7
|
+
export declare const SKILL_GLOBAL_WORKSPACE = "";
|
|
8
|
+
/** Max rendered frontmatter description length (loaders truncate anyway). */
|
|
9
|
+
export declare const MAX_SKILL_DESCRIPTION_LENGTH = 240;
|
|
10
|
+
/**
|
|
11
|
+
* Marker carried by every materialized file, right after the frontmatter.
|
|
12
|
+
* Patrol only touches files carrying it — hand-written skills in the same
|
|
13
|
+
* directories are never rewritten or removed.
|
|
14
|
+
*/
|
|
15
|
+
export declare const SKILL_GENERATED_MARKER = "<!-- generated by dsh-continual-evolve \u2014 the central store is the source of truth; edit via skill_write, manual edits are reverted by patrol -->";
|
|
16
|
+
/** One proposed skill mutation — the closed shape the write paths speak. */
|
|
17
|
+
export interface SkillProposal {
|
|
18
|
+
action: "create" | "update" | "delete" | "move-to-global";
|
|
19
|
+
/** Required for update/delete/move-to-global; optional for create (derived from title). */
|
|
20
|
+
id?: string | undefined;
|
|
21
|
+
/** Which partition update/delete targets. Create always lands in the project. */
|
|
22
|
+
scope?: string | undefined;
|
|
23
|
+
title?: string | undefined;
|
|
24
|
+
description?: string | undefined;
|
|
25
|
+
body?: string | undefined;
|
|
26
|
+
/** Trajectory provenance ("820-831"), kept for parity with memory proposals. */
|
|
27
|
+
sourceSeqs?: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
/** The merged field values a create/update would write (delete checks none). */
|
|
30
|
+
export interface SkillFields {
|
|
31
|
+
title: string;
|
|
32
|
+
description: string;
|
|
33
|
+
body: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Gates that depend only on the merged field values (not on store state).
|
|
37
|
+
* Runs the secret screen over the full visible text so a secret smuggled
|
|
38
|
+
* into any field blocks the write.
|
|
39
|
+
*/
|
|
40
|
+
export declare function skillFieldGateReasons(fields: SkillFields): string[];
|
|
41
|
+
/**
|
|
42
|
+
* Render a skill as a discoverable SKILL.md document. The frontmatter
|
|
43
|
+
* description carries a routing hint — title plus the first meaningful body
|
|
44
|
+
* line — instead of the bare title: the skill catalog matches on
|
|
45
|
+
* description, and a title-only description gives loaders nothing to route
|
|
46
|
+
* on.
|
|
47
|
+
*/
|
|
48
|
+
export declare function renderSkillMarkdown(skill: {
|
|
49
|
+
id: string;
|
|
50
|
+
title: string;
|
|
51
|
+
description: string;
|
|
52
|
+
body: string;
|
|
53
|
+
}): string;
|
|
54
|
+
/**
|
|
55
|
+
* Mechanical problems that would make the platform's skill loader ignore the
|
|
56
|
+
* rendered file. The file is still written, but the caller surfaces these:
|
|
57
|
+
* a silently ignored skill is worse than a noisy one.
|
|
58
|
+
*/
|
|
59
|
+
export declare function validateRenderedSkill(skill: {
|
|
60
|
+
id: string;
|
|
61
|
+
description: string;
|
|
62
|
+
}): string[];
|
|
63
|
+
/** Where project skills materialize: the workspace's own discovered skill directory. */
|
|
64
|
+
export declare function projectSkillsDir(workspaceId: string): string;
|
|
65
|
+
/** Where global skills materialize: the host-wide discovered skill directory. */
|
|
66
|
+
export declare function globalSkillsDir(): string;
|
|
67
|
+
/**
|
|
68
|
+
* Resolve and defend the materialized directory for a skill id. Ids are
|
|
69
|
+
* kebab slugs by gate, but the containment check stays: a caller bug must
|
|
70
|
+
* never turn a skill write into a write elsewhere.
|
|
71
|
+
*/
|
|
72
|
+
export declare function skillDir(root: string, id: string): string;
|
|
73
|
+
//# sourceMappingURL=skill-rules.d.ts.map
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mechanical write gates every skill passes before the store accepts it
|
|
3
|
+
* — the same "模型提议,代码保证" boundary as memories, minus the
|
|
4
|
+
* taxonomy: skills carry no type enum and no feedback body contract. Both
|
|
5
|
+
* entry points (the explicit `skill_write` tool and the manual move to the
|
|
6
|
+
* global scope) funnel through these checks; nothing reaches the database or
|
|
7
|
+
* the materialized files without them.
|
|
8
|
+
*
|
|
9
|
+
* File rendering follows the previously validated shape (pre-teardown
|
|
10
|
+
* `skill-render.ts`, itself checked against the platform loader): YAML-ish
|
|
11
|
+
* frontmatter (`name` + `description` carrying a routing hint) over the body.
|
|
12
|
+
* The official skill-creator template is no longer shipped locally, so the
|
|
13
|
+
* live host preset skills (same frontmatter contract) are the format anchor.
|
|
14
|
+
*/
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { join, resolve, sep } from "node:path";
|
|
17
|
+
import { isValidMemoryId, MEMORY_BODY_LIMIT, MEMORY_DESCRIPTION_LIMIT, MEMORY_TITLE_LIMIT, secretLeakReason, slugifyId, } from "./memory-rules.js";
|
|
18
|
+
export { isValidMemoryId as isValidSkillId, slugifyId as slugifySkillId };
|
|
19
|
+
/** Skill scopes: project rows live in their workspace partition, global rows in `""`. */
|
|
20
|
+
export const SKILL_SCOPES = ["project", "global"];
|
|
21
|
+
/** The global partition id (shared with the memory store's global state rows). */
|
|
22
|
+
export const SKILL_GLOBAL_WORKSPACE = "";
|
|
23
|
+
/** Max rendered frontmatter description length (loaders truncate anyway). */
|
|
24
|
+
export const MAX_SKILL_DESCRIPTION_LENGTH = 240;
|
|
25
|
+
/**
|
|
26
|
+
* Marker carried by every materialized file, right after the frontmatter.
|
|
27
|
+
* Patrol only touches files carrying it — hand-written skills in the same
|
|
28
|
+
* directories are never rewritten or removed.
|
|
29
|
+
*/
|
|
30
|
+
export const SKILL_GENERATED_MARKER = "<!-- generated by dsh-continual-evolve — the central store is the source of truth; edit via skill_write, manual edits are reverted by patrol -->";
|
|
31
|
+
/**
|
|
32
|
+
* Gates that depend only on the merged field values (not on store state).
|
|
33
|
+
* Runs the secret screen over the full visible text so a secret smuggled
|
|
34
|
+
* into any field blocks the write.
|
|
35
|
+
*/
|
|
36
|
+
export function skillFieldGateReasons(fields) {
|
|
37
|
+
const reasons = [];
|
|
38
|
+
if (fields.title.trim().length === 0) {
|
|
39
|
+
reasons.push("title must not be empty");
|
|
40
|
+
}
|
|
41
|
+
else if (fields.title.length > MEMORY_TITLE_LIMIT) {
|
|
42
|
+
reasons.push(`title exceeds ${MEMORY_TITLE_LIMIT} characters`);
|
|
43
|
+
}
|
|
44
|
+
if (fields.description.trim().length === 0) {
|
|
45
|
+
reasons.push("description must not be empty — it is the skill trigger");
|
|
46
|
+
}
|
|
47
|
+
else if (fields.description.length > MEMORY_DESCRIPTION_LIMIT) {
|
|
48
|
+
reasons.push(`description exceeds ${MEMORY_DESCRIPTION_LIMIT} characters`);
|
|
49
|
+
}
|
|
50
|
+
if (fields.body.trim().length === 0) {
|
|
51
|
+
reasons.push("body must not be empty");
|
|
52
|
+
}
|
|
53
|
+
else if (fields.body.length > MEMORY_BODY_LIMIT) {
|
|
54
|
+
reasons.push(`body exceeds the ${MEMORY_BODY_LIMIT}-character cap`);
|
|
55
|
+
}
|
|
56
|
+
const leak = secretLeakReason(`${fields.title}\n${fields.description}\n${fields.body}`);
|
|
57
|
+
if (leak !== undefined) {
|
|
58
|
+
reasons.push(leak);
|
|
59
|
+
}
|
|
60
|
+
return reasons;
|
|
61
|
+
}
|
|
62
|
+
/** Collapse prose to one line for the frontmatter description. */
|
|
63
|
+
function oneLine(text) {
|
|
64
|
+
return text.replace(/\s+/g, " ").trim();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* First content line usable as a routing hint: non-empty, not a Markdown
|
|
68
|
+
* heading, not a list marker, not frontmatter. Undefined when the body is
|
|
69
|
+
* effectively empty.
|
|
70
|
+
*/
|
|
71
|
+
function routingHint(body) {
|
|
72
|
+
for (const rawLine of body.split("\n")) {
|
|
73
|
+
const line = rawLine.trim();
|
|
74
|
+
if (line.length === 0)
|
|
75
|
+
continue;
|
|
76
|
+
if (line.startsWith("#") || line.startsWith("---") || line.startsWith("-") || line.startsWith("*"))
|
|
77
|
+
continue;
|
|
78
|
+
return oneLine(line);
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Render a skill as a discoverable SKILL.md document. The frontmatter
|
|
84
|
+
* description carries a routing hint — title plus the first meaningful body
|
|
85
|
+
* line — instead of the bare title: the skill catalog matches on
|
|
86
|
+
* description, and a title-only description gives loaders nothing to route
|
|
87
|
+
* on.
|
|
88
|
+
*/
|
|
89
|
+
export function renderSkillMarkdown(skill) {
|
|
90
|
+
const hint = routingHint(skill.body);
|
|
91
|
+
const stated = oneLine(skill.description);
|
|
92
|
+
const base = oneLine(skill.title);
|
|
93
|
+
// Prefer the author's own description; fall back to title + hint exactly
|
|
94
|
+
// like the pre-teardown renderer so re-materialized rows stay byte-stable.
|
|
95
|
+
let description;
|
|
96
|
+
if (stated.length > 0) {
|
|
97
|
+
description = stated;
|
|
98
|
+
}
|
|
99
|
+
else if (base.length === 0) {
|
|
100
|
+
description = (hint ?? "").slice(0, MAX_SKILL_DESCRIPTION_LENGTH);
|
|
101
|
+
}
|
|
102
|
+
else if (hint !== undefined && !base.toLowerCase().includes(hint.toLowerCase())) {
|
|
103
|
+
description = `${base} — use when: ${hint}`.slice(0, MAX_SKILL_DESCRIPTION_LENGTH);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
description = base.slice(0, MAX_SKILL_DESCRIPTION_LENGTH);
|
|
107
|
+
}
|
|
108
|
+
return ["---", `name: ${skill.id}`, `description: ${description}`, "---", "", SKILL_GENERATED_MARKER, "", skill.body.trim(), ""].join("\n");
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Mechanical problems that would make the platform's skill loader ignore the
|
|
112
|
+
* rendered file. The file is still written, but the caller surfaces these:
|
|
113
|
+
* a silently ignored skill is worse than a noisy one.
|
|
114
|
+
*/
|
|
115
|
+
export function validateRenderedSkill(skill) {
|
|
116
|
+
const problems = [];
|
|
117
|
+
if (!isValidMemoryId(skill.id)) {
|
|
118
|
+
problems.push(`name "${skill.id}" is not kebab-case — the platform ignores the file`);
|
|
119
|
+
}
|
|
120
|
+
const description = oneLine(skill.description);
|
|
121
|
+
if (description.length === 0) {
|
|
122
|
+
problems.push("empty description — the catalog has nothing to route on");
|
|
123
|
+
}
|
|
124
|
+
else if (description.length > MAX_SKILL_DESCRIPTION_LENGTH) {
|
|
125
|
+
problems.push(`description exceeds ${MAX_SKILL_DESCRIPTION_LENGTH} characters — loaders truncate it`);
|
|
126
|
+
}
|
|
127
|
+
if (/[\r\n]/.test(skill.description)) {
|
|
128
|
+
problems.push("description spans lines — frontmatter must stay one line");
|
|
129
|
+
}
|
|
130
|
+
return problems;
|
|
131
|
+
}
|
|
132
|
+
/** The DSH home the global skills directory lives under (same rule as the store path). */
|
|
133
|
+
function dshHome() {
|
|
134
|
+
const home = process.env.DSH_HOME?.trim();
|
|
135
|
+
return home !== undefined && home.length > 0 ? home : join(homedir(), ".dsh");
|
|
136
|
+
}
|
|
137
|
+
/** Where project skills materialize: the workspace's own discovered skill directory. */
|
|
138
|
+
export function projectSkillsDir(workspaceId) {
|
|
139
|
+
return join(workspaceId, ".agents", "skills");
|
|
140
|
+
}
|
|
141
|
+
/** Where global skills materialize: the host-wide discovered skill directory. */
|
|
142
|
+
export function globalSkillsDir() {
|
|
143
|
+
return join(dshHome(), "skills");
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Resolve and defend the materialized directory for a skill id. Ids are
|
|
147
|
+
* kebab slugs by gate, but the containment check stays: a caller bug must
|
|
148
|
+
* never turn a skill write into a write elsewhere.
|
|
149
|
+
*/
|
|
150
|
+
export function skillDir(root, id) {
|
|
151
|
+
const base = resolve(root);
|
|
152
|
+
const dir = resolve(join(base, id));
|
|
153
|
+
if (dir !== base && !dir.startsWith(`${base}/`) && !dir.startsWith(`${base}${sep}`)) {
|
|
154
|
+
throw new Error(`skill path escapes skills root: ${dir}`);
|
|
155
|
+
}
|
|
156
|
+
return dir;
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=skill-rules.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MemoryStore } from "./store.js";
|
|
2
|
+
import type { ToolsService, ToolDefinition } from "./memory-tools.js";
|
|
3
|
+
export type { ToolsService, ToolDefinition };
|
|
4
|
+
/**
|
|
5
|
+
* Register the skill tools and return a disposer that removes them (the
|
|
6
|
+
* host may return one; call only the functions among whatever comes back).
|
|
7
|
+
*/
|
|
8
|
+
export declare function registerSkillTools(tools: ToolsService, store: MemoryStore): () => void;
|
|
9
|
+
//# sourceMappingURL=skill-tools.d.ts.map
|