selftune 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,576 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { existsSync, readFileSync, statSync } from "node:fs";
4
+ import { basename, dirname, join } from "node:path";
5
+ import { parseArgs } from "node:util";
6
+ import {
7
+ CLAUDE_CODE_PROJECTS_DIR,
8
+ QUERY_LOG,
9
+ REPAIRED_SKILL_LOG,
10
+ REPAIRED_SKILL_SESSIONS_MARKER,
11
+ SKILL_LOG,
12
+ } from "../constants.js";
13
+ import { findTranscriptFiles } from "../ingestors/claude-replay.js";
14
+ import {
15
+ DEFAULT_CODEX_HOME,
16
+ findRolloutFiles,
17
+ findSkillNames,
18
+ parseRolloutFile,
19
+ } from "../ingestors/codex-rollout.js";
20
+ import type { QueryLogRecord, SkillUsageRecord } from "../types.js";
21
+ import { readJsonl } from "../utils/jsonl.js";
22
+ import { isActionableQueryText } from "../utils/query-filter.js";
23
+ import {
24
+ classifySkillPath,
25
+ findInstalledSkillPath,
26
+ findRepositoryClaudeSkillDirs,
27
+ findRepositorySkillDirs,
28
+ } from "../utils/skill-discovery.js";
29
+ import { writeRepairedSkillUsageRecords } from "../utils/skill-log.js";
30
+
31
+ interface ActionableUserMessage {
32
+ query: string;
33
+ timestamp: string;
34
+ }
35
+
36
+ export interface RepairSkillUsageResult {
37
+ repairedRecords: SkillUsageRecord[];
38
+ repairedSessionIds: Set<string>;
39
+ }
40
+
41
+ interface RebuiltSessionRecords {
42
+ records: SkillUsageRecord[];
43
+ sessionIds: Set<string>;
44
+ }
45
+
46
+ interface ExtractedSkillUsage {
47
+ processed: boolean;
48
+ records: SkillUsageRecord[];
49
+ }
50
+
51
+ interface ExtractedCodexSkillUsage extends ExtractedSkillUsage {
52
+ sessionId?: string;
53
+ }
54
+
55
+ interface ResolvedSkillPath {
56
+ skillPath: string;
57
+ resolutionSource: NonNullable<SkillUsageRecord["skill_path_resolution_source"]>;
58
+ }
59
+
60
+ function isEphemeralLauncherProjectRoot(projectRoot: string): boolean {
61
+ return projectRoot.startsWith("/tmp/") || projectRoot.startsWith("/private/tmp/");
62
+ }
63
+
64
+ function extractActionableUserText(content: unknown): string | null {
65
+ let text = "";
66
+
67
+ if (typeof content === "string") {
68
+ text = content.trim();
69
+ } else if (Array.isArray(content)) {
70
+ text = content
71
+ .filter(
72
+ (part): part is Record<string, unknown> =>
73
+ typeof part === "object" &&
74
+ part !== null &&
75
+ (part as Record<string, unknown>).type === "text",
76
+ )
77
+ .map((part) => (part.text as string) ?? "")
78
+ .filter(Boolean)
79
+ .join(" ")
80
+ .trim();
81
+ }
82
+
83
+ if (!text || text.length < 4) return null;
84
+ return isActionableQueryText(text) ? text : null;
85
+ }
86
+
87
+ function buildSkillPathLookup(records: SkillUsageRecord[]): Map<string, string> {
88
+ const counts = new Map<string, Map<string, number>>();
89
+
90
+ for (const record of records) {
91
+ if (typeof record.skill_name !== "string" || typeof record.skill_path !== "string") continue;
92
+ const skillName = record.skill_name.trim().toLowerCase();
93
+ const skillPath = record.skill_path.trim();
94
+ if (!skillName || !skillPath.endsWith("SKILL.md") || skillPath.startsWith("(")) continue;
95
+
96
+ if (!counts.has(skillName)) counts.set(skillName, new Map());
97
+ const skillCounts = counts.get(skillName);
98
+ if (!skillCounts) continue;
99
+ skillCounts.set(skillPath, (skillCounts.get(skillPath) ?? 0) + 1);
100
+ }
101
+
102
+ const lookup = new Map<string, string>();
103
+ for (const [skillName, skillCounts] of counts.entries()) {
104
+ const best = [...skillCounts.entries()].sort((a, b) => b[1] - a[1])[0];
105
+ if (best) lookup.set(skillName, best[0]);
106
+ }
107
+ return lookup;
108
+ }
109
+
110
+ function resolveCodexSkillPath(
111
+ skillName: string,
112
+ cwd: string,
113
+ homeDir: string = process.env.HOME ?? "",
114
+ codexHome: string = DEFAULT_CODEX_HOME,
115
+ ): ResolvedSkillPath {
116
+ const skillPath = findInstalledSkillPath(skillName, [
117
+ ...findRepositorySkillDirs(cwd),
118
+ join(homeDir, ".agents", "skills"),
119
+ "/etc/codex/skills",
120
+ join(codexHome, "skills"),
121
+ join(codexHome, "skills", ".system"),
122
+ ]);
123
+ return skillPath
124
+ ? { skillPath, resolutionSource: "installed_scope" }
125
+ : { skillPath: `(codex:${skillName})`, resolutionSource: "fallback" };
126
+ }
127
+
128
+ function optionalString(value: unknown): string | undefined {
129
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
130
+ }
131
+
132
+ function resolveClaudeSkillPath(
133
+ skillName: string,
134
+ sessionCwd: string | undefined,
135
+ homeDir: string = process.env.HOME ?? "",
136
+ codexHome: string = DEFAULT_CODEX_HOME,
137
+ ): ResolvedSkillPath {
138
+ const candidateDirs = [
139
+ ...(sessionCwd ? findRepositorySkillDirs(sessionCwd) : []),
140
+ ...(sessionCwd ? findRepositoryClaudeSkillDirs(sessionCwd) : []),
141
+ join(homeDir, ".agents", "skills"),
142
+ join(homeDir, ".claude", "skills"),
143
+ "/etc/codex/skills",
144
+ join(codexHome, "skills"),
145
+ join(codexHome, "skills", ".system"),
146
+ ];
147
+ const skillPath = findInstalledSkillPath(skillName, candidateDirs);
148
+ return skillPath
149
+ ? { skillPath, resolutionSource: "installed_scope" }
150
+ : { skillPath: `(repaired:${skillName})`, resolutionSource: "fallback" };
151
+ }
152
+
153
+ function extractToolResultText(content: unknown): string {
154
+ if (typeof content === "string") return content;
155
+ if (!Array.isArray(content)) return "";
156
+
157
+ return content
158
+ .map((part) => {
159
+ if (typeof part === "string") return part;
160
+ if (typeof part !== "object" || part === null) return "";
161
+ const block = part as Record<string, unknown>;
162
+ return optionalString(block.text) ?? optionalString(block.content) ?? "";
163
+ })
164
+ .filter(Boolean)
165
+ .join("\n");
166
+ }
167
+
168
+ function extractLauncherSkillBaseDir(content: string): string | undefined {
169
+ const match = content.match(/^Base directory for this skill:\s*(.+)$/m);
170
+ return match?.[1]?.trim() || undefined;
171
+ }
172
+
173
+ function applyLauncherSkillBaseDir(
174
+ pending: { skillName: string; recordIndex?: number },
175
+ launcherDir: string,
176
+ skillPathLookup: Map<string, string>,
177
+ repaired: SkillUsageRecord[],
178
+ homeDir: string,
179
+ codexHome: string,
180
+ ): void {
181
+ const launcherSkillPath = join(launcherDir, "SKILL.md");
182
+ skillPathLookup.set(pending.skillName.toLowerCase(), launcherSkillPath);
183
+ const classified = classifySkillPath(launcherSkillPath, homeDir, codexHome);
184
+ const launcherMetadata =
185
+ classified.skill_scope === "project" &&
186
+ classified.skill_project_root &&
187
+ isEphemeralLauncherProjectRoot(classified.skill_project_root)
188
+ ? { skill_scope: "unknown" as const }
189
+ : classified;
190
+
191
+ if (pending.recordIndex !== undefined) {
192
+ const record = repaired[pending.recordIndex];
193
+ if (record) {
194
+ record.skill_path = launcherSkillPath;
195
+ record.skill_scope = launcherMetadata.skill_scope;
196
+ if (launcherMetadata.skill_project_root) {
197
+ record.skill_project_root = launcherMetadata.skill_project_root;
198
+ } else {
199
+ delete record.skill_project_root;
200
+ }
201
+ if (launcherMetadata.skill_registry_dir) {
202
+ record.skill_registry_dir = launcherMetadata.skill_registry_dir;
203
+ } else {
204
+ delete record.skill_registry_dir;
205
+ }
206
+ record.skill_path_resolution_source = "launcher_base_dir";
207
+ }
208
+ }
209
+ }
210
+
211
+ function extractSessionSkillUsage(
212
+ transcriptPath: string,
213
+ skillPathLookup: Map<string, string>,
214
+ homeDir: string = process.env.HOME ?? "",
215
+ codexHome: string = DEFAULT_CODEX_HOME,
216
+ ): ExtractedSkillUsage {
217
+ if (!existsSync(transcriptPath)) return { processed: false, records: [] };
218
+
219
+ let content: string;
220
+ try {
221
+ content = readFileSync(transcriptPath, "utf-8");
222
+ } catch {
223
+ return { processed: false, records: [] };
224
+ }
225
+
226
+ const sessionId = basename(transcriptPath, ".jsonl");
227
+ const fallbackTimestamp = (() => {
228
+ try {
229
+ return statSync(transcriptPath).mtime.toISOString();
230
+ } catch {
231
+ return new Date().toISOString();
232
+ }
233
+ })();
234
+
235
+ let lastUserMessage: ActionableUserMessage | null = null;
236
+ let sessionCwd: string | undefined;
237
+ const seen = new Set<string>();
238
+ const pendingSkillCalls = new Map<string, { skillName: string; recordIndex?: number }>();
239
+ const repaired: SkillUsageRecord[] = [];
240
+
241
+ for (const rawLine of content.split("\n")) {
242
+ const line = rawLine.trim();
243
+ if (!line) continue;
244
+
245
+ let entry: Record<string, unknown>;
246
+ try {
247
+ entry = JSON.parse(line);
248
+ } catch {
249
+ continue;
250
+ }
251
+
252
+ const msg = (entry.message as Record<string, unknown>) ?? entry;
253
+ const role = (msg.role as string) ?? (entry.role as string) ?? "";
254
+ const timestamp =
255
+ (entry.timestamp as string) ?? (msg.timestamp as string) ?? lastUserMessage?.timestamp ?? "";
256
+ sessionCwd =
257
+ optionalString(entry.cwd) ??
258
+ optionalString(msg.cwd) ??
259
+ optionalString((entry.data as Record<string, unknown> | undefined)?.cwd) ??
260
+ sessionCwd;
261
+
262
+ if (role === "user") {
263
+ const userBlocks = Array.isArray(msg.content ?? entry.content ?? "")
264
+ ? ((msg.content ?? entry.content ?? "") as unknown[])
265
+ : [];
266
+ for (const block of userBlocks) {
267
+ if (typeof block !== "object" || block === null) continue;
268
+ const toolResult = block as Record<string, unknown>;
269
+ if (toolResult.type === "tool_result") {
270
+ const toolUseId = optionalString(toolResult.tool_use_id);
271
+ if (!toolUseId) continue;
272
+ const pending = pendingSkillCalls.get(toolUseId);
273
+ if (!pending) continue;
274
+
275
+ const launcherDir = extractLauncherSkillBaseDir(
276
+ extractToolResultText(toolResult.content),
277
+ );
278
+ if (!launcherDir) continue;
279
+
280
+ applyLauncherSkillBaseDir(
281
+ pending,
282
+ launcherDir,
283
+ skillPathLookup,
284
+ repaired,
285
+ homeDir,
286
+ codexHome,
287
+ );
288
+ pendingSkillCalls.delete(toolUseId);
289
+ continue;
290
+ }
291
+
292
+ if (toolResult.type === "text" && pendingSkillCalls.size === 1) {
293
+ const launcherDir = extractLauncherSkillBaseDir(extractToolResultText(toolResult.text));
294
+ if (!launcherDir) continue;
295
+
296
+ const [toolUseId, pending] = pendingSkillCalls.entries().next().value as [
297
+ string,
298
+ { skillName: string; recordIndex?: number },
299
+ ];
300
+ applyLauncherSkillBaseDir(
301
+ pending,
302
+ launcherDir,
303
+ skillPathLookup,
304
+ repaired,
305
+ homeDir,
306
+ codexHome,
307
+ );
308
+ pendingSkillCalls.delete(toolUseId);
309
+ }
310
+ }
311
+
312
+ const query = extractActionableUserText(msg.content ?? entry.content ?? "");
313
+ if (query) {
314
+ lastUserMessage = { query, timestamp: timestamp || fallbackTimestamp };
315
+ }
316
+ continue;
317
+ }
318
+
319
+ if (role !== "assistant") continue;
320
+
321
+ const blocks = Array.isArray(msg.content ?? entry.content ?? "")
322
+ ? ((msg.content ?? entry.content ?? "") as unknown[])
323
+ : [];
324
+
325
+ for (const block of blocks) {
326
+ if (typeof block !== "object" || block === null) continue;
327
+ const toolUse = block as Record<string, unknown>;
328
+ if (toolUse.type !== "tool_use") continue;
329
+
330
+ const input = (toolUse.input as Record<string, unknown>) ?? {};
331
+ const toolName = (toolUse.name as string) ?? "";
332
+
333
+ if (toolName === "Read") {
334
+ const filePath = (input.file_path as string) ?? "";
335
+ if (filePath.endsWith("SKILL.md")) {
336
+ const inferredSkillName = basename(dirname(filePath)).trim().toLowerCase();
337
+ if (inferredSkillName && !skillPathLookup.has(inferredSkillName)) {
338
+ skillPathLookup.set(inferredSkillName, filePath);
339
+ }
340
+ }
341
+ continue;
342
+ }
343
+
344
+ if (toolName !== "Skill" || !lastUserMessage) continue;
345
+
346
+ const skillName = ((input.skill as string) ?? (input.name as string) ?? "").trim();
347
+ if (!skillName) continue;
348
+ const toolUseId = optionalString(toolUse.id);
349
+
350
+ const dedupeKey = [sessionId, skillName, lastUserMessage.query].join("\u0000");
351
+ if (seen.has(dedupeKey)) continue;
352
+ seen.add(dedupeKey);
353
+
354
+ const knownSkillPath = skillPathLookup.get(skillName.toLowerCase());
355
+ const { skillPath, resolutionSource } = knownSkillPath
356
+ ? { skillPath: knownSkillPath, resolutionSource: "raw_log" as const }
357
+ : resolveClaudeSkillPath(skillName, sessionCwd, homeDir, codexHome);
358
+
359
+ const recordIndex =
360
+ repaired.push({
361
+ timestamp: timestamp || lastUserMessage.timestamp || fallbackTimestamp,
362
+ session_id: sessionId,
363
+ skill_name: skillName,
364
+ skill_path: skillPath,
365
+ ...classifySkillPath(skillPath, homeDir, codexHome),
366
+ skill_path_resolution_source: resolutionSource,
367
+ query: lastUserMessage.query,
368
+ triggered: true,
369
+ source: "claude_code_repair",
370
+ }) - 1;
371
+
372
+ if (toolUseId) {
373
+ pendingSkillCalls.set(toolUseId, { skillName, recordIndex });
374
+ }
375
+ }
376
+ }
377
+
378
+ return { processed: true, records: repaired };
379
+ }
380
+
381
+ function extractCodexSkillUsage(
382
+ rolloutPath: string,
383
+ skillPathLookup: Map<string, string>,
384
+ homeDir: string = process.env.HOME ?? "",
385
+ codexHome: string = DEFAULT_CODEX_HOME,
386
+ ): ExtractedCodexSkillUsage {
387
+ const parsed = parseRolloutFile(rolloutPath, findSkillNames());
388
+ if (!parsed) return { processed: false, records: [] };
389
+ if (parsed.skills_invoked.length === 0 || !parsed.query.trim()) {
390
+ return {
391
+ processed: true,
392
+ sessionId: parsed.session_id,
393
+ records: [],
394
+ };
395
+ }
396
+
397
+ return {
398
+ processed: true,
399
+ sessionId: parsed.session_id,
400
+ records: parsed.skills_invoked.map((skillName) => {
401
+ const knownSkillPath = skillPathLookup.get(skillName.toLowerCase());
402
+ const { skillPath, resolutionSource } = knownSkillPath
403
+ ? { skillPath: knownSkillPath, resolutionSource: "raw_log" as const }
404
+ : resolveCodexSkillPath(skillName, parsed.cwd, homeDir, codexHome);
405
+ return {
406
+ timestamp: parsed.timestamp,
407
+ session_id: parsed.session_id,
408
+ skill_name: skillName,
409
+ skill_path: skillPath,
410
+ ...classifySkillPath(skillPath, homeDir, codexHome),
411
+ skill_path_resolution_source: resolutionSource,
412
+ query: parsed.query.trim(),
413
+ triggered: true,
414
+ source: "codex_rollout_explicit",
415
+ };
416
+ }),
417
+ };
418
+ }
419
+
420
+ export function rebuildSkillUsageFromCodexRollouts(
421
+ rolloutPaths: string[],
422
+ rawSkillRecords: SkillUsageRecord[],
423
+ homeDir: string = process.env.HOME ?? "",
424
+ codexHome: string = DEFAULT_CODEX_HOME,
425
+ ): RebuiltSessionRecords {
426
+ const rebuiltSessionIds = new Set<string>();
427
+ const skillPathLookup = buildSkillPathLookup(rawSkillRecords);
428
+ const rebuiltRecords: SkillUsageRecord[] = [];
429
+
430
+ for (const rolloutPath of rolloutPaths) {
431
+ const extracted = extractCodexSkillUsage(rolloutPath, skillPathLookup, homeDir, codexHome);
432
+ if (extracted.processed && extracted.sessionId) {
433
+ rebuiltSessionIds.add(extracted.sessionId);
434
+ }
435
+ if (extracted.records.length === 0) continue;
436
+ rebuiltRecords.push(...extracted.records);
437
+ }
438
+
439
+ return { records: rebuiltRecords, sessionIds: rebuiltSessionIds };
440
+ }
441
+
442
+ export function rebuildSkillUsageFromTranscripts(
443
+ transcriptPaths: string[],
444
+ rawSkillRecords: SkillUsageRecord[],
445
+ homeDir: string = process.env.HOME ?? "",
446
+ codexHome: string = DEFAULT_CODEX_HOME,
447
+ ): RepairSkillUsageResult {
448
+ const repairedSessionIds = new Set<string>();
449
+ const skillPathLookup = buildSkillPathLookup(rawSkillRecords);
450
+ const repairedRecords: SkillUsageRecord[] = [];
451
+
452
+ for (const transcriptPath of transcriptPaths) {
453
+ const sessionId = basename(transcriptPath, ".jsonl");
454
+ const extracted = extractSessionSkillUsage(transcriptPath, skillPathLookup, homeDir, codexHome);
455
+ if (extracted.processed) {
456
+ repairedSessionIds.add(sessionId);
457
+ }
458
+ if (extracted.records.length === 0) continue;
459
+ repairedRecords.push(...extracted.records);
460
+ }
461
+
462
+ return { repairedRecords, repairedSessionIds };
463
+ }
464
+
465
+ export function cliMain(): void {
466
+ try {
467
+ const { values } = parseArgs({
468
+ options: {
469
+ "projects-dir": { type: "string", default: CLAUDE_CODE_PROJECTS_DIR },
470
+ "codex-home": { type: "string", default: DEFAULT_CODEX_HOME },
471
+ since: { type: "string" },
472
+ out: { type: "string", default: REPAIRED_SKILL_LOG },
473
+ "sessions-marker": { type: "string", default: REPAIRED_SKILL_SESSIONS_MARKER },
474
+ "skill-log": { type: "string", default: SKILL_LOG },
475
+ "dry-run": { type: "boolean", default: false },
476
+ help: { type: "boolean", default: false },
477
+ },
478
+ strict: true,
479
+ });
480
+
481
+ if (values.help) {
482
+ console.log(`selftune repair-skill-usage — Rebuild trustworthy skill usage from transcripts
483
+
484
+ Usage:
485
+ selftune repair-skill-usage [options]
486
+
487
+ Options:
488
+ --projects-dir <dir> Claude transcript directory (default: ~/.claude/projects)
489
+ --codex-home <dir> Codex home directory (default: ~/.codex)
490
+ --since <date> Only repair sessions modified on/after date
491
+ --out <path> Repaired overlay log path
492
+ --sessions-marker <path> Repaired session-id marker path
493
+ --skill-log <path> Raw skill usage log path
494
+ --dry-run Show counts without writing files
495
+ --help Show this help`);
496
+ process.exit(0);
497
+ }
498
+
499
+ let since: Date | undefined;
500
+ if (values.since) {
501
+ since = new Date(values.since);
502
+ if (Number.isNaN(since.getTime())) {
503
+ throw new Error(`Invalid --since date: ${values.since}`);
504
+ }
505
+ }
506
+
507
+ const transcriptPaths = findTranscriptFiles(
508
+ values["projects-dir"] ?? CLAUDE_CODE_PROJECTS_DIR,
509
+ since,
510
+ );
511
+ const rolloutPaths = findRolloutFiles(values["codex-home"] ?? DEFAULT_CODEX_HOME, since);
512
+ const rawSkillRecords = readJsonl<SkillUsageRecord>(values["skill-log"] ?? SKILL_LOG);
513
+ const queryRecords = readJsonl<QueryLogRecord>(QUERY_LOG);
514
+ const { repairedRecords, repairedSessionIds } = rebuildSkillUsageFromTranscripts(
515
+ transcriptPaths,
516
+ rawSkillRecords,
517
+ process.env.HOME ?? "",
518
+ values["codex-home"] ?? DEFAULT_CODEX_HOME,
519
+ );
520
+ const { records: codexRecords, sessionIds: codexSessionIds } =
521
+ rebuildSkillUsageFromCodexRollouts(
522
+ rolloutPaths,
523
+ rawSkillRecords,
524
+ process.env.HOME ?? "",
525
+ values["codex-home"] ?? DEFAULT_CODEX_HOME,
526
+ );
527
+ for (const sessionId of codexSessionIds) repairedSessionIds.add(sessionId);
528
+ repairedRecords.push(...codexRecords);
529
+
530
+ const matchedQueries = new Set(
531
+ repairedRecords.map((record) => record.query.toLowerCase().trim()),
532
+ );
533
+ const totalReinsQueries = queryRecords.filter(
534
+ (record) => typeof record.query === "string" && /\breins\b/i.test(record.query),
535
+ ).length;
536
+ const totalReinsMatches = repairedRecords.filter((record) =>
537
+ /\breins\b/i.test(record.query),
538
+ ).length;
539
+ const totalCodexMatches = repairedRecords.filter(
540
+ (record) => record.source === "codex_rollout_explicit",
541
+ ).length;
542
+
543
+ const summary = {
544
+ transcripts_scanned: transcriptPaths.length,
545
+ codex_rollouts_scanned: rolloutPaths.length,
546
+ repaired_sessions: repairedSessionIds.size,
547
+ repaired_records: repairedRecords.length,
548
+ codex_repaired_records: totalCodexMatches,
549
+ unique_matched_queries: matchedQueries.size,
550
+ reins_queries_seen: totalReinsQueries,
551
+ reins_skill_matches: totalReinsMatches,
552
+ output: values.out ?? REPAIRED_SKILL_LOG,
553
+ };
554
+
555
+ if (values["dry-run"]) {
556
+ console.log(JSON.stringify(summary, null, 2));
557
+ return;
558
+ }
559
+
560
+ writeRepairedSkillUsageRecords(
561
+ repairedRecords,
562
+ repairedSessionIds,
563
+ values.out ?? REPAIRED_SKILL_LOG,
564
+ values["sessions-marker"] ?? REPAIRED_SKILL_SESSIONS_MARKER,
565
+ );
566
+ console.log(JSON.stringify(summary, null, 2));
567
+ } catch (error) {
568
+ const message = error instanceof Error ? error.message : String(error);
569
+ console.error(`[ERROR] Failed to repair skill usage: ${message}`);
570
+ process.exit(1);
571
+ }
572
+ }
573
+
574
+ if (import.meta.main) {
575
+ cliMain();
576
+ }