selftune 0.2.0 → 0.2.2

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 (122) hide show
  1. package/.claude/agents/diagnosis-analyst.md +20 -10
  2. package/.claude/agents/evolution-reviewer.md +14 -1
  3. package/.claude/agents/integration-guide.md +18 -6
  4. package/.claude/agents/pattern-analyst.md +18 -5
  5. package/CHANGELOG.md +12 -4
  6. package/README.md +43 -35
  7. package/apps/local-dashboard/dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  8. package/apps/local-dashboard/dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  9. package/apps/local-dashboard/dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  10. package/apps/local-dashboard/dist/assets/index-C4EOTFZ2.js +15 -0
  11. package/apps/local-dashboard/dist/assets/index-bl-Webyd.css +1 -0
  12. package/apps/local-dashboard/dist/assets/vendor-react-U7zYD9Rg.js +60 -0
  13. package/apps/local-dashboard/dist/assets/vendor-table-B7VF2Ipl.js +26 -0
  14. package/apps/local-dashboard/dist/assets/vendor-ui-D7_zX_qy.js +346 -0
  15. package/apps/local-dashboard/dist/favicon.png +0 -0
  16. package/apps/local-dashboard/dist/index.html +17 -0
  17. package/apps/local-dashboard/dist/logo.png +0 -0
  18. package/apps/local-dashboard/dist/logo.svg +9 -0
  19. package/cli/selftune/badge/badge-data.ts +1 -1
  20. package/cli/selftune/badge/badge.ts +4 -8
  21. package/cli/selftune/canonical-export.ts +183 -0
  22. package/cli/selftune/constants.ts +28 -0
  23. package/cli/selftune/contribute/contribute.ts +1 -1
  24. package/cli/selftune/cron/setup.ts +17 -17
  25. package/cli/selftune/dashboard-contract.ts +202 -0
  26. package/cli/selftune/dashboard-server.ts +653 -186
  27. package/cli/selftune/dashboard.ts +41 -176
  28. package/cli/selftune/eval/baseline.ts +5 -4
  29. package/cli/selftune/eval/composability-v2.ts +273 -0
  30. package/cli/selftune/eval/hooks-to-evals.ts +34 -15
  31. package/cli/selftune/eval/unit-test-cli.ts +1 -1
  32. package/cli/selftune/evolution/evidence.ts +26 -0
  33. package/cli/selftune/evolution/evolve-body.ts +105 -11
  34. package/cli/selftune/evolution/evolve.ts +371 -25
  35. package/cli/selftune/evolution/extract-patterns.ts +87 -29
  36. package/cli/selftune/evolution/rollback.ts +2 -2
  37. package/cli/selftune/grading/auto-grade.ts +200 -0
  38. package/cli/selftune/grading/grade-session.ts +448 -97
  39. package/cli/selftune/grading/results.ts +42 -0
  40. package/cli/selftune/hooks/prompt-log.ts +172 -2
  41. package/cli/selftune/hooks/session-stop.ts +123 -3
  42. package/cli/selftune/hooks/skill-eval.ts +119 -3
  43. package/cli/selftune/index.ts +395 -116
  44. package/cli/selftune/ingestors/claude-replay.ts +140 -114
  45. package/cli/selftune/ingestors/codex-rollout.ts +345 -46
  46. package/cli/selftune/ingestors/codex-wrapper.ts +207 -39
  47. package/cli/selftune/ingestors/openclaw-ingest.ts +141 -8
  48. package/cli/selftune/ingestors/opencode-ingest.ts +193 -17
  49. package/cli/selftune/init.ts +227 -14
  50. package/cli/selftune/last.ts +14 -5
  51. package/cli/selftune/localdb/db.ts +63 -0
  52. package/cli/selftune/localdb/materialize.ts +428 -0
  53. package/cli/selftune/localdb/queries.ts +376 -0
  54. package/cli/selftune/localdb/schema.ts +204 -0
  55. package/cli/selftune/monitoring/watch.ts +66 -15
  56. package/cli/selftune/normalization.ts +682 -0
  57. package/cli/selftune/observability.ts +19 -44
  58. package/cli/selftune/orchestrate.ts +1073 -0
  59. package/cli/selftune/quickstart.ts +203 -0
  60. package/cli/selftune/repair/skill-usage.ts +576 -0
  61. package/cli/selftune/schedule.ts +561 -0
  62. package/cli/selftune/status.ts +48 -26
  63. package/cli/selftune/sync.ts +627 -0
  64. package/cli/selftune/types.ts +148 -0
  65. package/cli/selftune/utils/canonical-log.ts +45 -0
  66. package/cli/selftune/utils/hooks.ts +41 -0
  67. package/cli/selftune/utils/html.ts +27 -0
  68. package/cli/selftune/utils/llm-call.ts +78 -20
  69. package/cli/selftune/utils/math.ts +10 -0
  70. package/cli/selftune/utils/query-filter.ts +139 -0
  71. package/cli/selftune/utils/skill-discovery.ts +340 -0
  72. package/cli/selftune/utils/skill-log.ts +68 -0
  73. package/cli/selftune/utils/skill-usage-confidence.ts +18 -0
  74. package/cli/selftune/utils/transcript.ts +272 -26
  75. package/cli/selftune/workflows/discover.ts +254 -0
  76. package/cli/selftune/workflows/skill-md-writer.ts +288 -0
  77. package/cli/selftune/workflows/workflows.ts +188 -0
  78. package/package.json +21 -8
  79. package/packages/telemetry-contract/README.md +11 -0
  80. package/packages/telemetry-contract/fixtures/golden.json +87 -0
  81. package/packages/telemetry-contract/fixtures/golden.test.ts +42 -0
  82. package/packages/telemetry-contract/index.ts +1 -0
  83. package/packages/telemetry-contract/package.json +19 -0
  84. package/packages/telemetry-contract/src/index.ts +2 -0
  85. package/packages/telemetry-contract/src/types.ts +163 -0
  86. package/packages/telemetry-contract/src/validators.ts +109 -0
  87. package/skill/SKILL.md +84 -53
  88. package/skill/Workflows/AutoActivation.md +17 -16
  89. package/skill/Workflows/Badge.md +6 -0
  90. package/skill/Workflows/Baseline.md +46 -23
  91. package/skill/Workflows/Composability.md +12 -5
  92. package/skill/Workflows/Contribute.md +17 -14
  93. package/skill/Workflows/Cron.md +56 -79
  94. package/skill/Workflows/Dashboard.md +45 -34
  95. package/skill/Workflows/Doctor.md +30 -17
  96. package/skill/Workflows/Evals.md +64 -40
  97. package/skill/Workflows/EvolutionMemory.md +2 -0
  98. package/skill/Workflows/Evolve.md +102 -47
  99. package/skill/Workflows/EvolveBody.md +6 -6
  100. package/skill/Workflows/Grade.md +36 -31
  101. package/skill/Workflows/ImportSkillsBench.md +11 -5
  102. package/skill/Workflows/Ingest.md +43 -36
  103. package/skill/Workflows/Initialize.md +44 -30
  104. package/skill/Workflows/Orchestrate.md +139 -0
  105. package/skill/Workflows/Replay.md +39 -18
  106. package/skill/Workflows/Rollback.md +3 -3
  107. package/skill/Workflows/Schedule.md +61 -0
  108. package/skill/Workflows/Sync.md +88 -0
  109. package/skill/Workflows/UnitTest.md +34 -22
  110. package/skill/Workflows/Watch.md +14 -4
  111. package/skill/Workflows/Workflows.md +129 -0
  112. package/skill/assets/activation-rules-default.json +26 -0
  113. package/skill/assets/multi-skill-settings.json +63 -0
  114. package/skill/assets/single-skill-settings.json +57 -0
  115. package/skill/references/invocation-taxonomy.md +2 -2
  116. package/skill/references/logs.md +164 -2
  117. package/skill/references/setup-patterns.md +65 -0
  118. package/skill/references/version-history.md +40 -0
  119. package/skill/settings_snippet.json +1 -1
  120. package/templates/multi-skill-settings.json +7 -7
  121. package/templates/single-skill-settings.json +6 -6
  122. package/dashboard/index.html +0 -1680
@@ -0,0 +1,340 @@
1
+ import { existsSync, readdirSync, realpathSync, statSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+
4
+ export interface SkillPathMetadata {
5
+ skill_scope: "project" | "global" | "admin" | "system" | "unknown";
6
+ skill_project_root?: string;
7
+ skill_registry_dir?: string;
8
+ }
9
+
10
+ function normalizePath(value: string): string {
11
+ const resolved = resolve(value);
12
+ if (!existsSync(resolved)) return resolved;
13
+ try {
14
+ return realpathSync(resolved);
15
+ } catch {
16
+ return resolved;
17
+ }
18
+ }
19
+
20
+ export function escapeRegExp(value: string): string {
21
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22
+ }
23
+
24
+ export function containsWholeSkillMention(text: string, skillName: string): boolean {
25
+ const trimmedSkillName = skillName.trim();
26
+ if (!text || !trimmedSkillName) return false;
27
+
28
+ const pattern = new RegExp(
29
+ `(^|[^A-Za-z0-9_])${escapeRegExp(trimmedSkillName)}([^A-Za-z0-9_]|$)`,
30
+ "i",
31
+ );
32
+ return pattern.test(text);
33
+ }
34
+
35
+ export function extractExplicitSkillMentions(
36
+ text: string,
37
+ knownSkillNames: Iterable<string>,
38
+ ): Set<string> {
39
+ const names = new Set<string>();
40
+ if (!text) return names;
41
+
42
+ const normalizedText = text.trim();
43
+ if (!normalizedText) return names;
44
+
45
+ for (const skillName of knownSkillNames) {
46
+ const trimmedSkillName = skillName.trim();
47
+ if (!trimmedSkillName) continue;
48
+
49
+ const escapedSkillName = escapeRegExp(trimmedSkillName);
50
+ const patterns = [
51
+ new RegExp(`\\$${escapedSkillName}(?:\\b|$)`, "i"),
52
+ new RegExp(`\\b${escapedSkillName}\\s+skill\\b`, "i"),
53
+ new RegExp(
54
+ `\\b(?:use|using|run|invoke|apply|load|open|read|follow)\\s+${escapedSkillName}\\b`,
55
+ "i",
56
+ ),
57
+ new RegExp(`\\b(?:with|via|through)\\s+${escapedSkillName}\\b`, "i"),
58
+ new RegExp(
59
+ `\\b(?:initialize|init|configure|setup|set up|audit)\\s+${escapedSkillName}\\b`,
60
+ "i",
61
+ ),
62
+ ];
63
+
64
+ if (patterns.some((pattern) => pattern.test(normalizedText))) {
65
+ names.add(trimmedSkillName);
66
+ }
67
+ }
68
+
69
+ return names;
70
+ }
71
+
72
+ export function findInstalledSkillNames(dirs: string[]): Set<string> {
73
+ const names = new Set<string>();
74
+ for (const dir of dirs) {
75
+ if (!existsSync(dir)) continue;
76
+ for (const entry of readdirSync(dir)) {
77
+ const skillDir = join(dir, entry);
78
+ try {
79
+ if (!statSync(skillDir).isDirectory()) continue;
80
+
81
+ if (existsSync(join(skillDir, "SKILL.md"))) {
82
+ names.add(entry);
83
+ continue;
84
+ }
85
+
86
+ // Codex bundles built-in skills under nested scopes like .system/<skill>/SKILL.md.
87
+ for (const nestedEntry of readdirSync(skillDir)) {
88
+ const nestedSkillDir = join(skillDir, nestedEntry);
89
+ try {
90
+ if (
91
+ statSync(nestedSkillDir).isDirectory() &&
92
+ existsSync(join(nestedSkillDir, "SKILL.md"))
93
+ ) {
94
+ names.add(nestedEntry);
95
+ }
96
+ } catch {
97
+ // Skip unreadable nested entries.
98
+ }
99
+ }
100
+ } catch {
101
+ // Skip entries that can't be stat'd (broken symlinks, permission errors, etc.)
102
+ }
103
+ }
104
+ }
105
+ return names;
106
+ }
107
+
108
+ export function findInstalledSkillPath(skillName: string, dirs: string[]): string | undefined {
109
+ const trimmedName = skillName.trim();
110
+ if (!trimmedName) return undefined;
111
+
112
+ for (const dir of dirs) {
113
+ if (!existsSync(dir)) continue;
114
+
115
+ const directPath = join(dir, trimmedName, "SKILL.md");
116
+ if (existsSync(directPath)) {
117
+ try {
118
+ return realpathSync(directPath);
119
+ } catch {
120
+ return directPath;
121
+ }
122
+ }
123
+
124
+ try {
125
+ for (const entry of readdirSync(dir)) {
126
+ const nestedSkillPath = join(dir, entry, trimmedName, "SKILL.md");
127
+ if (!existsSync(nestedSkillPath)) continue;
128
+ try {
129
+ return realpathSync(nestedSkillPath);
130
+ } catch {
131
+ return nestedSkillPath;
132
+ }
133
+ }
134
+ } catch {
135
+ // Ignore unreadable directories.
136
+ }
137
+ }
138
+
139
+ return undefined;
140
+ }
141
+
142
+ export function findGitRepositoryRoot(startDir: string): string | undefined {
143
+ let current = resolve(startDir);
144
+ const seen = new Set<string>();
145
+
146
+ while (!seen.has(current)) {
147
+ seen.add(current);
148
+ if (existsSync(join(current, ".git"))) return current;
149
+
150
+ const parent = dirname(current);
151
+ if (parent === current) break;
152
+ current = parent;
153
+ }
154
+
155
+ return undefined;
156
+ }
157
+
158
+ export function findAncestorSkillDirs(
159
+ startDir: string,
160
+ relativeSkillPath: string,
161
+ stopDir?: string,
162
+ ): string[] {
163
+ const dirs: string[] = [];
164
+ let current = resolve(startDir);
165
+ const seen = new Set<string>();
166
+ const normalizedStopDir = stopDir ? resolve(stopDir) : undefined;
167
+
168
+ while (!seen.has(current)) {
169
+ seen.add(current);
170
+ dirs.push(join(current, relativeSkillPath));
171
+ if (normalizedStopDir && current === normalizedStopDir) break;
172
+ const parent = dirname(current);
173
+ if (parent === current) break;
174
+ current = parent;
175
+ }
176
+
177
+ return dirs;
178
+ }
179
+
180
+ export function findRepositorySkillDirs(startDir: string): string[] {
181
+ const repoRoot = findGitRepositoryRoot(startDir);
182
+ return findAncestorSkillDirs(startDir, ".agents/skills", repoRoot);
183
+ }
184
+
185
+ export function findRepositoryClaudeSkillDirs(startDir: string): string[] {
186
+ const repoRoot = findGitRepositoryRoot(startDir);
187
+ return findAncestorSkillDirs(startDir, ".claude/skills", repoRoot);
188
+ }
189
+
190
+ export function classifySkillPath(
191
+ skillPath: string,
192
+ homeDir: string = process.env.HOME ?? "",
193
+ codexHome: string = process.env.CODEX_HOME ?? join(homeDir, ".codex"),
194
+ ): SkillPathMetadata {
195
+ const trimmedPath = skillPath.trim();
196
+ if (!trimmedPath || trimmedPath.startsWith("(") || !trimmedPath.endsWith("SKILL.md")) {
197
+ return { skill_scope: "unknown" };
198
+ }
199
+
200
+ const normalizedPath = normalizePath(trimmedPath);
201
+ const normalizedHomeDir = homeDir ? normalizePath(homeDir) : "";
202
+ const globalAgentRegistry = join(homeDir, ".agents", "skills");
203
+ if (normalizedPath.startsWith(`${normalizePath(globalAgentRegistry)}/`)) {
204
+ return {
205
+ skill_scope: "global",
206
+ skill_registry_dir: normalizePath(globalAgentRegistry),
207
+ };
208
+ }
209
+
210
+ const globalClaudeRegistry = join(homeDir, ".claude", "skills");
211
+ if (normalizedPath.startsWith(`${normalizePath(globalClaudeRegistry)}/`)) {
212
+ return {
213
+ skill_scope: "global",
214
+ skill_registry_dir: normalizePath(globalClaudeRegistry),
215
+ };
216
+ }
217
+
218
+ const systemCodexRegistry = join(codexHome, "skills", ".system");
219
+ if (normalizedPath.startsWith(`${normalizePath(systemCodexRegistry)}/`)) {
220
+ return {
221
+ skill_scope: "system",
222
+ skill_registry_dir: normalizePath(systemCodexRegistry),
223
+ };
224
+ }
225
+
226
+ const userCodexRegistry = join(codexHome, "skills");
227
+ if (normalizedPath.startsWith(`${normalizePath(userCodexRegistry)}/`)) {
228
+ return {
229
+ skill_scope: "global",
230
+ skill_registry_dir: normalizePath(userCodexRegistry),
231
+ };
232
+ }
233
+
234
+ const adminRegistry = "/etc/codex/skills";
235
+ if (normalizedPath.startsWith(`${normalizePath(adminRegistry)}/`)) {
236
+ return {
237
+ skill_scope: "admin",
238
+ skill_registry_dir: normalizePath(adminRegistry),
239
+ };
240
+ }
241
+
242
+ const projectRegistries = ["/.agents/skills/", "/.claude/skills/"];
243
+ for (const marker of projectRegistries) {
244
+ const markerIndex = normalizedPath.lastIndexOf(marker);
245
+ if (markerIndex === -1) continue;
246
+
247
+ const projectRoot = normalizePath(normalizedPath.slice(0, markerIndex));
248
+ if (
249
+ !projectRoot ||
250
+ projectRoot === normalizedHomeDir ||
251
+ projectRoot === normalizePath(join(homeDir, ".claude"))
252
+ ) {
253
+ continue;
254
+ }
255
+
256
+ return {
257
+ skill_scope: "project",
258
+ skill_project_root: projectRoot,
259
+ skill_registry_dir: `${projectRoot}${marker.slice(0, -1)}`,
260
+ };
261
+ }
262
+
263
+ return { skill_scope: "unknown" };
264
+ }
265
+
266
+ export function extractSkillNamesFromInstructions(
267
+ text: string,
268
+ knownSkillNames?: Iterable<string>,
269
+ ): Set<string> {
270
+ const names = new Set<string>();
271
+ const knownSkillMap = new Map<string, string>();
272
+ if (knownSkillNames) {
273
+ for (const skillName of knownSkillNames) {
274
+ knownSkillMap.set(skillName.toLowerCase(), skillName);
275
+ }
276
+ }
277
+ let inAvailableSkillsSection = false;
278
+
279
+ for (const rawLine of text.split(/\r?\n/)) {
280
+ const line = rawLine.trim();
281
+ if (!line) continue;
282
+
283
+ if (line.toLowerCase() === "### available skills") {
284
+ inAvailableSkillsSection = true;
285
+ continue;
286
+ }
287
+
288
+ if (inAvailableSkillsSection && line.startsWith("### ")) {
289
+ break;
290
+ }
291
+
292
+ if (!inAvailableSkillsSection) continue;
293
+
294
+ const match = line.match(/^-\s*([^:]+):/);
295
+ if (match) {
296
+ const extractedName = match[1].trim();
297
+ const canonical = knownSkillMap.get(extractedName.toLowerCase()) ?? extractedName;
298
+ names.add(canonical);
299
+ }
300
+ }
301
+
302
+ return names;
303
+ }
304
+
305
+ export function extractSkillNamesFromPathReferences(
306
+ text: string,
307
+ knownSkillNames?: Iterable<string>,
308
+ ): Set<string> {
309
+ const names = new Set<string>();
310
+ if (!text) return names;
311
+
312
+ const knownSkillMap = new Map<string, string>();
313
+ if (knownSkillNames) {
314
+ for (const skillName of knownSkillNames) {
315
+ knownSkillMap.set(skillName.toLowerCase(), skillName);
316
+ }
317
+ }
318
+
319
+ const patterns = [
320
+ /(?:^|[\s"'`])(?:[^"'`\s]*?\.agents\/skills\/)([^/\s"'`]+)(?=\/)/gi,
321
+ /(?:^|[\s"'`])(?:[^"'`\s]*?\.codex\/skills\/(?:\.system\/)?)([^/\s"'`]+)(?=\/)/gi,
322
+ /(?:^|[\s"'`])(\/etc\/codex\/skills\/)([^/\s"'`]+)(?=\/)/gi,
323
+ ];
324
+
325
+ for (const pattern of patterns) {
326
+ let match = pattern.exec(text);
327
+ while (match !== null) {
328
+ const rawName = match[2] ?? match[1];
329
+ if (rawName) {
330
+ const canonical = knownSkillMap.get(rawName.toLowerCase()) ?? rawName;
331
+ if (knownSkillMap.size === 0 || knownSkillMap.has(rawName.toLowerCase())) {
332
+ names.add(canonical);
333
+ }
334
+ }
335
+ match = pattern.exec(text);
336
+ }
337
+ }
338
+
339
+ return names;
340
+ }
@@ -0,0 +1,68 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { REPAIRED_SKILL_LOG, REPAIRED_SKILL_SESSIONS_MARKER, SKILL_LOG } from "../constants.js";
4
+ import type { SkillUsageRecord } from "../types.js";
5
+ import { loadMarker, readJsonl, saveMarker } from "./jsonl.js";
6
+ import { filterActionableSkillUsageRecords } from "./query-filter.js";
7
+
8
+ function dedupeSkillUsageRecords(records: SkillUsageRecord[]): SkillUsageRecord[] {
9
+ const deduped = new Map<string, SkillUsageRecord>();
10
+
11
+ for (const record of records) {
12
+ const key = [
13
+ record.session_id,
14
+ record.skill_name,
15
+ record.query.trim(),
16
+ record.timestamp,
17
+ record.triggered ? "1" : "0",
18
+ ].join("\u0000");
19
+ if (!deduped.has(key)) {
20
+ deduped.set(key, record);
21
+ }
22
+ }
23
+
24
+ return [...deduped.values()].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
25
+ }
26
+
27
+ export function readEffectiveSkillUsageRecords(
28
+ rawSkillLogPath: string = SKILL_LOG,
29
+ repairedSkillLogPath: string = REPAIRED_SKILL_LOG,
30
+ repairedSessionsPath: string = REPAIRED_SKILL_SESSIONS_MARKER,
31
+ ): SkillUsageRecord[] {
32
+ const repairedRecords = filterActionableSkillUsageRecords(
33
+ readJsonl<SkillUsageRecord>(repairedSkillLogPath),
34
+ );
35
+ if (!existsSync(repairedSkillLogPath)) {
36
+ return dedupeSkillUsageRecords(
37
+ filterActionableSkillUsageRecords(readJsonl<SkillUsageRecord>(rawSkillLogPath)),
38
+ );
39
+ }
40
+
41
+ const repairedSessionIds = loadMarker(repairedSessionsPath);
42
+ const rawRecords = filterActionableSkillUsageRecords(
43
+ readJsonl<SkillUsageRecord>(rawSkillLogPath),
44
+ );
45
+ const unrepairedRawRecords =
46
+ repairedSessionIds.size === 0
47
+ ? rawRecords
48
+ : rawRecords.filter((record) => !repairedSessionIds.has(record.session_id));
49
+
50
+ return dedupeSkillUsageRecords([...repairedRecords, ...unrepairedRawRecords]);
51
+ }
52
+
53
+ export function writeRepairedSkillUsageRecords(
54
+ records: SkillUsageRecord[],
55
+ repairedSessionIds: Set<string>,
56
+ repairedSkillLogPath: string = REPAIRED_SKILL_LOG,
57
+ repairedSessionsPath: string = REPAIRED_SKILL_SESSIONS_MARKER,
58
+ ): void {
59
+ const dir = dirname(repairedSkillLogPath);
60
+ if (!existsSync(dir)) {
61
+ mkdirSync(dir, { recursive: true });
62
+ }
63
+
64
+ const normalized = dedupeSkillUsageRecords(filterActionableSkillUsageRecords(records));
65
+ const content = normalized.map((record) => JSON.stringify(record)).join("\n");
66
+ writeFileSync(repairedSkillLogPath, content ? `${content}\n` : "", "utf-8");
67
+ saveMarker(repairedSessionsPath, repairedSessionIds);
68
+ }
@@ -0,0 +1,18 @@
1
+ import type { SkillUsageRecord } from "../types.js";
2
+
3
+ const HIGH_CONFIDENCE_POSITIVE_SOURCES = new Set([
4
+ "claude_code_replay",
5
+ "claude_code_repair",
6
+ "codex_rollout_explicit",
7
+ ]);
8
+
9
+ export function isHighConfidencePositiveSkillRecord(
10
+ record: SkillUsageRecord,
11
+ skillName?: string,
12
+ ): boolean {
13
+ if (!record || record.triggered !== true) return false;
14
+ if (skillName && record.skill_name !== skillName) return false;
15
+
16
+ const source = record.source?.trim();
17
+ return !source || HIGH_CONFIDENCE_POSITIVE_SOURCES.has(source);
18
+ }