dsh-continual-evolve 0.15.4 → 0.15.6

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/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` for explicit
13
- * "记住/忘掉" instructions, `memory_read` for bodies and search;
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` for explicit
13
- * "记住/忘掉" instructions, `memory_read` for bodies and search;
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
  /**
@@ -1,4 +1,4 @@
1
- import { MEMORY_FILE_PREVIEW_LIMIT, type MemoryStore } from "./store.js";
1
+ import { MEMORY_FILE_PREVIEW_LIMIT, type MemoryStore, type ReviewSummary } from "./store.js";
2
2
  export { MEMORY_FILE_PREVIEW_LIMIT };
3
3
  /** One memory row the card renders. */
4
4
  export interface MemoryFileInfo {
@@ -32,6 +32,8 @@ export interface MemorySnapshot {
32
32
  orphanFtsRows: number;
33
33
  quarantined: number;
34
34
  } | null;
35
+ /** Review-loop audit over the workspace's extraction ledger (zeros when none). */
36
+ review: ReviewSummary;
35
37
  }
36
38
  /**
37
39
  * The read-only snapshot for one workspace partition. A workspace without
@@ -63,4 +65,60 @@ export declare function listCardWorkspaces(store: MemoryStore): {
63
65
  root: string;
64
66
  label: string;
65
67
  }[];
68
+ /** One skill row the card renders (scope resolved: project shadows global). */
69
+ export interface SkillFileInfo {
70
+ id: string;
71
+ title: string;
72
+ description: string;
73
+ /** `project` or `global` — which partition the row was read from. */
74
+ scope: string;
75
+ /** Row status; quarantined rows render in the patrol section, not here. */
76
+ status: string;
77
+ /** Update time in epoch milliseconds (the card's "updated" line). */
78
+ updatedAt: number;
79
+ /** Bodies served by id (V2 usage loop); 0 when never consumed. */
80
+ useCount: number;
81
+ /** Last consumption in epoch milliseconds, `null` when never consumed. */
82
+ lastUsedAt: number | null;
83
+ }
84
+ /** The full read-only skill snapshot the card renders for one workspace root. */
85
+ export interface SkillSnapshot {
86
+ root: string;
87
+ exists: boolean;
88
+ /** Set when the store query failed; null otherwise. */
89
+ readError: string | null;
90
+ /** Active row count (the number the card's stats line shows). */
91
+ fileCount: number;
92
+ /** Active rows, project first (the list the card renders). */
93
+ files: SkillFileInfo[];
94
+ /** Rows quarantined by patrol — displayed as 巡检异常, never in the list. */
95
+ quarantined: {
96
+ id: string;
97
+ scope: string;
98
+ reason: string;
99
+ }[];
100
+ }
101
+ /**
102
+ * The read-only skill snapshot for one workspace partition. Mirrors the
103
+ * memory snapshot's degrade contract: unknown partitions read `exists:
104
+ * false`, failing queries report `readError` instead of throwing.
105
+ */
106
+ export declare function skillSnapshot(store: MemoryStore, root: string): SkillSnapshot;
107
+ /** The content endpoint's outcomes for one skill body. */
108
+ export type SkillEntryContent = {
109
+ ok: true;
110
+ id: string;
111
+ scope: string;
112
+ content: string;
113
+ updatedAtMs: number;
114
+ } | {
115
+ ok: false;
116
+ reason: "absent" | "too-large";
117
+ };
118
+ /**
119
+ * Read one skill's body for the card's content preview — project shadowing
120
+ * global, keyed by id, never by file name. Bodies are schema-capped at 64KB,
121
+ * so `too-large` is dead code kept for the day the preview limit shrinks.
122
+ */
123
+ export declare function skillEntryContent(store: MemoryStore, root: string, id: string): SkillEntryContent;
66
124
  //# sourceMappingURL=memory-snapshot.d.ts.map
@@ -8,7 +8,8 @@
8
8
  */
9
9
  import { existsSync } from "node:fs";
10
10
  import { basename } from "node:path";
11
- import { MEMORY_FILE_PREVIEW_LIMIT } from "./store.js";
11
+ import { MEMORY_FILE_PREVIEW_LIMIT, zeroReviewSummary } from "./store.js";
12
+ import { skillUsagePartition } from "./skill-rules.js";
12
13
  export { MEMORY_FILE_PREVIEW_LIMIT };
13
14
  /**
14
15
  * The read-only snapshot for one workspace partition. A workspace without
@@ -26,6 +27,7 @@ export function memorySnapshot(store, root) {
26
27
  files: [],
27
28
  quarantined: [],
28
29
  lastPatrol: null,
30
+ review: zeroReviewSummary(),
29
31
  };
30
32
  let all;
31
33
  try {
@@ -64,8 +66,17 @@ export function memorySnapshot(store, root) {
64
66
  .filter((record) => record.status === "quarantined")
65
67
  .map((record) => ({ id: record.id, reason: quarantineReasonOf(store, record) })),
66
68
  lastPatrol,
69
+ review: reviewOf(store, workspaceRoot),
67
70
  };
68
71
  }
72
+ function reviewOf(store, root) {
73
+ try {
74
+ return store.reviewSummary(root);
75
+ }
76
+ catch {
77
+ return zeroReviewSummary();
78
+ }
79
+ }
69
80
  function infoOf(record) {
70
81
  return {
71
82
  id: record.id,
@@ -127,13 +138,123 @@ export function memoryEntryContent(store, root, id) {
127
138
  /** The workspace rows the card lists: active partitions whose directory survives. */
128
139
  export function listCardWorkspaces(store) {
129
140
  try {
130
- return store
131
- .listWorkspaces()
132
- .filter((workspace) => existsSync(workspace.workspaceId))
133
- .map((workspace) => ({ root: workspace.workspaceId, label: basename(workspace.workspaceId) }));
141
+ const seen = new Set();
142
+ const rows = [];
143
+ // One fence for every card route: partitions holding memories or
144
+ // skills. A skills-only workspace lists (its memory snapshot reads
145
+ // empty); global skills ride along with whichever workspace is open.
146
+ for (const workspace of [...store.listWorkspaces(), ...store.listSkillWorkspaces()]) {
147
+ if (workspace.workspaceId === "" || seen.has(workspace.workspaceId)) {
148
+ continue;
149
+ }
150
+ seen.add(workspace.workspaceId);
151
+ if (!existsSync(workspace.workspaceId)) {
152
+ continue;
153
+ }
154
+ rows.push({ root: workspace.workspaceId, label: basename(workspace.workspaceId) });
155
+ }
156
+ return rows;
134
157
  }
135
158
  catch {
136
159
  return [];
137
160
  }
138
161
  }
162
+ /** Project-first merge of one partition's skills with the global scope. */
163
+ function scopedSkills(store, root) {
164
+ const seen = new Set();
165
+ const merged = [];
166
+ for (const record of store.listAllSkills(root)) {
167
+ if (seen.has(record.id)) {
168
+ continue;
169
+ }
170
+ seen.add(record.id);
171
+ merged.push({ record, scope: "project" });
172
+ }
173
+ for (const record of store.listAllSkills("")) {
174
+ if (seen.has(record.id)) {
175
+ continue;
176
+ }
177
+ seen.add(record.id);
178
+ merged.push({ record, scope: "global" });
179
+ }
180
+ return merged;
181
+ }
182
+ function skillInfoOf(store, entry) {
183
+ let useCount = 0;
184
+ let lastUsedAt = null;
185
+ try {
186
+ const use = store.getSkillUse(skillUsagePartition(entry.scope, entry.record.workspaceId), entry.record.id, entry.scope);
187
+ if (use !== undefined) {
188
+ useCount = use.useCount;
189
+ lastUsedAt = use.lastUsedAt === null ? null : Date.parse(use.lastUsedAt) || 0;
190
+ }
191
+ }
192
+ catch {
193
+ // Usage is decoration: the snapshot degrades to zeros, never throws.
194
+ }
195
+ return {
196
+ id: entry.record.id,
197
+ title: entry.record.title,
198
+ description: entry.record.description,
199
+ scope: entry.scope,
200
+ status: entry.record.status,
201
+ updatedAt: Date.parse(entry.record.updatedAt) || 0,
202
+ useCount,
203
+ lastUsedAt,
204
+ };
205
+ }
206
+ /**
207
+ * The read-only skill snapshot for one workspace partition. Mirrors the
208
+ * memory snapshot's degrade contract: unknown partitions read `exists:
209
+ * false`, failing queries report `readError` instead of throwing.
210
+ */
211
+ export function skillSnapshot(store, root) {
212
+ const absent = { root, exists: false, readError: null, fileCount: 0, files: [], quarantined: [] };
213
+ let entries;
214
+ try {
215
+ entries = scopedSkills(store, root);
216
+ if (entries.length === 0) {
217
+ return absent;
218
+ }
219
+ }
220
+ catch (error) {
221
+ return { ...absent, exists: true, readError: error instanceof Error ? error.message : String(error) };
222
+ }
223
+ return {
224
+ root,
225
+ exists: true,
226
+ readError: null,
227
+ fileCount: entries.filter((entry) => entry.record.status === "active").length,
228
+ files: entries.filter((entry) => entry.record.status === "active").map((entry) => skillInfoOf(store, entry)),
229
+ quarantined: entries
230
+ .filter((entry) => entry.record.status === "quarantined")
231
+ .map((entry) => ({ id: entry.record.id, scope: entry.scope, reason: quarantineReasonOf(store, entry.record) })),
232
+ };
233
+ }
234
+ /**
235
+ * Read one skill's body for the card's content preview — project shadowing
236
+ * global, keyed by id, never by file name. Bodies are schema-capped at 64KB,
237
+ * so `too-large` is dead code kept for the day the preview limit shrinks.
238
+ */
239
+ export function skillEntryContent(store, root, id) {
240
+ if (id.length === 0) {
241
+ return { ok: false, reason: "absent" };
242
+ }
243
+ let entries;
244
+ try {
245
+ entries = scopedSkills(store, root);
246
+ }
247
+ catch {
248
+ return { ok: false, reason: "absent" };
249
+ }
250
+ const hit = entries.find((entry) => entry.record.id === id);
251
+ if (hit === undefined) {
252
+ return { ok: false, reason: "absent" };
253
+ }
254
+ /* v8 ignore next 3 */
255
+ if (hit.record.body.length > MEMORY_FILE_PREVIEW_LIMIT) {
256
+ return { ok: false, reason: "too-large" };
257
+ }
258
+ return { ok: true, id: hit.record.id, scope: hit.scope, content: hit.record.body, updatedAtMs: Date.parse(hit.record.updatedAt) || 0 };
259
+ }
139
260
  //# sourceMappingURL=memory-snapshot.js.map
@@ -0,0 +1,79 @@
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
+ * The store partition a skill scope reads and writes usage in: the global
69
+ * partition for global rows, the workspace itself otherwise. One function
70
+ * so the tool, card, and snapshot paths cannot drift apart.
71
+ */
72
+ export declare function skillUsagePartition(scope: string, cwd: string): string;
73
+ /**
74
+ * Resolve and defend the materialized directory for a skill id. Ids are
75
+ * kebab slugs by gate, but the containment check stays: a caller bug must
76
+ * never turn a skill write into a write elsewhere.
77
+ */
78
+ export declare function skillDir(root: string, id: string): string;
79
+ //# sourceMappingURL=skill-rules.d.ts.map
@@ -0,0 +1,166 @@
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
+ * The store partition a skill scope reads and writes usage in: the global
147
+ * partition for global rows, the workspace itself otherwise. One function
148
+ * so the tool, card, and snapshot paths cannot drift apart.
149
+ */
150
+ export function skillUsagePartition(scope, cwd) {
151
+ return scope === "global" ? SKILL_GLOBAL_WORKSPACE : cwd;
152
+ }
153
+ /**
154
+ * Resolve and defend the materialized directory for a skill id. Ids are
155
+ * kebab slugs by gate, but the containment check stays: a caller bug must
156
+ * never turn a skill write into a write elsewhere.
157
+ */
158
+ export function skillDir(root, id) {
159
+ const base = resolve(root);
160
+ const dir = resolve(join(base, id));
161
+ if (dir !== base && !dir.startsWith(`${base}/`) && !dir.startsWith(`${base}${sep}`)) {
162
+ throw new Error(`skill path escapes skills root: ${dir}`);
163
+ }
164
+ return dir;
165
+ }
166
+ //# 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