selftune 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/.claude/agents/diagnosis-analyst.md +20 -10
  2. package/.claude/agents/evolution-reviewer.md +14 -1
  3. package/.claude/agents/integration-guide.md +18 -6
  4. package/.claude/agents/pattern-analyst.md +18 -5
  5. package/CHANGELOG.md +12 -4
  6. package/README.md +43 -35
  7. package/apps/local-dashboard/dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  8. package/apps/local-dashboard/dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  9. package/apps/local-dashboard/dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  10. package/apps/local-dashboard/dist/assets/index-C4EOTFZ2.js +15 -0
  11. package/apps/local-dashboard/dist/assets/index-bl-Webyd.css +1 -0
  12. package/apps/local-dashboard/dist/assets/vendor-react-U7zYD9Rg.js +60 -0
  13. package/apps/local-dashboard/dist/assets/vendor-table-B7VF2Ipl.js +26 -0
  14. package/apps/local-dashboard/dist/assets/vendor-ui-D7_zX_qy.js +346 -0
  15. package/apps/local-dashboard/dist/favicon.png +0 -0
  16. package/apps/local-dashboard/dist/index.html +17 -0
  17. package/apps/local-dashboard/dist/logo.png +0 -0
  18. package/apps/local-dashboard/dist/logo.svg +9 -0
  19. package/cli/selftune/badge/badge-data.ts +1 -1
  20. package/cli/selftune/badge/badge.ts +4 -8
  21. package/cli/selftune/canonical-export.ts +183 -0
  22. package/cli/selftune/constants.ts +28 -0
  23. package/cli/selftune/contribute/contribute.ts +1 -1
  24. package/cli/selftune/cron/setup.ts +17 -17
  25. package/cli/selftune/dashboard-contract.ts +202 -0
  26. package/cli/selftune/dashboard-server.ts +653 -186
  27. package/cli/selftune/dashboard.ts +41 -176
  28. package/cli/selftune/eval/baseline.ts +5 -4
  29. package/cli/selftune/eval/composability-v2.ts +273 -0
  30. package/cli/selftune/eval/hooks-to-evals.ts +34 -15
  31. package/cli/selftune/eval/unit-test-cli.ts +1 -1
  32. package/cli/selftune/evolution/evidence.ts +26 -0
  33. package/cli/selftune/evolution/evolve-body.ts +105 -11
  34. package/cli/selftune/evolution/evolve.ts +371 -25
  35. package/cli/selftune/evolution/extract-patterns.ts +87 -29
  36. package/cli/selftune/evolution/rollback.ts +2 -2
  37. package/cli/selftune/grading/auto-grade.ts +200 -0
  38. package/cli/selftune/grading/grade-session.ts +448 -97
  39. package/cli/selftune/grading/results.ts +42 -0
  40. package/cli/selftune/hooks/prompt-log.ts +172 -2
  41. package/cli/selftune/hooks/session-stop.ts +123 -3
  42. package/cli/selftune/hooks/skill-eval.ts +119 -3
  43. package/cli/selftune/index.ts +395 -116
  44. package/cli/selftune/ingestors/claude-replay.ts +140 -114
  45. package/cli/selftune/ingestors/codex-rollout.ts +345 -46
  46. package/cli/selftune/ingestors/codex-wrapper.ts +207 -39
  47. package/cli/selftune/ingestors/openclaw-ingest.ts +141 -8
  48. package/cli/selftune/ingestors/opencode-ingest.ts +193 -17
  49. package/cli/selftune/init.ts +227 -14
  50. package/cli/selftune/last.ts +14 -5
  51. package/cli/selftune/localdb/db.ts +63 -0
  52. package/cli/selftune/localdb/materialize.ts +428 -0
  53. package/cli/selftune/localdb/queries.ts +376 -0
  54. package/cli/selftune/localdb/schema.ts +204 -0
  55. package/cli/selftune/monitoring/watch.ts +66 -15
  56. package/cli/selftune/normalization.ts +682 -0
  57. package/cli/selftune/observability.ts +19 -44
  58. package/cli/selftune/orchestrate.ts +1073 -0
  59. package/cli/selftune/quickstart.ts +203 -0
  60. package/cli/selftune/repair/skill-usage.ts +576 -0
  61. package/cli/selftune/schedule.ts +561 -0
  62. package/cli/selftune/status.ts +48 -26
  63. package/cli/selftune/sync.ts +627 -0
  64. package/cli/selftune/types.ts +148 -0
  65. package/cli/selftune/utils/canonical-log.ts +45 -0
  66. package/cli/selftune/utils/hooks.ts +41 -0
  67. package/cli/selftune/utils/html.ts +27 -0
  68. package/cli/selftune/utils/llm-call.ts +78 -20
  69. package/cli/selftune/utils/math.ts +10 -0
  70. package/cli/selftune/utils/query-filter.ts +139 -0
  71. package/cli/selftune/utils/skill-discovery.ts +340 -0
  72. package/cli/selftune/utils/skill-log.ts +68 -0
  73. package/cli/selftune/utils/skill-usage-confidence.ts +18 -0
  74. package/cli/selftune/utils/transcript.ts +272 -26
  75. package/cli/selftune/workflows/discover.ts +254 -0
  76. package/cli/selftune/workflows/skill-md-writer.ts +288 -0
  77. package/cli/selftune/workflows/workflows.ts +188 -0
  78. package/package.json +21 -8
  79. package/packages/telemetry-contract/README.md +11 -0
  80. package/packages/telemetry-contract/fixtures/golden.json +87 -0
  81. package/packages/telemetry-contract/fixtures/golden.test.ts +42 -0
  82. package/packages/telemetry-contract/index.ts +1 -0
  83. package/packages/telemetry-contract/package.json +19 -0
  84. package/packages/telemetry-contract/src/index.ts +2 -0
  85. package/packages/telemetry-contract/src/types.ts +163 -0
  86. package/packages/telemetry-contract/src/validators.ts +109 -0
  87. package/skill/SKILL.md +84 -53
  88. package/skill/Workflows/AutoActivation.md +17 -16
  89. package/skill/Workflows/Badge.md +6 -0
  90. package/skill/Workflows/Baseline.md +46 -23
  91. package/skill/Workflows/Composability.md +12 -5
  92. package/skill/Workflows/Contribute.md +17 -14
  93. package/skill/Workflows/Cron.md +56 -79
  94. package/skill/Workflows/Dashboard.md +45 -34
  95. package/skill/Workflows/Doctor.md +30 -17
  96. package/skill/Workflows/Evals.md +64 -40
  97. package/skill/Workflows/EvolutionMemory.md +2 -0
  98. package/skill/Workflows/Evolve.md +102 -47
  99. package/skill/Workflows/EvolveBody.md +6 -6
  100. package/skill/Workflows/Grade.md +36 -31
  101. package/skill/Workflows/ImportSkillsBench.md +11 -5
  102. package/skill/Workflows/Ingest.md +43 -36
  103. package/skill/Workflows/Initialize.md +44 -30
  104. package/skill/Workflows/Orchestrate.md +139 -0
  105. package/skill/Workflows/Replay.md +39 -18
  106. package/skill/Workflows/Rollback.md +3 -3
  107. package/skill/Workflows/Schedule.md +61 -0
  108. package/skill/Workflows/Sync.md +88 -0
  109. package/skill/Workflows/UnitTest.md +34 -22
  110. package/skill/Workflows/Watch.md +14 -4
  111. package/skill/Workflows/Workflows.md +129 -0
  112. package/skill/assets/activation-rules-default.json +26 -0
  113. package/skill/assets/multi-skill-settings.json +63 -0
  114. package/skill/assets/single-skill-settings.json +57 -0
  115. package/skill/references/invocation-taxonomy.md +2 -2
  116. package/skill/references/logs.md +164 -2
  117. package/skill/references/setup-patterns.md +65 -0
  118. package/skill/references/version-history.md +40 -0
  119. package/skill/settings_snippet.json +1 -1
  120. package/templates/multi-skill-settings.json +7 -7
  121. package/templates/single-skill-settings.json +6 -6
  122. package/dashboard/index.html +0 -1680
@@ -0,0 +1,63 @@
1
+ /**
2
+ * SQLite database lifecycle for selftune local materialized view store.
3
+ *
4
+ * Uses Bun's built-in SQLite driver. The database file lives at
5
+ * ~/.selftune/selftune.db and is treated as a disposable cache —
6
+ * it can always be rebuilt from the authoritative JSONL logs.
7
+ */
8
+
9
+ import { Database } from "bun:sqlite";
10
+ import { existsSync, mkdirSync } from "node:fs";
11
+ import { dirname, join } from "node:path";
12
+ import { SELFTUNE_CONFIG_DIR } from "../constants.js";
13
+ import { ALL_DDL } from "./schema.js";
14
+
15
+ /** Default database file path. */
16
+ export const DB_PATH = join(SELFTUNE_CONFIG_DIR, "selftune.db");
17
+
18
+ /**
19
+ * Open (or create) the selftune SQLite database at the given path.
20
+ * Runs all DDL to ensure the schema exists. Uses WAL mode for
21
+ * concurrent read/write safety.
22
+ *
23
+ * Pass ":memory:" for an in-memory database (useful for tests).
24
+ */
25
+ export function openDb(dbPath: string = DB_PATH): Database {
26
+ // Ensure parent directory exists for file-based databases
27
+ if (dbPath !== ":memory:") {
28
+ const dir = dirname(dbPath);
29
+ if (!existsSync(dir)) {
30
+ mkdirSync(dir, { recursive: true });
31
+ }
32
+ }
33
+
34
+ const db = new Database(dbPath);
35
+
36
+ // Enable WAL mode for better concurrent access
37
+ db.run("PRAGMA journal_mode = WAL");
38
+ db.run("PRAGMA foreign_keys = ON");
39
+
40
+ // Run all DDL statements
41
+ for (const ddl of ALL_DDL) {
42
+ db.run(ddl);
43
+ }
44
+
45
+ return db;
46
+ }
47
+
48
+ /**
49
+ * Get a metadata value from the _meta table.
50
+ */
51
+ export function getMeta(db: Database, key: string): string | null {
52
+ const row = db.query("SELECT value FROM _meta WHERE key = ?").get(key) as {
53
+ value: string;
54
+ } | null;
55
+ return row?.value ?? null;
56
+ }
57
+
58
+ /**
59
+ * Set a metadata value in the _meta table.
60
+ */
61
+ export function setMeta(db: Database, key: string, value: string): void {
62
+ db.run("INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)", [key, value]);
63
+ }
@@ -0,0 +1,428 @@
1
+ /**
2
+ * Materializer: reads JSONL source-of-truth logs and inserts structured
3
+ * records into the local SQLite database.
4
+ *
5
+ * Supports two modes:
6
+ * - Full rebuild: drops all data and re-inserts from scratch
7
+ * - Incremental: only inserts records newer than last materialization
8
+ */
9
+
10
+ import type { Database } from "bun:sqlite";
11
+ import type {
12
+ CanonicalExecutionFactRecord,
13
+ CanonicalPromptRecord,
14
+ CanonicalRecord,
15
+ CanonicalSessionRecord,
16
+ CanonicalSkillInvocationRecord,
17
+ } from "@selftune/telemetry-contract";
18
+ import {
19
+ CANONICAL_LOG,
20
+ EVOLUTION_AUDIT_LOG,
21
+ EVOLUTION_EVIDENCE_LOG,
22
+ ORCHESTRATE_RUN_LOG,
23
+ TELEMETRY_LOG,
24
+ } from "../constants.js";
25
+ import type { OrchestrateRunReport } from "../dashboard-contract.js";
26
+ import { readEvidenceTrail } from "../evolution/evidence.js";
27
+ import type {
28
+ EvolutionAuditEntry,
29
+ EvolutionEvidenceEntry,
30
+ SessionTelemetryRecord,
31
+ SkillUsageRecord,
32
+ } from "../types.js";
33
+ import { readCanonicalRecords } from "../utils/canonical-log.js";
34
+ import { readJsonl } from "../utils/jsonl.js";
35
+ import { readEffectiveSkillUsageRecords } from "../utils/skill-log.js";
36
+ import { getMeta, setMeta } from "./db.js";
37
+
38
+ /** Meta key tracking last materialization timestamp. */
39
+ const META_LAST_MATERIALIZED = "last_materialized_at";
40
+
41
+ /**
42
+ * Full rebuild: drop all data tables, then re-insert everything.
43
+ */
44
+ export function materializeFull(db: Database, options?: MaterializeOptions): MaterializeResult {
45
+ const tables = [
46
+ "skill_usage",
47
+ "session_telemetry",
48
+ "evolution_audit",
49
+ "evolution_evidence",
50
+ "execution_facts",
51
+ "skill_invocations",
52
+ "prompts",
53
+ "sessions",
54
+ "orchestrate_runs",
55
+ ];
56
+ for (const table of tables) {
57
+ db.run(`DELETE FROM ${table}`);
58
+ }
59
+
60
+ return materializeIncremental(db, { ...options, since: null });
61
+ }
62
+
63
+ export interface MaterializeOptions {
64
+ canonicalLogPath?: string;
65
+ telemetryLogPath?: string;
66
+ evolutionAuditPath?: string;
67
+ evolutionEvidencePath?: string;
68
+ orchestrateRunLogPath?: string;
69
+ since?: string | null;
70
+ }
71
+
72
+ export interface MaterializeResult {
73
+ sessions: number;
74
+ prompts: number;
75
+ skillInvocations: number;
76
+ executionFacts: number;
77
+ sessionTelemetry: number;
78
+ skillUsage: number;
79
+ evolutionAudit: number;
80
+ evolutionEvidence: number;
81
+ orchestrateRuns: number;
82
+ }
83
+
84
+ /**
85
+ * Incremental materialization: only insert records newer than last run.
86
+ * Uses INSERT OR IGNORE for idempotency on primary-keyed tables,
87
+ * and UNIQUE indexes for deduplication on append-only tables.
88
+ */
89
+ export function materializeIncremental(
90
+ db: Database,
91
+ options?: MaterializeOptions,
92
+ ): MaterializeResult {
93
+ const since = options?.since !== undefined ? options.since : getMeta(db, META_LAST_MATERIALIZED);
94
+ const now = new Date().toISOString();
95
+
96
+ const result: MaterializeResult = {
97
+ sessions: 0,
98
+ prompts: 0,
99
+ skillInvocations: 0,
100
+ executionFacts: 0,
101
+ sessionTelemetry: 0,
102
+ skillUsage: 0,
103
+ evolutionAudit: 0,
104
+ evolutionEvidence: 0,
105
+ orchestrateRuns: 0,
106
+ };
107
+
108
+ // -- Read all data BEFORE opening the transaction ---------------------------
109
+ // This keeps file I/O out of the write lock for better concurrency.
110
+
111
+ const canonical = readCanonicalRecords(options?.canonicalLogPath ?? CANONICAL_LOG);
112
+ const filteredCanonical = since ? canonical.filter((r) => r.normalized_at > since) : canonical;
113
+
114
+ // Pre-partition canonical records by kind (single pass instead of 4x full scan)
115
+ const byKind = new Map<string, CanonicalRecord[]>();
116
+ for (const r of filteredCanonical) {
117
+ const arr = byKind.get(r.record_kind);
118
+ if (arr) arr.push(r);
119
+ else byKind.set(r.record_kind, [r]);
120
+ }
121
+
122
+ const telemetry = readJsonl<SessionTelemetryRecord>(options?.telemetryLogPath ?? TELEMETRY_LOG);
123
+ const filteredTelemetry = since ? telemetry.filter((r) => r.timestamp > since) : telemetry;
124
+
125
+ const skills = readEffectiveSkillUsageRecords();
126
+ const filteredSkills = since ? skills.filter((r) => r.timestamp > since) : skills;
127
+
128
+ const audit = readJsonl<EvolutionAuditEntry>(options?.evolutionAuditPath ?? EVOLUTION_AUDIT_LOG);
129
+ const filteredAudit = since ? audit.filter((r) => r.timestamp > since) : audit;
130
+
131
+ const evidence = readEvidenceTrail(
132
+ undefined,
133
+ options?.evolutionEvidencePath ?? EVOLUTION_EVIDENCE_LOG,
134
+ );
135
+ const filteredEvidence = since ? evidence.filter((r) => r.timestamp > since) : evidence;
136
+
137
+ const orchestrateRuns = readJsonl<OrchestrateRunReport>(
138
+ options?.orchestrateRunLogPath ?? ORCHESTRATE_RUN_LOG,
139
+ );
140
+ const filteredOrchestrateRuns = since
141
+ ? orchestrateRuns.filter((r) => r.timestamp > since)
142
+ : orchestrateRuns;
143
+
144
+ // -- Insert everything inside a single transaction --------------------------
145
+ db.run("BEGIN TRANSACTION");
146
+ try {
147
+ result.sessions = insertSessions(db, byKind.get("session") ?? []);
148
+ result.prompts = insertPrompts(db, byKind.get("prompt") ?? []);
149
+ result.skillInvocations = insertSkillInvocations(db, byKind.get("skill_invocation") ?? []);
150
+ result.executionFacts = insertExecutionFacts(db, byKind.get("execution_fact") ?? []);
151
+ result.sessionTelemetry = insertSessionTelemetry(db, filteredTelemetry);
152
+ result.skillUsage = insertSkillUsage(db, filteredSkills);
153
+ result.evolutionAudit = insertEvolutionAudit(db, filteredAudit);
154
+ result.evolutionEvidence = insertEvolutionEvidence(db, filteredEvidence);
155
+ result.orchestrateRuns = insertOrchestrateRuns(db, filteredOrchestrateRuns);
156
+
157
+ setMeta(db, META_LAST_MATERIALIZED, now);
158
+ db.run("COMMIT");
159
+ } catch (err) {
160
+ db.run("ROLLBACK");
161
+ throw err;
162
+ }
163
+
164
+ return result;
165
+ }
166
+
167
+ // -- Insert helpers -----------------------------------------------------------
168
+
169
+ function insertSessions(db: Database, records: CanonicalRecord[]): number {
170
+ const stmt = db.prepare(`
171
+ INSERT OR IGNORE INTO sessions
172
+ (session_id, started_at, ended_at, platform, model, completion_status,
173
+ source_session_kind, agent_cli, workspace_path, repo_remote, branch,
174
+ schema_version, normalized_at)
175
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
176
+ `);
177
+
178
+ let count = 0;
179
+ for (const r of records) {
180
+ const s = r as CanonicalSessionRecord;
181
+ stmt.run(
182
+ s.session_id,
183
+ s.started_at ?? null,
184
+ s.ended_at ?? null,
185
+ s.platform,
186
+ s.model ?? null,
187
+ s.completion_status ?? null,
188
+ s.source_session_kind ?? null,
189
+ s.agent_cli ?? null,
190
+ s.workspace_path ?? null,
191
+ s.repo_remote ?? null,
192
+ s.branch ?? null,
193
+ s.schema_version,
194
+ s.normalized_at,
195
+ );
196
+ count++;
197
+ }
198
+ return count;
199
+ }
200
+
201
+ function insertPrompts(db: Database, records: CanonicalRecord[]): number {
202
+ const stmt = db.prepare(`
203
+ INSERT OR IGNORE INTO prompts
204
+ (prompt_id, session_id, occurred_at, prompt_kind, is_actionable, prompt_index, prompt_text)
205
+ VALUES (?, ?, ?, ?, ?, ?, ?)
206
+ `);
207
+
208
+ let count = 0;
209
+ for (const r of records) {
210
+ const p = r as CanonicalPromptRecord;
211
+ stmt.run(
212
+ p.prompt_id,
213
+ p.session_id,
214
+ p.occurred_at,
215
+ p.prompt_kind,
216
+ p.is_actionable ? 1 : 0,
217
+ p.prompt_index ?? null,
218
+ p.prompt_text,
219
+ );
220
+ count++;
221
+ }
222
+ return count;
223
+ }
224
+
225
+ function insertSkillInvocations(db: Database, records: CanonicalRecord[]): number {
226
+ const stmt = db.prepare(`
227
+ INSERT OR IGNORE INTO skill_invocations
228
+ (skill_invocation_id, session_id, occurred_at, skill_name, invocation_mode,
229
+ triggered, confidence, tool_name, matched_prompt_id)
230
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
231
+ `);
232
+
233
+ let count = 0;
234
+ for (const r of records) {
235
+ const si = r as CanonicalSkillInvocationRecord;
236
+ stmt.run(
237
+ si.skill_invocation_id,
238
+ si.session_id,
239
+ si.occurred_at,
240
+ si.skill_name,
241
+ si.invocation_mode,
242
+ si.triggered ? 1 : 0,
243
+ si.confidence,
244
+ si.tool_name ?? null,
245
+ si.matched_prompt_id ?? null,
246
+ );
247
+ count++;
248
+ }
249
+ return count;
250
+ }
251
+
252
+ function insertExecutionFacts(db: Database, records: CanonicalRecord[]): number {
253
+ const stmt = db.prepare(`
254
+ INSERT INTO execution_facts
255
+ (session_id, occurred_at, prompt_id, tool_calls_json, total_tool_calls,
256
+ assistant_turns, errors_encountered, input_tokens, output_tokens,
257
+ duration_ms, completion_status)
258
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
259
+ `);
260
+
261
+ let count = 0;
262
+ for (const r of records) {
263
+ const ef = r as CanonicalExecutionFactRecord;
264
+ stmt.run(
265
+ ef.session_id,
266
+ ef.occurred_at,
267
+ ef.prompt_id ?? null,
268
+ JSON.stringify(ef.tool_calls_json),
269
+ ef.total_tool_calls,
270
+ ef.assistant_turns,
271
+ ef.errors_encountered,
272
+ ef.input_tokens ?? null,
273
+ ef.output_tokens ?? null,
274
+ ef.duration_ms ?? null,
275
+ ef.completion_status ?? null,
276
+ );
277
+ count++;
278
+ }
279
+ return count;
280
+ }
281
+
282
+ function insertSessionTelemetry(db: Database, records: SessionTelemetryRecord[]): number {
283
+ const stmt = db.prepare(`
284
+ INSERT OR IGNORE INTO session_telemetry
285
+ (session_id, timestamp, cwd, transcript_path, tool_calls_json,
286
+ total_tool_calls, bash_commands_json, skills_triggered_json,
287
+ skills_invoked_json, assistant_turns, errors_encountered,
288
+ transcript_chars, last_user_query, source, input_tokens, output_tokens)
289
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
290
+ `);
291
+
292
+ let count = 0;
293
+ for (const r of records) {
294
+ stmt.run(
295
+ r.session_id,
296
+ r.timestamp,
297
+ r.cwd,
298
+ r.transcript_path,
299
+ JSON.stringify(r.tool_calls),
300
+ r.total_tool_calls,
301
+ JSON.stringify(r.bash_commands),
302
+ JSON.stringify(r.skills_triggered),
303
+ r.skills_invoked ? JSON.stringify(r.skills_invoked) : null,
304
+ r.assistant_turns,
305
+ r.errors_encountered,
306
+ r.transcript_chars,
307
+ r.last_user_query,
308
+ r.source ?? null,
309
+ r.input_tokens ?? null,
310
+ r.output_tokens ?? null,
311
+ );
312
+ count++;
313
+ }
314
+ return count;
315
+ }
316
+
317
+ function insertSkillUsage(db: Database, records: SkillUsageRecord[]): number {
318
+ // Uses INSERT OR IGNORE with a UNIQUE index on the dedup composite key
319
+ // (idx_skill_usage_dedup defined in schema.ts).
320
+ const stmt = db.prepare(`
321
+ INSERT OR IGNORE INTO skill_usage
322
+ (timestamp, session_id, skill_name, skill_path, skill_scope, query, triggered, source)
323
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
324
+ `);
325
+
326
+ let count = 0;
327
+ for (const r of records) {
328
+ stmt.run(
329
+ r.timestamp,
330
+ r.session_id,
331
+ r.skill_name,
332
+ r.skill_path,
333
+ r.skill_scope ?? null,
334
+ r.query,
335
+ r.triggered ? 1 : 0,
336
+ r.source ?? null,
337
+ );
338
+ count++;
339
+ }
340
+ return count;
341
+ }
342
+
343
+ function insertEvolutionAudit(db: Database, records: EvolutionAuditEntry[]): number {
344
+ // Uses INSERT OR IGNORE with a UNIQUE index on the dedup composite key
345
+ // (idx_evo_audit_dedup defined in schema.ts).
346
+ const stmt = db.prepare(`
347
+ INSERT OR IGNORE INTO evolution_audit
348
+ (timestamp, proposal_id, skill_name, action, details, eval_snapshot_json)
349
+ VALUES (?, ?, ?, ?, ?, ?)
350
+ `);
351
+
352
+ let count = 0;
353
+ for (const r of records) {
354
+ stmt.run(
355
+ r.timestamp,
356
+ r.proposal_id,
357
+ r.skill_name ?? null,
358
+ r.action,
359
+ r.details,
360
+ r.eval_snapshot ? JSON.stringify(r.eval_snapshot) : null,
361
+ );
362
+ count++;
363
+ }
364
+ return count;
365
+ }
366
+
367
+ function insertEvolutionEvidence(db: Database, records: EvolutionEvidenceEntry[]): number {
368
+ // Uses INSERT OR IGNORE with a UNIQUE index on the dedup composite key
369
+ // (idx_evo_evidence_dedup defined in schema.ts).
370
+ const stmt = db.prepare(`
371
+ INSERT OR IGNORE INTO evolution_evidence
372
+ (timestamp, proposal_id, skill_name, skill_path, target, stage,
373
+ rationale, confidence, details, original_text, proposed_text,
374
+ eval_set_json, validation_json)
375
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
376
+ `);
377
+
378
+ let count = 0;
379
+ for (const r of records) {
380
+ stmt.run(
381
+ r.timestamp,
382
+ r.proposal_id,
383
+ r.skill_name,
384
+ r.skill_path,
385
+ r.target,
386
+ r.stage,
387
+ r.rationale ?? null,
388
+ r.confidence ?? null,
389
+ r.details ?? null,
390
+ r.original_text ?? null,
391
+ r.proposed_text ?? null,
392
+ r.eval_set ? JSON.stringify(r.eval_set) : null,
393
+ r.validation ? JSON.stringify(r.validation) : null,
394
+ );
395
+ count++;
396
+ }
397
+ return count;
398
+ }
399
+
400
+ function insertOrchestrateRuns(db: Database, records: OrchestrateRunReport[]): number {
401
+ const stmt = db.prepare(`
402
+ INSERT OR IGNORE INTO orchestrate_runs
403
+ (run_id, timestamp, elapsed_ms, dry_run, approval_mode,
404
+ total_skills, evaluated, evolved, deployed, watched, skipped,
405
+ skill_actions_json)
406
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
407
+ `);
408
+
409
+ let count = 0;
410
+ for (const r of records) {
411
+ stmt.run(
412
+ r.run_id,
413
+ r.timestamp,
414
+ r.elapsed_ms,
415
+ r.dry_run ? 1 : 0,
416
+ r.approval_mode,
417
+ r.total_skills,
418
+ r.evaluated,
419
+ r.evolved,
420
+ r.deployed,
421
+ r.watched,
422
+ r.skipped,
423
+ JSON.stringify(r.skill_actions),
424
+ );
425
+ count++;
426
+ }
427
+ return count;
428
+ }