selftune 0.1.4 → 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 (153) hide show
  1. package/.claude/agents/diagnosis-analyst.md +156 -0
  2. package/.claude/agents/evolution-reviewer.md +180 -0
  3. package/.claude/agents/integration-guide.md +212 -0
  4. package/.claude/agents/pattern-analyst.md +160 -0
  5. package/CHANGELOG.md +46 -1
  6. package/README.md +105 -257
  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/assets/BeforeAfter.gif +0 -0
  20. package/assets/FeedbackLoop.gif +0 -0
  21. package/assets/logo.svg +9 -0
  22. package/assets/skill-health-badge.svg +20 -0
  23. package/cli/selftune/activation-rules.ts +171 -0
  24. package/cli/selftune/badge/badge-data.ts +108 -0
  25. package/cli/selftune/badge/badge-svg.ts +212 -0
  26. package/cli/selftune/badge/badge.ts +99 -0
  27. package/cli/selftune/canonical-export.ts +183 -0
  28. package/cli/selftune/constants.ts +103 -1
  29. package/cli/selftune/contribute/bundle.ts +314 -0
  30. package/cli/selftune/contribute/contribute.ts +214 -0
  31. package/cli/selftune/contribute/sanitize.ts +162 -0
  32. package/cli/selftune/cron/setup.ts +266 -0
  33. package/cli/selftune/dashboard-contract.ts +202 -0
  34. package/cli/selftune/dashboard-server.ts +1049 -0
  35. package/cli/selftune/dashboard.ts +43 -156
  36. package/cli/selftune/eval/baseline.ts +248 -0
  37. package/cli/selftune/eval/composability-v2.ts +273 -0
  38. package/cli/selftune/eval/composability.ts +117 -0
  39. package/cli/selftune/eval/generate-unit-tests.ts +143 -0
  40. package/cli/selftune/eval/hooks-to-evals.ts +101 -16
  41. package/cli/selftune/eval/import-skillsbench.ts +221 -0
  42. package/cli/selftune/eval/synthetic-evals.ts +172 -0
  43. package/cli/selftune/eval/unit-test-cli.ts +152 -0
  44. package/cli/selftune/eval/unit-test.ts +196 -0
  45. package/cli/selftune/evolution/deploy-proposal.ts +142 -1
  46. package/cli/selftune/evolution/evidence.ts +26 -0
  47. package/cli/selftune/evolution/evolve-body.ts +586 -0
  48. package/cli/selftune/evolution/evolve.ts +825 -116
  49. package/cli/selftune/evolution/extract-patterns.ts +105 -16
  50. package/cli/selftune/evolution/pareto.ts +314 -0
  51. package/cli/selftune/evolution/propose-body.ts +171 -0
  52. package/cli/selftune/evolution/propose-description.ts +100 -2
  53. package/cli/selftune/evolution/propose-routing.ts +166 -0
  54. package/cli/selftune/evolution/refine-body.ts +141 -0
  55. package/cli/selftune/evolution/rollback.ts +21 -4
  56. package/cli/selftune/evolution/validate-body.ts +254 -0
  57. package/cli/selftune/evolution/validate-proposal.ts +257 -35
  58. package/cli/selftune/evolution/validate-routing.ts +177 -0
  59. package/cli/selftune/grading/auto-grade.ts +200 -0
  60. package/cli/selftune/grading/grade-session.ts +513 -42
  61. package/cli/selftune/grading/pre-gates.ts +104 -0
  62. package/cli/selftune/grading/results.ts +42 -0
  63. package/cli/selftune/hooks/auto-activate.ts +185 -0
  64. package/cli/selftune/hooks/evolution-guard.ts +165 -0
  65. package/cli/selftune/hooks/prompt-log.ts +172 -2
  66. package/cli/selftune/hooks/session-stop.ts +123 -3
  67. package/cli/selftune/hooks/skill-change-guard.ts +112 -0
  68. package/cli/selftune/hooks/skill-eval.ts +119 -3
  69. package/cli/selftune/index.ts +415 -48
  70. package/cli/selftune/ingestors/claude-replay.ts +377 -0
  71. package/cli/selftune/ingestors/codex-rollout.ts +345 -46
  72. package/cli/selftune/ingestors/codex-wrapper.ts +207 -39
  73. package/cli/selftune/ingestors/openclaw-ingest.ts +573 -0
  74. package/cli/selftune/ingestors/opencode-ingest.ts +193 -17
  75. package/cli/selftune/init.ts +376 -16
  76. package/cli/selftune/last.ts +14 -5
  77. package/cli/selftune/localdb/db.ts +63 -0
  78. package/cli/selftune/localdb/materialize.ts +428 -0
  79. package/cli/selftune/localdb/queries.ts +376 -0
  80. package/cli/selftune/localdb/schema.ts +204 -0
  81. package/cli/selftune/memory/writer.ts +447 -0
  82. package/cli/selftune/monitoring/watch.ts +90 -16
  83. package/cli/selftune/normalization.ts +682 -0
  84. package/cli/selftune/observability.ts +19 -44
  85. package/cli/selftune/orchestrate.ts +1073 -0
  86. package/cli/selftune/quickstart.ts +203 -0
  87. package/cli/selftune/repair/skill-usage.ts +576 -0
  88. package/cli/selftune/schedule.ts +561 -0
  89. package/cli/selftune/status.ts +59 -33
  90. package/cli/selftune/sync.ts +627 -0
  91. package/cli/selftune/types.ts +525 -5
  92. package/cli/selftune/utils/canonical-log.ts +45 -0
  93. package/cli/selftune/utils/frontmatter.ts +217 -0
  94. package/cli/selftune/utils/hooks.ts +41 -0
  95. package/cli/selftune/utils/html.ts +27 -0
  96. package/cli/selftune/utils/llm-call.ts +103 -19
  97. package/cli/selftune/utils/math.ts +10 -0
  98. package/cli/selftune/utils/query-filter.ts +139 -0
  99. package/cli/selftune/utils/skill-discovery.ts +340 -0
  100. package/cli/selftune/utils/skill-log.ts +68 -0
  101. package/cli/selftune/utils/skill-usage-confidence.ts +18 -0
  102. package/cli/selftune/utils/transcript.ts +307 -26
  103. package/cli/selftune/utils/trigger-check.ts +89 -0
  104. package/cli/selftune/utils/tui.ts +156 -0
  105. package/cli/selftune/workflows/discover.ts +254 -0
  106. package/cli/selftune/workflows/skill-md-writer.ts +288 -0
  107. package/cli/selftune/workflows/workflows.ts +188 -0
  108. package/package.json +28 -11
  109. package/packages/telemetry-contract/README.md +11 -0
  110. package/packages/telemetry-contract/fixtures/golden.json +87 -0
  111. package/packages/telemetry-contract/fixtures/golden.test.ts +42 -0
  112. package/packages/telemetry-contract/index.ts +1 -0
  113. package/packages/telemetry-contract/package.json +19 -0
  114. package/packages/telemetry-contract/src/index.ts +2 -0
  115. package/packages/telemetry-contract/src/types.ts +163 -0
  116. package/packages/telemetry-contract/src/validators.ts +109 -0
  117. package/skill/SKILL.md +180 -33
  118. package/skill/Workflows/AutoActivation.md +145 -0
  119. package/skill/Workflows/Badge.md +124 -0
  120. package/skill/Workflows/Baseline.md +144 -0
  121. package/skill/Workflows/Composability.md +107 -0
  122. package/skill/Workflows/Contribute.md +94 -0
  123. package/skill/Workflows/Cron.md +132 -0
  124. package/skill/Workflows/Dashboard.md +214 -0
  125. package/skill/Workflows/Doctor.md +63 -14
  126. package/skill/Workflows/Evals.md +110 -18
  127. package/skill/Workflows/EvolutionMemory.md +154 -0
  128. package/skill/Workflows/Evolve.md +181 -21
  129. package/skill/Workflows/EvolveBody.md +159 -0
  130. package/skill/Workflows/Grade.md +36 -31
  131. package/skill/Workflows/ImportSkillsBench.md +117 -0
  132. package/skill/Workflows/Ingest.md +142 -21
  133. package/skill/Workflows/Initialize.md +91 -23
  134. package/skill/Workflows/Orchestrate.md +139 -0
  135. package/skill/Workflows/Replay.md +91 -0
  136. package/skill/Workflows/Rollback.md +23 -4
  137. package/skill/Workflows/Schedule.md +61 -0
  138. package/skill/Workflows/Sync.md +88 -0
  139. package/skill/Workflows/UnitTest.md +150 -0
  140. package/skill/Workflows/Watch.md +33 -1
  141. package/skill/Workflows/Workflows.md +129 -0
  142. package/skill/assets/activation-rules-default.json +26 -0
  143. package/skill/assets/multi-skill-settings.json +63 -0
  144. package/skill/assets/single-skill-settings.json +57 -0
  145. package/skill/references/invocation-taxonomy.md +2 -2
  146. package/skill/references/logs.md +164 -2
  147. package/skill/references/setup-patterns.md +65 -0
  148. package/skill/references/version-history.md +40 -0
  149. package/skill/settings_snippet.json +23 -0
  150. package/templates/activation-rules-default.json +27 -0
  151. package/templates/multi-skill-settings.json +64 -0
  152. package/templates/single-skill-settings.json +58 -0
  153. package/dashboard/index.html +0 -1119
@@ -17,35 +17,59 @@
17
17
  * ~/.claude/skill_usage_log.jsonl
18
18
  */
19
19
 
20
- import { existsSync, readdirSync, statSync } from "node:fs";
21
20
  import { homedir } from "node:os";
22
21
  import { join } from "node:path";
23
- import { QUERY_LOG, SKILL_LOG, TELEMETRY_LOG } from "../constants.js";
24
- import type { QueryLogRecord, SessionTelemetryRecord, SkillUsageRecord } from "../types.js";
22
+ import { CANONICAL_LOG, QUERY_LOG, SKILL_LOG, TELEMETRY_LOG } from "../constants.js";
23
+ import {
24
+ appendCanonicalRecords,
25
+ buildCanonicalExecutionFact,
26
+ buildCanonicalPrompt,
27
+ buildCanonicalSession,
28
+ buildCanonicalSkillInvocation,
29
+ type CanonicalBaseInput,
30
+ deriveInvocationMode,
31
+ derivePromptId,
32
+ deriveSkillInvocationId,
33
+ } from "../normalization.js";
34
+ import type {
35
+ CanonicalRecord,
36
+ QueryLogRecord,
37
+ SessionTelemetryRecord,
38
+ SkillUsageRecord,
39
+ } from "../types.js";
25
40
  import { appendJsonl } from "../utils/jsonl.js";
26
-
27
- const CODEX_SKILLS_DIRS = [
28
- join(process.cwd(), ".codex", "skills"),
29
- join(homedir(), ".codex", "skills"),
30
- ];
41
+ import {
42
+ classifySkillPath,
43
+ containsWholeSkillMention,
44
+ extractExplicitSkillMentions,
45
+ extractSkillNamesFromInstructions,
46
+ findInstalledSkillNames,
47
+ findInstalledSkillPath,
48
+ findRepositorySkillDirs,
49
+ } from "../utils/skill-discovery.js";
50
+
51
+ const SKILL_NAME_CACHE = new Map<string, Set<string>>();
31
52
 
32
53
  /** Return the set of skill names installed in Codex skill directories. */
33
- export function findCodexSkillNames(): Set<string> {
34
- const names = new Set<string>();
35
- for (const dir of CODEX_SKILLS_DIRS) {
36
- if (!existsSync(dir)) continue;
37
- for (const entry of readdirSync(dir)) {
38
- const skillDir = join(dir, entry);
39
- try {
40
- if (statSync(skillDir).isDirectory() && existsSync(join(skillDir, "SKILL.md"))) {
41
- names.add(entry);
42
- }
43
- } catch {
44
- // Skip broken symlinks or inaccessible entries
45
- }
46
- }
47
- }
48
- return names;
54
+ export function findCodexSkillNames(
55
+ cwd: string = process.cwd(),
56
+ homeDir: string = homedir(),
57
+ adminDir: string = "/etc/codex/skills",
58
+ codexHome: string = process.env.CODEX_HOME ?? join(homeDir, ".codex"),
59
+ ): Set<string> {
60
+ const cacheKey = [cwd, homeDir, adminDir, codexHome].join("\u0000");
61
+ const cached = SKILL_NAME_CACHE.get(cacheKey);
62
+ if (cached) return new Set(cached);
63
+
64
+ const names = findInstalledSkillNames([
65
+ ...findRepositorySkillDirs(cwd),
66
+ join(homeDir, ".agents", "skills"),
67
+ adminDir,
68
+ join(codexHome, "skills"),
69
+ join(codexHome, "skills", ".system"),
70
+ ]);
71
+ SKILL_NAME_CACHE.set(cacheKey, names);
72
+ return new Set(names);
49
73
  }
50
74
 
51
75
  /**
@@ -84,12 +108,29 @@ export function parseJsonlStream(lines: string[], skillNames: Set<string>): Pars
84
108
  let inputTokens = 0;
85
109
  let outputTokens = 0;
86
110
  const agentMessages: string[] = [];
87
-
88
- // Precompile word-boundary regex for each skill name (avoids rebuilding per item)
89
- const skillPatterns = Array.from(skillNames, (name) => ({
90
- name,
91
- pattern: new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i"),
92
- }));
111
+ const sessionSkillNames = new Set(skillNames);
112
+ const rememberSessionSkillNames = (text: unknown): void => {
113
+ if (typeof text !== "string" || !text) return;
114
+ for (const skillName of extractSkillNamesFromInstructions(text, sessionSkillNames)) {
115
+ sessionSkillNames.add(skillName);
116
+ }
117
+ };
118
+ const detectTriggeredSkills = (text: unknown): void => {
119
+ if (typeof text !== "string" || !text) return;
120
+ for (const skillName of sessionSkillNames) {
121
+ if (containsWholeSkillMention(text, skillName) && !skillsTriggered.includes(skillName)) {
122
+ skillsTriggered.push(skillName);
123
+ }
124
+ }
125
+ };
126
+ const detectExplicitPromptSkillMentions = (text: unknown): void => {
127
+ if (typeof text !== "string" || !text) return;
128
+ for (const skillName of extractExplicitSkillMentions(text, sessionSkillNames)) {
129
+ if (!skillsTriggered.includes(skillName)) {
130
+ skillsTriggered.push(skillName);
131
+ }
132
+ }
133
+ };
93
134
 
94
135
  for (const rawLine of lines) {
95
136
  const line = rawLine.trim();
@@ -106,6 +147,12 @@ export function parseJsonlStream(lines: string[], skillNames: Set<string>): Pars
106
147
 
107
148
  if (etype === "thread.started") {
108
149
  threadId = (event.thread_id as string) ?? "unknown";
150
+ } else if (etype === "session_meta") {
151
+ const payload = (event.payload as Record<string, unknown>) ?? {};
152
+ rememberSessionSkillNames(payload.instructions);
153
+ rememberSessionSkillNames(
154
+ (payload.base_instructions as Record<string, unknown> | undefined)?.text,
155
+ );
109
156
  } else if (etype === "turn.started") {
110
157
  turns += 1;
111
158
  } else if (etype === "turn.completed") {
@@ -137,6 +184,7 @@ export function parseJsonlStream(lines: string[], skillNames: Set<string>): Pars
137
184
  } else if (itemType === "agent_message") {
138
185
  const text = (item.text as string) ?? "";
139
186
  if (text) agentMessages.push(text.slice(0, 500));
187
+ detectTriggeredSkills(text);
140
188
  } else if (itemType === "reasoning") {
141
189
  toolCalls.reasoning = (toolCalls.reasoning ?? 0) + 1;
142
190
  }
@@ -144,14 +192,32 @@ export function parseJsonlStream(lines: string[], skillNames: Set<string>): Pars
144
192
 
145
193
  // Detect skill names in text on completed events (whole-word match)
146
194
  const textContent = ((item.text as string) ?? "") + ((item.command as string) ?? "");
147
- for (const { name: sName, pattern } of skillPatterns) {
148
- if (
149
- etype === "item.completed" &&
150
- !skillsTriggered.includes(sName) &&
151
- pattern.test(textContent)
152
- ) {
153
- skillsTriggered.push(sName);
195
+ if (etype === "item.completed") {
196
+ detectTriggeredSkills(textContent);
197
+ }
198
+ } else if (etype === "response_item") {
199
+ const payload = (event.payload as Record<string, unknown>) ?? {};
200
+ const itemType = (payload.type as string) ?? "";
201
+ if (itemType === "function_call") {
202
+ detectTriggeredSkills(payload.arguments);
203
+ } else if (itemType === "message") {
204
+ const content = Array.isArray(payload.content)
205
+ ? payload.content
206
+ .map((part) =>
207
+ typeof part === "object" && part
208
+ ? (((part as Record<string, unknown>).text as string | undefined) ?? "")
209
+ : "",
210
+ )
211
+ .join("\n")
212
+ : "";
213
+ rememberSessionSkillNames(content);
214
+ if ((payload.role as string) === "assistant") {
215
+ detectTriggeredSkills(content);
216
+ } else if ((payload.role as string) === "user") {
217
+ detectExplicitPromptSkillMentions(content);
154
218
  }
219
+ } else if (itemType === "agent_reasoning") {
220
+ detectTriggeredSkills(payload.text);
155
221
  }
156
222
  } else if (etype === "error") {
157
223
  errors += 1;
@@ -210,13 +276,25 @@ export function logSkillTrigger(
210
276
  skillName: string,
211
277
  prompt: string,
212
278
  sessionId: string,
279
+ cwd: string = process.cwd(),
213
280
  logPath: string = SKILL_LOG,
281
+ homeDir: string = homedir(),
282
+ codexHome: string = process.env.CODEX_HOME ?? join(homeDir, ".codex"),
214
283
  ): void {
284
+ const skillPath =
285
+ findInstalledSkillPath(skillName, [
286
+ ...findRepositorySkillDirs(cwd),
287
+ join(homeDir, ".agents", "skills"),
288
+ "/etc/codex/skills",
289
+ join(codexHome, "skills"),
290
+ join(codexHome, "skills", ".system"),
291
+ ]) ?? `(codex:${skillName})`;
215
292
  const record: SkillUsageRecord = {
216
293
  timestamp: new Date().toISOString(),
217
294
  session_id: sessionId,
218
295
  skill_name: skillName,
219
- skill_path: `(codex:${skillName})`,
296
+ skill_path: skillPath,
297
+ ...classifySkillPath(skillPath, homeDir, codexHome),
220
298
  query: prompt,
221
299
  triggered: true,
222
300
  source: "codex",
@@ -224,6 +302,92 @@ export function logSkillTrigger(
224
302
  appendJsonl(logPath, record);
225
303
  }
226
304
 
305
+ /** Build canonical records from a wrapper session. */
306
+ export function buildCanonicalRecordsFromWrapper(
307
+ metrics: ParsedCodexStream,
308
+ prompt: string,
309
+ sessionId: string,
310
+ cwd: string,
311
+ ): CanonicalRecord[] {
312
+ const records: CanonicalRecord[] = [];
313
+ const now = new Date().toISOString();
314
+ const baseInput: CanonicalBaseInput = {
315
+ platform: "codex",
316
+ capture_mode: "wrapper",
317
+ source_session_kind: "interactive",
318
+ session_id: sessionId,
319
+ raw_source_ref: { event_type: "codex_wrapper" },
320
+ };
321
+
322
+ records.push(
323
+ buildCanonicalSession({
324
+ ...baseInput,
325
+ started_at: now,
326
+ workspace_path: cwd || undefined,
327
+ }),
328
+ );
329
+
330
+ const promptEmitted = Boolean(prompt && prompt.length >= 4);
331
+ const promptId = promptEmitted ? derivePromptId(sessionId, 0) : undefined;
332
+
333
+ if (promptId) {
334
+ records.push(
335
+ buildCanonicalPrompt({
336
+ ...baseInput,
337
+ prompt_id: promptId,
338
+ occurred_at: now,
339
+ prompt_text: prompt,
340
+ prompt_index: 0,
341
+ }),
342
+ );
343
+ }
344
+
345
+ for (let i = 0; i < metrics.skills_triggered.length; i++) {
346
+ const skillName = metrics.skills_triggered[i];
347
+ const { invocation_mode, confidence } = deriveInvocationMode({
348
+ is_text_mention_only: true,
349
+ });
350
+ records.push(
351
+ buildCanonicalSkillInvocation({
352
+ ...baseInput,
353
+ skill_invocation_id: deriveSkillInvocationId(sessionId, skillName, i),
354
+ occurred_at: now,
355
+ matched_prompt_id: promptId,
356
+ skill_name: skillName,
357
+ skill_path: `(codex:${skillName})`,
358
+ invocation_mode,
359
+ triggered: true,
360
+ confidence,
361
+ }),
362
+ );
363
+ }
364
+
365
+ records.push(
366
+ buildCanonicalExecutionFact({
367
+ ...baseInput,
368
+ occurred_at: now,
369
+ prompt_id: promptId,
370
+ tool_calls_json: metrics.tool_calls,
371
+ total_tool_calls: metrics.total_tool_calls,
372
+ bash_commands_redacted: metrics.bash_commands,
373
+ assistant_turns: metrics.assistant_turns,
374
+ errors_encountered: metrics.errors_encountered,
375
+ input_tokens: metrics.input_tokens ?? undefined,
376
+ output_tokens: metrics.output_tokens ?? undefined,
377
+ }),
378
+ );
379
+
380
+ return records;
381
+ }
382
+
383
+ /** Write canonical records to appropriate log files. */
384
+ export function logCanonicalRecords(
385
+ records: CanonicalRecord[],
386
+ canonicalLogPath: string = CANONICAL_LOG,
387
+ ): void {
388
+ appendCanonicalRecords(records, canonicalLogPath);
389
+ }
390
+
227
391
  // --- CLI main ---
228
392
  export async function cliMain(): Promise<void> {
229
393
  const extraArgs = process.argv.slice(2);
@@ -308,9 +472,13 @@ export async function cliMain(): Promise<void> {
308
472
  logTelemetry(metricsWithoutThread, prompt, sessionId, cwd);
309
473
 
310
474
  for (const skillName of metrics.skills_triggered) {
311
- logSkillTrigger(skillName, prompt, sessionId);
475
+ logSkillTrigger(skillName, prompt, sessionId, cwd);
312
476
  }
313
477
 
478
+ // Emit canonical records (additive)
479
+ const canonical = buildCanonicalRecordsFromWrapper(metrics, prompt, sessionId, cwd);
480
+ logCanonicalRecords(canonical);
481
+
314
482
  process.exit(proc.exitCode ?? 0);
315
483
  } catch (e) {
316
484
  if (e instanceof Error && e.message.includes("ENOENT")) {