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.
- package/.claude/agents/diagnosis-analyst.md +20 -10
- package/.claude/agents/evolution-reviewer.md +14 -1
- package/.claude/agents/integration-guide.md +18 -6
- package/.claude/agents/pattern-analyst.md +18 -5
- package/CHANGELOG.md +12 -4
- package/README.md +43 -35
- package/apps/local-dashboard/dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/index-C4EOTFZ2.js +15 -0
- package/apps/local-dashboard/dist/assets/index-bl-Webyd.css +1 -0
- package/apps/local-dashboard/dist/assets/vendor-react-U7zYD9Rg.js +60 -0
- package/apps/local-dashboard/dist/assets/vendor-table-B7VF2Ipl.js +26 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-D7_zX_qy.js +346 -0
- package/apps/local-dashboard/dist/favicon.png +0 -0
- package/apps/local-dashboard/dist/index.html +17 -0
- package/apps/local-dashboard/dist/logo.png +0 -0
- package/apps/local-dashboard/dist/logo.svg +9 -0
- package/cli/selftune/badge/badge-data.ts +1 -1
- package/cli/selftune/badge/badge.ts +4 -8
- package/cli/selftune/canonical-export.ts +183 -0
- package/cli/selftune/constants.ts +28 -0
- package/cli/selftune/contribute/contribute.ts +1 -1
- package/cli/selftune/cron/setup.ts +17 -17
- package/cli/selftune/dashboard-contract.ts +202 -0
- package/cli/selftune/dashboard-server.ts +653 -186
- package/cli/selftune/dashboard.ts +41 -176
- package/cli/selftune/eval/baseline.ts +5 -4
- package/cli/selftune/eval/composability-v2.ts +273 -0
- package/cli/selftune/eval/hooks-to-evals.ts +34 -15
- package/cli/selftune/eval/unit-test-cli.ts +1 -1
- package/cli/selftune/evolution/evidence.ts +26 -0
- package/cli/selftune/evolution/evolve-body.ts +105 -11
- package/cli/selftune/evolution/evolve.ts +371 -25
- package/cli/selftune/evolution/extract-patterns.ts +87 -29
- package/cli/selftune/evolution/rollback.ts +2 -2
- package/cli/selftune/grading/auto-grade.ts +200 -0
- package/cli/selftune/grading/grade-session.ts +448 -97
- package/cli/selftune/grading/results.ts +42 -0
- package/cli/selftune/hooks/prompt-log.ts +172 -2
- package/cli/selftune/hooks/session-stop.ts +123 -3
- package/cli/selftune/hooks/skill-eval.ts +119 -3
- package/cli/selftune/index.ts +395 -116
- package/cli/selftune/ingestors/claude-replay.ts +140 -114
- package/cli/selftune/ingestors/codex-rollout.ts +345 -46
- package/cli/selftune/ingestors/codex-wrapper.ts +207 -39
- package/cli/selftune/ingestors/openclaw-ingest.ts +141 -8
- package/cli/selftune/ingestors/opencode-ingest.ts +193 -17
- package/cli/selftune/init.ts +227 -14
- package/cli/selftune/last.ts +14 -5
- package/cli/selftune/localdb/db.ts +63 -0
- package/cli/selftune/localdb/materialize.ts +428 -0
- package/cli/selftune/localdb/queries.ts +376 -0
- package/cli/selftune/localdb/schema.ts +204 -0
- package/cli/selftune/monitoring/watch.ts +66 -15
- package/cli/selftune/normalization.ts +682 -0
- package/cli/selftune/observability.ts +19 -44
- package/cli/selftune/orchestrate.ts +1073 -0
- package/cli/selftune/quickstart.ts +203 -0
- package/cli/selftune/repair/skill-usage.ts +576 -0
- package/cli/selftune/schedule.ts +561 -0
- package/cli/selftune/status.ts +48 -26
- package/cli/selftune/sync.ts +627 -0
- package/cli/selftune/types.ts +148 -0
- package/cli/selftune/utils/canonical-log.ts +45 -0
- package/cli/selftune/utils/hooks.ts +41 -0
- package/cli/selftune/utils/html.ts +27 -0
- package/cli/selftune/utils/llm-call.ts +78 -20
- package/cli/selftune/utils/math.ts +10 -0
- package/cli/selftune/utils/query-filter.ts +139 -0
- package/cli/selftune/utils/skill-discovery.ts +340 -0
- package/cli/selftune/utils/skill-log.ts +68 -0
- package/cli/selftune/utils/skill-usage-confidence.ts +18 -0
- package/cli/selftune/utils/transcript.ts +272 -26
- package/cli/selftune/workflows/discover.ts +254 -0
- package/cli/selftune/workflows/skill-md-writer.ts +288 -0
- package/cli/selftune/workflows/workflows.ts +188 -0
- package/package.json +21 -8
- package/packages/telemetry-contract/README.md +11 -0
- package/packages/telemetry-contract/fixtures/golden.json +87 -0
- package/packages/telemetry-contract/fixtures/golden.test.ts +42 -0
- package/packages/telemetry-contract/index.ts +1 -0
- package/packages/telemetry-contract/package.json +19 -0
- package/packages/telemetry-contract/src/index.ts +2 -0
- package/packages/telemetry-contract/src/types.ts +163 -0
- package/packages/telemetry-contract/src/validators.ts +109 -0
- package/skill/SKILL.md +84 -53
- package/skill/Workflows/AutoActivation.md +17 -16
- package/skill/Workflows/Badge.md +6 -0
- package/skill/Workflows/Baseline.md +46 -23
- package/skill/Workflows/Composability.md +12 -5
- package/skill/Workflows/Contribute.md +17 -14
- package/skill/Workflows/Cron.md +56 -79
- package/skill/Workflows/Dashboard.md +45 -34
- package/skill/Workflows/Doctor.md +30 -17
- package/skill/Workflows/Evals.md +64 -40
- package/skill/Workflows/EvolutionMemory.md +2 -0
- package/skill/Workflows/Evolve.md +102 -47
- package/skill/Workflows/EvolveBody.md +6 -6
- package/skill/Workflows/Grade.md +36 -31
- package/skill/Workflows/ImportSkillsBench.md +11 -5
- package/skill/Workflows/Ingest.md +43 -36
- package/skill/Workflows/Initialize.md +44 -30
- package/skill/Workflows/Orchestrate.md +139 -0
- package/skill/Workflows/Replay.md +39 -18
- package/skill/Workflows/Rollback.md +3 -3
- package/skill/Workflows/Schedule.md +61 -0
- package/skill/Workflows/Sync.md +88 -0
- package/skill/Workflows/UnitTest.md +34 -22
- package/skill/Workflows/Watch.md +14 -4
- package/skill/Workflows/Workflows.md +129 -0
- package/skill/assets/activation-rules-default.json +26 -0
- package/skill/assets/multi-skill-settings.json +63 -0
- package/skill/assets/single-skill-settings.json +57 -0
- package/skill/references/invocation-taxonomy.md +2 -2
- package/skill/references/logs.md +164 -2
- package/skill/references/setup-patterns.md +65 -0
- package/skill/references/version-history.md +40 -0
- package/skill/settings_snippet.json +1 -1
- package/templates/multi-skill-settings.json +7 -7
- package/templates/single-skill-settings.json +6 -6
- 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
|
+
}
|