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
@@ -0,0 +1,188 @@
1
+ /**
2
+ * workflows.ts
3
+ *
4
+ * CLI entry point and formatter for multi-skill workflow discovery and management.
5
+ *
6
+ * Exports:
7
+ * - formatWorkflows() (pure formatter, deterministic)
8
+ * - cliMain() (reads logs, discovers workflows, prints output or saves)
9
+ */
10
+
11
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
12
+ import { parseArgs } from "node:util";
13
+ import { TELEMETRY_LOG } from "../constants.js";
14
+ import type {
15
+ CodifiedWorkflow,
16
+ SessionTelemetryRecord,
17
+ WorkflowDiscoveryReport,
18
+ } from "../types.js";
19
+ import { readJsonl } from "../utils/jsonl.js";
20
+ import { readEffectiveSkillUsageRecords } from "../utils/skill-log.js";
21
+ import { discoverWorkflows } from "./discover.js";
22
+ import { appendWorkflow } from "./skill-md-writer.js";
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // formatWorkflows — pure formatter
26
+ // ---------------------------------------------------------------------------
27
+
28
+ export function formatWorkflows(report: WorkflowDiscoveryReport): string {
29
+ if (report.workflows.length === 0) {
30
+ return "No workflows discovered.";
31
+ }
32
+
33
+ const lines: string[] = [];
34
+ lines.push(`Discovered Workflows (from ${report.total_sessions_analyzed} sessions):`);
35
+ lines.push("");
36
+
37
+ for (let i = 0; i < report.workflows.length; i++) {
38
+ const wf = report.workflows[i];
39
+ const chain = wf.skills.join(" \u2192 ");
40
+ const synergy = wf.synergy_score.toFixed(2);
41
+ const consistency = Math.round(wf.sequence_consistency * 100);
42
+ const completion = Math.round(wf.completion_rate * 100);
43
+
44
+ lines.push(` ${i + 1}. ${chain}`);
45
+ lines.push(
46
+ ` Occurrences: ${wf.occurrence_count} | Synergy: ${synergy} | Consistency: ${consistency}% | Completion: ${completion}%`,
47
+ );
48
+ if (wf.representative_query) {
49
+ lines.push(` Common trigger: "${wf.representative_query}"`);
50
+ }
51
+ if (i < report.workflows.length - 1) {
52
+ lines.push("");
53
+ }
54
+ }
55
+
56
+ return lines.join("\n");
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // cliMain — reads logs, discovers workflows, prints or saves
61
+ // ---------------------------------------------------------------------------
62
+
63
+ export async function cliMain(): Promise<void> {
64
+ const { values, positionals } = parseArgs({
65
+ options: {
66
+ "min-occurrences": { type: "string" },
67
+ window: { type: "string" },
68
+ skill: { type: "string" },
69
+ "skill-path": { type: "string" },
70
+ json: { type: "boolean" },
71
+ },
72
+ strict: true,
73
+ allowPositionals: true,
74
+ });
75
+
76
+ const subcommand = positionals[0];
77
+ const minOccurrences = values["min-occurrences"]
78
+ ? Number.parseInt(values["min-occurrences"], 10)
79
+ : undefined;
80
+ if (minOccurrences !== undefined && (Number.isNaN(minOccurrences) || minOccurrences < 0)) {
81
+ console.error("[ERROR] --min-occurrences must be a non-negative integer.");
82
+ process.exit(1);
83
+ }
84
+ const window = values.window ? Number.parseInt(values.window, 10) : undefined;
85
+ if (window !== undefined && (Number.isNaN(window) || window < 0)) {
86
+ console.error("[ERROR] --window must be a non-negative integer.");
87
+ process.exit(1);
88
+ }
89
+
90
+ // Read telemetry and skill usage logs
91
+ const telemetry = readJsonl<SessionTelemetryRecord>(TELEMETRY_LOG);
92
+ const usage = readEffectiveSkillUsageRecords();
93
+
94
+ // Discover workflows
95
+ const report = discoverWorkflows(telemetry, usage, {
96
+ minOccurrences,
97
+ window,
98
+ skill: values.skill,
99
+ });
100
+
101
+ if (subcommand === "save") {
102
+ // Save subcommand: find workflow, append to SKILL.md
103
+ const nameArg = positionals[1];
104
+ if (!nameArg) {
105
+ console.error("[ERROR] Usage: selftune workflows save <name-or-index>");
106
+ process.exit(1);
107
+ }
108
+
109
+ // Match by numeric index (1-based) or workflow_id
110
+ let workflow = report.workflows.find((w) => w.workflow_id === nameArg);
111
+ if (!workflow) {
112
+ const idx = Number.parseInt(nameArg, 10);
113
+ if (!Number.isNaN(idx) && idx >= 1 && idx <= report.workflows.length) {
114
+ workflow = report.workflows[idx - 1];
115
+ }
116
+ }
117
+
118
+ if (!workflow) {
119
+ console.error(`[ERROR] No workflow found matching "${nameArg}".`);
120
+ console.error("Run 'selftune workflows' to see discovered workflows and their indices.");
121
+ process.exit(1);
122
+ }
123
+
124
+ // Determine SKILL.md path
125
+ let skillPath = values["skill-path"];
126
+ if (!skillPath) {
127
+ // Filter usage records to only sessions that contributed to this workflow
128
+ const sessionSet = new Set(workflow.session_ids);
129
+ const firstSkill = workflow.skills[0];
130
+ const matchingRecords = usage.filter(
131
+ (u) => u.skill_name === firstSkill && sessionSet.has(u.session_id),
132
+ );
133
+
134
+ // Collect unique skill_paths from matching records
135
+ const uniquePaths = [...new Set(matchingRecords.map((r) => r.skill_path))];
136
+
137
+ if (uniquePaths.length === 1) {
138
+ skillPath = uniquePaths[0];
139
+ } else if (uniquePaths.length > 1) {
140
+ // Ambiguous: multiple SKILL.md paths found across contributing sessions
141
+ console.error(`[ERROR] Multiple SKILL.md paths found for "${firstSkill}":`);
142
+ for (const p of uniquePaths) {
143
+ console.error(` - ${p}`);
144
+ }
145
+ console.error("Use --skill-path to specify which one to update.");
146
+ process.exit(1);
147
+ }
148
+ }
149
+
150
+ if (!skillPath || !existsSync(skillPath)) {
151
+ console.error(`[ERROR] Could not determine SKILL.md path. Use --skill-path to specify.`);
152
+ process.exit(1);
153
+ }
154
+
155
+ // Build CodifiedWorkflow
156
+ const codified: CodifiedWorkflow = {
157
+ name: workflow.skills.join("-"),
158
+ skills: workflow.skills,
159
+ description: workflow.representative_query || undefined,
160
+ source: "discovered",
161
+ discovered_from: {
162
+ workflow_id: workflow.workflow_id,
163
+ occurrence_count: workflow.occurrence_count,
164
+ synergy_score: workflow.synergy_score,
165
+ },
166
+ };
167
+
168
+ // Read, append, write
169
+ const content = readFileSync(skillPath, "utf-8");
170
+ const updated = appendWorkflow(content, codified);
171
+
172
+ if (updated === content) {
173
+ console.log(`Workflow "${codified.name}" already exists in ${skillPath}`);
174
+ } else {
175
+ writeFileSync(skillPath, updated, "utf-8");
176
+ console.log(`Saved workflow "${codified.name}" to ${skillPath}`);
177
+ }
178
+
179
+ return;
180
+ }
181
+
182
+ // Default: discover and display
183
+ if (values.json || !process.stdout.isTTY) {
184
+ console.log(JSON.stringify(report, null, 2));
185
+ } else {
186
+ console.log(formatWorkflows(report));
187
+ }
188
+ }
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "selftune",
3
- "version": "0.1.4",
4
- "description": "Skill observability and continuous improvement CLI for agent platforms",
3
+ "version": "0.2.1",
4
+ "description": "Self-improving skills CLI for AI agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Daniel Petro",
8
- "homepage": "https://github.com/WellDunDun/selftune#readme",
8
+ "homepage": "https://github.com/selftune-dev/selftune#readme",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "git+https://github.com/WellDunDun/selftune.git"
11
+ "url": "git+https://github.com/selftune-dev/selftune.git"
12
12
  },
13
13
  "bugs": {
14
- "url": "https://github.com/WellDunDun/selftune/issues"
14
+ "url": "https://github.com/selftune-dev/selftune/issues"
15
15
  },
16
16
  "funding": {
17
17
  "type": "github",
@@ -20,7 +20,7 @@
20
20
  "keywords": [
21
21
  "selftune",
22
22
  "skill",
23
- "observability",
23
+ "self-improving",
24
24
  "claude-code",
25
25
  "codex",
26
26
  "opencode",
@@ -37,22 +37,39 @@
37
37
  "selftune": "bin/selftune.cjs"
38
38
  },
39
39
  "files": [
40
+ "assets/",
40
41
  "bin/",
41
42
  "cli/selftune/",
42
- "dashboard/",
43
+ "apps/local-dashboard/dist/",
44
+ "packages/telemetry-contract/",
45
+ "templates/",
46
+ ".claude/agents/",
43
47
  "skill/",
44
48
  "README.md",
45
49
  "CHANGELOG.md"
46
50
  ],
47
51
  "scripts": {
48
- "lint": "bunx biome check .",
49
- "lint:fix": "bunx biome check --write .",
52
+ "dev": "sh -c 'if lsof -iTCP:7888 -sTCP:LISTEN >/dev/null 2>&1; then if curl -fsS http://127.0.0.1:7888/api/health | grep -q selftune-dashboard; then echo \"Using existing dashboard server on 7888\"; cd apps/local-dashboard && bun install && bunx vite --strictPort; else echo \"Port 7888 is occupied by a non-selftune service\"; exit 1; fi; else cd apps/local-dashboard && bun install && bun run dev; fi'",
53
+ "dev:dashboard": "bun run cli/selftune/index.ts dashboard --port 7888 --no-open",
54
+ "lint": "bunx @biomejs/biome check .",
55
+ "lint:fix": "bunx @biomejs/biome check --write .",
50
56
  "lint:arch": "bun run lint-architecture.ts",
51
57
  "test": "bun test",
52
- "check": "bun run lint && bun run lint:arch && bun test"
58
+ "test:fast": "bun test $(find tests -name '*.test.ts' ! -name 'evolve.test.ts' ! -name 'integration.test.ts' ! -name 'dashboard-server.test.ts' ! -path '*/blog-proof/*')",
59
+ "test:slow": "bun test tests/evolution/evolve.test.ts tests/evolution/integration.test.ts tests/monitoring/integration.test.ts tests/dashboard/dashboard-server.test.ts",
60
+ "build:dashboard": "cd apps/local-dashboard && bun install && bunx vite build",
61
+ "prepublishOnly": "bun run build:dashboard",
62
+ "check": "bun run lint && bun run lint:arch && bun test",
63
+ "start": "bun run cli/selftune/index.ts --help"
64
+ },
65
+ "workspaces": [
66
+ "packages/*"
67
+ ],
68
+ "dependencies": {
69
+ "@selftune/telemetry-contract": "workspace:*"
53
70
  },
54
71
  "devDependencies": {
55
- "@biomejs/biome": "^2.4.4",
72
+ "@biomejs/biome": "2.4.6",
56
73
  "@types/bun": "^1.1.0"
57
74
  }
58
75
  }
@@ -0,0 +1,11 @@
1
+ # @selftune/telemetry-contract
2
+
3
+ Canonical telemetry contract shared between local CLI normalization and cloud ingestion.
4
+
5
+ This package is intentionally small:
6
+
7
+ - canonical enums
8
+ - canonical record types
9
+ - lightweight runtime guards
10
+
11
+ It does not include adapter logic, projections, or product-specific analytics.
@@ -0,0 +1,87 @@
1
+ [
2
+ {
3
+ "_description": "Minimal valid session record",
4
+ "record_kind": "session",
5
+ "schema_version": "2.0",
6
+ "normalizer_version": "0.2.1",
7
+ "normalized_at": "2026-01-15T12:00:00Z",
8
+ "platform": "claude_code",
9
+ "capture_mode": "hook",
10
+ "raw_source_ref": { "path": "/tmp/raw/session-001.jsonl" },
11
+ "source_session_kind": "interactive",
12
+ "session_id": "golden-session-001",
13
+ "started_at": "2026-01-15T11:50:00Z",
14
+ "ended_at": "2026-01-15T12:05:00Z",
15
+ "completion_status": "completed"
16
+ },
17
+ {
18
+ "_description": "Minimal valid prompt record",
19
+ "record_kind": "prompt",
20
+ "schema_version": "2.0",
21
+ "normalizer_version": "0.2.1",
22
+ "normalized_at": "2026-01-15T12:00:00Z",
23
+ "platform": "claude_code",
24
+ "capture_mode": "hook",
25
+ "raw_source_ref": { "path": "/tmp/raw/session-001.jsonl", "line": 3 },
26
+ "source_session_kind": "interactive",
27
+ "session_id": "golden-session-001",
28
+ "prompt_id": "golden-prompt-001",
29
+ "occurred_at": "2026-01-15T11:51:00Z",
30
+ "prompt_text": "Fix the login bug",
31
+ "prompt_kind": "user",
32
+ "is_actionable": true
33
+ },
34
+ {
35
+ "_description": "Minimal valid skill_invocation record",
36
+ "record_kind": "skill_invocation",
37
+ "schema_version": "2.0",
38
+ "normalizer_version": "0.2.1",
39
+ "normalized_at": "2026-01-15T12:00:00Z",
40
+ "platform": "claude_code",
41
+ "capture_mode": "hook",
42
+ "raw_source_ref": { "path": "/tmp/raw/session-001.jsonl", "line": 7 },
43
+ "source_session_kind": "interactive",
44
+ "session_id": "golden-session-001",
45
+ "skill_invocation_id": "golden-invocation-001",
46
+ "occurred_at": "2026-01-15T11:52:00Z",
47
+ "matched_prompt_id": "golden-prompt-001",
48
+ "skill_name": "commit",
49
+ "invocation_mode": "explicit",
50
+ "triggered": true,
51
+ "confidence": 1.0
52
+ },
53
+ {
54
+ "_description": "Minimal valid execution_fact record",
55
+ "record_kind": "execution_fact",
56
+ "schema_version": "2.0",
57
+ "normalizer_version": "0.2.1",
58
+ "normalized_at": "2026-01-15T12:00:00Z",
59
+ "platform": "claude_code",
60
+ "capture_mode": "hook",
61
+ "raw_source_ref": { "path": "/tmp/raw/session-001.jsonl", "line": 15 },
62
+ "source_session_kind": "interactive",
63
+ "session_id": "golden-session-001",
64
+ "occurred_at": "2026-01-15T12:04:00Z",
65
+ "tool_calls_json": { "Read": 5, "Edit": 3, "Bash": 2 },
66
+ "total_tool_calls": 10,
67
+ "bash_commands_redacted": ["git status", "bun test"],
68
+ "assistant_turns": 4,
69
+ "errors_encountered": 0,
70
+ "completion_status": "completed"
71
+ },
72
+ {
73
+ "_description": "Minimal valid normalization_run record",
74
+ "record_kind": "normalization_run",
75
+ "schema_version": "2.0",
76
+ "normalizer_version": "0.2.1",
77
+ "normalized_at": "2026-01-15T12:00:00Z",
78
+ "platform": "claude_code",
79
+ "capture_mode": "hook",
80
+ "raw_source_ref": {},
81
+ "run_id": "golden-run-001",
82
+ "run_at": "2026-01-15T12:00:00Z",
83
+ "raw_records_seen": 42,
84
+ "canonical_records_written": 38,
85
+ "repair_applied": false
86
+ }
87
+ ]
@@ -0,0 +1,42 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { CANONICAL_SCHEMA_VERSION } from "../src/types.js";
5
+ import { isCanonicalRecord } from "../src/validators.js";
6
+
7
+ const fixtures = JSON.parse(
8
+ readFileSync(join(import.meta.dirname, "golden.json"), "utf-8"),
9
+ ) as Record<string, unknown>[];
10
+
11
+ describe("golden fixtures", () => {
12
+ test("all fixtures pass isCanonicalRecord", () => {
13
+ for (const fixture of fixtures) {
14
+ const desc = fixture._description ?? fixture.record_kind;
15
+ const result = isCanonicalRecord(fixture);
16
+ if (!result) throw new Error(`Failed: ${desc}`);
17
+ expect(result).toBe(true);
18
+ }
19
+ });
20
+
21
+ test("all fixtures use current schema version", () => {
22
+ for (const fixture of fixtures) {
23
+ expect(fixture.schema_version).toBe(CANONICAL_SCHEMA_VERSION);
24
+ }
25
+ });
26
+
27
+ test("covers every record_kind", () => {
28
+ const kinds = new Set(fixtures.map((f) => f.record_kind));
29
+ expect(kinds).toContain("session");
30
+ expect(kinds).toContain("prompt");
31
+ expect(kinds).toContain("skill_invocation");
32
+ expect(kinds).toContain("execution_fact");
33
+ expect(kinds).toContain("normalization_run");
34
+ });
35
+
36
+ test("mutated fixtures fail validation", () => {
37
+ for (const fixture of fixtures) {
38
+ const bad = { ...fixture, schema_version: "0.0" };
39
+ expect(isCanonicalRecord(bad)).toBe(false);
40
+ }
41
+ });
42
+ });
@@ -0,0 +1 @@
1
+ export * from "./src/index.js";
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@selftune/telemetry-contract",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "Canonical telemetry schema, types, and validators for selftune",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "author": "Daniel Petro",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/selftune-dev/selftune.git",
12
+ "directory": "packages/telemetry-contract"
13
+ },
14
+ "exports": {
15
+ ".": "./index.ts",
16
+ "./types": "./src/types.ts",
17
+ "./validators": "./src/validators.ts"
18
+ }
19
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./types.js";
2
+ export * from "./validators.js";
@@ -0,0 +1,163 @@
1
+ export const CANONICAL_SCHEMA_VERSION = "2.0" as const;
2
+ export type CanonicalSchemaVersion = typeof CANONICAL_SCHEMA_VERSION;
3
+
4
+ export const CANONICAL_PLATFORMS = ["claude_code", "codex", "opencode", "openclaw"] as const;
5
+ export type CanonicalPlatform = (typeof CANONICAL_PLATFORMS)[number];
6
+
7
+ export const CANONICAL_CAPTURE_MODES = [
8
+ "hook",
9
+ "replay",
10
+ "wrapper",
11
+ "batch_ingest",
12
+ "repair",
13
+ ] as const;
14
+ export type CanonicalCaptureMode = (typeof CANONICAL_CAPTURE_MODES)[number];
15
+
16
+ export const CANONICAL_SOURCE_SESSION_KINDS = [
17
+ "interactive",
18
+ "replayed",
19
+ "synthetic",
20
+ "repaired",
21
+ ] as const;
22
+ export type CanonicalSourceSessionKind = (typeof CANONICAL_SOURCE_SESSION_KINDS)[number];
23
+
24
+ export const CANONICAL_PROMPT_KINDS = [
25
+ "user",
26
+ "continuation",
27
+ "task_notification",
28
+ "teammate_message",
29
+ "system_instruction",
30
+ "tool_output",
31
+ "meta",
32
+ "unknown",
33
+ ] as const;
34
+ export type CanonicalPromptKind = (typeof CANONICAL_PROMPT_KINDS)[number];
35
+
36
+ export const CANONICAL_INVOCATION_MODES = ["explicit", "implicit", "inferred", "repaired"] as const;
37
+ export type CanonicalInvocationMode = (typeof CANONICAL_INVOCATION_MODES)[number];
38
+
39
+ export const CANONICAL_COMPLETION_STATUSES = [
40
+ "completed",
41
+ "failed",
42
+ "interrupted",
43
+ "cancelled",
44
+ "unknown",
45
+ ] as const;
46
+ export type CanonicalCompletionStatus = (typeof CANONICAL_COMPLETION_STATUSES)[number];
47
+
48
+ export const CANONICAL_RECORD_KINDS = [
49
+ "session",
50
+ "prompt",
51
+ "skill_invocation",
52
+ "execution_fact",
53
+ "normalization_run",
54
+ ] as const;
55
+ export type CanonicalRecordKind = (typeof CANONICAL_RECORD_KINDS)[number];
56
+
57
+ export interface CanonicalRawSourceRef {
58
+ path?: string;
59
+ line?: number;
60
+ event_type?: string;
61
+ raw_id?: string;
62
+ metadata?: Record<string, unknown>;
63
+ }
64
+
65
+ export interface CanonicalRecordBase {
66
+ record_kind: CanonicalRecordKind;
67
+ schema_version: CanonicalSchemaVersion;
68
+ normalizer_version: string;
69
+ normalized_at: string;
70
+ platform: CanonicalPlatform;
71
+ capture_mode: CanonicalCaptureMode;
72
+ raw_source_ref: CanonicalRawSourceRef;
73
+ }
74
+
75
+ export interface CanonicalSessionRecordBase extends CanonicalRecordBase {
76
+ source_session_kind: CanonicalSourceSessionKind;
77
+ session_id: string;
78
+ }
79
+
80
+ export interface CanonicalSessionRecord extends CanonicalSessionRecordBase {
81
+ record_kind: "session";
82
+ started_at?: string;
83
+ ended_at?: string;
84
+ external_session_id?: string;
85
+ parent_session_id?: string;
86
+ agent_id?: string;
87
+ agent_type?: string;
88
+ agent_cli?: string;
89
+ session_key?: string;
90
+ channel?: string;
91
+ workspace_path?: string;
92
+ repo_root?: string;
93
+ repo_remote?: string;
94
+ branch?: string;
95
+ commit_sha?: string;
96
+ permission_mode?: string;
97
+ approval_policy?: string;
98
+ sandbox_policy?: string;
99
+ provider?: string;
100
+ model?: string;
101
+ completion_status?: CanonicalCompletionStatus;
102
+ end_reason?: string;
103
+ }
104
+
105
+ export interface CanonicalPromptRecord extends CanonicalSessionRecordBase {
106
+ record_kind: "prompt";
107
+ prompt_id: string;
108
+ occurred_at: string;
109
+ prompt_text: string;
110
+ prompt_hash?: string;
111
+ prompt_kind: CanonicalPromptKind;
112
+ is_actionable: boolean;
113
+ prompt_index?: number;
114
+ parent_prompt_id?: string;
115
+ source_message_id?: string;
116
+ }
117
+
118
+ export interface CanonicalSkillInvocationRecord extends CanonicalSessionRecordBase {
119
+ record_kind: "skill_invocation";
120
+ skill_invocation_id: string;
121
+ occurred_at: string;
122
+ matched_prompt_id?: string;
123
+ skill_name: string;
124
+ skill_path?: string;
125
+ skill_version_hash?: string;
126
+ invocation_mode: CanonicalInvocationMode;
127
+ triggered: boolean;
128
+ confidence: number;
129
+ tool_name?: string;
130
+ tool_call_id?: string;
131
+ }
132
+
133
+ export interface CanonicalExecutionFactRecord extends CanonicalSessionRecordBase {
134
+ record_kind: "execution_fact";
135
+ occurred_at: string;
136
+ prompt_id?: string;
137
+ tool_calls_json: Record<string, number>;
138
+ total_tool_calls: number;
139
+ bash_commands_redacted: string[];
140
+ assistant_turns: number;
141
+ errors_encountered: number;
142
+ input_tokens?: number;
143
+ output_tokens?: number;
144
+ duration_ms?: number;
145
+ completion_status?: CanonicalCompletionStatus;
146
+ end_reason?: string;
147
+ }
148
+
149
+ export interface CanonicalNormalizationRunRecord extends CanonicalRecordBase {
150
+ record_kind: "normalization_run";
151
+ run_id: string;
152
+ run_at: string;
153
+ raw_records_seen: number;
154
+ canonical_records_written: number;
155
+ repair_applied: boolean;
156
+ }
157
+
158
+ export type CanonicalRecord =
159
+ | CanonicalSessionRecord
160
+ | CanonicalPromptRecord
161
+ | CanonicalSkillInvocationRecord
162
+ | CanonicalExecutionFactRecord
163
+ | CanonicalNormalizationRunRecord;