@lmzhen/dsh-evolution-skill-discovery 0.31.1 → 0.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/main-entry.js CHANGED
@@ -1,8 +1,2520 @@
1
- import { O as buildDomainGenerationPrompt, Z as ProjectionJournal, _ as buildGenerationRequest, c as AssignmentWorker, et as DomainMapFileStore, g as ViewSnapshotStore, k as ProjectionRuntime, l as JsonAssignmentQueue, o as installDiscoveryAwareness, ot as DomainGenerationCoordinator, r as renderDomainDescription, rt as DomainPolicyStore, s as installAssignmentSidecar, t as descriptionHashOf, u as DomainAssignmentService, v as resolveGenerationPort } from "./domain-description-3MIOYgVH.js";
2
- import { _ as skillKeyOf, c as SkillSearchCache, d as SkillIndex, r as registerSkillSearchTool } from "./tool-CvUTwKvW.js";
3
1
  import { isAbsolute, join, resolve } from "node:path";
4
- import { randomUUID } from "node:crypto";
5
- import { authorityFrom, evolutionIoAdapter, evolutionRoot, newSkillLibrary } from "@lmzhen/dsh-evolution-core";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { authorityFrom, evolutionIoAdapter, evolutionRoot, isRecord, makeSerialQueue, newSkillLibrary, parseFrontmatter } from "@lmzhen/dsh-evolution-core";
4
+ import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
5
+ import { escapeText } from "@deepseek-ai/dsh-skill";
6
+ import MiniSearch from "minisearch";
7
+ import { defineTool } from "@deepseek-ai/dsh-tools";
8
+ //#region lib/types/domain-generation.js
9
+ /** @returns stable id derived from title, not the title itself. */
10
+ function mintDomainId(title) {
11
+ return "domain-" + createHash("sha256").update(title).digest("hex").slice(0, 12);
12
+ }
13
+ function nonEmpty(value) {
14
+ return typeof value === "string" && value.trim().length > 0;
15
+ }
16
+ function nonEmptyList(value) {
17
+ return Array.isArray(value) && value.length > 0 && value.every(nonEmpty);
18
+ }
19
+ function forbiddenKeys(value) {
20
+ return [
21
+ "primaryParent",
22
+ "primaryParentId",
23
+ "parent",
24
+ "embedding",
25
+ "embeddingVector",
26
+ "taxonomyMethod"
27
+ ].filter((key) => key in value);
28
+ }
29
+ /**
30
+ * Validate an initial generation proposal and mint ids. Does not call an LLM
31
+ * and does not read skill bodies; embedding taxonomy is rejected.
32
+ * @param input - skills the proposal was generated from.
33
+ * @param proposal - LLM output.
34
+ * @returns normalized domains or stable errors.
35
+ */
36
+ function validateDomainGeneration(input, proposal) {
37
+ const errors = [];
38
+ if (proposal.domains.length < 8 || proposal.domains.length > 12) errors.push("domain count must be 8-12");
39
+ const known = new Set(input.skills.map((skill) => skill.name));
40
+ const ids = /* @__PURE__ */ new Set();
41
+ const titles = /* @__PURE__ */ new Set();
42
+ const domains = [];
43
+ for (const candidate of proposal.domains) {
44
+ if (candidate === null || typeof candidate !== "object") {
45
+ errors.push("domain is not an object");
46
+ continue;
47
+ }
48
+ const bad = forbiddenKeys(candidate);
49
+ if (bad.length > 0) errors.push("forbidden fields: " + bad.join(","));
50
+ if (!nonEmpty(candidate.title) || !nonEmpty(candidate.purpose)) {
51
+ errors.push("domain title/purpose is empty");
52
+ continue;
53
+ }
54
+ if (!nonEmptyList(candidate.inScope) || !nonEmptyList(candidate.outOfScope) || !nonEmptyList(candidate.triggers)) errors.push("domain scope/triggers are empty");
55
+ if (!Array.isArray(candidate.evidenceSkillKeys) || candidate.evidenceSkillKeys.length === 0) errors.push("domain evidence is empty");
56
+ if (candidate.evidenceSkillKeys?.some((key) => !known.has(key)) === true) errors.push("domain evidence references unknown skill");
57
+ const id = mintDomainId(candidate.title);
58
+ if (ids.has(id)) errors.push("duplicate minted domain id");
59
+ if (titles.has(candidate.title)) errors.push("duplicate domain title");
60
+ ids.add(id);
61
+ titles.add(candidate.title);
62
+ domains.push({
63
+ ...candidate,
64
+ id
65
+ });
66
+ }
67
+ if ("embedding" in proposal || "taxonomyMethod" in proposal) errors.push("embedding taxonomy is not allowed");
68
+ return errors.length === 0 ? {
69
+ ok: true,
70
+ domains
71
+ } : {
72
+ ok: false,
73
+ errors
74
+ };
75
+ }
76
+ //#endregion
77
+ //#region lib/types/domain-bootstrap.js
78
+ /**
79
+ * Write an initial LLM proposal using live skill facts. Code owns IDs and
80
+ * validation; no synthetic description/hash is written.
81
+ * @param store - map owner.
82
+ * @param proposal - 8–12 domain proposal.
83
+ * @param facts - real rows with stable keys and three-field hashes.
84
+ * @param expectedMapVersion - CAS precondition.
85
+ * @param provenance - prompt/run identity for audit.
86
+ * @returns committed snapshot.
87
+ */
88
+ async function bootstrapDomainMapWithFacts(store, proposal, facts, expectedMapVersion, provenance) {
89
+ const checked = validateDomainGeneration({ skills: facts.map(({ name, description, whenToUse }) => ({
90
+ name,
91
+ description,
92
+ ...whenToUse === void 0 ? {} : { whenToUse }
93
+ })) }, proposal);
94
+ if (!checked.ok) throw new Error("invalid domain proposal: " + checked.errors.join("; "));
95
+ const byName = new Map(facts.map((fact) => [fact.name, fact]));
96
+ const now = Date.now();
97
+ const qualityOf = (domain) => {
98
+ const triggerCount = domain.triggers.filter((value) => value.trim() !== "").length;
99
+ const scopeCount = domain.inScope.filter((value) => value.trim() !== "").length;
100
+ const duplicate = checked.domains.some((other) => other.id !== domain.id && other.purpose.trim().toLowerCase() === domain.purpose.trim().toLowerCase());
101
+ const fields = [
102
+ domain.purpose.trim() !== "",
103
+ triggerCount > 0,
104
+ scopeCount > 0,
105
+ domain.outOfScope.some((value) => value.trim() !== ""),
106
+ !duplicate
107
+ ];
108
+ return {
109
+ score: fields.filter(Boolean).length / fields.length,
110
+ triggerCount,
111
+ scopeCount
112
+ };
113
+ };
114
+ const domains = checked.domains.map((domain) => ({
115
+ id: domain.id,
116
+ name: domain.title,
117
+ purpose: domain.purpose,
118
+ description: domain.purpose,
119
+ inScope: domain.inScope,
120
+ outOfScope: domain.outOfScope,
121
+ triggers: domain.triggers,
122
+ parentIds: [],
123
+ status: "provisional",
124
+ version: 1,
125
+ createdAt: now,
126
+ updatedAt: now,
127
+ descriptionHash: "bootstrap",
128
+ descriptionVersion: 1,
129
+ descriptionStatus: "fresh",
130
+ provenance: {
131
+ source: "llm",
132
+ promptVersion: provenance.promptVersion,
133
+ sourceDigest: provenance.sourceDigest,
134
+ model: provenance.model,
135
+ runId: provenance.runId
136
+ },
137
+ quality: qualityOf(domain)
138
+ }));
139
+ const primary = /* @__PURE__ */ new Set();
140
+ const edges = [];
141
+ for (const [domainIndex, domain] of checked.domains.entries()) for (const name of domain.evidenceSkillKeys) {
142
+ const fact = byName.get(name);
143
+ if (fact === void 0) continue;
144
+ const role = primary.has(fact.skillKey) ? "secondary" : "primary";
145
+ if (role === "primary") primary.add(fact.skillKey);
146
+ edges.push({
147
+ skillKey: fact.skillKey,
148
+ domainId: checked.domains[domainIndex].id,
149
+ role,
150
+ confidence: 1,
151
+ source: "create",
152
+ skillHash: fact.threeFieldHash,
153
+ mapVersion: expectedMapVersion,
154
+ status: "active",
155
+ assignedAt: now
156
+ });
157
+ }
158
+ return store.commit(expectedMapVersion, {
159
+ mapVersion: String(Number(expectedMapVersion) + 1),
160
+ domains,
161
+ edges
162
+ });
163
+ }
164
+ //#endregion
165
+ //#region lib/types/domain-generation-coordinator.js
166
+ /** Single-flight initial map generation; mapVersion CAS is the final arbiter. */
167
+ var DomainGenerationCoordinator = class {
168
+ store;
169
+ port;
170
+ inflight = /* @__PURE__ */ new Map();
171
+ epoch = 0;
172
+ constructor(store, port) {
173
+ this.store = store;
174
+ this.port = port;
175
+ }
176
+ /** @returns current map version; increment makes older requests stale. */
177
+ get currentEpoch() {
178
+ return this.epoch;
179
+ }
180
+ /**
181
+ * Generate an initial map once per scope; concurrent calls share one job.
182
+ * @param request - live skills, stable keys, prompt, and CAS precondition.
183
+ * @returns committed or existing snapshot.
184
+ */
185
+ async ensure(request) {
186
+ const current = await this.store.read();
187
+ if (current.domains.length > 0) return current;
188
+ const existing = this.inflight.get(request.mapScope);
189
+ if (existing !== void 0) return existing;
190
+ this.epoch += 1;
191
+ const epoch = this.epoch;
192
+ const job = this.run(request, epoch).finally(() => {
193
+ this.inflight.delete(request.mapScope);
194
+ });
195
+ this.inflight.set(request.mapScope, job);
196
+ return job;
197
+ }
198
+ async run(request, epoch) {
199
+ const current = await this.store.read();
200
+ if (current.domains.length > 0) return current;
201
+ const proposal = await this.port.generate({
202
+ skills: request.facts.map(({ name, description, whenToUse }) => ({
203
+ name,
204
+ description,
205
+ ...whenToUse === void 0 ? {} : { whenToUse }
206
+ })),
207
+ prompt: request.prompt,
208
+ ...request.signal === void 0 ? {} : { signal: request.signal }
209
+ });
210
+ if (epoch !== this.epoch) {
211
+ const latest = await this.store.read();
212
+ if (latest.domains.length > 0) return latest;
213
+ throw new Error("domain generation superseded by a newer source snapshot");
214
+ }
215
+ return bootstrapDomainMapWithFacts(this.store, proposal, request.facts, request.expectedMapVersion, {
216
+ promptVersion: request.promptVersion,
217
+ sourceDigest: request.sourceDigest,
218
+ model: request.model,
219
+ runId: request.runId
220
+ });
221
+ }
222
+ };
223
+ //#endregion
224
+ //#region lib/types/domain-policy.js
225
+ /** Invalid policy layer (e.g. attempts to edit taxonomy). */
226
+ var DomainPolicyValidationError = class extends Error {
227
+ code = "domain-policy-invalid";
228
+ constructor(message) {
229
+ super(message);
230
+ this.name = "DomainPolicyValidationError";
231
+ }
232
+ };
233
+ const ALLOWED = new Set([
234
+ "mode",
235
+ "pinDomains",
236
+ "disableDomains",
237
+ "revision"
238
+ ]);
239
+ const MODES = new Set([
240
+ "off",
241
+ "auto",
242
+ "on"
243
+ ]);
244
+ function checkLayer(layer) {
245
+ for (const key of Object.keys(layer)) if (!ALLOWED.has(key)) throw new DomainPolicyValidationError("unsupported policy field " + key);
246
+ if (layer.mode !== void 0 && !MODES.has(layer.mode)) throw new DomainPolicyValidationError("unsupported policy mode");
247
+ }
248
+ function knownOnly(values, known, degraded) {
249
+ const result = /* @__PURE__ */ new Set();
250
+ for (const value of values ?? []) {
251
+ if (known !== void 0 && !known.has(value)) {
252
+ degraded.add(value);
253
+ continue;
254
+ }
255
+ result.add(value);
256
+ }
257
+ return result;
258
+ }
259
+ /**
260
+ * Merge global -> workspace -> scope policy. Disable wins over pin; unknown
261
+ * domains are ignored and reported as degraded.
262
+ * @param global - mandatory global layer.
263
+ * @param workspace - optional workspace overlay.
264
+ * @param scope - optional scope overlay.
265
+ * @param knownDomainIds - optional known ids for validation.
266
+ * @returns immutable effective policy.
267
+ */
268
+ function mergeDomainPolicy(global, workspace, scope, knownDomainIds) {
269
+ checkLayer(global);
270
+ if (workspace) checkLayer(workspace);
271
+ if (scope) checkLayer(scope);
272
+ const degraded = /* @__PURE__ */ new Set();
273
+ const blocked = new Set([
274
+ ...knownOnly(global.disableDomains, knownDomainIds, degraded),
275
+ ...knownOnly(workspace?.disableDomains, knownDomainIds, degraded),
276
+ ...knownOnly(scope?.disableDomains, knownDomainIds, degraded)
277
+ ]);
278
+ const pinned = new Set([
279
+ ...knownOnly(global.pinDomains, knownDomainIds, degraded),
280
+ ...knownOnly(workspace?.pinDomains, knownDomainIds, degraded),
281
+ ...knownOnly(scope?.pinDomains, knownDomainIds, degraded)
282
+ ]);
283
+ for (const id of blocked) pinned.delete(id);
284
+ return {
285
+ mode: scope?.mode ?? workspace?.mode ?? global.mode ?? "off",
286
+ pinned,
287
+ disabled: blocked,
288
+ degraded: [...degraded].sort(),
289
+ globalRevision: global.revision,
290
+ workspaceRevision: workspace?.revision,
291
+ scopeRevision: scope?.revision
292
+ };
293
+ }
294
+ //#endregion
295
+ //#region lib/types/identity.js
296
+ function sha256(value) {
297
+ return createHash("sha256").update(value).digest("hex");
298
+ }
299
+ function canonical(value) {
300
+ if (value instanceof Set) return { $set: [...value].map(canonical).sort((left, right) => JSON.stringify(left) < JSON.stringify(right) ? -1 : JSON.stringify(left) > JSON.stringify(right) ? 1 : 0) };
301
+ if (value instanceof Map) return { $map: [...value.entries()].map(([key, entry]) => [canonical(key), canonical(entry)]).sort((left, right) => JSON.stringify(left) < JSON.stringify(right) ? -1 : JSON.stringify(left) > JSON.stringify(right) ? 1 : 0) };
302
+ if (Array.isArray(value)) return value.map(canonical);
303
+ if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => [key, canonical(entry)]));
304
+ return value;
305
+ }
306
+ function digest$1(value) {
307
+ return sha256(JSON.stringify(canonical(value)));
308
+ }
309
+ /** @returns stable key for the winning provider/source/name tuple. */
310
+ function skillKeyOf(summary) {
311
+ return JSON.stringify([
312
+ summary.provider,
313
+ summary.source,
314
+ summary.name
315
+ ]);
316
+ }
317
+ /** @returns stable hash of the canonical cwd and optional scope key. */
318
+ function viewKeyOf(input) {
319
+ return digest$1([resolve(input.cwd), input.scopeKey ?? null]);
320
+ }
321
+ /** @returns hash of the three fields used by discovery/retrieval. */
322
+ function threeFieldHashOf(input) {
323
+ return digest$1([
324
+ input.name,
325
+ input.description,
326
+ input.whenToUse ?? null
327
+ ]);
328
+ }
329
+ /** @returns normalized row for one skill summary. */
330
+ function skillIndexRowOf(summary, managed, protectedSkill = false, pinned = false) {
331
+ return {
332
+ skillKey: skillKeyOf(summary),
333
+ name: summary.name,
334
+ description: summary.description,
335
+ ...summary.whenToUse === void 0 ? {} : { whenToUse: summary.whenToUse },
336
+ provider: summary.provider,
337
+ source: summary.source,
338
+ managed,
339
+ modelInvocable: summary.invocation.modelInvocable,
340
+ protected: protectedSkill,
341
+ pinned,
342
+ threeFieldHash: threeFieldHashOf(summary)
343
+ };
344
+ }
345
+ /** @returns digest of the sorted rows plus completeness. */
346
+ function catalogRevisionOf(rows, complete) {
347
+ return digest$1([complete, rows.map((row) => [
348
+ row.skillKey,
349
+ row.name,
350
+ row.description,
351
+ row.whenToUse ?? null,
352
+ row.provider,
353
+ row.source,
354
+ row.managed,
355
+ row.modelInvocable,
356
+ row.protected,
357
+ row.pinned,
358
+ row.threeFieldHash
359
+ ])]);
360
+ }
361
+ /** @returns digest of a concrete per-view snapshot. */
362
+ function viewCatalogDigestOf(viewKey, rows, complete) {
363
+ return digest$1([viewKey, catalogRevisionOf(rows, complete)]);
364
+ }
365
+ /** @returns digest of effective policy plus its revisions. */
366
+ function policyDigestOf(input) {
367
+ return digest$1([
368
+ input.globalPolicyRevision,
369
+ input.workspaceOverlayRevision ?? null,
370
+ input.scopeOverlayRevision ?? null,
371
+ input.effectivePolicy
372
+ ]);
373
+ }
374
+ /** @returns view-cache identity for one projection. */
375
+ function projectionDigestOf(input) {
376
+ return digest$1([
377
+ input.viewKey,
378
+ input.mapVersion,
379
+ input.policyDigest,
380
+ input.viewCatalogDigest,
381
+ input.adapterVersion,
382
+ input.projectionSchemaVersion,
383
+ input.discoveryMode
384
+ ]);
385
+ }
386
+ /** @returns view-cache identity for one search index. */
387
+ function searchIndexKeyOf(input) {
388
+ return digest$1([
389
+ input.viewKey,
390
+ input.viewCatalogDigest,
391
+ input.policyDigest,
392
+ input.tokenizerVersion,
393
+ input.indexSchemaVersion,
394
+ input.discoveryMode
395
+ ]);
396
+ }
397
+ //#endregion
398
+ //#region lib/types/domain-policy-store.js
399
+ /** Resolves global/workspace/scope policy without touching DomainMap or mapVersion. */
400
+ var DomainPolicyStore = class {
401
+ input;
402
+ constructor(input) {
403
+ this.input = input;
404
+ }
405
+ /**
406
+ * @param knownDomainIds - optional live id set; overrides the construction-time set.
407
+ * @returns effective policy with unknown ids reported as degraded.
408
+ */
409
+ resolve(knownDomainIds = this.input.knownDomainIds) {
410
+ return mergeDomainPolicy(this.input.global, this.input.workspace, this.input.scope, knownDomainIds);
411
+ }
412
+ /**
413
+ * @param knownDomainIds - optional live id set used for the effective policy.
414
+ * @returns stable digest of effective policy and all layer revisions.
415
+ */
416
+ digest(knownDomainIds = this.input.knownDomainIds) {
417
+ const policy = this.resolve(knownDomainIds);
418
+ return policyDigestOf({
419
+ globalPolicyRevision: policy.globalRevision ?? "0",
420
+ ...policy.workspaceRevision === void 0 ? {} : { workspaceOverlayRevision: policy.workspaceRevision },
421
+ ...policy.scopeRevision === void 0 ? {} : { scopeOverlayRevision: policy.scopeRevision },
422
+ effectivePolicy: policy
423
+ });
424
+ }
425
+ };
426
+ /** Typed failure for an invalid persisted file. */
427
+ var DomainMapCorruptError = class extends Error {
428
+ code = "domain-map-corrupt";
429
+ constructor(message) {
430
+ super(message);
431
+ this.name = "DomainMapCorruptError";
432
+ }
433
+ };
434
+ /** Typed failure for a CAS mismatch. */
435
+ var DomainMapVersionConflict = class extends Error {
436
+ code = "domain-map-version-conflict";
437
+ expected;
438
+ actual;
439
+ constructor(expected, actual) {
440
+ super("domain map version conflict: expected " + expected + ", found " + actual);
441
+ this.expected = expected;
442
+ this.actual = actual;
443
+ this.name = "DomainMapVersionConflict";
444
+ }
445
+ };
446
+ /** Typed failure for an invalid write. */
447
+ var DomainMapValidationError = class extends Error {
448
+ code = "domain-map-invalid";
449
+ constructor(message) {
450
+ super(message);
451
+ this.name = "DomainMapValidationError";
452
+ }
453
+ };
454
+ function emptySnapshot() {
455
+ return {
456
+ mapVersion: "0",
457
+ domains: [],
458
+ edges: []
459
+ };
460
+ }
461
+ function assertNode(node) {
462
+ if (!isRecord(node) || typeof node.id !== "string" || node.id.length === 0) throw new DomainMapCorruptError("domain node id is missing");
463
+ if (typeof node.name !== "string" || typeof node.purpose !== "string" || typeof node.description !== "string") throw new DomainMapCorruptError("domain node text fields are missing");
464
+ if (!Array.isArray(node.inScope) || !Array.isArray(node.outOfScope) || !Array.isArray(node.triggers) || !Array.isArray(node.parentIds)) throw new DomainMapCorruptError("domain node arrays are invalid");
465
+ if (node.status !== "active" && node.status !== "provisional" && node.status !== "deprecated") throw new DomainMapCorruptError("domain node status is invalid");
466
+ }
467
+ function assertEdge(edge) {
468
+ if (!isRecord(edge) || typeof edge.skillKey !== "string" || typeof edge.domainId !== "string") throw new DomainMapCorruptError("skill edge identity is missing");
469
+ if (edge.role !== "primary" && edge.role !== "secondary") throw new DomainMapCorruptError("skill edge role is invalid");
470
+ if (edge.status !== "active" && edge.status !== "stale" && edge.status !== "pending") throw new DomainMapCorruptError("skill edge status is invalid");
471
+ if (typeof edge.confidence !== "number" || edge.confidence < 0 || edge.confidence > 1) throw new DomainMapCorruptError("skill edge confidence is invalid");
472
+ }
473
+ function validateWrite(next) {
474
+ if (next.mapVersion.length === 0) throw new DomainMapValidationError("mapVersion must be non-empty");
475
+ const ids = /* @__PURE__ */ new Set();
476
+ for (const node of next.domains) {
477
+ assertNode(node);
478
+ if (ids.has(node.id)) throw new DomainMapValidationError("duplicate domain id " + node.id);
479
+ ids.add(node.id);
480
+ }
481
+ for (const node of next.domains) for (const parent of node.parentIds) {
482
+ if (parent === node.id) throw new DomainMapValidationError("domain self-parent " + node.id);
483
+ if (!ids.has(parent)) throw new DomainMapValidationError("unknown parent domain " + parent);
484
+ }
485
+ const edgeKeys = /* @__PURE__ */ new Set();
486
+ const primaries = /* @__PURE__ */ new Map();
487
+ for (const edge of next.edges) {
488
+ assertEdge(edge);
489
+ if (!ids.has(edge.domainId)) throw new DomainMapValidationError("unknown edge domain " + edge.domainId);
490
+ const key = edge.skillKey + "\0" + edge.domainId;
491
+ if (edgeKeys.has(key)) throw new DomainMapValidationError("duplicate skill/domain edge");
492
+ edgeKeys.add(key);
493
+ if (edge.role === "primary") {
494
+ const count = (primaries.get(edge.skillKey) ?? 0) + 1;
495
+ if (count > 1) throw new DomainMapValidationError("more than one primary domain for " + edge.skillKey);
496
+ primaries.set(edge.skillKey, count);
497
+ }
498
+ }
499
+ }
500
+ /**
501
+ * JSON-backed DomainMap owner. One file, schemaVersion, CAS mapVersion, and a
502
+ * best-effort .bak for rollback; no evolution-state tables or graph DB.
503
+ */
504
+ var DomainMapFileStore = class {
505
+ queue = Promise.resolve();
506
+ io;
507
+ path;
508
+ now;
509
+ constructor(io, path, now = Date.now) {
510
+ this.io = io;
511
+ this.path = path;
512
+ this.now = now;
513
+ }
514
+ get backupPath() {
515
+ return this.path + ".bak";
516
+ }
517
+ parse(content) {
518
+ if (content === null) return emptySnapshot();
519
+ let value;
520
+ try {
521
+ value = JSON.parse(content);
522
+ } catch {
523
+ throw new DomainMapCorruptError("domain map is not valid JSON");
524
+ }
525
+ if (!isRecord(value) || value.schemaVersion !== 1 || typeof value.mapVersion !== "string" || !Array.isArray(value.domains) || !Array.isArray(value.edges)) throw new DomainMapCorruptError("domain map schema is unsupported");
526
+ for (const node of value.domains) assertNode(node);
527
+ for (const edge of value.edges) assertEdge(edge);
528
+ return {
529
+ mapVersion: value.mapVersion,
530
+ domains: value.domains,
531
+ edges: value.edges
532
+ };
533
+ }
534
+ async read() {
535
+ return this.parse(await this.io.readText(this.path));
536
+ }
537
+ async commitNow(current, expected, next) {
538
+ const snapshot = this.parse(current);
539
+ if (snapshot.mapVersion !== expected) throw new DomainMapVersionConflict(expected, snapshot.mapVersion);
540
+ if (next.mapVersion === expected) throw new DomainMapValidationError("mapVersion must change on commit");
541
+ validateWrite(next);
542
+ if (current !== null) await this.io.writeText(this.backupPath, current);
543
+ const file = {
544
+ schemaVersion: 1,
545
+ mapVersion: next.mapVersion,
546
+ updatedAt: this.now(),
547
+ domains: next.domains,
548
+ edges: next.edges
549
+ };
550
+ return JSON.stringify(file);
551
+ }
552
+ async commit(expectedMapVersion, next) {
553
+ const run = async () => {
554
+ if (this.io.transact !== void 0) await this.io.transact(this.path, (current) => this.commitNow(current, expectedMapVersion, next));
555
+ else {
556
+ const current = await this.io.readText(this.path);
557
+ await this.io.writeText(this.path, await this.commitNow(current, expectedMapVersion, next));
558
+ }
559
+ };
560
+ const pending = this.queue.then(run, run);
561
+ this.queue = pending.catch(() => {});
562
+ await pending;
563
+ return this.read();
564
+ }
565
+ async rollback() {
566
+ const backup = await this.io.readText(this.backupPath);
567
+ if (backup === null) throw new DomainMapCorruptError("domain map backup is missing");
568
+ const snapshot = this.parse(backup);
569
+ await this.io.writeText(this.path, backup);
570
+ return snapshot;
571
+ }
572
+ };
573
+ //#endregion
574
+ //#region lib/types/projection-journal.js
575
+ function keyOf(record) {
576
+ return record.sessionId + "|" + record.viewKey + "|" + record.projectionDigest + "|" + record.codecVersion;
577
+ }
578
+ /** Durable projection journal; no base means restore must fail closed. */
579
+ var ProjectionJournal = class {
580
+ io;
581
+ path;
582
+ maxRecords;
583
+ cache;
584
+ serial = makeSerialQueue();
585
+ constructor(io, path, options = {}) {
586
+ this.io = io;
587
+ this.path = path;
588
+ this.maxRecords = options.maxRecords ?? 512;
589
+ }
590
+ async read() {
591
+ if (this.cache !== void 0) return this.cache;
592
+ const content = await this.io.readText(this.path);
593
+ if (content === null) {
594
+ this.cache = {
595
+ schemaVersion: 1,
596
+ records: {}
597
+ };
598
+ return this.cache;
599
+ }
600
+ try {
601
+ const parsed = JSON.parse(content);
602
+ this.cache = parsed.schemaVersion === 1 && typeof parsed.records === "object" && parsed.records !== null ? {
603
+ schemaVersion: 1,
604
+ records: parsed.records
605
+ } : {
606
+ schemaVersion: 1,
607
+ records: {}
608
+ };
609
+ } catch {
610
+ this.cache = {
611
+ schemaVersion: 1,
612
+ records: {}
613
+ };
614
+ }
615
+ return this.cache;
616
+ }
617
+ /** Persist one projection record; later records with the same key win. */
618
+ async put(record) {
619
+ await this.serial(async () => {
620
+ const records = {
621
+ ...(await this.read()).records,
622
+ [keyOf(record)]: record
623
+ };
624
+ const ordered = Object.entries(records).sort((left, right) => right[1].createdAt - left[1].createdAt).slice(0, this.maxRecords);
625
+ this.cache = {
626
+ schemaVersion: 1,
627
+ records: Object.fromEntries(ordered)
628
+ };
629
+ await this.io.writeText(this.path, JSON.stringify(this.cache));
630
+ });
631
+ }
632
+ /** @returns the exact projection record for one view/digest/codec, if present. */
633
+ async get(sessionId, viewKey, projectionDigest, codecVersion) {
634
+ return this.serial(async () => (await this.read()).records[keyOf({
635
+ sessionId,
636
+ viewKey,
637
+ projectionDigest,
638
+ codecVersion
639
+ })]);
640
+ }
641
+ /** @returns newest record for a view, used when leaving ready without a known digest. */
642
+ async latest(sessionId, viewKey, codecVersion) {
643
+ return this.serial(async () => {
644
+ return Object.values((await this.read()).records).filter((record) => record.sessionId === sessionId && record.viewKey === viewKey && record.codecVersion === codecVersion).sort((left, right) => right.createdAt - left.createdAt)[0];
645
+ });
646
+ }
647
+ /** Delete one record after a successful restore. */
648
+ async remove(sessionId, viewKey, projectionDigest, codecVersion) {
649
+ await this.serial(async () => {
650
+ const records = { ...(await this.read()).records };
651
+ delete records[keyOf({
652
+ sessionId,
653
+ viewKey,
654
+ projectionDigest,
655
+ codecVersion
656
+ })];
657
+ this.cache = {
658
+ schemaVersion: 1,
659
+ records
660
+ };
661
+ await this.io.writeText(this.path, JSON.stringify(this.cache));
662
+ });
663
+ }
664
+ };
665
+ //#endregion
666
+ //#region lib/types/domain-quality.js
667
+ function tokens(values) {
668
+ return new Set(values.join(" ").toLowerCase().split(/[^\p{Letter}\p{Number}]+/u).filter((token) => token.length > 0));
669
+ }
670
+ function jaccard(left, right) {
671
+ if (left.size === 0 && right.size === 0) return 1;
672
+ let overlap = 0;
673
+ for (const token of left) if (right.has(token)) overlap += 1;
674
+ return overlap / (left.size + right.size - overlap);
675
+ }
676
+ /**
677
+ * Evaluate whether a domain definition is specific, triggered, and distinct.
678
+ * @param domain - candidate domain.
679
+ * @param peers - other visible domains.
680
+ * @returns auditable issues; does not mutate the candidate.
681
+ */
682
+ function evaluateDomainQuality(domain, peers) {
683
+ const issues = [];
684
+ if (domain.purpose.trim().length === 0) issues.push("empty-purpose");
685
+ else if (domain.purpose.trim().length < 8) issues.push("vague-purpose");
686
+ if (domain.inScope.length === 0) issues.push("empty-in-scope");
687
+ if (domain.outOfScope.length === 0) issues.push("empty-out-of-scope");
688
+ if (domain.triggers.length === 0) issues.push("empty-triggers");
689
+ const own = tokens([...domain.inScope, ...domain.triggers]);
690
+ for (const peer of peers) {
691
+ if (domain.name.trim().toLowerCase() === peer.name.trim().toLowerCase() || domain.purpose.trim().toLowerCase() === peer.purpose.trim().toLowerCase()) {
692
+ issues.push("indistinguishable");
693
+ break;
694
+ }
695
+ if (jaccard(own, tokens([...peer.inScope, ...peer.triggers])) > .8) {
696
+ issues.push("overlaps-peer");
697
+ break;
698
+ }
699
+ }
700
+ const unique = [...new Set(issues)];
701
+ return {
702
+ passed: unique.length === 0,
703
+ issues: unique,
704
+ score: Math.max(0, 1 - unique.length * .2)
705
+ };
706
+ }
707
+ //#endregion
708
+ //#region lib/types/security.js
709
+ /**
710
+ * Treat description text as data: strip control characters, escape HTML
711
+ * delimiters, and cap length. It never turns data into instructions.
712
+ * @param value - untrusted domain/skill text.
713
+ * @param limit - maximum characters retained.
714
+ * @returns escaped text plus truncation flag.
715
+ */
716
+ function sanitizeUntrustedText(value, limit = 2e3) {
717
+ const escaped = value.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
718
+ return escaped.length <= limit ? {
719
+ text: escaped,
720
+ truncated: false
721
+ } : {
722
+ text: escaped.slice(0, limit),
723
+ truncated: true
724
+ };
725
+ }
726
+ //#endregion
727
+ //#region lib/types/projection.js
728
+ function escapeData(value) {
729
+ return sanitizeUntrustedText(value, 2e3).text;
730
+ }
731
+ function renderCards(cards) {
732
+ if (cards.length === 0) return "";
733
+ return [
734
+ "<system-reminder>",
735
+ "Domain cards summarize where skills cluster; they support awareness, not retrieval. skill_search matches task text to skill summaries, skill(name) loads an exact skill, and the full catalog remains the fallback.",
736
+ "<skill_domains>",
737
+ ...cards.map((card) => [
738
+ "- ",
739
+ escapeData(card.id),
740
+ " ",
741
+ escapeData(card.title),
742
+ ": ",
743
+ escapeData(card.purpose),
744
+ " triggers=",
745
+ card.triggers.map(escapeData).join(","),
746
+ " | managed=",
747
+ String(card.managedModelInvocableCount),
748
+ " external=",
749
+ String(card.externalCatalogCount),
750
+ " pending=",
751
+ String(card.pendingCount),
752
+ " unclassified=",
753
+ String(card.unclassifiedCount ?? 0),
754
+ " state=",
755
+ card.state,
756
+ " descriptionStatus=",
757
+ card.descriptionStatus,
758
+ " asOf=",
759
+ escapeData(card.asOf),
760
+ " mapVersion=",
761
+ escapeData(card.mapVersion),
762
+ " mapStale=",
763
+ String(card.mapStale)
764
+ ].join("")),
765
+ "</skill_domains>",
766
+ "</system-reminder>"
767
+ ].join("\n");
768
+ }
769
+ /**
770
+ * Pure catalog projection: hide only active managed entries, preserve every
771
+ * other entry, and render L1 cards. Incomplete input fails open.
772
+ * @param input - view rows, active managed keys, cards, and identity inputs.
773
+ * @returns projected entries/context/hidden names and digest.
774
+ */
775
+ function projectCatalog(input) {
776
+ if (!input.complete) return {
777
+ catalogEntries: input.entries,
778
+ context: "",
779
+ hiddenNames: [],
780
+ scope: input.scope,
781
+ digest: projectionDigestOf({
782
+ viewKey: input.viewKey,
783
+ mapVersion: input.mapVersion,
784
+ policyDigest: input.policyDigest,
785
+ viewCatalogDigest: input.viewCatalogDigest,
786
+ adapterVersion: input.adapterVersion,
787
+ projectionSchemaVersion: input.projectionSchemaVersion,
788
+ discoveryMode: input.discoveryMode
789
+ }),
790
+ complete: false
791
+ };
792
+ const rowsByName = /* @__PURE__ */ new Map();
793
+ for (const row of input.rows) {
794
+ const list = rowsByName.get(row.name);
795
+ if (list === void 0) rowsByName.set(row.name, [row]);
796
+ else list.push(row);
797
+ }
798
+ const hiddenNames = input.entries.filter((entry) => {
799
+ const candidates = rowsByName.get(entry.name);
800
+ if (candidates === void 0 || candidates.length !== 1) return false;
801
+ const row = candidates[0];
802
+ return row.managed && row.modelInvocable && !row.protected && input.activeManagedSkillKeys.has(row.skillKey);
803
+ }).map((entry) => entry.name).sort();
804
+ const hidden = new Set(hiddenNames);
805
+ return {
806
+ catalogEntries: input.entries.filter((entry) => !hidden.has(entry.name)),
807
+ context: renderCards(input.cards),
808
+ hiddenNames,
809
+ scope: input.scope,
810
+ digest: projectionDigestOf({
811
+ viewKey: input.viewKey,
812
+ mapVersion: input.mapVersion,
813
+ policyDigest: input.policyDigest,
814
+ viewCatalogDigest: input.viewCatalogDigest,
815
+ adapterVersion: input.adapterVersion,
816
+ projectionSchemaVersion: input.projectionSchemaVersion,
817
+ discoveryMode: input.discoveryMode
818
+ }),
819
+ complete: true
820
+ };
821
+ }
822
+ //#endregion
823
+ //#region lib/types/domain-projection.js
824
+ function qualityPasses(node, domains) {
825
+ return evaluateDomainQuality(node, domains.filter((peer) => peer.id !== node.id)).passed;
826
+ }
827
+ function buildDomainProjection(input) {
828
+ const mode = input.mode ?? "on";
829
+ if (mode === "off") return projectCatalog({
830
+ entries: input.entries,
831
+ rows: input.rows,
832
+ activeManagedSkillKeys: /* @__PURE__ */ new Set(),
833
+ cards: [],
834
+ viewKey: input.viewKey,
835
+ scope: input.scope,
836
+ mapVersion: input.mapVersion,
837
+ policyDigest: input.policyDigest,
838
+ viewCatalogDigest: input.viewCatalogDigest,
839
+ adapterVersion: input.adapterVersion,
840
+ projectionSchemaVersion: input.projectionSchemaVersion,
841
+ discoveryMode: input.discoveryMode,
842
+ complete: false
843
+ });
844
+ const staleByAge = input.staleAfterHours !== void 0 && input.snapshot.domains.some((node) => input.now !== void 0 && input.now - node.updatedAt > input.staleAfterHours * 36e5);
845
+ const mapStale = input.mapStale === true || staleByAge;
846
+ if (!input.complete) return projectCatalog({
847
+ entries: input.entries,
848
+ rows: input.rows,
849
+ activeManagedSkillKeys: /* @__PURE__ */ new Set(),
850
+ cards: [],
851
+ viewKey: input.viewKey,
852
+ scope: input.scope,
853
+ mapVersion: input.mapVersion,
854
+ policyDigest: input.policyDigest,
855
+ viewCatalogDigest: input.viewCatalogDigest,
856
+ adapterVersion: input.adapterVersion,
857
+ projectionSchemaVersion: input.projectionSchemaVersion,
858
+ discoveryMode: input.discoveryMode,
859
+ complete: false
860
+ });
861
+ const rowByKey = new Map(input.rows.map((row) => [row.skillKey, row]));
862
+ const staleEdgeDomains = new Set(input.snapshot.edges.filter((edge) => edge.status === "active" && edge.role === "primary" && rowByKey.get(edge.skillKey) !== void 0 && edge.skillHash !== rowByKey.get(edge.skillKey).threeFieldHash).map((edge) => edge.domainId));
863
+ const linkedKeys = new Set(input.snapshot.edges.filter((edge) => edge.status === "active" || edge.status === "pending").map((edge) => edge.skillKey));
864
+ const unclassifiedCount = input.rows.filter((row) => row.modelInvocable && !linkedKeys.has(row.skillKey)).length;
865
+ const domains = input.snapshot.domains.filter((node) => node.status === "active" || node.status === "provisional");
866
+ const cardDomains = domains.filter((node) => node.parentIds.length <= (input.maxVisibleDepth ?? 3) - 1 && node.parentIds.length === 0).filter((node) => !input.policy.disabled.has(node.id)).filter((node) => input.policy.pinned.has(node.id) || qualityPasses(node, domains)).sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0).slice(0, Math.max(1, input.maxCards ?? 12));
867
+ const domainById = new Map(domains.map((node) => [node.id, node]));
868
+ const cards = cardDomains.map((node) => {
869
+ let managedModelInvocableCount = 0;
870
+ let externalCatalogCount = 0;
871
+ let pendingCount = 0;
872
+ for (const edge of input.snapshot.edges) {
873
+ if (edge.domainId !== node.id) continue;
874
+ const row = rowByKey.get(edge.skillKey);
875
+ if (row === void 0) continue;
876
+ if (edge.status === "pending") {
877
+ pendingCount += 1;
878
+ continue;
879
+ }
880
+ if (edge.status !== "active") continue;
881
+ if (row.managed && row.modelInvocable && !row.protected && !row.pinned) managedModelInvocableCount += 1;
882
+ else if (!row.managed) externalCatalogCount += 1;
883
+ }
884
+ return {
885
+ id: node.id,
886
+ title: node.name,
887
+ purpose: node.purpose.slice(0, 80),
888
+ descriptionStatus: node.descriptionStatus === "fresh" && input.descriptionStaleAfterHours !== void 0 && input.now !== void 0 && input.now - node.updatedAt > input.descriptionStaleAfterHours * 36e5 ? "stale" : node.descriptionStatus,
889
+ triggers: node.triggers.slice(0, 3),
890
+ managedModelInvocableCount,
891
+ externalCatalogCount,
892
+ managedCount: managedModelInvocableCount,
893
+ externalCount: externalCatalogCount,
894
+ pendingCount,
895
+ unclassifiedCount,
896
+ state: node.status === "provisional" ? "provisional" : "active",
897
+ asOf: new Date(node.updatedAt).toISOString(),
898
+ mapVersion: input.mapVersion,
899
+ mapStale: mapStale || staleEdgeDomains.has(node.id)
900
+ };
901
+ });
902
+ const budget = input.tokenBudget;
903
+ if (budget !== void 0 && budget > 0) while (cards.length > 1 && Math.ceil(JSON.stringify(cards).length / 4) > budget) cards.pop();
904
+ const cardIds = new Set(cards.map((card) => card.id));
905
+ const wouldHide = /* @__PURE__ */ new Set();
906
+ for (const edge of input.snapshot.edges) {
907
+ if (edge.status !== "active" || edge.role !== "primary") continue;
908
+ const node = domainById.get(edge.domainId);
909
+ const row = rowByKey.get(edge.skillKey);
910
+ if (node === void 0 || row === void 0) continue;
911
+ if (node.status !== "active" || !cardIds.has(node.id) || !row.managed || !row.modelInvocable || row.protected || row.pinned) continue;
912
+ if (edge.skillHash !== row.threeFieldHash) continue;
913
+ if (input.policy.disabled.has(node.id)) continue;
914
+ wouldHide.add(edge.skillKey);
915
+ }
916
+ const activeManaged = mode === "on" && !mapStale && input.searchAvailable !== false && input.policy.degraded.length === 0 ? wouldHide : /* @__PURE__ */ new Set();
917
+ const projected = projectCatalog({
918
+ entries: input.entries,
919
+ rows: input.rows,
920
+ activeManagedSkillKeys: activeManaged,
921
+ cards,
922
+ viewKey: input.viewKey,
923
+ scope: input.scope,
924
+ mapVersion: input.mapVersion,
925
+ policyDigest: input.policyDigest,
926
+ viewCatalogDigest: input.viewCatalogDigest,
927
+ adapterVersion: input.adapterVersion,
928
+ projectionSchemaVersion: input.projectionSchemaVersion,
929
+ discoveryMode: input.discoveryMode,
930
+ complete: true
931
+ });
932
+ return mode === "on" ? projected : projected;
933
+ }
934
+ //#endregion
935
+ //#region lib/types/skill-index.js
936
+ function bySkillKey(left, right) {
937
+ return left.skillKey < right.skillKey ? -1 : left.skillKey > right.skillKey ? 1 : 0;
938
+ }
939
+ /** Read-only view model; it does not cache or own a provider. */
940
+ var SkillIndex = class {
941
+ source;
942
+ constructor(source) {
943
+ this.source = source;
944
+ }
945
+ /**
946
+ * Read one view from the source.
947
+ * @param query - cwd/scope and cancellation for this observation.
948
+ * @returns normalized rows and completeness; incomplete data is not hidden.
949
+ */
950
+ async read(query) {
951
+ const [snapshot, managed, protectedKeys, pinnedKeys] = await Promise.all([
952
+ this.source.snapshot(query),
953
+ this.source.managedSkillKeys(),
954
+ this.source.protectedSkillKeys?.() ?? Promise.resolve(/* @__PURE__ */ new Set()),
955
+ this.source.pinnedSkillKeys?.() ?? Promise.resolve(/* @__PURE__ */ new Set())
956
+ ]);
957
+ const rows = snapshot.skills.map((summary) => skillIndexRowOf(summary, managed.has(skillKeyOf(summary)), protectedKeys.has(skillKeyOf(summary)), pinnedKeys.has(skillKeyOf(summary)))).sort(bySkillKey);
958
+ return {
959
+ viewKey: viewKeyOf(query),
960
+ catalogRevision: catalogRevisionOf(rows, snapshot.complete),
961
+ viewCatalogDigest: viewCatalogDigestOf(viewKeyOf(query), rows, snapshot.complete),
962
+ rows,
963
+ complete: snapshot.complete
964
+ };
965
+ }
966
+ };
967
+ //#endregion
968
+ //#region lib/types/catalog-codec.js
969
+ function textOf(message) {
970
+ return message.content.map((block) => block.type === "text" ? block.text : "").join("\n");
971
+ }
972
+ function sourceOf(message) {
973
+ if (message.source.kind !== "skill-catalog") return void 0;
974
+ return message.source;
975
+ }
976
+ /** @returns captured catalog when text has the known available_skills block. */
977
+ function captureCatalog(message) {
978
+ const source = sourceOf(message);
979
+ if (source === void 0 || source.form !== "catalog") return void 0;
980
+ const text = textOf(message);
981
+ if (!text.includes("<available_skills>") || !text.includes("</available_skills>")) return void 0;
982
+ return {
983
+ text,
984
+ source,
985
+ entries: source.entries,
986
+ update: source.update === true
987
+ };
988
+ }
989
+ function renderEntries(entries) {
990
+ return entries.map((entry) => "- `" + entry.name + "`: " + escapeText(entry.description)).join("\n");
991
+ }
992
+ /**
993
+ * Replace only the available_skills block; preamble, footer, and update marker stay byte-identical.
994
+ * @param capture - platform message captured before projection.
995
+ * @param entries - projected entries.
996
+ * @returns a platform-sourced message with the same framing.
997
+ */
998
+ function renderCatalog(capture, entries) {
999
+ const start = capture.text.indexOf("<available_skills>");
1000
+ const end = capture.text.indexOf("</available_skills>", start);
1001
+ return createUserMessage({
1002
+ content: [{
1003
+ type: "text",
1004
+ text: capture.text.slice(0, start + 18) + "\n" + renderEntries(entries) + "\n" + capture.text.slice(end)
1005
+ }],
1006
+ source: {
1007
+ ...capture.source,
1008
+ entries: capture.source.entries
1009
+ }
1010
+ });
1011
+ }
1012
+ /**
1013
+ * Build a replacement catalog when no current catalog message exists.
1014
+ * @param sourceEntries - full platform entries; keeps the platform digest stable.
1015
+ * @param entries - projected entries.
1016
+ * @returns a skill-catalog message with update=true.
1017
+ */
1018
+ function renderCatalogUpdate(sourceEntries, entries) {
1019
+ return createUserMessage({
1020
+ content: [{
1021
+ type: "text",
1022
+ text: [
1023
+ "<system-reminder>",
1024
+ "The available skill catalog changed. This complete catalog replaces every earlier available-skills list in this session:",
1025
+ "",
1026
+ "<available_skills>",
1027
+ ...entries.map((entry) => "- `" + entry.name + "`: " + escapeText(entry.description)),
1028
+ "</available_skills>",
1029
+ "",
1030
+ "Use only names in this replacement catalog. If the user names a listed skill, or the task clearly matches its description, call the `skill` tool with the exact name before acting.",
1031
+ "A user may also invoke a skill directly; its <skill_content> block then appears in this conversation. Follow it, and do not call the `skill` tool again for that skill.",
1032
+ "</system-reminder>"
1033
+ ].join("\n")
1034
+ }],
1035
+ source: {
1036
+ kind: "skill-catalog",
1037
+ form: "catalog",
1038
+ update: true,
1039
+ entries: sourceEntries
1040
+ }
1041
+ });
1042
+ }
1043
+ /** @returns the exact platform message captured before projection. */
1044
+ function restoreCatalog(capture) {
1045
+ return createUserMessage({
1046
+ content: [{
1047
+ type: "text",
1048
+ text: capture.text
1049
+ }],
1050
+ source: capture.source
1051
+ });
1052
+ }
1053
+ //#endregion
1054
+ //#region lib/types/projection-adapter.js
1055
+ /** @returns the first platform catalog message in the current decision. */
1056
+ function findCatalogMessage(decision) {
1057
+ if (decision.kind !== "enter") return void 0;
1058
+ return decision.messages.find((message) => message.source.kind === "skill-catalog");
1059
+ }
1060
+ /**
1061
+ * Replace the current catalog message with projected entries, preserving its framing.
1062
+ * @param decision - current pre-step decision.
1063
+ * @param capture - platform catalog captured from the same decision.
1064
+ * @param entries - projected entries.
1065
+ * @returns a new decision; unchanged when no catalog message exists.
1066
+ */
1067
+ function applyCapturedProjection(decision, capture, entries) {
1068
+ if (decision.kind !== "enter") return decision;
1069
+ const index = decision.messages.findIndex((message) => message.source.kind === "skill-catalog");
1070
+ if (index < 0) return decision;
1071
+ const messages = [...decision.messages];
1072
+ messages[index] = renderCatalog(capture, entries);
1073
+ return {
1074
+ ...decision,
1075
+ messages
1076
+ };
1077
+ }
1078
+ /**
1079
+ * Restore the exact captured platform catalog in the current decision.
1080
+ * @param decision - current pre-step decision.
1081
+ * @param capture - durable platform catalog capture.
1082
+ * @returns a new decision; unchanged when no catalog message exists.
1083
+ */
1084
+ function restoreCapturedProjection(decision, capture) {
1085
+ if (decision.kind !== "enter") return decision;
1086
+ const withoutContext = decision.messages.filter((message) => message.source.kind !== "evolution-skill-projection");
1087
+ const index = withoutContext.findIndex((message) => message.source.kind === "skill-catalog");
1088
+ if (index < 0) return decision;
1089
+ const messages = [...withoutContext];
1090
+ messages[index] = restoreCatalog(capture);
1091
+ return {
1092
+ ...decision,
1093
+ messages
1094
+ };
1095
+ }
1096
+ /**
1097
+ * Append a replacement catalog when the current decision has no catalog message.
1098
+ * @param decision - current pre-step decision.
1099
+ * @param sourceEntries - full platform entries for the stable digest.
1100
+ * @param result - projection result.
1101
+ * @returns decision with replacement catalog plus optional context.
1102
+ */
1103
+ function applyReplacementDecision(decision, sourceEntries, result) {
1104
+ if (decision.kind !== "enter") return decision;
1105
+ const update = renderCatalogUpdate(sourceEntries, result.catalogEntries);
1106
+ const context = renderProjectionContext(result);
1107
+ const messages = context === void 0 ? [...decision.messages, update] : [
1108
+ ...decision.messages,
1109
+ update,
1110
+ context
1111
+ ];
1112
+ return {
1113
+ ...decision,
1114
+ messages
1115
+ };
1116
+ }
1117
+ /**
1118
+ * Append a full replacement catalog without a current catalog message.
1119
+ * @param decision - current pre-step decision.
1120
+ * @param capture - durable platform capture.
1121
+ * @returns decision with a full replacement catalog.
1122
+ */
1123
+ function applyRestoreUpdate(decision, capture) {
1124
+ if (decision.kind !== "enter") return decision;
1125
+ return {
1126
+ ...decision,
1127
+ messages: [...decision.messages, renderCatalogUpdate(capture.source.entries, capture.entries)]
1128
+ };
1129
+ }
1130
+ /** @returns captured current catalog or undefined when the shape is unknown. */
1131
+ function captureDecisionCatalog(decision) {
1132
+ const message = findCatalogMessage(decision);
1133
+ return message === void 0 ? void 0 : captureCatalog(message);
1134
+ }
1135
+ /** @returns a model-visible projection context message, or undefined when empty. */
1136
+ function renderProjectionContext(result) {
1137
+ if (result.context === "") return void 0;
1138
+ return createUserMessage({
1139
+ content: [{
1140
+ type: "text",
1141
+ text: result.context
1142
+ }],
1143
+ source: {
1144
+ kind: "evolution-skill-projection",
1145
+ entries: result.catalogEntries,
1146
+ hiddenNames: result.hiddenNames,
1147
+ digest: result.digest
1148
+ }
1149
+ });
1150
+ }
1151
+ /**
1152
+ * Apply the complete projection: replace the current catalog and append one context message.
1153
+ * @param decision - current pre-step decision.
1154
+ * @param capture - platform catalog captured from the same decision.
1155
+ * @param result - complete projection.
1156
+ * @returns new decision; unchanged when no catalog exists.
1157
+ */
1158
+ function applyProjectedDecision(decision, capture, result) {
1159
+ const withCatalog = applyCapturedProjection(decision, capture, result.catalogEntries);
1160
+ const context = renderProjectionContext(result);
1161
+ if (context === void 0 || withCatalog.kind !== "enter") return withCatalog;
1162
+ return withCatalog.messages.some((message) => message.source.kind === "evolution-skill-projection" && message.source.digest === result.digest) ? withCatalog : {
1163
+ ...withCatalog,
1164
+ messages: [...withCatalog.messages, context]
1165
+ };
1166
+ }
1167
+ //#endregion
1168
+ //#region lib/types/view-state.js
1169
+ /**
1170
+ * Derive one view's state without consulting another view.
1171
+ * @param input - per-view health and identity.
1172
+ * @returns state with reasons; hidden safety is evaluated by canHide.
1173
+ */
1174
+ function deriveViewState(input) {
1175
+ const reasons = [...input.reasons ?? []];
1176
+ if (input.mode === "off") return {
1177
+ ...input,
1178
+ phase: "off",
1179
+ reasons
1180
+ };
1181
+ if (!input.catalogComplete) reasons.push("catalog-incomplete");
1182
+ if (input.map === "empty" || input.map === "corrupt") {
1183
+ reasons.push(input.map === "corrupt" ? "map-corrupt" : "map-empty");
1184
+ return {
1185
+ ...input,
1186
+ phase: "unavailable",
1187
+ reasons
1188
+ };
1189
+ }
1190
+ if (input.map === "stale") reasons.push("map-stale");
1191
+ if (input.search === "degraded") reasons.push("search-degraded");
1192
+ if (input.policy === "degraded") reasons.push("policy-degraded");
1193
+ if (!input.catalogComplete || input.search === "degraded" || input.policy === "degraded") return {
1194
+ ...input,
1195
+ phase: "degraded",
1196
+ reasons
1197
+ };
1198
+ return {
1199
+ ...input,
1200
+ phase: "ready",
1201
+ reasons
1202
+ };
1203
+ }
1204
+ /** @returns true when this view may render cards, even if hiding is blocked. */
1205
+ function canProject(state) {
1206
+ return (state.phase === "ready" || state.phase === "degraded") && (state.map === "ready" || state.map === "stale");
1207
+ }
1208
+ /** Bounded view-state cache keyed by view identity and codec version. */
1209
+ var ViewStateRegistry = class {
1210
+ entries = /* @__PURE__ */ new Map();
1211
+ limit;
1212
+ ttlMs;
1213
+ now;
1214
+ constructor(options = {}) {
1215
+ this.limit = options.limit ?? 64;
1216
+ this.ttlMs = options.ttlMs ?? 10 * 6e4;
1217
+ this.now = options.now ?? Date.now;
1218
+ }
1219
+ /** @returns cache key including all identity components that can change a view. */
1220
+ key(state, codecVersion) {
1221
+ return state.viewKey + "|" + state.mapVersion + "|" + state.policyDigest + "|" + codecVersion;
1222
+ }
1223
+ set(state, codecVersion) {
1224
+ const key = this.key(state, codecVersion);
1225
+ this.entries.delete(key);
1226
+ this.entries.set(key, {
1227
+ state,
1228
+ at: this.now()
1229
+ });
1230
+ this.prune();
1231
+ }
1232
+ get(viewKey, mapVersion, policyDigest, codecVersion) {
1233
+ const key = viewKey + "|" + mapVersion + "|" + policyDigest + "|" + codecVersion;
1234
+ const entry = this.entries.get(key);
1235
+ if (entry === void 0) return void 0;
1236
+ if (this.now() - entry.at > this.ttlMs) {
1237
+ this.entries.delete(key);
1238
+ return;
1239
+ }
1240
+ return entry.state;
1241
+ }
1242
+ deleteView(viewKey) {
1243
+ for (const [key, entry] of this.entries) if (entry.state.viewKey === viewKey) this.entries.delete(key);
1244
+ }
1245
+ prune() {
1246
+ const now = this.now();
1247
+ for (const [key, entry] of this.entries) if (now - entry.at > this.ttlMs) this.entries.delete(key);
1248
+ while (this.entries.size > this.limit) {
1249
+ const oldest = this.entries.keys().next().value;
1250
+ if (oldest === void 0) break;
1251
+ this.entries.delete(oldest);
1252
+ }
1253
+ }
1254
+ };
1255
+ //#endregion
1256
+ //#region lib/types/projection-runtime.js
1257
+ function mapStateOf(snapshot, staleAfterHours, now) {
1258
+ if (snapshot === void 0) return "corrupt";
1259
+ if (snapshot.domains.length === 0) return "empty";
1260
+ if (staleAfterHours !== void 0 && snapshot.domains.some((node) => now - node.updatedAt > staleAfterHours * 36e5)) return "stale";
1261
+ return "ready";
1262
+ }
1263
+ function effectiveMode(mode) {
1264
+ return mode === "off" ? "off" : mode === "auto" ? "shadow" : "on";
1265
+ }
1266
+ function catalogText(message) {
1267
+ return message.content.map((block) => block.type === "text" ? block.text ?? "" : "").join("");
1268
+ }
1269
+ function fullCatalogText(capture) {
1270
+ return catalogText(renderCatalog(capture, capture.entries));
1271
+ }
1272
+ function sessionCatalogCapture(agent) {
1273
+ const readEvents = agent.session.readEvents;
1274
+ if (readEvents === void 0) return void 0;
1275
+ let projected;
1276
+ for (const event of readEvents()) {
1277
+ if (event.type !== "user/message" || typeof event.data !== "object" || event.data === null) continue;
1278
+ const message = event.data;
1279
+ if (message.source.kind !== "skill-catalog") continue;
1280
+ const capture = captureCatalog(message);
1281
+ if (capture === void 0) continue;
1282
+ projected = capture;
1283
+ if (catalogText(message) === fullCatalogText(capture)) return capture;
1284
+ }
1285
+ return projected;
1286
+ }
1287
+ /** Per-view projection runtime; no global readiness and no history edits. */
1288
+ var ProjectionRuntime = class {
1289
+ ports;
1290
+ states = new ViewStateRegistry();
1291
+ last = {
1292
+ action: "none",
1293
+ digest: "",
1294
+ at: 0
1295
+ };
1296
+ lastByAgent = /* @__PURE__ */ new Map();
1297
+ constructor(ports) {
1298
+ this.ports = ports;
1299
+ }
1300
+ /** @returns last action for one agent, or the latest process-wide action. */
1301
+ report(agent) {
1302
+ return agent === void 0 ? this.last : this.lastByAgent.get(String(agent.id)) ?? this.last;
1303
+ }
1304
+ mark(agent, action, digest, content = "") {
1305
+ const report = {
1306
+ action,
1307
+ digest,
1308
+ content,
1309
+ at: Date.now()
1310
+ };
1311
+ this.last = report;
1312
+ this.lastByAgent.set(String(agent.id), report);
1313
+ }
1314
+ /** @returns current state for diagnostics, if this process has seen the view. */
1315
+ stateFor(viewKey, mapVersion, policyDigest) {
1316
+ return this.states.get(viewKey, mapVersion, policyDigest, this.ports.codecVersion);
1317
+ }
1318
+ async restore(agent, viewKey, decision, currentCapture) {
1319
+ const record = await this.ports.journal.latest(String(agent.id), viewKey, this.ports.codecVersion);
1320
+ const sessionBase = sessionCatalogCapture(agent);
1321
+ const fullSession = sessionBase !== void 0 && sessionBase.text === fullCatalogText(sessionBase) ? sessionBase : void 0;
1322
+ const durable = currentCapture !== void 0 && currentCapture.text !== fullCatalogText(currentCapture) ? fullSession ?? record?.base : currentCapture ?? fullSession ?? record?.base;
1323
+ if (durable === void 0) {
1324
+ this.mark(agent, "blocked", "");
1325
+ return decision;
1326
+ }
1327
+ const fullText = fullCatalogText(durable);
1328
+ const previous = this.report(agent);
1329
+ if (previous.action === "restored" && previous.content === fullText) return decision;
1330
+ const restored = currentCapture === void 0 ? applyRestoreUpdate(decision, durable) : restoreCapturedProjection(decision, durable);
1331
+ if (record !== void 0) await this.ports.journal.remove(record.sessionId, record.viewKey, record.projectionDigest, record.codecVersion);
1332
+ this.mark(agent, "restored", record?.projectionDigest ?? "", fullText);
1333
+ return restored;
1334
+ }
1335
+ async readState(agent, signal) {
1336
+ const cwd = await this.ports.workingDirectory.ensure(agent, signal);
1337
+ const view = await new SkillIndex(this.ports.skillIndexSource).read({
1338
+ cwd,
1339
+ scopeKey: String(agent.id),
1340
+ scope: agent,
1341
+ signal
1342
+ });
1343
+ let snapshot;
1344
+ try {
1345
+ snapshot = await this.ports.map.read();
1346
+ } catch {
1347
+ snapshot = void 0;
1348
+ }
1349
+ const knownDomainIds = snapshot === void 0 ? void 0 : new Set(snapshot.domains.map((node) => node.id));
1350
+ const store = this.ports.policyFor === void 0 ? this.ports.policy : await this.ports.policyFor(cwd);
1351
+ const policy = store.resolve(knownDomainIds);
1352
+ const policyDigest = store.digest(knownDomainIds);
1353
+ const search = this.ports.searchAvailable?.() === false ? "degraded" : "ready";
1354
+ const state = deriveViewState({
1355
+ viewKey: view.viewKey,
1356
+ mode: effectiveMode(policy.mode),
1357
+ map: mapStateOf(snapshot, this.ports.staleAfterHours, Date.now()),
1358
+ search,
1359
+ policy: policy.degraded.length === 0 ? "ready" : "degraded",
1360
+ catalogComplete: view.complete,
1361
+ mapVersion: snapshot?.mapVersion ?? "0",
1362
+ policyDigest,
1363
+ viewCatalogDigest: view.viewCatalogDigest
1364
+ });
1365
+ this.states.set(state, this.ports.codecVersion);
1366
+ return {
1367
+ view,
1368
+ snapshot,
1369
+ policy,
1370
+ policyDigest,
1371
+ state
1372
+ };
1373
+ }
1374
+ /** @returns current per-view state without projecting; used by diagnostic commands. */
1375
+ async inspect(agent, signal) {
1376
+ if (this.ports.mode === "off") return {
1377
+ viewKey: "off",
1378
+ phase: "off",
1379
+ map: "empty",
1380
+ search: "ready",
1381
+ policy: "ready",
1382
+ reasons: [],
1383
+ mapVersion: "0",
1384
+ policyDigest: "",
1385
+ viewCatalogDigest: ""
1386
+ };
1387
+ return (await this.readState(agent, signal)).state;
1388
+ }
1389
+ /**
1390
+ * Project or restore one current pre-step decision.
1391
+ * @param agent - session owner; identity comes from workingDirectory.
1392
+ * @param decision - current waterfall decision.
1393
+ * @param signal - request cancellation.
1394
+ * @returns projected, restored, or unchanged decision.
1395
+ */
1396
+ async project(agent, decision, signal) {
1397
+ if (this.ports.mode === "off") return decision;
1398
+ if (this.ports.isFrozen !== void 0 && await this.ports.isFrozen(agent)) return decision;
1399
+ const { view, snapshot, policy, policyDigest, state } = await this.readState(agent, signal);
1400
+ const currentCapture = captureDecisionCatalog(decision);
1401
+ let capture = currentCapture;
1402
+ if (capture === void 0) capture = (await this.ports.journal.latest(String(agent.id), view.viewKey, this.ports.codecVersion))?.base;
1403
+ if (capture === void 0) capture = sessionCatalogCapture(agent);
1404
+ if (!canProject(state) || snapshot === void 0) return this.restore(agent, view.viewKey, decision, currentCapture);
1405
+ if (capture === void 0) {
1406
+ this.mark(agent, "blocked", "");
1407
+ return decision;
1408
+ }
1409
+ const projection = buildDomainProjection({
1410
+ entries: capture.entries,
1411
+ rows: view.rows,
1412
+ complete: view.complete,
1413
+ snapshot,
1414
+ policy,
1415
+ viewKey: view.viewKey,
1416
+ scope: String(agent.id),
1417
+ mapVersion: snapshot.mapVersion,
1418
+ policyDigest,
1419
+ viewCatalogDigest: view.viewCatalogDigest,
1420
+ adapterVersion: this.ports.adapterVersion,
1421
+ projectionSchemaVersion: this.ports.projectionSchemaVersion,
1422
+ discoveryMode: this.ports.mode,
1423
+ ...this.ports.maxCards === void 0 ? {} : { maxCards: this.ports.maxCards },
1424
+ ...this.ports.tokenBudget === void 0 ? {} : { tokenBudget: this.ports.tokenBudget },
1425
+ searchAvailable: state.search === "ready",
1426
+ mode: effectiveMode(policy.mode),
1427
+ mapStale: state.map === "stale",
1428
+ now: Date.now(),
1429
+ ...this.ports.staleAfterHours === void 0 ? {} : { staleAfterHours: this.ports.staleAfterHours },
1430
+ ...this.ports.descriptionStaleAfterHours === void 0 ? {} : { descriptionStaleAfterHours: this.ports.descriptionStaleAfterHours }
1431
+ });
1432
+ if (!projection.complete) return this.restore(agent, view.viewKey, decision, currentCapture);
1433
+ const expectedText = renderCatalog(capture, projection.catalogEntries).content.map((block) => block.type === "text" ? block.text : "").join("");
1434
+ if (currentCapture !== void 0 && currentCapture.text === expectedText) return decision;
1435
+ const previous = this.report(agent);
1436
+ if (currentCapture === void 0 && previous.action === "applied" && previous.digest === projection.digest && previous.content === expectedText) return decision;
1437
+ await this.ports.journal.put({
1438
+ sessionId: String(agent.id),
1439
+ viewKey: view.viewKey,
1440
+ projectionDigest: projection.digest,
1441
+ codecVersion: this.ports.codecVersion,
1442
+ mapVersion: snapshot.mapVersion,
1443
+ policyDigest,
1444
+ base: capture,
1445
+ createdAt: Date.now()
1446
+ });
1447
+ this.mark(agent, "applied", projection.digest, expectedText);
1448
+ return currentCapture === void 0 ? applyReplacementDecision(decision, capture.source.entries, projection) : applyProjectedDecision(decision, capture, projection);
1449
+ }
1450
+ };
1451
+ //#endregion
1452
+ //#region lib/types/search.js
1453
+ /** Tokenizer version; changing the algorithm requires rebuilding indexes. */
1454
+ const SKILL_SEARCH_TOKENIZER_VERSION = "cjk-bigram-v1";
1455
+ /** Fields indexed by skill search; no body/path. */
1456
+ const SKILL_SEARCH_FIELDS = [
1457
+ "name",
1458
+ "description",
1459
+ "whenToUse"
1460
+ ];
1461
+ /** Index schema version; bump when indexed fields/options change. */
1462
+ const SKILL_SEARCH_INDEX_SCHEMA_VERSION = "skill-search-v1";
1463
+ const CJK = String.raw`[[\p{scx=Han}\p{scx=Hira}\p{scx=Kana}\p{scx=Hang}]&&[\p{L}\p{M}\p{N}]]`;
1464
+ const RUN = new RegExp(String.raw`${CJK}+|[[\p{L}\p{M}\p{N}]--${CJK}]+`, "gv");
1465
+ const CJK_START = new RegExp(String.raw`^${CJK}`, "v");
1466
+ /**
1467
+ * Split NFKC text into CJK overlapping bigrams plus tail, and ASCII/other runs.
1468
+ * Parity source: plugin-market/src/search.ts searchTerms.
1469
+ * @param text - indexed or queried text.
1470
+ * @returns terms in text order; callers lowercase.
1471
+ */
1472
+ function searchTerms(text) {
1473
+ return [...text.normalize("NFKC").matchAll(RUN)].flatMap(([run]) => {
1474
+ if (!CJK_START.test(run)) return [run];
1475
+ const chars = Array.from(run);
1476
+ return [...chars.slice(1).map((char, position) => chars[position] + char), chars.at(-1)];
1477
+ });
1478
+ }
1479
+ function clampLimit(limit) {
1480
+ if (limit === void 0) return 3;
1481
+ return Math.min(20, Math.max(1, Math.trunc(limit)));
1482
+ }
1483
+ function build(rows) {
1484
+ const engine = new MiniSearch({
1485
+ fields: [...SKILL_SEARCH_FIELDS],
1486
+ storeFields: [...SKILL_SEARCH_FIELDS],
1487
+ tokenize: searchTerms,
1488
+ processTerm: (term) => term.toLowerCase(),
1489
+ searchOptions: {
1490
+ boost: {
1491
+ name: 3,
1492
+ whenToUse: 2
1493
+ },
1494
+ prefix: true,
1495
+ combineWith: "AND",
1496
+ fuzzy: (term) => term.length >= 4 && !CJK_START.test(term) ? .2 : false,
1497
+ maxFuzzy: 2
1498
+ }
1499
+ });
1500
+ const seen = /* @__PURE__ */ new Set();
1501
+ const documents = [];
1502
+ for (const row of rows) {
1503
+ if (!row.modelInvocable || seen.has(row.skillKey)) continue;
1504
+ seen.add(row.skillKey);
1505
+ documents.push({
1506
+ id: row.skillKey,
1507
+ name: row.name,
1508
+ description: row.description,
1509
+ whenToUse: row.whenToUse ?? ""
1510
+ });
1511
+ }
1512
+ engine.addAll(documents);
1513
+ return engine;
1514
+ }
1515
+ /** Per-view MiniSearch index; no global engine and no projection-only filtering. */
1516
+ var SkillSearchIndex = class SkillSearchIndex {
1517
+ engine;
1518
+ staleState = false;
1519
+ viewKey;
1520
+ constructor(viewKey, engine) {
1521
+ this.viewKey = viewKey;
1522
+ this.engine = engine;
1523
+ }
1524
+ /** @returns an index over every modelInvocable row in this view. */
1525
+ static fromRows(viewKey, rows) {
1526
+ return new SkillSearchIndex(viewKey, build(rows));
1527
+ }
1528
+ /** Mark the current index stale before a rebuild finishes. */
1529
+ markStale() {
1530
+ this.staleState = true;
1531
+ }
1532
+ /** Rebuild from this view's full rows; a failed rebuild keeps the old engine stale. */
1533
+ async rebuild(rows) {
1534
+ this.staleState = true;
1535
+ try {
1536
+ this.engine = build(rows);
1537
+ this.staleState = false;
1538
+ return true;
1539
+ } catch {
1540
+ return false;
1541
+ }
1542
+ }
1543
+ /** @returns hits, truncation, and whether the index is stale. */
1544
+ async search(query, options) {
1545
+ const limit = clampLimit(query.limit);
1546
+ const terms = searchTerms(query.query);
1547
+ if (terms.length === 0) return {
1548
+ results: [],
1549
+ truncated: false,
1550
+ stale: this.staleState
1551
+ };
1552
+ let fallback = false;
1553
+ let hits = this.engine.search(query.query, { combineWith: "AND" });
1554
+ if (hits.length === 0 && terms.length > 1) {
1555
+ const relaxed = this.engine.search(query.query, { combineWith: "OR" });
1556
+ fallback = relaxed.length > 0;
1557
+ hits = relaxed;
1558
+ }
1559
+ const allowed = options?.allowedSkillKeys;
1560
+ const filtered = allowed === void 0 ? hits : hits.filter((hit) => allowed.has(String(hit.id)));
1561
+ return {
1562
+ results: filtered.slice(0, limit).map((hit) => {
1563
+ const fields = new Set(Object.values(hit.match).flat());
1564
+ return {
1565
+ name: String(hit.name),
1566
+ description: String(hit.description),
1567
+ ...hit.whenToUse === void 0 || hit.whenToUse === "" ? {} : { whenToUse: String(hit.whenToUse) },
1568
+ matchedBy: fields.has("name") ? "name" : fields.has("whenToUse") ? "whenToUse" : "description"
1569
+ };
1570
+ }),
1571
+ truncated: filtered.length > limit,
1572
+ ...fallback ? { fallback: true } : {},
1573
+ stale: this.staleState
1574
+ };
1575
+ }
1576
+ };
1577
+ /** Per-view cache keyed by searchIndexKey; never shares one engine across views. */
1578
+ var SkillSearchCache = class {
1579
+ entries = /* @__PURE__ */ new Map();
1580
+ /**
1581
+ * Get or build one view index. A failed rebuild retains the previous engine
1582
+ * and marks it stale; no previous engine means undefined.
1583
+ * @param key - identity inputs for searchIndexKey.
1584
+ * @param rows - current full rows for the view.
1585
+ * @returns index plus stale state, or undefined when no index is available.
1586
+ */
1587
+ async get(key, rows) {
1588
+ const cacheKey = searchIndexKeyOf({
1589
+ viewKey: key.viewKey,
1590
+ viewCatalogDigest: key.viewCatalogDigest,
1591
+ policyDigest: key.policyDigest,
1592
+ tokenizerVersion: SKILL_SEARCH_TOKENIZER_VERSION,
1593
+ indexSchemaVersion: SKILL_SEARCH_INDEX_SCHEMA_VERSION,
1594
+ discoveryMode: key.discoveryMode
1595
+ });
1596
+ const existing = this.entries.get(cacheKey);
1597
+ if (existing !== void 0) return {
1598
+ index: existing.index,
1599
+ stale: false
1600
+ };
1601
+ try {
1602
+ const index = SkillSearchIndex.fromRows(key.viewKey, rows);
1603
+ this.entries.set(cacheKey, {
1604
+ key: cacheKey,
1605
+ viewKey: key.viewKey,
1606
+ index
1607
+ });
1608
+ return {
1609
+ index,
1610
+ stale: false
1611
+ };
1612
+ } catch {
1613
+ const old = [...this.entries.values()].find((entry) => entry.viewKey === key.viewKey);
1614
+ if (old === void 0) return void 0;
1615
+ old.index.markStale();
1616
+ return {
1617
+ index: old.index,
1618
+ stale: true
1619
+ };
1620
+ }
1621
+ }
1622
+ /** Drop every index for one view. */
1623
+ invalidate(viewKey) {
1624
+ for (const [cacheKey, entry] of this.entries) if (entry.viewKey === viewKey) this.entries.delete(cacheKey);
1625
+ }
1626
+ /** Drop every cached view index. */
1627
+ clear() {
1628
+ this.entries.clear();
1629
+ }
1630
+ };
1631
+ //#endregion
1632
+ //#region lib/types/tool.js
1633
+ /** Static model-facing description; no dynamic domain list/tree. */
1634
+ const SKILL_SEARCH_DESCRIPTION = "Search skill summaries by name, description, or whenToUse when coverage is uncertain. Results are summaries, not bodies; a matching name can be loaded directly. Empty results are inconclusive; the full catalog remains available.";
1635
+ /** @returns response or a degraded hint; never silent empty on failure. */
1636
+ async function searchWithDegraded(provider, query, allowedSkillKeys, agent, signal) {
1637
+ if (provider === void 0) return {
1638
+ results: [],
1639
+ truncated: false,
1640
+ degraded: true,
1641
+ hint: "Skill search is unavailable. Use the full catalog or load an exact skill by name with skill(name)."
1642
+ };
1643
+ try {
1644
+ return await provider.search(query, allowedSkillKeys === void 0 ? void 0 : { allowedSkillKeys }, agent, signal);
1645
+ } catch {
1646
+ return {
1647
+ results: [],
1648
+ truncated: false,
1649
+ degraded: true,
1650
+ hint: "Skill search failed. Use the full catalog or load an exact skill by name with skill(name)."
1651
+ };
1652
+ }
1653
+ }
1654
+ function toToolResponse(response) {
1655
+ return {
1656
+ results: response.results.map((hit) => ({
1657
+ name: hit.name,
1658
+ description: hit.description,
1659
+ ...hit.whenToUse === void 0 ? {} : { whenToUse: hit.whenToUse },
1660
+ ...hit.matchedBy === void 0 ? {} : { matchedBy: hit.matchedBy }
1661
+ })),
1662
+ truncated: response.truncated,
1663
+ ...response.fallback === void 0 ? {} : { fallback: response.fallback },
1664
+ ...response.stale === void 0 ? {} : { stale: response.stale },
1665
+ ...response.degraded === void 0 ? {} : { degraded: response.degraded },
1666
+ ...response.hint === void 0 ? {} : { hint: response.hint }
1667
+ };
1668
+ }
1669
+ /**
1670
+ * Register skill_search on the optional tools service. The description/schema are static; the
1671
+ * provider is the only dynamic dependency.
1672
+ * platform-declaration anchor: tools is declared in core PLATFORM_SERVICE_PROBES.
1673
+ * @param ctx - plugin context.
1674
+ * @param provider - search service or undefined when unavailable.
1675
+ * @returns tool disposer.
1676
+ */
1677
+ function registerSkillSearchTool(ctx, provider) {
1678
+ const tools = ctx.get("tools");
1679
+ if (tools === void 0) return () => {};
1680
+ return tools.register(defineTool({
1681
+ name: "skill_search",
1682
+ description: SKILL_SEARCH_DESCRIPTION,
1683
+ parameters: {
1684
+ query: {
1685
+ type: "string",
1686
+ required: true,
1687
+ description: "Search terms; CJK and ASCII are supported."
1688
+ },
1689
+ limit: {
1690
+ type: "integer",
1691
+ description: "Maximum results; defaults to 3, capped at 20."
1692
+ }
1693
+ },
1694
+ output: {
1695
+ schema: {
1696
+ type: "object",
1697
+ additionalProperties: false,
1698
+ properties: {
1699
+ results: {
1700
+ type: "array",
1701
+ required: true,
1702
+ items: {
1703
+ type: "object",
1704
+ additionalProperties: false,
1705
+ properties: {
1706
+ name: {
1707
+ type: "string",
1708
+ required: true
1709
+ },
1710
+ description: {
1711
+ type: "string",
1712
+ required: true
1713
+ },
1714
+ whenToUse: { type: "string" },
1715
+ matchedBy: { type: "string" }
1716
+ }
1717
+ }
1718
+ },
1719
+ truncated: {
1720
+ type: "boolean",
1721
+ required: true
1722
+ },
1723
+ fallback: { type: "boolean" },
1724
+ stale: { type: "boolean" },
1725
+ degraded: { type: "boolean" },
1726
+ hint: { type: "string" }
1727
+ }
1728
+ },
1729
+ render: (_args, value) => [{
1730
+ type: "text",
1731
+ text: value.degraded === true ? "Skill search is degraded: " + (value.hint ?? "use the full catalog or skill(name).") : "Found " + value.results.length + " skill result(s)" + (value.fallback === true ? " (OR fallback)." : ".") + (value.stale === true ? " (stale index)." : "") + "\n" + value.results.map((result) => "- " + result.name + ": " + result.description + (result.whenToUse === void 0 ? "" : " | when: " + result.whenToUse)).join("\n")
1732
+ }]
1733
+ },
1734
+ async execute(args, exec) {
1735
+ try {
1736
+ const allowed = await provider?.allowedSkillKeys?.(exec.agent, exec.signal);
1737
+ return toToolResponse(await searchWithDegraded(provider, {
1738
+ query: args.query,
1739
+ limit: args.limit
1740
+ }, allowed, exec.agent, exec.signal));
1741
+ } catch (error) {
1742
+ return {
1743
+ results: [],
1744
+ truncated: false,
1745
+ degraded: true,
1746
+ hint: "Skill search failed: " + (error instanceof Error ? error.message : String(error))
1747
+ };
1748
+ }
1749
+ }
1750
+ }));
1751
+ }
1752
+ //#endregion
1753
+ //#region lib/types/domain-generation-prompt.js
1754
+ /** @returns a parameterized initial-domain prompt; default 8–12. */
1755
+ function buildDomainGenerationPrompt(options = {}) {
1756
+ const requestedMin = options.minDomains ?? 8;
1757
+ const requestedMax = options.maxDomains ?? 12;
1758
+ const valid = Number.isSafeInteger(requestedMin) && Number.isSafeInteger(requestedMax) && requestedMin >= 2 && requestedMax >= requestedMin && requestedMax <= 20;
1759
+ return `You are designing a first-level domain map for a skill library.
1760
+ Input is limited to each skill's name, description, and whenToUse; do not infer from skill bodies.
1761
+ Choose ${valid ? requestedMin : 8}–${valid ? requestedMax : 12} domains that help a model decide where to look. Domains are an awareness layer, not a complete taxonomy.
1762
+ Each domain needs a name, one-sentence purpose, in-scope, out-of-scope, triggers, and evidence skill names copied from the input.
1763
+ Keep domains coherent, distinguishable, and recognizable from a task description; a single skill is not a domain. Do not invent skills or domains.
1764
+ Prefer clear boundaries over exhaustive coverage. Leave a skill unassigned when evidence is weak rather than forcing it. The caller assigns IDs.
1765
+ Return only a JSON object with a "domains" array. Each domain must have title, purpose, inScope, outOfScope, triggers, and evidenceSkillKeys. No markdown fences and no prose.`;
1766
+ }
1767
+ const DOMAIN_GENERATION_PROMPT_V2 = buildDomainGenerationPrompt();
1768
+ createHash("sha256").update(DOMAIN_GENERATION_PROMPT_V2, "utf8").digest("hex");
1769
+ //#endregion
1770
+ //#region lib/types/domain-generation-llm.js
1771
+ /** Typed failure from LLM transport/parse/validation. */
1772
+ var DomainGenerationError = class extends Error {
1773
+ code;
1774
+ constructor(code, message) {
1775
+ super(message);
1776
+ this.code = code;
1777
+ this.name = "DomainGenerationError";
1778
+ }
1779
+ };
1780
+ /** @returns parsed proposal with the caller's skill set; throws on malformed/empty output. */
1781
+ function parseDomainGenerationProposal(text, skills) {
1782
+ const cleaned = text.trim().replace(/^\`\`\`(?:json)?\s*/i, "").replace(/\`\`\`$/, "").trim();
1783
+ if (cleaned.length === 0) throw new DomainGenerationError("invalid-output", "domain generation returned empty output");
1784
+ let parsed;
1785
+ try {
1786
+ parsed = JSON.parse(cleaned);
1787
+ } catch (error) {
1788
+ throw new DomainGenerationError("invalid-output", "domain generation returned non-JSON output: " + (error instanceof Error ? error.message : String(error)));
1789
+ }
1790
+ const candidate = isRecord(parsed) && Array.isArray(parsed.domains) ? { domains: parsed.domains } : Array.isArray(parsed) ? { domains: parsed } : void 0;
1791
+ if (candidate === void 0) throw new DomainGenerationError("invalid-output", "domain generation output must be an object with domains[]");
1792
+ const checked = validateDomainGeneration({ skills }, candidate);
1793
+ if (!checked.ok) throw new DomainGenerationError("invalid-proposal", "invalid domain proposal: " + checked.errors.join("; "));
1794
+ return candidate;
1795
+ }
1796
+ /** Production LLM adapter; validates before returning, never writes the map. */
1797
+ var LlmDomainGenerationPort = class {
1798
+ llm;
1799
+ options;
1800
+ constructor(llm, options) {
1801
+ this.llm = llm;
1802
+ this.options = {
1803
+ provider: options.provider,
1804
+ model: options.model,
1805
+ maxTokens: options.maxTokens ?? 4096,
1806
+ timeoutMs: options.timeoutMs ?? 12e4,
1807
+ attempts: options.attempts ?? 3
1808
+ };
1809
+ }
1810
+ /**
1811
+ * Generate one validated domain proposal.
1812
+ * @param input - live facts, prompt, and cancellation.
1813
+ * @returns validated proposal with 8–12 domains and known evidence.
1814
+ */
1815
+ async generate(input) {
1816
+ const timeout = AbortSignal.timeout(this.options.timeoutMs);
1817
+ const signal = input.signal === void 0 ? timeout : AbortSignal.any([input.signal, timeout]);
1818
+ const skillContext = "\n\nAvailable skills (copy evidenceSkillKeys only from these names):\n" + input.skills.map((skill) => "- " + skill.name + ": " + skill.description + (skill.whenToUse === void 0 ? "" : " | when: " + skill.whenToUse)).join("\n");
1819
+ const basePrompt = input.prompt + skillContext;
1820
+ let feedback = "";
1821
+ let last;
1822
+ for (let attempt = 1; attempt <= this.options.attempts; attempt += 1) try {
1823
+ const prompt = feedback === "" ? basePrompt : basePrompt + "\nPrevious output was rejected: " + feedback + "\nReturn only the structured result with 8-12 domains.";
1824
+ const assembler = new BlockAssembler();
1825
+ for await (const chunk of this.llm.stream({
1826
+ provider: this.options.provider,
1827
+ model: this.options.model,
1828
+ messages: [createUserMessage({
1829
+ content: [{
1830
+ type: "text",
1831
+ text: prompt
1832
+ }],
1833
+ source: {
1834
+ kind: "evolution-skill-generation",
1835
+ form: "notice",
1836
+ summary: "initial domain generation"
1837
+ }
1838
+ })],
1839
+ maxTokens: this.options.maxTokens,
1840
+ signal
1841
+ })) assembler.push(chunk);
1842
+ return parseDomainGenerationProposal(assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("\n"), input.skills);
1843
+ } catch (error) {
1844
+ if (!(error instanceof DomainGenerationError) || error.code !== "invalid-output" && error.code !== "invalid-proposal") {
1845
+ if (timeout.aborted) throw new DomainGenerationError("timeout", "domain generation timed out after " + this.options.timeoutMs + "ms");
1846
+ throw new DomainGenerationError("llm-unavailable", "domain generation llm failed: " + (error instanceof Error ? error.message : String(error)));
1847
+ }
1848
+ last = error;
1849
+ feedback = error.message;
1850
+ }
1851
+ throw last ?? new DomainGenerationError("invalid-output", "domain generation failed");
1852
+ }
1853
+ };
1854
+ //#endregion
1855
+ //#region lib/types/generation-input.js
1856
+ /**
1857
+ * Convert live model-invocable rows into the initial-map writer's real facts.
1858
+ * @param rows - current view rows.
1859
+ * @returns stable keys and three-field hashes, never synthetic values.
1860
+ */
1861
+ function bootstrapFactsFromRows(rows) {
1862
+ return rows.filter((row) => row.modelInvocable).map((row) => ({
1863
+ name: row.name,
1864
+ description: row.description,
1865
+ ...row.whenToUse === void 0 ? {} : { whenToUse: row.whenToUse },
1866
+ skillKey: row.skillKey,
1867
+ threeFieldHash: row.threeFieldHash
1868
+ }));
1869
+ }
1870
+ //#endregion
1871
+ //#region lib/types/generation-composition.js
1872
+ /**
1873
+ * Resolve the production port; mode:on without an llm service fails loud.
1874
+ * @param input - mode and concrete llm service.
1875
+ * @returns port, or undefined for mode:off.
1876
+ */
1877
+ function resolveGenerationPort(input) {
1878
+ if (input.mode === "off") return void 0;
1879
+ if (input.llm === void 0) throw new DomainGenerationError("llm-unavailable", "evolution-skill-discovery: mode=" + input.mode + " requires an llm service for initial domain generation");
1880
+ return new LlmDomainGenerationPort(input.llm, {
1881
+ provider: input.provider,
1882
+ model: input.model,
1883
+ ...input.maxTokens === void 0 ? {} : { maxTokens: input.maxTokens },
1884
+ ...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs },
1885
+ ...input.attempts === void 0 ? {} : { attempts: input.attempts }
1886
+ });
1887
+ }
1888
+ /**
1889
+ * Build a global-map generation request from live rows.
1890
+ * @param input - map scope, source identity, rows, and prompt.
1891
+ * @returns request with real facts; never reads config arrays.
1892
+ */
1893
+ function buildGenerationRequest(input) {
1894
+ return {
1895
+ mapScope: input.mapScope,
1896
+ sourceDigest: input.sourceDigest,
1897
+ expectedMapVersion: input.mapVersion,
1898
+ facts: bootstrapFactsFromRows(input.rows),
1899
+ prompt: input.prompt,
1900
+ promptVersion: input.promptVersion,
1901
+ model: input.model,
1902
+ runId: input.runId
1903
+ };
1904
+ }
1905
+ //#endregion
1906
+ //#region lib/types/view-snapshot-store.js
1907
+ /** Durable per-session view snapshot; old epochs stay frozen. */
1908
+ var ViewSnapshotStore = class {
1909
+ io;
1910
+ path;
1911
+ constructor(io, path) {
1912
+ this.io = io;
1913
+ this.path = path;
1914
+ }
1915
+ async read() {
1916
+ const content = await this.io.readText(this.path);
1917
+ if (content === null) return {
1918
+ schemaVersion: 1,
1919
+ sessions: {}
1920
+ };
1921
+ try {
1922
+ const parsed = JSON.parse(content);
1923
+ return parsed.schemaVersion === 1 && typeof parsed.sessions === "object" && parsed.sessions !== null ? {
1924
+ schemaVersion: 1,
1925
+ sessions: parsed.sessions
1926
+ } : {
1927
+ schemaVersion: 1,
1928
+ sessions: {}
1929
+ };
1930
+ } catch {
1931
+ return {
1932
+ schemaVersion: 1,
1933
+ sessions: {}
1934
+ };
1935
+ }
1936
+ }
1937
+ /** Persist one session snapshot; first writer wins unless overwrite is explicit. */
1938
+ async put(snapshot, options = {}) {
1939
+ const current = await this.read();
1940
+ if (current.sessions[snapshot.sessionId] !== void 0 && options.overwrite !== true) return;
1941
+ const sessions = {
1942
+ ...current.sessions,
1943
+ [snapshot.sessionId]: {
1944
+ viewKey: snapshot.viewKey,
1945
+ cwd: snapshot.cwd,
1946
+ viewCatalogDigest: snapshot.viewCatalogDigest,
1947
+ mapVersion: snapshot.mapVersion,
1948
+ configDigest: snapshot.configDigest,
1949
+ featureEpoch: snapshot.featureEpoch,
1950
+ createdAt: snapshot.createdAt
1951
+ }
1952
+ };
1953
+ await this.io.writeText(this.path, JSON.stringify({
1954
+ schemaVersion: 1,
1955
+ sessions
1956
+ }));
1957
+ }
1958
+ /** @returns frozen snapshot for one session, if recorded. */
1959
+ async get(sessionId) {
1960
+ const entry = (await this.read()).sessions[sessionId];
1961
+ return entry === void 0 ? void 0 : {
1962
+ sessionId,
1963
+ ...entry
1964
+ };
1965
+ }
1966
+ /**
1967
+ * @returns true for legacy sessions without a snapshot and for older feature epochs.
1968
+ */
1969
+ async isFrozen(sessionId, currentFeatureEpoch) {
1970
+ const snapshot = await this.get(sessionId);
1971
+ return snapshot === void 0 || snapshot.featureEpoch !== currentFeatureEpoch;
1972
+ }
1973
+ };
1974
+ //#endregion
1975
+ //#region lib/types/assignment.js
1976
+ function digest(value) {
1977
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
1978
+ }
1979
+ /** @returns idempotency key for one proposal observation. */
1980
+ function assignmentKey(input) {
1981
+ return digest([
1982
+ input.skillKey,
1983
+ input.threeFieldHash,
1984
+ input.proposalDigest,
1985
+ input.mapVersion
1986
+ ]);
1987
+ }
1988
+ /** @returns stable proposal digest; undefined proposal is digest of null. */
1989
+ function proposalDigest(proposal) {
1990
+ return digest(proposal ?? null);
1991
+ }
1992
+ /** @returns a closed-set pending reason or undefined when the proposal can enqueue. */
1993
+ function validateProposal(proposal) {
1994
+ if (proposal.mode !== "existing" && proposal.mode !== "new") return "schema-invalid";
1995
+ if (typeof proposal.confidence !== "number" || proposal.confidence < 0 || proposal.confidence > 1 || typeof proposal.reason !== "string" || typeof proposal.mapVersion !== "string") return "schema-invalid";
1996
+ if (proposal.mode === "existing" && (typeof proposal.id !== "string" || proposal.id.length === 0)) return "invalid-id";
1997
+ if (proposal.mode === "new") {
1998
+ const proposed = proposal.proposed;
1999
+ if (proposed === void 0 || proposed.title.trim().length === 0 || proposed.purpose.trim().length === 0 || proposed.inScope.length === 0 || proposed.outOfScope.length === 0 || proposed.triggers.length === 0) return "new-domain-conflict";
2000
+ }
2001
+ }
2002
+ /** @returns true only when whenToUse is covered by in/out/triggers; names alone do not count. */
2003
+ function coveredByDomain(whenToUse, domain) {
2004
+ const text = whenToUse.toLowerCase();
2005
+ if (text.length === 0) return false;
2006
+ if (domain.outOfScope.some((value) => value.length > 0 && text.includes(value.toLowerCase()))) return false;
2007
+ return domain.triggers.some((value) => value.length > 0 && text.includes(value.toLowerCase())) || domain.inScope.some((value) => value.length > 0 && text.includes(value.toLowerCase()));
2008
+ }
2009
+ /** One assignment sidecar service; the only map write path for create. */
2010
+ var DomainAssignmentService = class {
2011
+ queue;
2012
+ store;
2013
+ options;
2014
+ constructor(queue, store, options) {
2015
+ this.queue = queue;
2016
+ this.store = store;
2017
+ this.options = options;
2018
+ }
2019
+ /** Stage a create proposal without queueing it. */
2020
+ stage(submission) {
2021
+ return {
2022
+ id: digest([
2023
+ "staged",
2024
+ submission.skillKey,
2025
+ submission.threeFieldHash
2026
+ ]),
2027
+ submission
2028
+ };
2029
+ }
2030
+ /** Approve a staged create; only this call enqueues. */
2031
+ async approve(staged) {
2032
+ return this.submit(staged.submission);
2033
+ }
2034
+ /**
2035
+ * Submit a create proposal after SkillLibrary.create succeeded.
2036
+ * @param submission - skill identity, three-field hash, permission, proposal.
2037
+ * @returns queue/pending/unclassified; never rolls back create.
2038
+ */
2039
+ async submit(submission) {
2040
+ if (!this.options.createClassificationEnabled) return {
2041
+ kind: "unclassified",
2042
+ reason: "feature-off"
2043
+ };
2044
+ if (!submission.modelInvocable) return {
2045
+ kind: "unclassified",
2046
+ reason: "not-model-invocable"
2047
+ };
2048
+ if (submission.proposal === void 0) {
2049
+ if (this.options.createCardsAvailable === true) {
2050
+ const reason = "schema-invalid";
2051
+ return {
2052
+ kind: "pending",
2053
+ entry: {
2054
+ key: digest([
2055
+ "missing-proposal",
2056
+ submission.skillKey,
2057
+ submission.threeFieldHash
2058
+ ]),
2059
+ submission,
2060
+ proposalDigest: proposalDigest(void 0),
2061
+ createdAt: this.now(),
2062
+ status: "pending",
2063
+ pendingReason: reason
2064
+ },
2065
+ reason
2066
+ };
2067
+ }
2068
+ return {
2069
+ kind: "unclassified",
2070
+ reason: "no-proposal"
2071
+ };
2072
+ }
2073
+ const invalid = validateProposal(submission.proposal);
2074
+ const pdigest = proposalDigest(submission.proposal);
2075
+ const key = assignmentKey({
2076
+ skillKey: submission.skillKey,
2077
+ threeFieldHash: submission.threeFieldHash,
2078
+ proposalDigest: pdigest,
2079
+ mapVersion: submission.proposal.mapVersion
2080
+ });
2081
+ let queued;
2082
+ try {
2083
+ queued = await this.queue.enqueue({
2084
+ key,
2085
+ submission,
2086
+ proposalDigest: pdigest,
2087
+ createdAt: submission.now ?? this.now()
2088
+ });
2089
+ } catch {
2090
+ return {
2091
+ kind: "pending",
2092
+ entry: {
2093
+ key,
2094
+ submission,
2095
+ proposalDigest: pdigest,
2096
+ createdAt: submission.now ?? this.now(),
2097
+ status: "pending",
2098
+ pendingReason: "queue-unavailable"
2099
+ },
2100
+ reason: "queue-unavailable"
2101
+ };
2102
+ }
2103
+ if (invalid !== void 0) {
2104
+ await this.queue.update(key, "pending", invalid);
2105
+ return {
2106
+ kind: "pending",
2107
+ entry: {
2108
+ ...queued.entry,
2109
+ status: "pending",
2110
+ pendingReason: invalid
2111
+ },
2112
+ reason: invalid
2113
+ };
2114
+ }
2115
+ return {
2116
+ kind: "queued",
2117
+ entry: queued.entry,
2118
+ duplicate: queued.duplicate
2119
+ };
2120
+ }
2121
+ /** Process one queued entry with the current map; CAS failure becomes pending. */
2122
+ async process(entry) {
2123
+ const proposal = entry.submission.proposal;
2124
+ if (proposal === void 0) return {
2125
+ kind: "pending",
2126
+ entry,
2127
+ reason: "schema-invalid"
2128
+ };
2129
+ const current = await this.store.read();
2130
+ if (proposal.mapVersion !== current.mapVersion) {
2131
+ await this.queue.update(entry.key, "pending", "map-version-stale");
2132
+ return {
2133
+ kind: "pending",
2134
+ entry: {
2135
+ ...entry,
2136
+ status: "pending",
2137
+ pendingReason: "map-version-stale"
2138
+ },
2139
+ reason: "map-version-stale"
2140
+ };
2141
+ }
2142
+ try {
2143
+ const next = buildNext(current, entry, proposal, this.options.provisionalThreshold ?? 3);
2144
+ const committed = await this.store.commit(current.mapVersion, {
2145
+ mapVersion: next.mapVersion,
2146
+ domains: next.domains,
2147
+ edges: next.edges
2148
+ });
2149
+ const status = next.provisional ? "provisional" : "applied";
2150
+ await this.queue.update(entry.key, status);
2151
+ return {
2152
+ kind: status,
2153
+ entry: {
2154
+ ...entry,
2155
+ status
2156
+ },
2157
+ mapVersion: committed.mapVersion
2158
+ };
2159
+ } catch (error) {
2160
+ if (error instanceof DomainMapVersionConflict) {
2161
+ await this.queue.update(entry.key, "pending", "map-version-stale");
2162
+ return {
2163
+ kind: "pending",
2164
+ entry: {
2165
+ ...entry,
2166
+ status: "pending",
2167
+ pendingReason: "map-version-stale"
2168
+ },
2169
+ reason: "map-version-stale"
2170
+ };
2171
+ }
2172
+ const reason = error instanceof Error && (error.message === "unknown-domain" || error.message === "new-domain-conflict" || error.message === "schema-invalid") ? error.message : "queue-unavailable";
2173
+ await this.queue.update(entry.key, "pending", reason);
2174
+ return {
2175
+ kind: "pending",
2176
+ entry: {
2177
+ ...entry,
2178
+ status: "pending",
2179
+ pendingReason: reason
2180
+ },
2181
+ reason
2182
+ };
2183
+ }
2184
+ }
2185
+ /** Manually confirm one provisional domain; no semantic reclassification. */
2186
+ async confirm(domainId) {
2187
+ const current = await this.store.read();
2188
+ const domain = current.domains.find((node) => node.id === domainId);
2189
+ if (domain === void 0 || domain.status !== "provisional") return false;
2190
+ const domains = current.domains.map((node) => node.id === domainId ? {
2191
+ ...node,
2192
+ status: "active",
2193
+ updatedAt: this.now()
2194
+ } : node);
2195
+ await this.store.commit(current.mapVersion, {
2196
+ mapVersion: nextMapVersion$1(current.mapVersion),
2197
+ domains,
2198
+ edges: current.edges
2199
+ });
2200
+ return true;
2201
+ }
2202
+ now() {
2203
+ return this.options.now?.() ?? Date.now();
2204
+ }
2205
+ };
2206
+ function nextMapVersion$1(version) {
2207
+ const numeric = Number(version);
2208
+ return Number.isSafeInteger(numeric) && numeric >= 0 ? String(numeric + 1) : version + "+1";
2209
+ }
2210
+ function buildNext(current, entry, proposal, threshold) {
2211
+ const edge = {
2212
+ skillKey: entry.submission.skillKey,
2213
+ domainId: "",
2214
+ role: "primary",
2215
+ confidence: proposal.confidence,
2216
+ source: "create",
2217
+ skillHash: entry.submission.threeFieldHash,
2218
+ mapVersion: nextMapVersion$1(current.mapVersion),
2219
+ status: "active",
2220
+ assignedAt: entry.createdAt
2221
+ };
2222
+ const secondaries = (proposal.secondaryDomainIds ?? []).map((id) => {
2223
+ const secondary = current.domains.find((node) => node.id === id);
2224
+ if (secondary === void 0) throw new Error("unknown-domain");
2225
+ return {
2226
+ ...edge,
2227
+ domainId: secondary.id,
2228
+ role: "secondary"
2229
+ };
2230
+ });
2231
+ if (proposal.mode === "existing") {
2232
+ const domain = current.domains.find((node) => node.id === proposal.id);
2233
+ if (domain === void 0) throw new Error("unknown-domain");
2234
+ if (!coveredByDomain(entry.submission.whenToUse ?? "", domain)) throw new Error("new-domain-conflict");
2235
+ const members = new Set(current.edges.filter((item) => item.domainId === domain.id).map((item) => item.skillKey));
2236
+ members.add(entry.submission.skillKey);
2237
+ const promote = domain.status === "provisional" && members.size >= threshold;
2238
+ const domains = promote ? current.domains.map((node) => node.id === domain.id ? {
2239
+ ...node,
2240
+ status: "active",
2241
+ updatedAt: entry.createdAt
2242
+ } : node) : current.domains;
2243
+ return {
2244
+ mapVersion: nextMapVersion$1(current.mapVersion),
2245
+ domains,
2246
+ edges: [
2247
+ ...current.edges,
2248
+ {
2249
+ ...edge,
2250
+ domainId: domain.id
2251
+ },
2252
+ ...secondaries.filter((secondary) => secondary.domainId !== domain.id)
2253
+ ],
2254
+ provisional: domain.status === "provisional" && !promote
2255
+ };
2256
+ }
2257
+ const proposed = proposal.proposed;
2258
+ if (proposed === void 0) throw new Error("schema-invalid");
2259
+ const normalizedTitle = proposed.title.trim().toLowerCase();
2260
+ const existing = current.domains.find((node) => node.name.trim().toLowerCase() === normalizedTitle);
2261
+ if (existing !== void 0 && existing.status !== "provisional") throw new Error("new-domain-conflict");
2262
+ const id = existing?.id ?? mintDomainId(proposed.title);
2263
+ if (current.domains.some((node) => node.id !== id && node.triggers.some((trigger) => proposed.triggers.includes(trigger)))) throw new Error("new-domain-conflict");
2264
+ const domain = existing ?? {
2265
+ id,
2266
+ name: proposed.title,
2267
+ purpose: proposed.purpose,
2268
+ description: proposed.purpose,
2269
+ inScope: proposed.inScope,
2270
+ outOfScope: proposed.outOfScope,
2271
+ triggers: proposed.triggers,
2272
+ parentIds: [],
2273
+ status: "provisional",
2274
+ version: 1,
2275
+ createdAt: entry.createdAt,
2276
+ updatedAt: entry.createdAt,
2277
+ descriptionHash: "pending",
2278
+ descriptionVersion: 1,
2279
+ descriptionStatus: "fresh",
2280
+ provenance: { source: "create" },
2281
+ quality: {}
2282
+ };
2283
+ const members = new Set(current.edges.filter((item) => item.domainId === id).map((item) => item.skillKey));
2284
+ members.add(entry.submission.skillKey);
2285
+ const promote = members.size >= threshold;
2286
+ const finalDomain = promote ? {
2287
+ ...domain,
2288
+ status: "active",
2289
+ updatedAt: entry.createdAt
2290
+ } : domain;
2291
+ return {
2292
+ mapVersion: nextMapVersion$1(current.mapVersion),
2293
+ domains: existing === void 0 ? [...current.domains, finalDomain] : current.domains.map((node) => node.id === id ? finalDomain : node),
2294
+ edges: [
2295
+ ...current.edges,
2296
+ {
2297
+ ...edge,
2298
+ domainId: id
2299
+ },
2300
+ ...secondaries.filter((secondary) => secondary.domainId !== id)
2301
+ ],
2302
+ provisional: !promote
2303
+ };
2304
+ }
2305
+ //#endregion
2306
+ //#region lib/types/assignment-queue.js
2307
+ const COLLECTION = "assignment-queue";
2308
+ /** Durable assignment queue; process-local cache over one JSON file. */
2309
+ var JsonAssignmentQueue = class {
2310
+ entries = /* @__PURE__ */ new Map();
2311
+ loadPromise;
2312
+ io;
2313
+ path;
2314
+ store;
2315
+ constructor(io, path, store) {
2316
+ this.io = io;
2317
+ this.path = path;
2318
+ this.store = store;
2319
+ }
2320
+ load() {
2321
+ this.loadPromise ??= (async () => {
2322
+ if (this.store !== void 0) {
2323
+ for (const entry of await this.store.list(COLLECTION)) this.entries.set(entry.key, entry);
2324
+ if (this.entries.size > 0) return;
2325
+ }
2326
+ const content = await this.io.readText(this.path);
2327
+ if (content === null) return;
2328
+ try {
2329
+ const parsed = JSON.parse(content);
2330
+ if (parsed.schemaVersion !== 1 || !Array.isArray(parsed.entries)) return;
2331
+ for (const entry of parsed.entries) {
2332
+ this.entries.set(entry.key, entry);
2333
+ if (this.store !== void 0) await this.store.transact(COLLECTION, entry.key, async () => entry);
2334
+ }
2335
+ } catch {}
2336
+ })();
2337
+ return this.loadPromise;
2338
+ }
2339
+ async persist() {
2340
+ if (this.store === void 0) await this.io.writeText(this.path, JSON.stringify({
2341
+ schemaVersion: 1,
2342
+ entries: [...this.entries.values()]
2343
+ }));
2344
+ }
2345
+ async enqueue(input) {
2346
+ await this.load();
2347
+ const existing = this.entries.get(input.key);
2348
+ if (existing !== void 0) return {
2349
+ entry: existing,
2350
+ duplicate: true
2351
+ };
2352
+ const entry = {
2353
+ ...input,
2354
+ status: "queued"
2355
+ };
2356
+ if (this.store !== void 0) {
2357
+ let duplicate = false;
2358
+ let stored = entry;
2359
+ await this.store.transact(COLLECTION, input.key, async (current) => {
2360
+ if (current !== void 0) {
2361
+ duplicate = true;
2362
+ stored = current;
2363
+ return current;
2364
+ }
2365
+ return entry;
2366
+ });
2367
+ this.entries.set(input.key, stored);
2368
+ return {
2369
+ entry: stored,
2370
+ duplicate
2371
+ };
2372
+ }
2373
+ this.entries.set(input.key, entry);
2374
+ await this.persist();
2375
+ return {
2376
+ entry,
2377
+ duplicate: false
2378
+ };
2379
+ }
2380
+ async update(key, status, pendingReason) {
2381
+ await this.load();
2382
+ const entry = this.entries.get(key);
2383
+ if (entry === void 0) return;
2384
+ const nextEntry = {
2385
+ ...entry,
2386
+ status,
2387
+ pendingReason
2388
+ };
2389
+ if (this.store !== void 0) await this.store.transact(COLLECTION, key, async (current) => current === void 0 ? nextEntry : {
2390
+ ...current,
2391
+ status,
2392
+ pendingReason
2393
+ });
2394
+ this.entries.set(key, nextEntry);
2395
+ await this.persist();
2396
+ }
2397
+ list() {
2398
+ return [...this.entries.values()];
2399
+ }
2400
+ };
2401
+ //#endregion
2402
+ //#region lib/types/worker.js
2403
+ /** Drivable worker; the composition may call drain on a schedule. */
2404
+ var AssignmentWorker = class {
2405
+ service;
2406
+ queue;
2407
+ constructor(service, queue) {
2408
+ this.service = service;
2409
+ this.queue = queue;
2410
+ }
2411
+ /** Process queued entries once; returns processed count. */
2412
+ async drain(limit = 10) {
2413
+ const entries = this.queue.list().filter((entry) => entry.status === "queued").slice(0, Math.max(0, limit));
2414
+ for (const entry of entries) await this.service.process(entry);
2415
+ return entries.length;
2416
+ }
2417
+ };
2418
+ //#endregion
2419
+ //#region lib/types/assignment-sidecar.js
2420
+ function submitCreate(service, args, options) {
2421
+ if (typeof args.name !== "string" || args.name.length === 0) return void 0;
2422
+ const parsed = parseFrontmatter(typeof args.content === "string" ? args.content : "");
2423
+ const description = parsed?.frontmatter.description ?? "";
2424
+ const rawWhenToUse = parsed?.frontmatter.whenToUse;
2425
+ const whenToUse = typeof rawWhenToUse === "string" ? rawWhenToUse : void 0;
2426
+ const domain = args.domain;
2427
+ const proposal = typeof domain === "object" && domain !== null ? domain : void 0;
2428
+ return service.submit({
2429
+ skillKey: skillKeyOf({
2430
+ provider: options.provider ?? "dsh-evolution",
2431
+ source: options.source ?? "user-dsh",
2432
+ name: args.name
2433
+ }),
2434
+ threeFieldHash: threeFieldHashOf({
2435
+ name: args.name,
2436
+ description,
2437
+ ...whenToUse === void 0 ? {} : { whenToUse }
2438
+ }),
2439
+ modelInvocable: parsed?.frontmatter["disable-model-invocation"] !== true,
2440
+ ...whenToUse === void 0 ? {} : { whenToUse },
2441
+ ...proposal === void 0 ? {} : { proposal }
2442
+ }).then(() => options.onSubmitted?.());
2443
+ }
2444
+ /**
2445
+ * Observe successful create calls and submit their optional domain proposal.
2446
+ * Direct creates submit immediately; staged creates submit only after approval.
2447
+ * @param ctx - plugin context with tools and approval events.
2448
+ * @param service - the one assignment service.
2449
+ * @param options - provider/source identity for skillKey.
2450
+ * @returns disposer for both listeners.
2451
+ */
2452
+ function installAssignmentSidecar(ctx, service, options = {}) {
2453
+ const stopResult = ctx.on("tools/result", (exec, result) => {
2454
+ if (result.isError || exec.name !== "skill_manage" || exec.agent === void 0) return;
2455
+ if (result.value?.pending_id !== void 0) return;
2456
+ const args = exec.arguments;
2457
+ if (typeof args !== "object" || args === null) return;
2458
+ const record = args;
2459
+ if (record.action !== "create") return;
2460
+ submitCreate(service, record, options)?.catch(() => {});
2461
+ });
2462
+ const stopApproval = ctx.on("evolution/approval-decided", ({ record, action, ok }) => {
2463
+ if (!ok || action !== "approve" || record.kind !== "skill" || typeof record.args !== "object" || record.args === null) return;
2464
+ const raw = record.args;
2465
+ const operation = typeof raw.operation === "object" && raw.operation !== null ? raw.operation : raw;
2466
+ if (operation.action !== "create") return;
2467
+ submitCreate(service, operation, options)?.catch(() => {});
2468
+ });
2469
+ return () => {
2470
+ stopResult();
2471
+ stopApproval();
2472
+ };
2473
+ }
2474
+ //#endregion
2475
+ //#region lib/types/awareness.js
2476
+ /** Static 1–2 sentence awareness; no domain list/tree. */
2477
+ const DISCOVERY_AWARENESS = "When skill coverage for a task is uncertain, the domain cards and skill_search can reduce it; search is optional, not a required step.";
2478
+ /**
2479
+ * Register the static awareness section; absent systemPrompt is a no-op.
2480
+ * platform-declaration anchor: the optional systemPrompt service declared in core PLATFORM_SERVICE_PROBES.
2481
+ */
2482
+ function installDiscoveryAwareness(ctx) {
2483
+ const prompt = ctx.get("systemPrompt");
2484
+ if (prompt === void 0) return () => {};
2485
+ return prompt.section({
2486
+ name: "evolution-skill-discovery-awareness",
2487
+ order: 40,
2488
+ text: DISCOVERY_AWARENESS,
2489
+ interpolate: false
2490
+ });
2491
+ }
2492
+ //#endregion
2493
+ //#region lib/types/domain-description.js
2494
+ /** @returns hash of child ids/description versions, never member skill names. */
2495
+ function descriptionHashOf(domain, children) {
2496
+ const childRefs = children.map((child) => JSON.stringify([child.id, child.descriptionVersion])).sort();
2497
+ return createHash("sha256").update(JSON.stringify([
2498
+ domain.id,
2499
+ domain.descriptionVersion,
2500
+ childRefs
2501
+ ])).digest("hex");
2502
+ }
2503
+ /**
2504
+ * Render a domain description from definition fields only. It never appends
2505
+ * evidence/member skill names and is capped to 600 characters.
2506
+ * @param domain - domain definition.
2507
+ * @returns description text.
2508
+ */
2509
+ function renderDomainDescription(domain) {
2510
+ return [
2511
+ domain.name + ":" + domain.purpose,
2512
+ "适用:" + domain.inScope.join(";"),
2513
+ "不适用:" + domain.outOfScope.join(";"),
2514
+ "触发:" + domain.triggers.join(";")
2515
+ ].join("\n").slice(0, 600);
2516
+ }
2517
+ //#endregion
6
2518
  //#region lib/types/description-refresh.js
7
2519
  function nextMapVersion(version) {
8
2520
  const numeric = Number(version);
@@ -228,7 +2740,7 @@ function apply(ctx, config) {
228
2740
  ...config.descriptionStaleAfterHours === void 0 ? {} : { descriptionStaleAfterHours: config.descriptionStaleAfterHours },
229
2741
  isFrozen: async (agent) => snapshots.isFrozen(String(agent.session.id), config.featureEpoch)
230
2742
  });
231
- const queue = new JsonAssignmentQueue(io, config.assignmentQueuePath ?? join(root, "evolution", "assignment-queue.json"));
2743
+ const queue = new JsonAssignmentQueue(io, config.assignmentQueuePath ?? join(root, "evolution", "assignment-queue.json"), ctx.get("evolutionStateStore"));
232
2744
  const assignment = new DomainAssignmentService(queue, map, {
233
2745
  createClassificationEnabled: config.createClassificationEnabled ?? false,
234
2746
  ...config.provisionalThreshold === void 0 ? {} : { provisionalThreshold: config.provisionalThreshold },