@prodero/curator 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/ARCHITECTURE.md +298 -0
  2. package/CHANGELOG.md +16 -0
  3. package/CONTRIBUTING.md +9 -0
  4. package/LICENSE +21 -0
  5. package/README.md +94 -2
  6. package/SECURITY.md +7 -0
  7. package/bin/prodero +7 -0
  8. package/dist/cli.js +228 -0
  9. package/dist/context-instructions.js +2 -0
  10. package/dist/contracts.js +50 -0
  11. package/dist/conversation-presentation.js +262 -0
  12. package/dist/core/agent-instructions.js +39 -0
  13. package/dist/core/applicability-label.js +11 -0
  14. package/dist/core/applicability.js +143 -0
  15. package/dist/core/bootstrap.js +129 -0
  16. package/dist/core/consolidation.js +315 -0
  17. package/dist/core/constants.js +21 -0
  18. package/dist/core/context-allocation.js +54 -0
  19. package/dist/core/context-snapshot.js +12 -0
  20. package/dist/core/context.js +489 -0
  21. package/dist/core/curation-clauses.js +68 -0
  22. package/dist/core/curation-evidence.js +98 -0
  23. package/dist/core/curation-graphical.js +105 -0
  24. package/dist/core/curation-review-presentation.js +4 -0
  25. package/dist/core/curation-review-state.js +62 -0
  26. package/dist/core/curation-review.js +573 -0
  27. package/dist/core/curation-revision.js +106 -0
  28. package/dist/core/curation-semantic-contract.js +239 -0
  29. package/dist/core/curation-semantic-jobs.js +280 -0
  30. package/dist/core/diagnostics.js +17 -0
  31. package/dist/core/format.js +114 -0
  32. package/dist/core/ids.js +6 -0
  33. package/dist/core/imports.js +169 -0
  34. package/dist/core/index.js +93 -0
  35. package/dist/core/instruction-review.js +86 -0
  36. package/dist/core/integrity-files.js +174 -0
  37. package/dist/core/intelligence.js +95 -0
  38. package/dist/core/knowledge-analysis.js +46 -0
  39. package/dist/core/knowledge-authority.js +143 -0
  40. package/dist/core/knowledge-curation.js +316 -0
  41. package/dist/core/knowledge-engine.js +475 -0
  42. package/dist/core/knowledge-graph.js +191 -0
  43. package/dist/core/knowledge-lifecycle.js +212 -0
  44. package/dist/core/knowledge-management.js +113 -0
  45. package/dist/core/knowledge-migration.js +84 -0
  46. package/dist/core/knowledge-policy.js +38 -0
  47. package/dist/core/knowledge-provenance.js +90 -0
  48. package/dist/core/knowledge-write-policy.js +85 -0
  49. package/dist/core/language.js +14 -0
  50. package/dist/core/matching.js +6 -0
  51. package/dist/core/memory-index.js +239 -0
  52. package/dist/core/memory-intelligence.js +288 -0
  53. package/dist/core/memory-link-repair.js +66 -0
  54. package/dist/core/memory-operations.js +122 -0
  55. package/dist/core/memory-pressure.js +212 -0
  56. package/dist/core/memory-sync-curator.js +62 -0
  57. package/dist/core/memory-sync.js +463 -0
  58. package/dist/core/memory-transformation-bundle.js +163 -0
  59. package/dist/core/memory-transformation-proposal.js +135 -0
  60. package/dist/core/memory-transformation.js +108 -0
  61. package/dist/core/memory.js +186 -0
  62. package/dist/core/migrations.js +569 -0
  63. package/dist/core/path-scope.js +27 -0
  64. package/dist/core/plugin-files.js +116 -0
  65. package/dist/core/project-root.js +39 -0
  66. package/dist/core/project.js +634 -0
  67. package/dist/core/promotion.js +279 -0
  68. package/dist/core/reconciliation.js +61 -0
  69. package/dist/core/secrets.js +67 -0
  70. package/dist/core/semantic-safety.js +33 -0
  71. package/dist/core/session-index.js +112 -0
  72. package/dist/core/session-review.js +150 -0
  73. package/dist/core/sessions.js +289 -0
  74. package/dist/core/settings.js +76 -0
  75. package/dist/core/skill-curation.js +239 -0
  76. package/dist/core/skill-polish.js +41 -0
  77. package/dist/core/skills.js +368 -0
  78. package/dist/core/storage.js +408 -0
  79. package/dist/core/sync-transaction.js +254 -0
  80. package/dist/core/telemetry.js +64 -0
  81. package/dist/core/templates.js +53 -0
  82. package/dist/core/types.js +2 -0
  83. package/dist/core/update.js +320 -0
  84. package/dist/curation-review-adapter.js +92 -0
  85. package/dist/diagnostic-boundary.js +54 -0
  86. package/dist/filesystem/permissions.js +39 -0
  87. package/dist/hosts/adapters.js +119 -0
  88. package/dist/hosts/files.js +133 -0
  89. package/dist/hosts/manager.js +150 -0
  90. package/dist/hosts/public-entry.js +47 -0
  91. package/dist/hosts/runtime-command.js +66 -0
  92. package/dist/hosts/runtime.js +139 -0
  93. package/dist/hosts/wizard.js +114 -0
  94. package/dist/knowledge-table.js +38 -0
  95. package/dist/mcp-development.js +69 -0
  96. package/dist/mcp-workspace.js +141 -0
  97. package/dist/memory-presentation.js +172 -0
  98. package/dist/plugins/api.js +2 -0
  99. package/dist/plugins/context.js +20 -0
  100. package/dist/plugins/mcp.js +72 -0
  101. package/dist/plugins/registry.js +106 -0
  102. package/dist/presentation-cards.js +32 -0
  103. package/dist/presentation-server.js +79 -0
  104. package/dist/presentation-trace.js +174 -0
  105. package/dist/presentation.js +412 -0
  106. package/dist/providers/mcp-sampling.js +45 -0
  107. package/dist/server.js +656 -0
  108. package/dist/settings-definition.js +22 -0
  109. package/dist/skill-instructions.js +2 -0
  110. package/dist-ui/index.html +214 -0
  111. package/docs/APPLICABILITY.md +151 -0
  112. package/docs/COMPATIBILITY.md +49 -0
  113. package/docs/CONTEXT-SELECTION-V2.md +61 -0
  114. package/docs/CURATION-APP-REVIEW.md +156 -0
  115. package/docs/CURATION-CLAUSE-REFINEMENT.md +45 -0
  116. package/docs/IMPORTER-CURATION.md +142 -0
  117. package/docs/KNOWLEDGE-AUTHORITY.md +165 -0
  118. package/docs/KNOWLEDGE-SCHEMA-V2.md +150 -0
  119. package/docs/MIGRATIONS.md +99 -0
  120. package/docs/PRIVACY.md +28 -0
  121. package/docs/RECOVERY.md +56 -0
  122. package/docs/RELEASING.md +40 -0
  123. package/docs/hosts.md +134 -0
  124. package/docs/plugins.md +83 -0
  125. package/docs/ui-themes.md +43 -0
  126. package/instructions/AGENTS.md +33 -0
  127. package/package.json +88 -4
  128. package/plugins/migrate_ai_studio/README.md +101 -0
  129. package/plugins/migrate_ai_studio/index.cjs +396 -0
  130. package/plugins/migrate_ai_studio/instructions/audit.md +25 -0
  131. package/plugins/migrate_ai_studio/instructions/classify.md +41 -0
  132. package/plugins/migrate_ai_studio/instructions/migrate.md +34 -0
  133. package/plugins/migrate_ai_studio/instructions/verify.md +20 -0
  134. package/plugins/migrate_ai_studio/plugin.json +11 -0
  135. package/plugins/migrate_ai_studio/schema.cjs +20 -0
  136. package/plugins/migrate_ai_studio/ui/dist/index.html +77 -0
  137. package/prodero.release.json +5 -0
@@ -0,0 +1,368 @@
1
+ import { writeFileExclusive } from "./storage.js";
2
+ import { assertNoSecrets } from "./secrets.js";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { z } from "zod";
6
+ import { frontmatter, parseFrontmatter } from "./format.js";
7
+ import { sortableId } from "./ids.js";
8
+ import { tokens } from "./matching.js";
9
+ import { exists, checkedDataPath, ensureDataDirectory, readConfig, sha256, writeFileAtomic } from "./storage.js";
10
+ import { withSyncLock, recoverSyncTransaction, safeProjectPath, commitSyncTransaction, fileHash } from "./sync-transaction.js";
11
+ import { estimateTokens } from "./telemetry.js";
12
+ export const skillInputSchema = z.object({
13
+ title: z.string().trim().min(1).max(200).regex(/^[^\r\n]+$/, "Use a single-line title"),
14
+ description: z.string().trim().min(1).max(1_000),
15
+ scope: z.string().trim().min(1).max(300).default("project"),
16
+ triggers: z.array(z.string().trim().min(1).max(300)).max(20).default([]),
17
+ relatedKnowledge: z.array(z.string().regex(/^[a-zA-Z0-9_-]+$/).max(200)).max(8).default([]),
18
+ references: z.array(z.string().trim().min(1).max(500)).max(30).default([]),
19
+ content: z.string().trim().min(1).max(20_000),
20
+ });
21
+ export function renderSkill(skill) {
22
+ assertNoSecrets(skill);
23
+ return frontmatter({
24
+ kind: "project-skill", version: 2, id: skill.id, title: skill.title, status: skill.status,
25
+ scope: skill.scope, description: skill.description, triggers: skill.triggers,
26
+ origin: skill.origin, created_at: skill.createdAt, updated_at: skill.updatedAt,
27
+ source_sessions: skill.sourceSessions, derived_from: skill.derivedFrom,
28
+ references: skill.references,
29
+ ...(skill.relatedKnowledge.length ? { related_knowledge: skill.relatedKnowledge } : {}),
30
+ ...(skill.promotedTo ? { review_status: "promoted", promoted_to: skill.promotedTo, candidate_title: skill.candidateTitle } : {}),
31
+ proposed_reason: skill.proposedReason, reviewed_at: skill.reviewedAt, review_reason: skill.reviewReason,
32
+ }, skillMarkdown(skill));
33
+ }
34
+ export function parseSkill(text, filename) {
35
+ assertNoSecrets(text);
36
+ const normalizedText = text.replace(/\r\n/g, "\n");
37
+ if (normalizedText.startsWith("---\n") && normalizedText.indexOf("\n---\n", 4) < 0)
38
+ throw new Error(`Invalid project skill frontmatter: ${filename}`);
39
+ const { metadata: m, body } = parseFrontmatter(normalizedText, filename);
40
+ const legacy = (m.kind === undefined || m.kind === "skill") && m.version === undefined && !filename.startsWith("skill_") && !(typeof m.id === "string" && m.id.startsWith("skill_"));
41
+ const title = typeof m.title === "string" ? m.title : body.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? path.basename(filename, ".md");
42
+ const strings = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
43
+ const nullable = (value) => typeof value === "string" ? value : null;
44
+ if (m.status !== undefined && !["active", "candidate", "dismissed", "superseded"].includes(String(m.status)))
45
+ throw new Error(`Invalid project skill status: ${filename}`);
46
+ if (!legacy && (m.kind !== "project-skill" || ![1, 2].includes(m.version) || !["active", "candidate", "dismissed", "superseded"].includes(String(m.status))))
47
+ throw new Error(`Invalid project skill: ${filename}`);
48
+ if (!legacy && typeof m.id !== "string")
49
+ throw new Error(`Invalid project skill ID: ${filename}`);
50
+ for (const field of ["created_at", "updated_at"])
51
+ if ((!legacy || m[field] !== undefined) && (typeof m[field] !== "string" || !Number.isFinite(Date.parse(m[field]))))
52
+ throw new Error(`Invalid project skill ${field}: ${filename}`);
53
+ for (const field of ["source_sessions", "derived_from"])
54
+ if ((!legacy || m[field] !== undefined) && (!Array.isArray(m[field]) || !m[field].every(v => typeof v === "string")))
55
+ throw new Error(`Invalid project skill ${field}: ${filename}`);
56
+ const id = typeof m.id === "string" ? m.id : path.basename(filename, ".md");
57
+ if (!/^[a-zA-Z0-9_-]+$/.test(id) || (!legacy && filename !== `${id}.md`))
58
+ throw new Error(`Invalid project skill ID: ${filename}`);
59
+ let procedureBody;
60
+ if (m.version === 2) {
61
+ const prefix = skillPreamble({ title, description: String(m.description ?? ""), scope: String(m.scope ?? "project"), triggers: strings(m.triggers) });
62
+ if (!body.startsWith(prefix))
63
+ throw new Error(`Invalid Project Skill sections: ${filename}`);
64
+ const references = strings(m.references);
65
+ const sources = strings(m.derived_from);
66
+ const related = relatedKnowledgeMarkdown(strings(m.related_knowledge));
67
+ const suffix = `${related}${references.length ? `\n\n## References\n\n${references.map(r => `- ${r}`).join("\n")}` : ""}${sources.length ? `\n\n## Based on\n\n${m.candidate_title ?? title}\n\n${sources.map(r => `- ${r}`).join("\n")}` : ""}`;
68
+ const remaining = body.slice(prefix.length).trimEnd();
69
+ if (suffix && !remaining.endsWith(suffix))
70
+ throw new Error(`Invalid Project Skill references: ${filename}`);
71
+ procedureBody = (suffix ? remaining.slice(0, -suffix.length) : remaining).trim();
72
+ }
73
+ const content = procedureBody ?? (legacy ? body.trim() : body.replace(/^# [^\n]*\n+/, "").trim());
74
+ const parsedInput = skillInputSchema.safeParse({ title, content, description: m.description ?? (legacy ? title : undefined), scope: m.scope ?? "project", triggers: m.triggers ?? [], references: m.references ?? [], relatedKnowledge: m.related_knowledge ?? [] });
75
+ if (!parsedInput.success)
76
+ throw new Error(`Invalid project skill: ${filename} (${parsedInput.error.issues.map(i => i.path.join(".")).join(", ")}).`);
77
+ if (!legacy && !["explicit-user", "session-inference", "legacy"].includes(String(m.origin)))
78
+ throw new Error(`Invalid project skill origin: ${filename}`);
79
+ const input = parsedInput.data;
80
+ return {
81
+ ...input, id, status: (m.status ?? "active"),
82
+ origin: legacy ? "legacy" : m.origin,
83
+ createdAt: nullable(m.created_at) ?? new Date(0).toISOString(), updatedAt: nullable(m.updated_at) ?? new Date(0).toISOString(),
84
+ sourceSessions: strings(m.source_sessions), derivedFrom: strings(m.derived_from),
85
+ proposedReason: nullable(m.proposed_reason), reviewedAt: nullable(m.reviewed_at), reviewReason: nullable(m.review_reason),
86
+ ...(typeof m.promoted_to === "string" ? { promotedTo: m.promoted_to, candidateTitle: String(m.candidate_title ?? title) } : {}),
87
+ path: `skills/${filename}`,
88
+ };
89
+ }
90
+ export async function listProjectSkills(root) {
91
+ const skills = [];
92
+ for (const relative of ["skills", "local/skills"]) {
93
+ const directory = await checkedDataPath(root, relative);
94
+ if (!(await exists(directory)))
95
+ continue;
96
+ const entries = (await fs.readdir(directory, { withFileTypes: true })).filter(e => (e.isFile() || e.isSymbolicLink()) && e.name.endsWith(".md") && !e.name.startsWith(".")).sort((a, b) => a.name.localeCompare(b.name));
97
+ for (const entry of entries) {
98
+ const skill = parseSkill(await fs.readFile(await checkedDataPath(root, relative, entry.name), "utf8"), entry.name);
99
+ skill.path = `${relative}/${entry.name}`;
100
+ // The local receipt preserves observation replay identity after reviewed publication.
101
+ if (relative === "local/skills" && skill.status === "superseded" && skill.promotedTo === skill.id && skills.some(s => s.id === skill.id))
102
+ continue;
103
+ skills.push(skill);
104
+ }
105
+ }
106
+ if (new Set(skills.map((skill) => skill.id)).size !== skills.length)
107
+ throw new Error("Duplicate project skill IDs; resolve the Markdown files before continuing.");
108
+ return skills.sort((a, b) => a.title.localeCompare(b.title) || a.id.localeCompare(b.id));
109
+ }
110
+ async function requireProject(root) {
111
+ if (!(await readConfig(root)))
112
+ throw new Error("Prodero is not initialized for this project.");
113
+ }
114
+ export async function createProjectSkill(root, input, sourceSession) {
115
+ assertNoSecrets([input, sourceSession]);
116
+ return withSyncLock(root, () => createProjectSkillLocked(root, input, sourceSession), { waitMs: 5_000 });
117
+ }
118
+ async function createProjectSkillLocked(root, input, sourceSession) {
119
+ await requireProject(root);
120
+ const now = new Date();
121
+ const id = sortableId("skill", now);
122
+ const skill = {
123
+ ...skillInputSchema.parse(input), id, status: "active", origin: "explicit-user",
124
+ createdAt: now.toISOString(), updatedAt: now.toISOString(),
125
+ sourceSessions: sourceSession ? [sourceSession] : [], derivedFrom: [], proposedReason: null,
126
+ reviewedAt: null, reviewReason: null, path: `skills/${id}.md`,
127
+ };
128
+ await writeFileExclusive(await checkedDataPath(root, skill.path), renderSkill(skill));
129
+ return skill;
130
+ }
131
+ /** Stable observation identity makes replay safe even after promotion or dismissal. */
132
+ export async function createSkillCandidate(root, input, provenance, now = new Date(), writes) {
133
+ assertNoSecrets([input, provenance]);
134
+ if (writes)
135
+ return createSkillCandidateLocked(root, input, provenance, now, writes);
136
+ return withSyncLock(root, () => createSkillCandidateLocked(root, input, provenance, now), { waitMs: 5_000 });
137
+ }
138
+ async function createSkillCandidateLocked(root, input, provenance, now, writes) {
139
+ await requireProject(root);
140
+ const id = `skill_${sha256(provenance.derivedFrom).slice(0, 32)}`;
141
+ const filename = `${id}.md`;
142
+ await ensureDataDirectory(root, "local/skills");
143
+ const target = await checkedDataPath(root, "local/skills", filename);
144
+ const published = (await listProjectSkills(root)).find(s => s.id === id);
145
+ if (published) {
146
+ if (!published.derivedFrom.includes(provenance.derivedFrom))
147
+ throw new Error("Project skill provenance collision");
148
+ return { skill: published, created: false };
149
+ }
150
+ const skill = {
151
+ ...skillInputSchema.parse(input), id, status: "candidate", origin: "session-inference",
152
+ createdAt: now.toISOString(), updatedAt: now.toISOString(),
153
+ sourceSessions: [provenance.sessionId], derivedFrom: [provenance.derivedFrom],
154
+ proposedReason: provenance.proposedReason ?? null, reviewedAt: null, reviewReason: null, path: `local/skills/${filename}`,
155
+ };
156
+ if (writes) {
157
+ const staged = writes.get(skill.path);
158
+ if (staged || await exists(target)) {
159
+ const existing = parseSkill(staged?.after ?? await fs.readFile(target, "utf8"), filename);
160
+ if (!existing.derivedFrom.includes(provenance.derivedFrom))
161
+ throw new Error(`Project skill provenance collision: ${id}`);
162
+ return { skill: existing, created: false };
163
+ }
164
+ writes.set(skill.path, { path: skill.path, before: null, after: renderSkill(skill) });
165
+ return { skill, created: true };
166
+ }
167
+ try {
168
+ await writeFileExclusive(target, renderSkill(skill));
169
+ return { skill, created: true };
170
+ }
171
+ catch (error) {
172
+ if (error.code !== "EEXIST")
173
+ throw error;
174
+ const existing = parseSkill(await fs.readFile(target, "utf8"), filename);
175
+ if (!existing.derivedFrom.includes(provenance.derivedFrom))
176
+ throw new Error(`Project skill provenance collision: ${id}`);
177
+ return { skill: existing, created: false };
178
+ }
179
+ }
180
+ export async function getProjectSkill(root, id) {
181
+ if (!/^[a-zA-Z0-9_-]+$/.test(id))
182
+ throw new Error("Invalid project skill ID.");
183
+ const skill = (await listProjectSkills(root)).find((item) => item.id === id);
184
+ if (!skill)
185
+ throw new Error(`Project skill ${id} was not found.`);
186
+ return skill;
187
+ }
188
+ export async function reviewProjectSkill(root, id, action, reason, approval) {
189
+ assertNoSecrets([reason, approval]);
190
+ await requireProject(root);
191
+ return withSyncLock(root, async () => {
192
+ await recoverSyncTransaction(root);
193
+ const skill = await getProjectSkill(root, id);
194
+ if (action === "dismiss" && !skill.path.startsWith("local/"))
195
+ throw new Error("Legacy Versioned skill proposals require explicit repository maintenance before local dismissal. Move the proposal into .prodero/local/skills and review the Git diff.");
196
+ const targetStatus = action === "promote" ? "active" : "dismissed";
197
+ if (skill.status === targetStatus && action === "dismiss")
198
+ return skill;
199
+ const activeRevision = skill.status === "active" && approval?.preview.target?.id === id && approval.preview.disposition?.action === "update";
200
+ if (skill.status !== "candidate" && !activeRevision)
201
+ throw new Error(`Only candidate skills can be ${action === "promote" ? "promoted" : "dismissed"}.`);
202
+ if (activeRevision && action === "dismiss")
203
+ throw new Error("Cancel an active skill revision without dismissing the active skill.");
204
+ if (action === "promote") {
205
+ if (!approval?.approved)
206
+ throw new Error("Workflow approval of a Project Skill preview is required.");
207
+ const { preview } = approval;
208
+ const { validateSkillDraft } = await import("./skill-curation.js");
209
+ const draft = await validateSkillDraft(root, preview.draft);
210
+ if (preview.candidateId !== id || preview.candidateRevision !== candidateRevision(skill) || preview.previewId !== previewIdentity(skill, draft, preview.target, preview.disposition))
211
+ throw new Error("The candidate or preview changed. Open a fresh preview before approving.");
212
+ if (preview.disposition?.action === "covered")
213
+ throw new Error("Candidate already covered. Review the existing skill or dismiss the candidate.");
214
+ if (preview.target) {
215
+ const target = await getProjectSkill(root, preview.target.id);
216
+ if (target.status !== "active" || candidateRevision(target) !== preview.target.revision)
217
+ throw new Error("The existing skill changed. Open a fresh preview.");
218
+ if (target.id !== id) {
219
+ const beforeCandidate = await fileHash(await safeProjectPath(root, skill.path));
220
+ const beforeTarget = await fileHash(await safeProjectPath(root, target.path));
221
+ Object.assign(target, draft);
222
+ target.sourceSessions = [...new Set([...target.sourceSessions, ...skill.sourceSessions])];
223
+ target.derivedFrom = [...new Set([...target.derivedFrom, ...skill.derivedFrom])];
224
+ target.updatedAt = target.reviewedAt = new Date().toISOString();
225
+ target.reviewReason = reason?.trim() || null;
226
+ skill.status = "superseded";
227
+ skill.promotedTo = target.id;
228
+ skill.candidateTitle = skill.title;
229
+ skill.updatedAt = skill.reviewedAt = target.updatedAt;
230
+ skill.reviewReason = reason?.trim() || null;
231
+ const { discoverSessions } = await import("./session-index.js");
232
+ const checkpointId = sortableId("chk");
233
+ const checkpoint = { id: checkpointId, createdAt: target.updatedAt, previousCheckpoint: (await discoverSessions(root)).latestCheckpointId, sessions: [], consolidation: "not-performed", createdMemory: [], updatedMemory: [], supersededMemory: [], duplicateMemory: [], conflicts: [], summary: { sessions: 0, candidates: 0, created: 0, consolidated: 0, duplicates: 0, superseded: 0, conflicts: 0 }, note: `Human-approved skill update: ${id} → ${target.id}` };
234
+ await commitSyncTransaction(root, [{ path: target.path, before: beforeTarget, after: renderSkill(target) }, { path: skill.path, before: beforeCandidate, after: renderSkill(skill) }, { path: `checkpoints/${checkpointId}.json`, before: null, after: JSON.stringify(checkpoint, null, 2) + "\n" }]);
235
+ return target;
236
+ }
237
+ }
238
+ skill.promotedTo = id;
239
+ skill.candidateTitle ??= skill.title;
240
+ Object.assign(skill, draft);
241
+ }
242
+ skill.status = targetStatus;
243
+ skill.updatedAt = skill.reviewedAt = new Date().toISOString();
244
+ skill.reviewReason = reason?.trim() || null;
245
+ if (action === "promote" && skill.path.startsWith("local/")) {
246
+ const before = await fileHash(await safeProjectPath(root, skill.path));
247
+ const receipt = { ...skill, status: "superseded", promotedTo: skill.id };
248
+ const localPath = skill.path;
249
+ skill.path = `skills/${skill.id}.md`;
250
+ const { discoverSessions } = await import("./session-index.js");
251
+ const checkpointId = sortableId("chk");
252
+ const checkpoint = { id: checkpointId, createdAt: skill.updatedAt, previousCheckpoint: (await discoverSessions(root)).latestCheckpointId, sessions: [], consolidation: "not-performed", createdMemory: [], updatedMemory: [], supersededMemory: [], duplicateMemory: [], conflicts: [], summary: { sessions: 0, candidates: 0, created: 0, consolidated: 0, duplicates: 0, superseded: 0, conflicts: 0 }, note: `Human-approved skill publication: ${id}` };
253
+ await commitSyncTransaction(root, [{ path: localPath, before, after: renderSkill(receipt) }, { path: skill.path, before: null, after: renderSkill(skill) }, { path: `checkpoints/${checkpointId}.json`, before: null, after: JSON.stringify(checkpoint, null, 2) + "\n" }]);
254
+ }
255
+ else
256
+ await writeFileAtomic(await checkedDataPath(root, skill.path), renderSkill(skill));
257
+ return skill;
258
+ });
259
+ }
260
+ export function skillCounts(skills) {
261
+ return { active: skills.filter((s) => s.status === "active").length, candidate: skills.filter((s) => s.status === "candidate").length, dismissed: skills.filter((s) => s.status === "dismissed").length };
262
+ }
263
+ /** Context text includes every instructional field and is charged independently of knowledge. */
264
+ export function skillContext(skill) {
265
+ const content = skill.origin === "legacy" ? skill.content : skillMarkdown(skill);
266
+ return { id: skill.id, path: skill.path, title: skill.title, content, description: skill.description, triggers: skill.triggers, category: "skill", scope: skill.scope, estimatedTokens: estimateTokens(content) };
267
+ }
268
+ export function scoreSkill(query, skill) {
269
+ // Include path components as well as complete paths for module-scoped queries.
270
+ const tokenize = (value) => tokens(`${value} ${value.replace(/[._/-]/g, " ")}`);
271
+ const generic = new Set(["test", "tests", "testing", "verify", "verification", "check", "checks", "run", "change", "changes", "changing", "project", "projects", "temporary", "fixture", "fixtures", "procedure", "procedures", "files", "file", "src", "core", "ts", "mjs", "md", "build", "existing", "state", "status", "task", "tasks", "update", "add", "implement"]);
272
+ const queryTokens = new Set([...tokenize(query)].filter((word) => !generic.has(word)));
273
+ // Broad nouns alone cannot establish the domain of a procedure (e.g. npm cache vs derived cache).
274
+ const ambiguous = new Set(["cache", "caches", "caching", "data", "local", "memory", "server", "client", "application", "recovery"]);
275
+ const fields = [
276
+ ["title", skill.title, 8], ["description", skill.description, 4],
277
+ ["trigger", skill.triggers.join(" "), 10], ["scope", skill.scope === "project" ? "" : skill.scope, 6],
278
+ ];
279
+ const signals = fields.flatMap(([field, value, weight]) => {
280
+ const fieldTokens = tokenize(value);
281
+ const matches = [...queryTokens].filter((token) => fieldTokens.has(token)).sort();
282
+ return matches.length ? [{ field, matches, score: matches.length * weight }] : [];
283
+ });
284
+ const matched = new Set(signals.flatMap(s => s.matches));
285
+ const domainMatch = [...matched].some(t => !ambiguous.has(t));
286
+ const enoughContext = matched.size >= 2;
287
+ return { score: domainMatch || enoughContext ? signals.reduce((sum, signal) => sum + signal.score, 0) : 0, signals };
288
+ }
289
+ export async function projectSkillsView(root, options = {}) {
290
+ await requireProject(root);
291
+ const all = await listProjectSkills(root);
292
+ const filtered = all.filter((skill) => options.status ? skill.status === options.status : ["active", "candidate"].includes(skill.status));
293
+ const offset = Math.max(0, options.offset ?? 0);
294
+ const limit = Math.min(50, Math.max(1, options.limit ?? 20));
295
+ const skills = filtered.slice(offset, offset + limit).map(({ content, ...skill }) => ({ ...skill, contentPreview: content.slice(0, 500) }));
296
+ return { root, skills, counts: skillCounts(all), offset, total: filtered.length, nextOffset: offset + skills.length < filtered.length ? offset + skills.length : null };
297
+ }
298
+ /** A portable readable body; the resolver uses this same text for the agent and UI. */
299
+ function skillPreamble(skill) {
300
+ const when = skill.triggers.length ? skill.triggers.map(t => `- ${t}`).join("\n") : `- Working within ${skill.scope}: ${skill.title}`;
301
+ return `# ${skill.title}\n\n## Purpose\n\n${skill.description}\n\nScope: ${skill.scope}\n\n## Use when\n\n${when}\n\n## Procedure\n\n`;
302
+ }
303
+ function relatedKnowledgeMarkdown(ids) {
304
+ return ids.length ? `\n\n## Related knowledge\n\n${ids.map(id => `- [${id}](../memory/${id}.md)`).join("\n")}` : "";
305
+ }
306
+ export function skillMarkdown(skill) {
307
+ const references = skill.references.length ? `\n\n## References\n\n${skill.references.map(r => `- ${r}`).join("\n")}` : "";
308
+ const sources = skill.derivedFrom?.length ? `\n\n## Based on\n\n${skill.candidateTitle ?? skill.title}\n\n${skill.derivedFrom.map(r => `- ${r}`).join("\n")}` : "";
309
+ return `${skillPreamble(skill)}${skill.content}${relatedKnowledgeMarkdown(skill.relatedKnowledge ?? [])}${references}${sources}`;
310
+ }
311
+ export function candidateRevision(skill) { return sha256(JSON.stringify(skill)); }
312
+ function previewIdentity(skill, draft, target, disposition) { return sha256(JSON.stringify([candidateRevision(skill), draft, target ?? null, disposition ?? null])); }
313
+ export function deterministicSkillDraft(candidate) {
314
+ const references = [...new Set([...candidate.references, ...[...candidate.content.matchAll(/`([^`]+)`/g)].map(m => m[1]).filter(r => r.length <= 500 && /(?:\/|\.(?:md|ts|js|mjs|json|yaml|yml)$)/i.test(r))])].slice(0, 30);
315
+ return skillInputSchema.parse({ title: candidate.title, description: candidate.description, scope: candidate.scope, triggers: candidate.triggers, content: candidate.content, references, relatedKnowledge: candidate.relatedKnowledge });
316
+ }
317
+ export async function previewProjectSkill(root, id, options = {}) {
318
+ assertNoSecrets([options.draft, options.previousPreview]);
319
+ const candidate = await getProjectSkill(root, id);
320
+ if (candidate.status !== "candidate" && !(candidate.status === "active" && (options.recurate || options.previousPreview)))
321
+ throw new Error("Only candidate skills can be previewed; explicitly request re-curation to propose an active skill update.");
322
+ let draft = deterministicSkillDraft(candidate);
323
+ let mode = "deterministic";
324
+ let notice;
325
+ let disposition;
326
+ let target;
327
+ let reasoningSummary;
328
+ let relatedKnowledge;
329
+ const { validateSkillDraft } = await import("./skill-curation.js");
330
+ if (options.draft) {
331
+ if (options.recurate)
332
+ throw new Error("Edit and re-curate are separate actions.");
333
+ if (options.previousPreview) {
334
+ const previous = options.previousPreview;
335
+ if (previous.candidateId !== id || previous.previewId !== previewIdentity(candidate, skillInputSchema.parse(previous.draft), previous.target, previous.disposition))
336
+ throw new Error("The candidate changed. Open a fresh preview.");
337
+ disposition = previous.disposition;
338
+ target = previous.target;
339
+ reasoningSummary = previous.reasoningSummary;
340
+ relatedKnowledge = previous.relatedKnowledge;
341
+ }
342
+ draft = await validateSkillDraft(root, options.draft);
343
+ mode = "edited";
344
+ }
345
+ else if (options.provider?.curateSkill) {
346
+ const { curateProjectSkill } = await import("./skill-curation.js");
347
+ const result = await curateProjectSkill(root, candidate, options.provider);
348
+ ({ draft, mode, notice, disposition, reasoningSummary, relatedKnowledge } = result);
349
+ target = "target" in result ? result.target : undefined;
350
+ }
351
+ else if (options.provider?.polishSkill) {
352
+ const { polishSkillDraft } = await import("./skill-polish.js");
353
+ const result = await polishSkillDraft(options.provider, draft);
354
+ draft = result.draft;
355
+ mode = result.mode;
356
+ notice = result.notice;
357
+ }
358
+ if (candidate.status === "active")
359
+ disposition = { action: "update", targetSkillId: id };
360
+ if (disposition?.targetSkillId) {
361
+ const current = await getProjectSkill(root, disposition.targetSkillId);
362
+ if (current.status !== "active" || (target && target.revision !== candidateRevision(current)))
363
+ throw new Error("The existing skill changed. Open a fresh preview.");
364
+ target = { id: current.id, title: current.title, revision: candidateRevision(current) };
365
+ }
366
+ return { candidateId: id, candidateRevision: candidateRevision(candidate), previewId: previewIdentity(candidate, draft, target, disposition), draft, mode, ...(notice ? { notice } : {}), ...(disposition ? { disposition } : {}), ...(target ? { target } : {}), ...(reasoningSummary ? { reasoningSummary } : {}), ...(relatedKnowledge?.length ? { relatedKnowledge } : {}) };
367
+ }
368
+ //# sourceMappingURL=skills.js.map