@caupulican/pi-adaptative 0.80.76 → 0.80.77

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.
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Skill curator (Hermes-parity #32). Reflection (R7) promotes recurring procedures into SKILL.md files;
3
+ * without curation they accumulate forever, bloating tool/context and raising per-turn cost. The curator
4
+ * tracks usage of PROMOTED skills (frontmatter `promoted: true`) and PROPOSES — never auto-applies —
5
+ * archiving stale ones and consolidating overlapping ones. Hand-authored user skills are never touched.
6
+ *
7
+ * Design (locked with agy): propose-only, session-start + idle triggers (not per-turn), restorable
8
+ * archive (non-destructive), and consolidation is a flagged suggestion (never an auto-merge).
9
+ */
10
+ /** Per-promoted-skill signal the proposal logic reasons over. Pure data — no I/O. */
11
+ export interface PromotedSkillInfo {
12
+ name: string;
13
+ /** When the skill file was created (ms epoch); guards a freshly-promoted skill from instant archival. */
14
+ createdMs: number;
15
+ /** Last time the skill was loaded/used (ms epoch); 0 if never used. */
16
+ lastUsedMs: number;
17
+ useCount: number;
18
+ /** Tokens from name+description+body, for overlap detection. */
19
+ keywords: string[];
20
+ }
21
+ export interface CuratorOptions {
22
+ /** A promoted skill unused and older than this many days is proposed for archival. Default 30. */
23
+ staleDays: number;
24
+ /** Token-Jaccard ≥ this between two promoted skills flags them for consolidation. Default 0.5. */
25
+ overlapThreshold: number;
26
+ /** Current time (ms epoch); injected so the proposal logic stays pure/testable. */
27
+ now: number;
28
+ }
29
+ export declare const DEFAULT_CURATOR_OPTIONS: Omit<CuratorOptions, "now">;
30
+ export interface CurationProposals {
31
+ /** Promoted skills proposed for (restorable) archival, with a human reason. */
32
+ archive: Array<{
33
+ name: string;
34
+ reason: string;
35
+ }>;
36
+ /** Pairs of promoted skills that overlap enough to consider merging (flag only, never auto-merge). */
37
+ consolidate: Array<{
38
+ names: [string, string];
39
+ overlap: number;
40
+ }>;
41
+ }
42
+ /**
43
+ * Pure proposal logic: decide which promoted skills to PROPOSE archiving (stale + unused) and which pairs
44
+ * overlap enough to PROPOSE consolidating. Returns suggestions only; the caller applies them on approval.
45
+ */
46
+ export declare function computeCurationProposals(skills: PromotedSkillInfo[], opts: CuratorOptions): CurationProposals;
47
+ /**
48
+ * Filesystem layer over {@link computeCurationProposals}: reads promoted SKILL.md files + the usage
49
+ * sidecar, and archives/restores skills non-destructively. The current time is injected so callers (and
50
+ * tests) control "now".
51
+ */
52
+ export declare class SkillCurator {
53
+ private readonly skillsDir;
54
+ private readonly archiveDir;
55
+ private readonly usageFile;
56
+ constructor(skillsDir: string);
57
+ /** Record that a promoted skill was loaded/used (bumps count + last-used). Best-effort. */
58
+ recordUse(name: string, now: number): void;
59
+ /** Build the proposals from the current promoted-skill corpus. */
60
+ proposeCuration(now: number, options?: Partial<Omit<CuratorOptions, "now">>): CurationProposals;
61
+ /** Move a promoted skill into `.archive/` (restorable). Returns true if archived. */
62
+ archiveSkill(name: string): boolean;
63
+ /** Restore an archived skill back into the active skills dir. Returns true if restored. */
64
+ restoreSkill(name: string): boolean;
65
+ loadPromotedSkills(): PromotedSkillInfo[];
66
+ private isPromoted;
67
+ private loadUsage;
68
+ }
69
+ /** True if a SKILL.md's YAML frontmatter declares `promoted: true` (reflection-generated). */
70
+ export declare function isPromotedFrontmatter(content: string): boolean;
71
+ //# sourceMappingURL=skill-curator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-curator.d.ts","sourceRoot":"","sources":["../../../src/core/learning/skill-curator.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,uFAAqF;AACrF,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,yGAAyG;IACzG,SAAS,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC9B,kGAAkG;IAClG,SAAS,EAAE,MAAM,CAAC;IAClB,oGAAkG;IAClG,gBAAgB,EAAE,MAAM,CAAC;IACzB,mFAAmF;IACnF,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,eAAO,MAAM,uBAAuB,EAAE,IAAI,CAAC,cAAc,EAAE,KAAK,CAG/D,CAAC;AAEF,MAAM,WAAW,iBAAiB;IACjC,+EAA+E;IAC/E,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjD,sGAAsG;IACtG,WAAW,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACjE;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,iBAAiB,EAAE,EAAE,IAAI,EAAE,cAAc,GAAG,iBAAiB,CAgC7G;AAWD;;;;GAIG;AACH,qBAAa,YAAY;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IAEnC,YAAY,SAAS,EAAE,MAAM,EAI5B;IAED,2FAA2F;IAC3F,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CASzC;IAED,kEAAkE;IAClE,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,KAAK,CAAC,CAAM,GAAG,iBAAiB,CAOlG;IAED,qFAAqF;IACrF,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUlC;IAED,2FAA2F;IAC3F,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUlC;IAED,kBAAkB,IAAI,iBAAiB,EAAE,CA+BxC;IAED,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,SAAS;CAOjB;AAED,8FAA8F;AAC9F,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAI9D","sourcesContent":["/**\n * Skill curator (Hermes-parity #32). Reflection (R7) promotes recurring procedures into SKILL.md files;\n * without curation they accumulate forever, bloating tool/context and raising per-turn cost. The curator\n * tracks usage of PROMOTED skills (frontmatter `promoted: true`) and PROPOSES — never auto-applies —\n * archiving stale ones and consolidating overlapping ones. Hand-authored user skills are never touched.\n *\n * Design (locked with agy): propose-only, session-start + idle triggers (not per-turn), restorable\n * archive (non-destructive), and consolidation is a flagged suggestion (never an auto-merge).\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { jaccard, tokenize } from \"../tools/skill-audit.ts\";\n\n/** Per-promoted-skill signal the proposal logic reasons over. Pure data — no I/O. */\nexport interface PromotedSkillInfo {\n\tname: string;\n\t/** When the skill file was created (ms epoch); guards a freshly-promoted skill from instant archival. */\n\tcreatedMs: number;\n\t/** Last time the skill was loaded/used (ms epoch); 0 if never used. */\n\tlastUsedMs: number;\n\tuseCount: number;\n\t/** Tokens from name+description+body, for overlap detection. */\n\tkeywords: string[];\n}\n\nexport interface CuratorOptions {\n\t/** A promoted skill unused and older than this many days is proposed for archival. Default 30. */\n\tstaleDays: number;\n\t/** Token-Jaccard ≥ this between two promoted skills flags them for consolidation. Default 0.5. */\n\toverlapThreshold: number;\n\t/** Current time (ms epoch); injected so the proposal logic stays pure/testable. */\n\tnow: number;\n}\n\nexport const DEFAULT_CURATOR_OPTIONS: Omit<CuratorOptions, \"now\"> = {\n\tstaleDays: 30,\n\toverlapThreshold: 0.5,\n};\n\nexport interface CurationProposals {\n\t/** Promoted skills proposed for (restorable) archival, with a human reason. */\n\tarchive: Array<{ name: string; reason: string }>;\n\t/** Pairs of promoted skills that overlap enough to consider merging (flag only, never auto-merge). */\n\tconsolidate: Array<{ names: [string, string]; overlap: number }>;\n}\n\n/**\n * Pure proposal logic: decide which promoted skills to PROPOSE archiving (stale + unused) and which pairs\n * overlap enough to PROPOSE consolidating. Returns suggestions only; the caller applies them on approval.\n */\nexport function computeCurationProposals(skills: PromotedSkillInfo[], opts: CuratorOptions): CurationProposals {\n\tconst staleMs = opts.staleDays * 86_400_000;\n\tconst archive: CurationProposals[\"archive\"] = [];\n\tfor (const s of skills) {\n\t\t// \"Stale\" = never recently used AND not freshly promoted: measure age from the most recent of\n\t\t// last-use / creation so a brand-new skill isn't archived before it has had a chance to be used.\n\t\tconst lastSeen = Math.max(s.lastUsedMs, s.createdMs);\n\t\tconst ageMs = opts.now - lastSeen;\n\t\tif (ageMs > staleMs) {\n\t\t\tconst days = Math.floor(ageMs / 86_400_000);\n\t\t\tarchive.push({\n\t\t\t\tname: s.name,\n\t\t\t\treason: s.useCount === 0 ? `never used, ${days}d old` : `unused for ${days}d (${s.useCount} total uses)`,\n\t\t\t});\n\t\t}\n\t}\n\n\tconst consolidate: CurationProposals[\"consolidate\"] = [];\n\tconst archiving = new Set(archive.map((a) => a.name));\n\tfor (let i = 0; i < skills.length; i++) {\n\t\tfor (let j = i + 1; j < skills.length; j++) {\n\t\t\tconst a = skills[i];\n\t\t\tconst b = skills[j];\n\t\t\t// Don't propose consolidating something already proposed for archival.\n\t\t\tif (archiving.has(a.name) || archiving.has(b.name)) continue;\n\t\t\tconst overlap = jaccard(a.keywords, b.keywords);\n\t\t\tif (overlap >= opts.overlapThreshold) {\n\t\t\t\tconsolidate.push({ names: [a.name, b.name], overlap });\n\t\t\t}\n\t\t}\n\t}\n\treturn { archive, consolidate };\n}\n\ninterface UsageRecord {\n\tlastUsedMs: number;\n\tuseCount: number;\n}\ntype UsageMap = Record<string, UsageRecord>;\n\n/** Cap on how much of a skill body feeds keyword extraction (keeps overlap detection cheap). */\nconst KEYWORD_SOURCE_CAP = 4000;\n\n/**\n * Filesystem layer over {@link computeCurationProposals}: reads promoted SKILL.md files + the usage\n * sidecar, and archives/restores skills non-destructively. The current time is injected so callers (and\n * tests) control \"now\".\n */\nexport class SkillCurator {\n\tprivate readonly skillsDir: string;\n\tprivate readonly archiveDir: string;\n\tprivate readonly usageFile: string;\n\n\tconstructor(skillsDir: string) {\n\t\tthis.skillsDir = skillsDir;\n\t\tthis.archiveDir = join(skillsDir, \".archive\");\n\t\tthis.usageFile = join(skillsDir, \".usage.json\");\n\t}\n\n\t/** Record that a promoted skill was loaded/used (bumps count + last-used). Best-effort. */\n\trecordUse(name: string, now: number): void {\n\t\ttry {\n\t\t\tconst usage = this.loadUsage();\n\t\t\tconst prev = usage[name] ?? { lastUsedMs: 0, useCount: 0 };\n\t\t\tusage[name] = { lastUsedMs: now, useCount: prev.useCount + 1 };\n\t\t\twriteFileSync(this.usageFile, JSON.stringify(usage, null, 2), \"utf-8\");\n\t\t} catch {\n\t\t\t// usage tracking must never disrupt a turn\n\t\t}\n\t}\n\n\t/** Build the proposals from the current promoted-skill corpus. */\n\tproposeCuration(now: number, options: Partial<Omit<CuratorOptions, \"now\">> = {}): CurationProposals {\n\t\tconst skills = this.loadPromotedSkills();\n\t\treturn computeCurationProposals(skills, {\n\t\t\tnow,\n\t\t\tstaleDays: options.staleDays ?? DEFAULT_CURATOR_OPTIONS.staleDays,\n\t\t\toverlapThreshold: options.overlapThreshold ?? DEFAULT_CURATOR_OPTIONS.overlapThreshold,\n\t\t});\n\t}\n\n\t/** Move a promoted skill into `.archive/` (restorable). Returns true if archived. */\n\tarchiveSkill(name: string): boolean {\n\t\ttry {\n\t\t\tconst from = join(this.skillsDir, name);\n\t\t\tif (!existsSync(join(from, \"SKILL.md\")) || !this.isPromoted(name)) return false;\n\t\t\tmkdirSync(this.archiveDir, { recursive: true });\n\t\t\trenameSync(from, join(this.archiveDir, name));\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/** Restore an archived skill back into the active skills dir. Returns true if restored. */\n\trestoreSkill(name: string): boolean {\n\t\ttry {\n\t\t\tconst from = join(this.archiveDir, name);\n\t\t\tconst to = join(this.skillsDir, name);\n\t\t\tif (!existsSync(join(from, \"SKILL.md\")) || existsSync(to)) return false;\n\t\t\trenameSync(from, to);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tloadPromotedSkills(): PromotedSkillInfo[] {\n\t\tconst out: PromotedSkillInfo[] = [];\n\t\tlet entries: string[];\n\t\ttry {\n\t\t\tentries = readdirSync(this.skillsDir);\n\t\t} catch {\n\t\t\treturn out;\n\t\t}\n\t\tconst usage = this.loadUsage();\n\t\tfor (const name of entries) {\n\t\t\tif (name.startsWith(\".\")) continue; // skip .archive, .usage.json\n\t\t\tconst file = join(this.skillsDir, name, \"SKILL.md\");\n\t\t\tlet raw: string;\n\t\t\tlet createdMs = 0;\n\t\t\ttry {\n\t\t\t\traw = readFileSync(file, \"utf-8\");\n\t\t\t\tcreatedMs = statSync(file).birthtimeMs || statSync(file).mtimeMs;\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!isPromotedFrontmatter(raw)) continue;\n\t\t\tconst u = usage[name] ?? { lastUsedMs: 0, useCount: 0 };\n\t\t\tout.push({\n\t\t\t\tname,\n\t\t\t\tcreatedMs,\n\t\t\t\tlastUsedMs: u.lastUsedMs,\n\t\t\t\tuseCount: u.useCount,\n\t\t\t\tkeywords: tokenize(raw.slice(0, KEYWORD_SOURCE_CAP)),\n\t\t\t});\n\t\t}\n\t\treturn out;\n\t}\n\n\tprivate isPromoted(name: string): boolean {\n\t\ttry {\n\t\t\treturn isPromotedFrontmatter(readFileSync(join(this.skillsDir, name, \"SKILL.md\"), \"utf-8\"));\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate loadUsage(): UsageMap {\n\t\ttry {\n\t\t\treturn JSON.parse(readFileSync(this.usageFile, \"utf-8\")) as UsageMap;\n\t\t} catch {\n\t\t\treturn {};\n\t\t}\n\t}\n}\n\n/** True if a SKILL.md's YAML frontmatter declares `promoted: true` (reflection-generated). */\nexport function isPromotedFrontmatter(content: string): boolean {\n\tconst fm = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n\tif (!fm) return false;\n\treturn /^\\s*promoted\\s*:\\s*true\\s*$/im.test(fm[1]);\n}\n"]}
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Skill curator (Hermes-parity #32). Reflection (R7) promotes recurring procedures into SKILL.md files;
3
+ * without curation they accumulate forever, bloating tool/context and raising per-turn cost. The curator
4
+ * tracks usage of PROMOTED skills (frontmatter `promoted: true`) and PROPOSES — never auto-applies —
5
+ * archiving stale ones and consolidating overlapping ones. Hand-authored user skills are never touched.
6
+ *
7
+ * Design (locked with agy): propose-only, session-start + idle triggers (not per-turn), restorable
8
+ * archive (non-destructive), and consolidation is a flagged suggestion (never an auto-merge).
9
+ */
10
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { jaccard, tokenize } from "../tools/skill-audit.js";
13
+ export const DEFAULT_CURATOR_OPTIONS = {
14
+ staleDays: 30,
15
+ overlapThreshold: 0.5,
16
+ };
17
+ /**
18
+ * Pure proposal logic: decide which promoted skills to PROPOSE archiving (stale + unused) and which pairs
19
+ * overlap enough to PROPOSE consolidating. Returns suggestions only; the caller applies them on approval.
20
+ */
21
+ export function computeCurationProposals(skills, opts) {
22
+ const staleMs = opts.staleDays * 86_400_000;
23
+ const archive = [];
24
+ for (const s of skills) {
25
+ // "Stale" = never recently used AND not freshly promoted: measure age from the most recent of
26
+ // last-use / creation so a brand-new skill isn't archived before it has had a chance to be used.
27
+ const lastSeen = Math.max(s.lastUsedMs, s.createdMs);
28
+ const ageMs = opts.now - lastSeen;
29
+ if (ageMs > staleMs) {
30
+ const days = Math.floor(ageMs / 86_400_000);
31
+ archive.push({
32
+ name: s.name,
33
+ reason: s.useCount === 0 ? `never used, ${days}d old` : `unused for ${days}d (${s.useCount} total uses)`,
34
+ });
35
+ }
36
+ }
37
+ const consolidate = [];
38
+ const archiving = new Set(archive.map((a) => a.name));
39
+ for (let i = 0; i < skills.length; i++) {
40
+ for (let j = i + 1; j < skills.length; j++) {
41
+ const a = skills[i];
42
+ const b = skills[j];
43
+ // Don't propose consolidating something already proposed for archival.
44
+ if (archiving.has(a.name) || archiving.has(b.name))
45
+ continue;
46
+ const overlap = jaccard(a.keywords, b.keywords);
47
+ if (overlap >= opts.overlapThreshold) {
48
+ consolidate.push({ names: [a.name, b.name], overlap });
49
+ }
50
+ }
51
+ }
52
+ return { archive, consolidate };
53
+ }
54
+ /** Cap on how much of a skill body feeds keyword extraction (keeps overlap detection cheap). */
55
+ const KEYWORD_SOURCE_CAP = 4000;
56
+ /**
57
+ * Filesystem layer over {@link computeCurationProposals}: reads promoted SKILL.md files + the usage
58
+ * sidecar, and archives/restores skills non-destructively. The current time is injected so callers (and
59
+ * tests) control "now".
60
+ */
61
+ export class SkillCurator {
62
+ skillsDir;
63
+ archiveDir;
64
+ usageFile;
65
+ constructor(skillsDir) {
66
+ this.skillsDir = skillsDir;
67
+ this.archiveDir = join(skillsDir, ".archive");
68
+ this.usageFile = join(skillsDir, ".usage.json");
69
+ }
70
+ /** Record that a promoted skill was loaded/used (bumps count + last-used). Best-effort. */
71
+ recordUse(name, now) {
72
+ try {
73
+ const usage = this.loadUsage();
74
+ const prev = usage[name] ?? { lastUsedMs: 0, useCount: 0 };
75
+ usage[name] = { lastUsedMs: now, useCount: prev.useCount + 1 };
76
+ writeFileSync(this.usageFile, JSON.stringify(usage, null, 2), "utf-8");
77
+ }
78
+ catch {
79
+ // usage tracking must never disrupt a turn
80
+ }
81
+ }
82
+ /** Build the proposals from the current promoted-skill corpus. */
83
+ proposeCuration(now, options = {}) {
84
+ const skills = this.loadPromotedSkills();
85
+ return computeCurationProposals(skills, {
86
+ now,
87
+ staleDays: options.staleDays ?? DEFAULT_CURATOR_OPTIONS.staleDays,
88
+ overlapThreshold: options.overlapThreshold ?? DEFAULT_CURATOR_OPTIONS.overlapThreshold,
89
+ });
90
+ }
91
+ /** Move a promoted skill into `.archive/` (restorable). Returns true if archived. */
92
+ archiveSkill(name) {
93
+ try {
94
+ const from = join(this.skillsDir, name);
95
+ if (!existsSync(join(from, "SKILL.md")) || !this.isPromoted(name))
96
+ return false;
97
+ mkdirSync(this.archiveDir, { recursive: true });
98
+ renameSync(from, join(this.archiveDir, name));
99
+ return true;
100
+ }
101
+ catch {
102
+ return false;
103
+ }
104
+ }
105
+ /** Restore an archived skill back into the active skills dir. Returns true if restored. */
106
+ restoreSkill(name) {
107
+ try {
108
+ const from = join(this.archiveDir, name);
109
+ const to = join(this.skillsDir, name);
110
+ if (!existsSync(join(from, "SKILL.md")) || existsSync(to))
111
+ return false;
112
+ renameSync(from, to);
113
+ return true;
114
+ }
115
+ catch {
116
+ return false;
117
+ }
118
+ }
119
+ loadPromotedSkills() {
120
+ const out = [];
121
+ let entries;
122
+ try {
123
+ entries = readdirSync(this.skillsDir);
124
+ }
125
+ catch {
126
+ return out;
127
+ }
128
+ const usage = this.loadUsage();
129
+ for (const name of entries) {
130
+ if (name.startsWith("."))
131
+ continue; // skip .archive, .usage.json
132
+ const file = join(this.skillsDir, name, "SKILL.md");
133
+ let raw;
134
+ let createdMs = 0;
135
+ try {
136
+ raw = readFileSync(file, "utf-8");
137
+ createdMs = statSync(file).birthtimeMs || statSync(file).mtimeMs;
138
+ }
139
+ catch {
140
+ continue;
141
+ }
142
+ if (!isPromotedFrontmatter(raw))
143
+ continue;
144
+ const u = usage[name] ?? { lastUsedMs: 0, useCount: 0 };
145
+ out.push({
146
+ name,
147
+ createdMs,
148
+ lastUsedMs: u.lastUsedMs,
149
+ useCount: u.useCount,
150
+ keywords: tokenize(raw.slice(0, KEYWORD_SOURCE_CAP)),
151
+ });
152
+ }
153
+ return out;
154
+ }
155
+ isPromoted(name) {
156
+ try {
157
+ return isPromotedFrontmatter(readFileSync(join(this.skillsDir, name, "SKILL.md"), "utf-8"));
158
+ }
159
+ catch {
160
+ return false;
161
+ }
162
+ }
163
+ loadUsage() {
164
+ try {
165
+ return JSON.parse(readFileSync(this.usageFile, "utf-8"));
166
+ }
167
+ catch {
168
+ return {};
169
+ }
170
+ }
171
+ }
172
+ /** True if a SKILL.md's YAML frontmatter declares `promoted: true` (reflection-generated). */
173
+ export function isPromotedFrontmatter(content) {
174
+ const fm = content.match(/^---\n([\s\S]*?)\n---/);
175
+ if (!fm)
176
+ return false;
177
+ return /^\s*promoted\s*:\s*true\s*$/im.test(fm[1]);
178
+ }
179
+ //# sourceMappingURL=skill-curator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-curator.js","sourceRoot":"","sources":["../../../src/core/learning/skill-curator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Skill curator (Hermes-parity #32). Reflection (R7) promotes recurring procedures into SKILL.md files;\n * without curation they accumulate forever, bloating tool/context and raising per-turn cost. The curator\n * tracks usage of PROMOTED skills (frontmatter `promoted: true`) and PROPOSES — never auto-applies —\n * archiving stale ones and consolidating overlapping ones. Hand-authored user skills are never touched.\n *\n * Design (locked with agy): propose-only, session-start + idle triggers (not per-turn), restorable\n * archive (non-destructive), and consolidation is a flagged suggestion (never an auto-merge).\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { jaccard, tokenize } from \"../tools/skill-audit.ts\";\n\n/** Per-promoted-skill signal the proposal logic reasons over. Pure data — no I/O. */\nexport interface PromotedSkillInfo {\n\tname: string;\n\t/** When the skill file was created (ms epoch); guards a freshly-promoted skill from instant archival. */\n\tcreatedMs: number;\n\t/** Last time the skill was loaded/used (ms epoch); 0 if never used. */\n\tlastUsedMs: number;\n\tuseCount: number;\n\t/** Tokens from name+description+body, for overlap detection. */\n\tkeywords: string[];\n}\n\nexport interface CuratorOptions {\n\t/** A promoted skill unused and older than this many days is proposed for archival. Default 30. */\n\tstaleDays: number;\n\t/** Token-Jaccard ≥ this between two promoted skills flags them for consolidation. Default 0.5. */\n\toverlapThreshold: number;\n\t/** Current time (ms epoch); injected so the proposal logic stays pure/testable. */\n\tnow: number;\n}\n\nexport const DEFAULT_CURATOR_OPTIONS: Omit<CuratorOptions, \"now\"> = {\n\tstaleDays: 30,\n\toverlapThreshold: 0.5,\n};\n\nexport interface CurationProposals {\n\t/** Promoted skills proposed for (restorable) archival, with a human reason. */\n\tarchive: Array<{ name: string; reason: string }>;\n\t/** Pairs of promoted skills that overlap enough to consider merging (flag only, never auto-merge). */\n\tconsolidate: Array<{ names: [string, string]; overlap: number }>;\n}\n\n/**\n * Pure proposal logic: decide which promoted skills to PROPOSE archiving (stale + unused) and which pairs\n * overlap enough to PROPOSE consolidating. Returns suggestions only; the caller applies them on approval.\n */\nexport function computeCurationProposals(skills: PromotedSkillInfo[], opts: CuratorOptions): CurationProposals {\n\tconst staleMs = opts.staleDays * 86_400_000;\n\tconst archive: CurationProposals[\"archive\"] = [];\n\tfor (const s of skills) {\n\t\t// \"Stale\" = never recently used AND not freshly promoted: measure age from the most recent of\n\t\t// last-use / creation so a brand-new skill isn't archived before it has had a chance to be used.\n\t\tconst lastSeen = Math.max(s.lastUsedMs, s.createdMs);\n\t\tconst ageMs = opts.now - lastSeen;\n\t\tif (ageMs > staleMs) {\n\t\t\tconst days = Math.floor(ageMs / 86_400_000);\n\t\t\tarchive.push({\n\t\t\t\tname: s.name,\n\t\t\t\treason: s.useCount === 0 ? `never used, ${days}d old` : `unused for ${days}d (${s.useCount} total uses)`,\n\t\t\t});\n\t\t}\n\t}\n\n\tconst consolidate: CurationProposals[\"consolidate\"] = [];\n\tconst archiving = new Set(archive.map((a) => a.name));\n\tfor (let i = 0; i < skills.length; i++) {\n\t\tfor (let j = i + 1; j < skills.length; j++) {\n\t\t\tconst a = skills[i];\n\t\t\tconst b = skills[j];\n\t\t\t// Don't propose consolidating something already proposed for archival.\n\t\t\tif (archiving.has(a.name) || archiving.has(b.name)) continue;\n\t\t\tconst overlap = jaccard(a.keywords, b.keywords);\n\t\t\tif (overlap >= opts.overlapThreshold) {\n\t\t\t\tconsolidate.push({ names: [a.name, b.name], overlap });\n\t\t\t}\n\t\t}\n\t}\n\treturn { archive, consolidate };\n}\n\ninterface UsageRecord {\n\tlastUsedMs: number;\n\tuseCount: number;\n}\ntype UsageMap = Record<string, UsageRecord>;\n\n/** Cap on how much of a skill body feeds keyword extraction (keeps overlap detection cheap). */\nconst KEYWORD_SOURCE_CAP = 4000;\n\n/**\n * Filesystem layer over {@link computeCurationProposals}: reads promoted SKILL.md files + the usage\n * sidecar, and archives/restores skills non-destructively. The current time is injected so callers (and\n * tests) control \"now\".\n */\nexport class SkillCurator {\n\tprivate readonly skillsDir: string;\n\tprivate readonly archiveDir: string;\n\tprivate readonly usageFile: string;\n\n\tconstructor(skillsDir: string) {\n\t\tthis.skillsDir = skillsDir;\n\t\tthis.archiveDir = join(skillsDir, \".archive\");\n\t\tthis.usageFile = join(skillsDir, \".usage.json\");\n\t}\n\n\t/** Record that a promoted skill was loaded/used (bumps count + last-used). Best-effort. */\n\trecordUse(name: string, now: number): void {\n\t\ttry {\n\t\t\tconst usage = this.loadUsage();\n\t\t\tconst prev = usage[name] ?? { lastUsedMs: 0, useCount: 0 };\n\t\t\tusage[name] = { lastUsedMs: now, useCount: prev.useCount + 1 };\n\t\t\twriteFileSync(this.usageFile, JSON.stringify(usage, null, 2), \"utf-8\");\n\t\t} catch {\n\t\t\t// usage tracking must never disrupt a turn\n\t\t}\n\t}\n\n\t/** Build the proposals from the current promoted-skill corpus. */\n\tproposeCuration(now: number, options: Partial<Omit<CuratorOptions, \"now\">> = {}): CurationProposals {\n\t\tconst skills = this.loadPromotedSkills();\n\t\treturn computeCurationProposals(skills, {\n\t\t\tnow,\n\t\t\tstaleDays: options.staleDays ?? DEFAULT_CURATOR_OPTIONS.staleDays,\n\t\t\toverlapThreshold: options.overlapThreshold ?? DEFAULT_CURATOR_OPTIONS.overlapThreshold,\n\t\t});\n\t}\n\n\t/** Move a promoted skill into `.archive/` (restorable). Returns true if archived. */\n\tarchiveSkill(name: string): boolean {\n\t\ttry {\n\t\t\tconst from = join(this.skillsDir, name);\n\t\t\tif (!existsSync(join(from, \"SKILL.md\")) || !this.isPromoted(name)) return false;\n\t\t\tmkdirSync(this.archiveDir, { recursive: true });\n\t\t\trenameSync(from, join(this.archiveDir, name));\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/** Restore an archived skill back into the active skills dir. Returns true if restored. */\n\trestoreSkill(name: string): boolean {\n\t\ttry {\n\t\t\tconst from = join(this.archiveDir, name);\n\t\t\tconst to = join(this.skillsDir, name);\n\t\t\tif (!existsSync(join(from, \"SKILL.md\")) || existsSync(to)) return false;\n\t\t\trenameSync(from, to);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tloadPromotedSkills(): PromotedSkillInfo[] {\n\t\tconst out: PromotedSkillInfo[] = [];\n\t\tlet entries: string[];\n\t\ttry {\n\t\t\tentries = readdirSync(this.skillsDir);\n\t\t} catch {\n\t\t\treturn out;\n\t\t}\n\t\tconst usage = this.loadUsage();\n\t\tfor (const name of entries) {\n\t\t\tif (name.startsWith(\".\")) continue; // skip .archive, .usage.json\n\t\t\tconst file = join(this.skillsDir, name, \"SKILL.md\");\n\t\t\tlet raw: string;\n\t\t\tlet createdMs = 0;\n\t\t\ttry {\n\t\t\t\traw = readFileSync(file, \"utf-8\");\n\t\t\t\tcreatedMs = statSync(file).birthtimeMs || statSync(file).mtimeMs;\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!isPromotedFrontmatter(raw)) continue;\n\t\t\tconst u = usage[name] ?? { lastUsedMs: 0, useCount: 0 };\n\t\t\tout.push({\n\t\t\t\tname,\n\t\t\t\tcreatedMs,\n\t\t\t\tlastUsedMs: u.lastUsedMs,\n\t\t\t\tuseCount: u.useCount,\n\t\t\t\tkeywords: tokenize(raw.slice(0, KEYWORD_SOURCE_CAP)),\n\t\t\t});\n\t\t}\n\t\treturn out;\n\t}\n\n\tprivate isPromoted(name: string): boolean {\n\t\ttry {\n\t\t\treturn isPromotedFrontmatter(readFileSync(join(this.skillsDir, name, \"SKILL.md\"), \"utf-8\"));\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate loadUsage(): UsageMap {\n\t\ttry {\n\t\t\treturn JSON.parse(readFileSync(this.usageFile, \"utf-8\")) as UsageMap;\n\t\t} catch {\n\t\t\treturn {};\n\t\t}\n\t}\n}\n\n/** True if a SKILL.md's YAML frontmatter declares `promoted: true` (reflection-generated). */\nexport function isPromotedFrontmatter(content: string): boolean {\n\tconst fm = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n\tif (!fm) return false;\n\treturn /^\\s*promoted\\s*:\\s*true\\s*$/im.test(fm[1]);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"slash-commands.d.ts","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAElE,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,sBAAsB,EAAE,aAAa,CAAC,mBAAmB,CA0CrE,CAAC","sourcesContent":["import { APP_NAME } from \"../config.ts\";\nimport type { SourceInfo } from \"./source-info.ts\";\n\nexport type SlashCommandSource = \"extension\" | \"prompt\" | \"skill\";\n\nexport interface SlashCommandInfo {\n\tname: string;\n\tdescription?: string;\n\tsource: SlashCommandSource;\n\tsourceInfo: SourceInfo;\n}\n\nexport interface BuiltinSlashCommand {\n\tname: string;\n\tdescription: string;\n}\n\nexport const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [\n\t{ name: \"settings\", description: \"Open settings menu\" },\n\t{ name: \"autonomy\", description: \"Show or set autonomy mode (/autonomy full)\" },\n\t{ name: \"auto-learn\", description: \"Show Auto Learn/reflection status or run now (/auto-learn run)\" },\n\t{ name: \"model\", description: \"Select model (opens selector UI)\" },\n\t{ name: \"profiles\", description: \"Select a runtime profile for this session\" },\n\t{ name: \"scoped-models\", description: \"Enable/disable models for Ctrl+P cycling\" },\n\t{ name: \"export\", description: \"Export session (HTML default, or specify path: .html/.jsonl)\" },\n\t{ name: \"import\", description: \"Import and resume a session from a JSONL file\" },\n\t{ name: \"share\", description: \"Share session as a secret GitHub gist\" },\n\t{ name: \"copy\", description: \"Copy last agent message to clipboard\" },\n\t{ name: \"name\", description: \"Set session display name\" },\n\t{ name: \"session\", description: \"Show session info and stats\" },\n\t{ name: \"changelog\", description: \"Show changelog entries\" },\n\t{ name: \"hotkeys\", description: \"Show all keyboard shortcuts\" },\n\t{ name: \"fork\", description: \"Create a new fork from a previous user message (optional name: /fork <name>)\" },\n\t{\n\t\tname: \"clone\",\n\t\tdescription: \"Duplicate the current session at the current position (optional name: /clone <name>)\",\n\t},\n\t{ name: \"tree\", description: \"Navigate session tree (switch branches)\" },\n\t{ name: \"trust\", description: \"Trust or untrust this project folder\" },\n\t{ name: \"login\", description: \"Configure provider authentication\" },\n\t{ name: \"logout\", description: \"Remove provider authentication\" },\n\t{ name: \"new\", description: \"Start a new session (optional name: /new <name>)\" },\n\t{ name: \"compact\", description: \"Manually compact the session context\" },\n\t{ name: \"resume\", description: \"Resume a different session\" },\n\t{ name: \"reload\", description: \"Reload keybindings, extensions, skills, prompts, and themes\" },\n\t{\n\t\tname: \"install-resources\",\n\t\tdescription:\n\t\t\t\"Copy resources from a trusted directory to user local settings (/install-resources <dir> [--force])\",\n\t},\n\t{\n\t\tname: \"config-backup\",\n\t\tdescription: \"Backup profiles and resource settings to a JSON file (/config-backup [file])\",\n\t},\n\t{\n\t\tname: \"config-restore\",\n\t\tdescription: \"Restore profiles and resource settings from a JSON file (/config-restore <file>)\",\n\t},\n\t{ name: \"quit\", description: `Quit ${APP_NAME}` },\n];\n"]}
1
+ {"version":3,"file":"slash-commands.d.ts","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAElE,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,sBAAsB,EAAE,aAAa,CAAC,mBAAmB,CA2CrE,CAAC","sourcesContent":["import { APP_NAME } from \"../config.ts\";\nimport type { SourceInfo } from \"./source-info.ts\";\n\nexport type SlashCommandSource = \"extension\" | \"prompt\" | \"skill\";\n\nexport interface SlashCommandInfo {\n\tname: string;\n\tdescription?: string;\n\tsource: SlashCommandSource;\n\tsourceInfo: SourceInfo;\n}\n\nexport interface BuiltinSlashCommand {\n\tname: string;\n\tdescription: string;\n}\n\nexport const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [\n\t{ name: \"settings\", description: \"Open settings menu\" },\n\t{ name: \"autonomy\", description: \"Show or set autonomy mode (/autonomy full)\" },\n\t{ name: \"auto-learn\", description: \"Show Auto Learn/reflection status or run now (/auto-learn run)\" },\n\t{ name: \"model\", description: \"Select model (opens selector UI)\" },\n\t{ name: \"profiles\", description: \"Select a runtime profile for this session\" },\n\t{ name: \"scoped-models\", description: \"Enable/disable models for Ctrl+P cycling\" },\n\t{ name: \"export\", description: \"Export session (HTML default, or specify path: .html/.jsonl)\" },\n\t{ name: \"import\", description: \"Import and resume a session from a JSONL file\" },\n\t{ name: \"share\", description: \"Share session as a secret GitHub gist\" },\n\t{ name: \"copy\", description: \"Copy last agent message to clipboard\" },\n\t{ name: \"name\", description: \"Set session display name\" },\n\t{ name: \"session\", description: \"Show session info and stats\" },\n\t{ name: \"changelog\", description: \"Show changelog entries\" },\n\t{ name: \"hotkeys\", description: \"Show all keyboard shortcuts\" },\n\t{ name: \"fork\", description: \"Create a new fork from a previous user message (optional name: /fork <name>)\" },\n\t{\n\t\tname: \"clone\",\n\t\tdescription: \"Duplicate the current session at the current position (optional name: /clone <name>)\",\n\t},\n\t{ name: \"tree\", description: \"Navigate session tree (switch branches)\" },\n\t{ name: \"trust\", description: \"Trust or untrust this project folder\" },\n\t{ name: \"login\", description: \"Configure provider authentication\" },\n\t{ name: \"logout\", description: \"Remove provider authentication\" },\n\t{ name: \"new\", description: \"Start a new session (optional name: /new <name>)\" },\n\t{ name: \"compact\", description: \"Manually compact the session context\" },\n\t{ name: \"curate\", description: \"Review/archive stale or overlapping reflection-promoted skills\" },\n\t{ name: \"resume\", description: \"Resume a different session\" },\n\t{ name: \"reload\", description: \"Reload keybindings, extensions, skills, prompts, and themes\" },\n\t{\n\t\tname: \"install-resources\",\n\t\tdescription:\n\t\t\t\"Copy resources from a trusted directory to user local settings (/install-resources <dir> [--force])\",\n\t},\n\t{\n\t\tname: \"config-backup\",\n\t\tdescription: \"Backup profiles and resource settings to a JSON file (/config-backup [file])\",\n\t},\n\t{\n\t\tname: \"config-restore\",\n\t\tdescription: \"Restore profiles and resource settings from a JSON file (/config-restore <file>)\",\n\t},\n\t{ name: \"quit\", description: `Quit ${APP_NAME}` },\n];\n"]}
@@ -25,6 +25,7 @@ export const BUILTIN_SLASH_COMMANDS = [
25
25
  { name: "logout", description: "Remove provider authentication" },
26
26
  { name: "new", description: "Start a new session (optional name: /new <name>)" },
27
27
  { name: "compact", description: "Manually compact the session context" },
28
+ { name: "curate", description: "Review/archive stale or overlapping reflection-promoted skills" },
28
29
  { name: "resume", description: "Resume a different session" },
29
30
  { name: "reload", description: "Reload keybindings, extensions, skills, prompts, and themes" },
30
31
  {
@@ -1 +1 @@
1
- {"version":3,"file":"slash-commands.js","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAiBxC,MAAM,CAAC,MAAM,sBAAsB,GAAuC;IACzE,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,oBAAoB,EAAE;IACvD,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,4CAA4C,EAAE;IAC/E,EAAE,IAAI,EAAE,YAAY,EAAE,WAAW,EAAE,gEAAgE,EAAE;IACrG,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,kCAAkC,EAAE;IAClE,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,2CAA2C,EAAE;IAC9E,EAAE,IAAI,EAAE,eAAe,EAAE,WAAW,EAAE,0CAA0C,EAAE;IAClF,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,8DAA8D,EAAE;IAC/F,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+CAA+C,EAAE;IAChF,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uCAAuC,EAAE;IACvE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACrE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,0BAA0B,EAAE;IACzD,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,wBAAwB,EAAE;IAC5D,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,8EAA8E,EAAE;IAC7G;QACC,IAAI,EAAE,OAAO;QACb,WAAW,EAAE,sFAAsF;KACnG;IACD,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,yCAAyC,EAAE;IACxE,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACtE,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,mCAAmC,EAAE;IACnE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,gCAAgC,EAAE;IACjE,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,kDAAkD,EAAE;IAChF,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACxE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,4BAA4B,EAAE;IAC7D,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,6DAA6D,EAAE;IAC9F;QACC,IAAI,EAAE,mBAAmB;QACzB,WAAW,EACV,qGAAqG;KACtG;IACD;QACC,IAAI,EAAE,eAAe;QACrB,WAAW,EAAE,8EAA8E;KAC3F;IACD;QACC,IAAI,EAAE,gBAAgB;QACtB,WAAW,EAAE,kFAAkF;KAC/F;IACD,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,QAAQ,EAAE,EAAE;CACjD,CAAC","sourcesContent":["import { APP_NAME } from \"../config.ts\";\nimport type { SourceInfo } from \"./source-info.ts\";\n\nexport type SlashCommandSource = \"extension\" | \"prompt\" | \"skill\";\n\nexport interface SlashCommandInfo {\n\tname: string;\n\tdescription?: string;\n\tsource: SlashCommandSource;\n\tsourceInfo: SourceInfo;\n}\n\nexport interface BuiltinSlashCommand {\n\tname: string;\n\tdescription: string;\n}\n\nexport const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [\n\t{ name: \"settings\", description: \"Open settings menu\" },\n\t{ name: \"autonomy\", description: \"Show or set autonomy mode (/autonomy full)\" },\n\t{ name: \"auto-learn\", description: \"Show Auto Learn/reflection status or run now (/auto-learn run)\" },\n\t{ name: \"model\", description: \"Select model (opens selector UI)\" },\n\t{ name: \"profiles\", description: \"Select a runtime profile for this session\" },\n\t{ name: \"scoped-models\", description: \"Enable/disable models for Ctrl+P cycling\" },\n\t{ name: \"export\", description: \"Export session (HTML default, or specify path: .html/.jsonl)\" },\n\t{ name: \"import\", description: \"Import and resume a session from a JSONL file\" },\n\t{ name: \"share\", description: \"Share session as a secret GitHub gist\" },\n\t{ name: \"copy\", description: \"Copy last agent message to clipboard\" },\n\t{ name: \"name\", description: \"Set session display name\" },\n\t{ name: \"session\", description: \"Show session info and stats\" },\n\t{ name: \"changelog\", description: \"Show changelog entries\" },\n\t{ name: \"hotkeys\", description: \"Show all keyboard shortcuts\" },\n\t{ name: \"fork\", description: \"Create a new fork from a previous user message (optional name: /fork <name>)\" },\n\t{\n\t\tname: \"clone\",\n\t\tdescription: \"Duplicate the current session at the current position (optional name: /clone <name>)\",\n\t},\n\t{ name: \"tree\", description: \"Navigate session tree (switch branches)\" },\n\t{ name: \"trust\", description: \"Trust or untrust this project folder\" },\n\t{ name: \"login\", description: \"Configure provider authentication\" },\n\t{ name: \"logout\", description: \"Remove provider authentication\" },\n\t{ name: \"new\", description: \"Start a new session (optional name: /new <name>)\" },\n\t{ name: \"compact\", description: \"Manually compact the session context\" },\n\t{ name: \"resume\", description: \"Resume a different session\" },\n\t{ name: \"reload\", description: \"Reload keybindings, extensions, skills, prompts, and themes\" },\n\t{\n\t\tname: \"install-resources\",\n\t\tdescription:\n\t\t\t\"Copy resources from a trusted directory to user local settings (/install-resources <dir> [--force])\",\n\t},\n\t{\n\t\tname: \"config-backup\",\n\t\tdescription: \"Backup profiles and resource settings to a JSON file (/config-backup [file])\",\n\t},\n\t{\n\t\tname: \"config-restore\",\n\t\tdescription: \"Restore profiles and resource settings from a JSON file (/config-restore <file>)\",\n\t},\n\t{ name: \"quit\", description: `Quit ${APP_NAME}` },\n];\n"]}
1
+ {"version":3,"file":"slash-commands.js","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAiBxC,MAAM,CAAC,MAAM,sBAAsB,GAAuC;IACzE,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,oBAAoB,EAAE;IACvD,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,4CAA4C,EAAE;IAC/E,EAAE,IAAI,EAAE,YAAY,EAAE,WAAW,EAAE,gEAAgE,EAAE;IACrG,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,kCAAkC,EAAE;IAClE,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,2CAA2C,EAAE;IAC9E,EAAE,IAAI,EAAE,eAAe,EAAE,WAAW,EAAE,0CAA0C,EAAE;IAClF,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,8DAA8D,EAAE;IAC/F,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+CAA+C,EAAE;IAChF,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uCAAuC,EAAE;IACvE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACrE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,0BAA0B,EAAE;IACzD,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,wBAAwB,EAAE;IAC5D,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,8EAA8E,EAAE;IAC7G;QACC,IAAI,EAAE,OAAO;QACb,WAAW,EAAE,sFAAsF;KACnG;IACD,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,yCAAyC,EAAE;IACxE,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACtE,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,mCAAmC,EAAE;IACnE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,gCAAgC,EAAE;IACjE,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,kDAAkD,EAAE;IAChF,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACxE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,gEAAgE,EAAE;IACjG,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,4BAA4B,EAAE;IAC7D,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,6DAA6D,EAAE;IAC9F;QACC,IAAI,EAAE,mBAAmB;QACzB,WAAW,EACV,qGAAqG;KACtG;IACD;QACC,IAAI,EAAE,eAAe;QACrB,WAAW,EAAE,8EAA8E;KAC3F;IACD;QACC,IAAI,EAAE,gBAAgB;QACtB,WAAW,EAAE,kFAAkF;KAC/F;IACD,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,QAAQ,EAAE,EAAE;CACjD,CAAC","sourcesContent":["import { APP_NAME } from \"../config.ts\";\nimport type { SourceInfo } from \"./source-info.ts\";\n\nexport type SlashCommandSource = \"extension\" | \"prompt\" | \"skill\";\n\nexport interface SlashCommandInfo {\n\tname: string;\n\tdescription?: string;\n\tsource: SlashCommandSource;\n\tsourceInfo: SourceInfo;\n}\n\nexport interface BuiltinSlashCommand {\n\tname: string;\n\tdescription: string;\n}\n\nexport const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [\n\t{ name: \"settings\", description: \"Open settings menu\" },\n\t{ name: \"autonomy\", description: \"Show or set autonomy mode (/autonomy full)\" },\n\t{ name: \"auto-learn\", description: \"Show Auto Learn/reflection status or run now (/auto-learn run)\" },\n\t{ name: \"model\", description: \"Select model (opens selector UI)\" },\n\t{ name: \"profiles\", description: \"Select a runtime profile for this session\" },\n\t{ name: \"scoped-models\", description: \"Enable/disable models for Ctrl+P cycling\" },\n\t{ name: \"export\", description: \"Export session (HTML default, or specify path: .html/.jsonl)\" },\n\t{ name: \"import\", description: \"Import and resume a session from a JSONL file\" },\n\t{ name: \"share\", description: \"Share session as a secret GitHub gist\" },\n\t{ name: \"copy\", description: \"Copy last agent message to clipboard\" },\n\t{ name: \"name\", description: \"Set session display name\" },\n\t{ name: \"session\", description: \"Show session info and stats\" },\n\t{ name: \"changelog\", description: \"Show changelog entries\" },\n\t{ name: \"hotkeys\", description: \"Show all keyboard shortcuts\" },\n\t{ name: \"fork\", description: \"Create a new fork from a previous user message (optional name: /fork <name>)\" },\n\t{\n\t\tname: \"clone\",\n\t\tdescription: \"Duplicate the current session at the current position (optional name: /clone <name>)\",\n\t},\n\t{ name: \"tree\", description: \"Navigate session tree (switch branches)\" },\n\t{ name: \"trust\", description: \"Trust or untrust this project folder\" },\n\t{ name: \"login\", description: \"Configure provider authentication\" },\n\t{ name: \"logout\", description: \"Remove provider authentication\" },\n\t{ name: \"new\", description: \"Start a new session (optional name: /new <name>)\" },\n\t{ name: \"compact\", description: \"Manually compact the session context\" },\n\t{ name: \"curate\", description: \"Review/archive stale or overlapping reflection-promoted skills\" },\n\t{ name: \"resume\", description: \"Resume a different session\" },\n\t{ name: \"reload\", description: \"Reload keybindings, extensions, skills, prompts, and themes\" },\n\t{\n\t\tname: \"install-resources\",\n\t\tdescription:\n\t\t\t\"Copy resources from a trusted directory to user local settings (/install-resources <dir> [--force])\",\n\t},\n\t{\n\t\tname: \"config-backup\",\n\t\tdescription: \"Backup profiles and resource settings to a JSON file (/config-backup [file])\",\n\t},\n\t{\n\t\tname: \"config-restore\",\n\t\tdescription: \"Restore profiles and resource settings from a JSON file (/config-restore <file>)\",\n\t},\n\t{ name: \"quit\", description: `Quit ${APP_NAME}` },\n];\n"]}
@@ -541,6 +541,13 @@ export declare class InteractiveMode {
541
541
  private handleClearCommand;
542
542
  private copyResourcesRecursively;
543
543
  private handleInstallResourcesCommand;
544
+ /**
545
+ * `/curate` — skill curator (#32). With no args, lists reflection-promoted skills proposed for
546
+ * archival (stale/unused) and pairs proposed for consolidation (overlapping). PROPOSE-ONLY: the user
547
+ * applies actions explicitly via `/curate archive <name>` / `/curate restore <name>`. Never touches
548
+ * hand-authored skills; archival is restorable.
549
+ */
550
+ private handleCurateCommand;
544
551
  private handleConfigBackupCommand;
545
552
  private handleConfigRestoreCommand;
546
553
  private handleDebugCommand;