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
@@ -25,8 +25,24 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
25
25
  import { homedir } from "node:os";
26
26
  import { basename, join } from "node:path";
27
27
  import { parseArgs } from "node:util";
28
- import { QUERY_LOG, SKILL_LOG, TELEMETRY_LOG } from "../constants.js";
29
- import type { QueryLogRecord, SkillUsageRecord } from "../types.js";
28
+ import { CANONICAL_LOG, QUERY_LOG, SKILL_LOG, TELEMETRY_LOG } from "../constants.js";
29
+ import {
30
+ appendCanonicalRecords,
31
+ buildCanonicalExecutionFact,
32
+ buildCanonicalPrompt,
33
+ buildCanonicalSession,
34
+ buildCanonicalSkillInvocation,
35
+ type CanonicalBaseInput,
36
+ deriveInvocationMode,
37
+ derivePromptId,
38
+ deriveSkillInvocationId,
39
+ } from "../normalization.js";
40
+ import type {
41
+ CanonicalRecord,
42
+ QueryLogRecord,
43
+ SessionTelemetryRecord,
44
+ SkillUsageRecord,
45
+ } from "../types.js";
30
46
  import { appendJsonl, loadMarker, saveMarker } from "../utils/jsonl.js";
31
47
 
32
48
  const XDG_DATA_HOME = process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
@@ -48,6 +64,26 @@ const OPENCODE_SKILLS_DIRS = [
48
64
  join(homedir(), ".config", "opencode", "skills"),
49
65
  ];
50
66
 
67
+ interface TriggeredSkillDetection {
68
+ skill_name: string;
69
+ has_skill_md_read: boolean;
70
+ }
71
+
72
+ function escapeRegExp(value: string): string {
73
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
74
+ }
75
+
76
+ function containsWholeSkillMention(text: string, skillName: string): boolean {
77
+ const trimmedSkillName = skillName.trim();
78
+ if (!text || !trimmedSkillName) return false;
79
+
80
+ const pattern = new RegExp(
81
+ `(^|[^A-Za-z0-9_])${escapeRegExp(trimmedSkillName)}([^A-Za-z0-9_]|$)`,
82
+ "i",
83
+ );
84
+ return pattern.test(text);
85
+ }
86
+
51
87
  /** Return skill names from OpenCode skill directories. */
52
88
  export function findSkillNames(dirs: string[] = OPENCODE_SKILLS_DIRS): Set<string> {
53
89
  const names = new Set<string>();
@@ -79,9 +115,12 @@ export interface ParsedSession {
79
115
  total_tool_calls: number;
80
116
  bash_commands: string[];
81
117
  skills_triggered: string[];
118
+ skill_detections?: TriggeredSkillDetection[];
82
119
  assistant_turns: number;
83
120
  errors_encountered: number;
84
121
  transcript_chars: number;
122
+ /** True when local session JSON is metadata-only (no embedded messages). */
123
+ is_metadata_only?: boolean;
85
124
  }
86
125
 
87
126
  /** Return a human-readable schema summary for --show-schema. */
@@ -202,10 +241,24 @@ export function readSessionsFromSqlite(
202
241
  let firstUserQuery = "";
203
242
  const toolCalls: Record<string, number> = {};
204
243
  const bashCommands: string[] = [];
205
- const skillsTriggered: string[] = [];
244
+ const skillDetections = new Map<string, TriggeredSkillDetection>();
206
245
  let errors = 0;
207
246
  let assistantTurns = 0;
208
247
 
248
+ const noteSkillDetection = (skillName: string, hasSkillMdRead: boolean): void => {
249
+ const normalizedSkillName = skillName.trim();
250
+ if (!normalizedSkillName) return;
251
+ const existing = skillDetections.get(normalizedSkillName);
252
+ if (existing) {
253
+ existing.has_skill_md_read = existing.has_skill_md_read || hasSkillMdRead;
254
+ return;
255
+ }
256
+ skillDetections.set(normalizedSkillName, {
257
+ skill_name: normalizedSkillName,
258
+ has_skill_md_read: hasSkillMdRead,
259
+ });
260
+ };
261
+
209
262
  for (const msg of msgRows) {
210
263
  const role = (msg.role as string) ?? "";
211
264
  const blocks = normalizeContent(msg.content ?? "[]");
@@ -251,9 +304,7 @@ export function readSessionsFromSqlite(
251
304
  const filePath = (inp.file_path as string) ?? (inp.path as string) ?? "";
252
305
  if (basename(filePath).toUpperCase() === "SKILL.MD") {
253
306
  const skillName = basename(join(filePath, ".."));
254
- if (!skillsTriggered.includes(skillName)) {
255
- skillsTriggered.push(skillName);
256
- }
307
+ noteSkillDetection(skillName, true);
257
308
  }
258
309
  }
259
310
  }
@@ -271,8 +322,8 @@ export function readSessionsFromSqlite(
271
322
  // Check text content for skill name mentions
272
323
  const textContent = (block.text as string) ?? "";
273
324
  for (const skillName of skillNames) {
274
- if (textContent.includes(skillName) && !skillsTriggered.includes(skillName)) {
275
- skillsTriggered.push(skillName);
325
+ if (containsWholeSkillMention(textContent, skillName)) {
326
+ noteSkillDetection(skillName, false);
276
327
  }
277
328
  }
278
329
  }
@@ -299,7 +350,8 @@ export function readSessionsFromSqlite(
299
350
  tool_calls: toolCalls,
300
351
  total_tool_calls: Object.values(toolCalls).reduce((a, b) => a + b, 0),
301
352
  bash_commands: bashCommands,
302
- skills_triggered: skillsTriggered,
353
+ skills_triggered: [...skillDetections.values()].map((entry) => entry.skill_name),
354
+ skill_detections: [...skillDetections.values()],
303
355
  assistant_turns: assistantTurns,
304
356
  errors_encountered: errors,
305
357
  transcript_chars: 0,
@@ -349,13 +401,30 @@ export function readSessionsFromJsonFiles(
349
401
  const timestamp = new Date(created * 1000).toISOString();
350
402
  const messages = (data.messages as Array<Record<string, unknown>>) ?? [];
351
403
 
404
+ // Detect metadata-only session files (no message bodies)
405
+ const isMetadataOnly = messages.length === 0;
406
+
352
407
  let firstUserQuery = "";
353
408
  const toolCalls: Record<string, number> = {};
354
409
  const bashCommands: string[] = [];
355
- const skillsTriggered: string[] = [];
410
+ const skillDetections = new Map<string, TriggeredSkillDetection>();
356
411
  let errors = 0;
357
412
  let turns = 0;
358
413
 
414
+ const noteSkillDetection = (skillName: string, hasSkillMdRead: boolean): void => {
415
+ const normalizedSkillName = skillName.trim();
416
+ if (!normalizedSkillName) return;
417
+ const existing = skillDetections.get(normalizedSkillName);
418
+ if (existing) {
419
+ existing.has_skill_md_read = existing.has_skill_md_read || hasSkillMdRead;
420
+ return;
421
+ }
422
+ skillDetections.set(normalizedSkillName, {
423
+ skill_name: normalizedSkillName,
424
+ has_skill_md_read: hasSkillMdRead,
425
+ });
426
+ };
427
+
359
428
  for (const msg of messages) {
360
429
  const role = (msg.role as string) ?? "";
361
430
  const blocks = normalizeContent(msg.content ?? []);
@@ -385,17 +454,15 @@ export function readSessionsFromJsonFiles(
385
454
  const fp = (inp.file_path as string) ?? "";
386
455
  if (basename(fp).toUpperCase() === "SKILL.MD") {
387
456
  const sn = basename(join(fp, ".."));
388
- if (!skillsTriggered.includes(sn)) {
389
- skillsTriggered.push(sn);
390
- }
457
+ noteSkillDetection(sn, true);
391
458
  }
392
459
  }
393
460
  }
394
461
 
395
462
  const text = (block.text as string) ?? "";
396
463
  for (const skillName of skillNames) {
397
- if (text.includes(skillName) && !skillsTriggered.includes(skillName)) {
398
- skillsTriggered.push(skillName);
464
+ if (containsWholeSkillMention(text, skillName)) {
465
+ noteSkillDetection(skillName, false);
399
466
  }
400
467
  }
401
468
  }
@@ -422,10 +489,12 @@ export function readSessionsFromJsonFiles(
422
489
  tool_calls: toolCalls,
423
490
  total_tool_calls: Object.values(toolCalls).reduce((a, b) => a + b, 0),
424
491
  bash_commands: bashCommands,
425
- skills_triggered: skillsTriggered,
492
+ skills_triggered: [...skillDetections.values()].map((entry) => entry.skill_name),
493
+ skill_detections: [...skillDetections.values()],
426
494
  assistant_turns: turns,
427
495
  errors_encountered: errors,
428
496
  transcript_chars: statSync(filePath).size,
497
+ is_metadata_only: isMetadataOnly,
429
498
  });
430
499
  }
431
500
 
@@ -439,6 +508,7 @@ export function writeSession(
439
508
  queryLogPath: string = QUERY_LOG,
440
509
  telemetryLogPath: string = TELEMETRY_LOG,
441
510
  skillLogPath: string = SKILL_LOG,
511
+ canonicalLogPath: string = CANONICAL_LOG,
442
512
  ): void {
443
513
  const { query: prompt, session_id: sessionId, skills_triggered: skills } = session;
444
514
 
@@ -460,7 +530,21 @@ export function writeSession(
460
530
  appendJsonl(queryLogPath, queryRecord, "all_queries");
461
531
  }
462
532
 
463
- const { query: _q, ...telemetry } = session;
533
+ const telemetry: SessionTelemetryRecord = {
534
+ timestamp: session.timestamp,
535
+ session_id: session.session_id,
536
+ cwd: session.cwd,
537
+ transcript_path: session.transcript_path,
538
+ tool_calls: session.tool_calls,
539
+ total_tool_calls: session.total_tool_calls,
540
+ bash_commands: session.bash_commands,
541
+ skills_triggered: session.skills_triggered,
542
+ assistant_turns: session.assistant_turns,
543
+ errors_encountered: session.errors_encountered,
544
+ transcript_chars: session.transcript_chars,
545
+ last_user_query: session.last_user_query,
546
+ source: session.source,
547
+ };
464
548
  appendJsonl(telemetryLogPath, telemetry, "session_telemetry");
465
549
 
466
550
  for (const skillName of skills) {
@@ -475,6 +559,98 @@ export function writeSession(
475
559
  };
476
560
  appendJsonl(skillLogPath, skillRecord, "skill_usage");
477
561
  }
562
+
563
+ // --- Canonical normalization records (additive) ---
564
+ const canonicalRecords = buildCanonicalRecordsFromOpenCode(session);
565
+ appendCanonicalRecords(canonicalRecords, canonicalLogPath);
566
+ }
567
+
568
+ /** Build canonical records from a parsed OpenCode session. */
569
+ export function buildCanonicalRecordsFromOpenCode(session: ParsedSession): CanonicalRecord[] {
570
+ const records: CanonicalRecord[] = [];
571
+ const sourceKind = session.is_metadata_only ? ("replayed" as const) : ("replayed" as const);
572
+ const baseInput: CanonicalBaseInput = {
573
+ platform: "opencode",
574
+ capture_mode: "batch_ingest",
575
+ source_session_kind: sourceKind,
576
+ session_id: session.session_id,
577
+ raw_source_ref: {
578
+ path: session.transcript_path,
579
+ event_type: session.source,
580
+ metadata: session.is_metadata_only ? { metadata_only: true } : undefined,
581
+ },
582
+ };
583
+
584
+ records.push(
585
+ buildCanonicalSession({
586
+ ...baseInput,
587
+ started_at: session.timestamp,
588
+ workspace_path: session.cwd || undefined,
589
+ }),
590
+ );
591
+
592
+ const promptEmitted = Boolean(
593
+ session.query && session.query.length >= 4 && !session.is_metadata_only,
594
+ );
595
+ const promptId = promptEmitted ? derivePromptId(session.session_id, 0) : undefined;
596
+
597
+ if (promptId) {
598
+ records.push(
599
+ buildCanonicalPrompt({
600
+ ...baseInput,
601
+ prompt_id: promptId,
602
+ occurred_at: session.timestamp,
603
+ prompt_text: session.query,
604
+ prompt_index: 0,
605
+ }),
606
+ );
607
+ }
608
+
609
+ const skillDetections =
610
+ session.skill_detections ??
611
+ session.skills_triggered.map((skillName) => ({
612
+ skill_name: skillName,
613
+ has_skill_md_read: false,
614
+ }));
615
+
616
+ for (let i = 0; i < skillDetections.length; i++) {
617
+ const detection = skillDetections[i];
618
+ const skillName = detection.skill_name;
619
+ const { invocation_mode, confidence } = deriveInvocationMode({
620
+ has_skill_md_read: detection.has_skill_md_read,
621
+ is_text_mention_only: !detection.has_skill_md_read,
622
+ });
623
+ records.push(
624
+ buildCanonicalSkillInvocation({
625
+ ...baseInput,
626
+ skill_invocation_id: deriveSkillInvocationId(session.session_id, skillName, i),
627
+ occurred_at: session.timestamp,
628
+ matched_prompt_id: promptId,
629
+ skill_name: skillName,
630
+ skill_path: `(opencode:${skillName})`,
631
+ invocation_mode,
632
+ triggered: true,
633
+ confidence,
634
+ }),
635
+ );
636
+ }
637
+
638
+ if (!session.is_metadata_only) {
639
+ records.push(
640
+ buildCanonicalExecutionFact({
641
+ ...baseInput,
642
+ occurred_at: session.timestamp,
643
+ prompt_id: promptId,
644
+ tool_calls_json: session.tool_calls,
645
+ total_tool_calls: session.total_tool_calls,
646
+ bash_commands_redacted: session.bash_commands,
647
+ assistant_turns: session.assistant_turns,
648
+ errors_encountered: session.errors_encountered,
649
+ }),
650
+ );
651
+ }
652
+
653
+ return records;
478
654
  }
479
655
 
480
656
  // --- CLI main ---