selftune 0.2.6 → 0.2.8

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 (119) hide show
  1. package/README.md +1 -0
  2. package/apps/local-dashboard/dist/assets/index-Bk9vSHHd.js +15 -0
  3. package/apps/local-dashboard/dist/assets/index-CRtLkBTi.css +1 -0
  4. package/apps/local-dashboard/dist/assets/vendor-react-BQH_6WrG.js +60 -0
  5. package/apps/local-dashboard/dist/assets/{vendor-table-B7VF2Ipl.js → vendor-table-dK1QMLq9.js} +1 -1
  6. package/apps/local-dashboard/dist/assets/{vendor-ui-r2k_Ku_V.js → vendor-ui-CO2mrx6e.js} +60 -65
  7. package/apps/local-dashboard/dist/index.html +5 -5
  8. package/cli/selftune/activation-rules.ts +30 -9
  9. package/cli/selftune/agent-guidance.ts +96 -0
  10. package/cli/selftune/alpha-identity.ts +157 -0
  11. package/cli/selftune/alpha-upload/build-payloads.ts +151 -0
  12. package/cli/selftune/alpha-upload/client.ts +113 -0
  13. package/cli/selftune/alpha-upload/flush.ts +191 -0
  14. package/cli/selftune/alpha-upload/index.ts +194 -0
  15. package/cli/selftune/alpha-upload/queue.ts +252 -0
  16. package/cli/selftune/alpha-upload/stage-canonical.ts +242 -0
  17. package/cli/selftune/alpha-upload-contract.ts +52 -0
  18. package/cli/selftune/auth/device-code.ts +110 -0
  19. package/cli/selftune/auto-update.ts +130 -0
  20. package/cli/selftune/badge/badge.ts +19 -9
  21. package/cli/selftune/canonical-export.ts +16 -3
  22. package/cli/selftune/constants.ts +28 -8
  23. package/cli/selftune/contribute/bundle.ts +32 -5
  24. package/cli/selftune/dashboard-contract.ts +32 -1
  25. package/cli/selftune/dashboard-server.ts +256 -692
  26. package/cli/selftune/dashboard.ts +1 -1
  27. package/cli/selftune/eval/baseline.ts +11 -7
  28. package/cli/selftune/eval/hooks-to-evals.ts +27 -9
  29. package/cli/selftune/eval/synthetic-evals.ts +54 -1
  30. package/cli/selftune/evolution/audit.ts +24 -19
  31. package/cli/selftune/evolution/constitutional.ts +176 -0
  32. package/cli/selftune/evolution/evidence.ts +18 -13
  33. package/cli/selftune/evolution/evolve-body.ts +104 -7
  34. package/cli/selftune/evolution/evolve.ts +195 -22
  35. package/cli/selftune/evolution/propose-body.ts +18 -1
  36. package/cli/selftune/evolution/propose-description.ts +27 -2
  37. package/cli/selftune/evolution/rollback.ts +11 -15
  38. package/cli/selftune/export.ts +84 -0
  39. package/cli/selftune/grading/auto-grade.ts +13 -4
  40. package/cli/selftune/grading/grade-session.ts +16 -6
  41. package/cli/selftune/hooks/evolution-guard.ts +26 -9
  42. package/cli/selftune/hooks/prompt-log.ts +23 -9
  43. package/cli/selftune/hooks/session-stop.ts +78 -15
  44. package/cli/selftune/hooks/skill-eval.ts +189 -10
  45. package/cli/selftune/index.ts +274 -2
  46. package/cli/selftune/ingestors/claude-replay.ts +48 -21
  47. package/cli/selftune/init.ts +249 -47
  48. package/cli/selftune/last.ts +7 -7
  49. package/cli/selftune/localdb/db.ts +90 -10
  50. package/cli/selftune/localdb/direct-write.ts +531 -0
  51. package/cli/selftune/localdb/materialize.ts +296 -42
  52. package/cli/selftune/localdb/queries.ts +325 -32
  53. package/cli/selftune/localdb/schema.ts +109 -0
  54. package/cli/selftune/monitoring/watch.ts +26 -8
  55. package/cli/selftune/normalization.ts +85 -15
  56. package/cli/selftune/observability.ts +248 -2
  57. package/cli/selftune/orchestrate.ts +165 -20
  58. package/cli/selftune/quickstart.ts +34 -10
  59. package/cli/selftune/repair/skill-usage.ts +12 -2
  60. package/cli/selftune/routes/actions.ts +77 -0
  61. package/cli/selftune/routes/badge.ts +66 -0
  62. package/cli/selftune/routes/doctor.ts +12 -0
  63. package/cli/selftune/routes/index.ts +14 -0
  64. package/cli/selftune/routes/orchestrate-runs.ts +13 -0
  65. package/cli/selftune/routes/overview.ts +14 -0
  66. package/cli/selftune/routes/report.ts +293 -0
  67. package/cli/selftune/routes/skill-report.ts +230 -0
  68. package/cli/selftune/status.ts +203 -7
  69. package/cli/selftune/sync.ts +13 -1
  70. package/cli/selftune/types.ts +50 -0
  71. package/cli/selftune/utils/jsonl.ts +58 -1
  72. package/cli/selftune/utils/selftune-meta.ts +38 -0
  73. package/cli/selftune/utils/skill-log.ts +30 -4
  74. package/cli/selftune/utils/transcript.ts +15 -0
  75. package/cli/selftune/workflows/workflows.ts +7 -6
  76. package/package.json +10 -6
  77. package/packages/telemetry-contract/fixtures/complete-push.ts +184 -0
  78. package/packages/telemetry-contract/fixtures/evidence-only-push.ts +58 -0
  79. package/packages/telemetry-contract/fixtures/golden.json +1 -0
  80. package/packages/telemetry-contract/fixtures/index.ts +4 -0
  81. package/packages/telemetry-contract/fixtures/partial-push-no-sessions.ts +40 -0
  82. package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +79 -0
  83. package/packages/telemetry-contract/package.json +6 -1
  84. package/packages/telemetry-contract/src/index.ts +1 -0
  85. package/packages/telemetry-contract/src/schemas.ts +215 -0
  86. package/packages/telemetry-contract/src/types.ts +3 -1
  87. package/packages/telemetry-contract/src/validators.ts +3 -1
  88. package/packages/telemetry-contract/tests/compatibility.test.ts +144 -0
  89. package/packages/ui/package.json +4 -0
  90. package/packages/ui/src/components/ActivityTimeline.tsx +61 -29
  91. package/packages/ui/src/components/section-cards.tsx +31 -14
  92. package/packages/ui/src/types.ts +1 -0
  93. package/skill/SKILL.md +214 -174
  94. package/skill/Workflows/AlphaUpload.md +45 -0
  95. package/skill/Workflows/Baseline.md +18 -12
  96. package/skill/Workflows/Composability.md +3 -3
  97. package/skill/Workflows/Dashboard.md +44 -91
  98. package/skill/Workflows/Doctor.md +93 -66
  99. package/skill/Workflows/Evals.md +49 -40
  100. package/skill/Workflows/Evolve.md +76 -28
  101. package/skill/Workflows/EvolveBody.md +37 -38
  102. package/skill/Workflows/Initialize.md +172 -26
  103. package/skill/Workflows/Orchestrate.md +11 -2
  104. package/skill/Workflows/Sync.md +23 -0
  105. package/skill/Workflows/Watch.md +2 -5
  106. package/skill/agents/diagnosis-analyst.md +163 -0
  107. package/skill/agents/evolution-reviewer.md +149 -0
  108. package/skill/agents/integration-guide.md +154 -0
  109. package/skill/agents/pattern-analyst.md +149 -0
  110. package/skill/assets/multi-skill-settings.json +1 -1
  111. package/skill/assets/single-skill-settings.json +1 -1
  112. package/skill/references/interactive-config.md +39 -0
  113. package/skill/references/invocation-taxonomy.md +34 -0
  114. package/skill/references/logs.md +9 -1
  115. package/skill/references/setup-patterns.md +3 -3
  116. package/skill/settings_snippet.json +1 -1
  117. package/apps/local-dashboard/dist/assets/index-C75H1Q3n.css +0 -1
  118. package/apps/local-dashboard/dist/assets/index-axE4kz3Q.js +0 -15
  119. package/apps/local-dashboard/dist/assets/vendor-react-U7zYD9Rg.js +0 -60
@@ -45,13 +45,13 @@ export function getOverviewPayload(db: Database): OverviewPayload {
45
45
  // Skill usage (bounded to most recent 2000)
46
46
  const skillRows = db
47
47
  .query(
48
- `SELECT timestamp, session_id, skill_name, skill_path, query, triggered, source
49
- FROM skill_usage
50
- ORDER BY timestamp DESC
48
+ `SELECT occurred_at, session_id, skill_name, skill_path, query, triggered, source
49
+ FROM skill_invocations
50
+ ORDER BY occurred_at DESC
51
51
  LIMIT 2000`,
52
52
  )
53
53
  .all() as Array<{
54
- timestamp: string;
54
+ occurred_at: string;
55
55
  session_id: string;
56
56
  skill_name: string;
57
57
  skill_path: string;
@@ -61,7 +61,7 @@ export function getOverviewPayload(db: Database): OverviewPayload {
61
61
  }>;
62
62
 
63
63
  const skills = skillRows.map((row) => ({
64
- timestamp: row.timestamp,
64
+ timestamp: row.occurred_at,
65
65
  session_id: row.session_id,
66
66
  skill_name: row.skill_name,
67
67
  skill_path: row.skill_path,
@@ -73,7 +73,7 @@ export function getOverviewPayload(db: Database): OverviewPayload {
73
73
  // Evolution audit (bounded to most recent 500)
74
74
  const evolution = db
75
75
  .query(
76
- `SELECT timestamp, proposal_id, action, details
76
+ `SELECT timestamp, proposal_id, skill_name, action, details
77
77
  FROM evolution_audit
78
78
  ORDER BY timestamp DESC
79
79
  LIMIT 500`,
@@ -81,6 +81,7 @@ export function getOverviewPayload(db: Database): OverviewPayload {
81
81
  .all() as Array<{
82
82
  timestamp: string;
83
83
  proposal_id: string;
84
+ skill_name: string | null;
84
85
  action: string;
85
86
  details: string;
86
87
  }>;
@@ -90,7 +91,7 @@ export function getOverviewPayload(db: Database): OverviewPayload {
90
91
  .query(
91
92
  `SELECT
92
93
  (SELECT COUNT(*) FROM session_telemetry) as telemetry,
93
- (SELECT COUNT(*) FROM skill_usage) as skills,
94
+ (SELECT COUNT(*) FROM skill_invocations) as skills,
94
95
  (SELECT COUNT(*) FROM evolution_audit) as evolution,
95
96
  (SELECT COUNT(*) FROM evolution_evidence) as evidence,
96
97
  (SELECT COUNT(*) FROM sessions) as sessions,
@@ -105,18 +106,18 @@ export function getOverviewPayload(db: Database): OverviewPayload {
105
106
  prompts: number;
106
107
  };
107
108
 
108
- // Unmatched queries: skill_usage entries where triggered = 0 and no other
109
+ // Unmatched queries: skill_invocations entries where triggered = 0 and no other
109
110
  // record for the same query text triggered
110
111
  const unmatchedRows = db
111
112
  .query(
112
- `SELECT su.timestamp, su.session_id, su.query
113
- FROM skill_usage su
114
- WHERE su.triggered = 0
113
+ `SELECT si.occurred_at AS timestamp, si.session_id, si.query
114
+ FROM skill_invocations si
115
+ WHERE si.triggered = 0
115
116
  AND NOT EXISTS (
116
- SELECT 1 FROM skill_usage su2
117
- WHERE su2.query = su.query AND su2.triggered = 1
117
+ SELECT 1 FROM skill_invocations si2
118
+ WHERE si2.query = si.query AND si2.triggered = 1
118
119
  )
119
- ORDER BY su.timestamp DESC
120
+ ORDER BY si.occurred_at DESC
120
121
  LIMIT 500`,
121
122
  )
122
123
  .all() as Array<{ timestamp: string; session_id: string; query: string }>;
@@ -144,7 +145,7 @@ export function getSkillReportPayload(db: Database, skillName: string): SkillRep
144
145
  `SELECT
145
146
  COUNT(*) as total_checks,
146
147
  SUM(CASE WHEN triggered = 1 THEN 1 ELSE 0 END) as triggered_count
147
- FROM skill_usage
148
+ FROM skill_invocations
148
149
  WHERE skill_name = ?`,
149
150
  )
150
151
  .get(skillName) as { total_checks: number; triggered_count: number };
@@ -156,14 +157,14 @@ export function getSkillReportPayload(db: Database, skillName: string): SkillRep
156
157
  // Recent invocations (last 100)
157
158
  const invocationRows = db
158
159
  .query(
159
- `SELECT timestamp, session_id, query, triggered, source
160
- FROM skill_usage
160
+ `SELECT occurred_at, session_id, query, triggered, source
161
+ FROM skill_invocations
161
162
  WHERE skill_name = ?
162
- ORDER BY timestamp DESC
163
+ ORDER BY occurred_at DESC
163
164
  LIMIT 100`,
164
165
  )
165
166
  .all(skillName) as Array<{
166
- timestamp: string;
167
+ occurred_at: string;
167
168
  session_id: string;
168
169
  query: string;
169
170
  triggered: number;
@@ -171,7 +172,7 @@ export function getSkillReportPayload(db: Database, skillName: string): SkillRep
171
172
  }>;
172
173
 
173
174
  const recent_invocations = invocationRows.map((row) => ({
174
- timestamp: row.timestamp,
175
+ timestamp: row.occurred_at,
175
176
  session_id: row.session_id,
176
177
  query: row.query,
177
178
  triggered: row.triggered === 1,
@@ -218,7 +219,7 @@ export function getSkillReportPayload(db: Database, skillName: string): SkillRep
218
219
 
219
220
  // Unique sessions count
220
221
  const sessionsRow = db
221
- .query(`SELECT COUNT(DISTINCT session_id) as c FROM skill_usage WHERE skill_name = ?`)
222
+ .query(`SELECT COUNT(DISTINCT session_id) as c FROM skill_invocations WHERE skill_name = ?`)
222
223
  .get(skillName) as { c: number };
223
224
 
224
225
  return {
@@ -241,16 +242,21 @@ export function getSkillsList(db: Database): SkillSummary[] {
241
242
  const rows = db
242
243
  .query(
243
244
  `SELECT
244
- su.skill_name,
245
- (SELECT s2.skill_scope FROM skill_usage s2
246
- WHERE s2.skill_name = su.skill_name AND s2.skill_scope IS NOT NULL
247
- ORDER BY s2.timestamp DESC LIMIT 1) as skill_scope,
245
+ si.skill_name,
246
+ COALESCE(
247
+ (SELECT s2.skill_scope FROM skill_invocations s2
248
+ WHERE s2.skill_name = si.skill_name AND s2.skill_scope IS NOT NULL
249
+ ORDER BY s2.occurred_at DESC LIMIT 1),
250
+ (SELECT su.skill_scope FROM skill_usage su
251
+ WHERE su.skill_name = si.skill_name AND su.skill_scope IS NOT NULL
252
+ ORDER BY su.timestamp DESC LIMIT 1)
253
+ ) as skill_scope,
248
254
  COUNT(*) as total_checks,
249
- SUM(CASE WHEN su.triggered = 1 THEN 1 ELSE 0 END) as triggered_count,
250
- COUNT(DISTINCT su.session_id) as unique_sessions,
251
- MAX(su.timestamp) as last_seen
252
- FROM skill_usage su
253
- GROUP BY su.skill_name
255
+ SUM(CASE WHEN si.triggered = 1 THEN 1 ELSE 0 END) as triggered_count,
256
+ COUNT(DISTINCT si.session_id) as unique_sessions,
257
+ MAX(si.occurred_at) as last_seen
258
+ FROM skill_invocations si
259
+ GROUP BY si.skill_name
254
260
  ORDER BY total_checks DESC`,
255
261
  )
256
262
  .all() as Array<{
@@ -354,9 +360,296 @@ export function getOrchestrateRuns(db: Database, limit = 20): OrchestrateRunRepo
354
360
  }));
355
361
  }
356
362
 
363
+ // -- Generic read queries (Phase 3: replace readJsonl calls) ------------------
364
+
365
+ /**
366
+ * Read all session telemetry records from SQLite.
367
+ * Replaces: readJsonl<SessionTelemetryRecord>(TELEMETRY_LOG)
368
+ */
369
+ export function querySessionTelemetry(db: Database): Array<{
370
+ timestamp: string;
371
+ session_id: string;
372
+ cwd: string;
373
+ transcript_path: string;
374
+ tool_calls: Record<string, number>;
375
+ total_tool_calls: number;
376
+ bash_commands: string[];
377
+ skills_triggered: string[];
378
+ skills_invoked?: string[];
379
+ assistant_turns: number;
380
+ errors_encountered: number;
381
+ transcript_chars: number;
382
+ last_user_query: string;
383
+ source?: string;
384
+ input_tokens?: number;
385
+ output_tokens?: number;
386
+ }> {
387
+ const rows = db.query(`SELECT * FROM session_telemetry ORDER BY timestamp DESC`).all() as Array<
388
+ Record<string, unknown>
389
+ >;
390
+ return rows.map((r) => ({
391
+ timestamp: r.timestamp as string,
392
+ session_id: r.session_id as string,
393
+ cwd: r.cwd as string,
394
+ transcript_path: r.transcript_path as string,
395
+ tool_calls: (safeParseJson(r.tool_calls_json as string) as Record<string, number>) ?? {},
396
+ total_tool_calls: r.total_tool_calls as number,
397
+ bash_commands: safeParseJsonArray<string>(r.bash_commands_json as string),
398
+ skills_triggered: safeParseJsonArray<string>(r.skills_triggered_json as string),
399
+ skills_invoked: r.skills_invoked_json
400
+ ? safeParseJsonArray<string>(r.skills_invoked_json as string)
401
+ : undefined,
402
+ assistant_turns: r.assistant_turns as number,
403
+ errors_encountered: r.errors_encountered as number,
404
+ transcript_chars: (r.transcript_chars as number) ?? 0,
405
+ last_user_query: (r.last_user_query as string) ?? "",
406
+ source: r.source as string | undefined,
407
+ input_tokens: r.input_tokens as number | undefined,
408
+ output_tokens: r.output_tokens as number | undefined,
409
+ }));
410
+ }
411
+
412
+ /**
413
+ * Read all skill invocation records from SQLite.
414
+ * Replaces: readEffectiveSkillUsageRecords()
415
+ */
416
+ export function querySkillRecords(db: Database): Array<{
417
+ timestamp: string;
418
+ session_id: string;
419
+ skill_name: string;
420
+ skill_path: string;
421
+ skill_scope?: string;
422
+ query: string;
423
+ triggered: boolean;
424
+ source?: string;
425
+ }> {
426
+ const rows = db
427
+ .query(
428
+ `SELECT occurred_at, session_id, skill_name, skill_path, skill_scope, query, triggered, source
429
+ FROM skill_invocations ORDER BY occurred_at DESC`,
430
+ )
431
+ .all() as Array<Record<string, unknown>>;
432
+ return rows.map((r) => ({
433
+ timestamp: r.occurred_at as string,
434
+ session_id: r.session_id as string,
435
+ skill_name: r.skill_name as string,
436
+ skill_path: r.skill_path as string,
437
+ skill_scope: r.skill_scope as string | undefined,
438
+ query: r.query as string,
439
+ triggered: (r.triggered as number) === 1,
440
+ source: r.source as string | undefined,
441
+ }));
442
+ }
443
+
444
+ /** @deprecated Use querySkillRecords instead. Kept for backward compatibility. */
445
+ export const querySkillUsageRecords = querySkillRecords;
446
+
447
+ /**
448
+ * Read all query log records from SQLite.
449
+ * Replaces: readJsonl<QueryLogRecord>(QUERY_LOG)
450
+ */
451
+ export function queryQueryLog(db: Database): Array<{
452
+ timestamp: string;
453
+ session_id: string;
454
+ query: string;
455
+ source?: string;
456
+ }> {
457
+ return db
458
+ .query(`SELECT timestamp, session_id, query, source FROM queries ORDER BY timestamp DESC`)
459
+ .all() as Array<{ timestamp: string; session_id: string; query: string; source?: string }>;
460
+ }
461
+
462
+ /**
463
+ * Read all evolution audit entries from SQLite.
464
+ * Replaces: readJsonl<EvolutionAuditEntry>(EVOLUTION_AUDIT_LOG)
465
+ */
466
+ export function queryEvolutionAudit(
467
+ db: Database,
468
+ skillName?: string,
469
+ ): Array<{
470
+ timestamp: string;
471
+ proposal_id: string;
472
+ skill_name?: string;
473
+ action: string;
474
+ details: string;
475
+ eval_snapshot?: Record<string, unknown>;
476
+ }> {
477
+ const sql = skillName
478
+ ? `SELECT * FROM evolution_audit
479
+ WHERE skill_name = ?
480
+ OR (skill_name IS NULL AND proposal_id LIKE 'evo-' || ? || '-%')
481
+ ORDER BY timestamp DESC`
482
+ : `SELECT * FROM evolution_audit ORDER BY timestamp DESC`;
483
+ const rows = (skillName ? db.query(sql).all(skillName, skillName) : db.query(sql).all()) as Array<
484
+ Record<string, unknown>
485
+ >;
486
+ return rows.map((r) => ({
487
+ timestamp: r.timestamp as string,
488
+ proposal_id: r.proposal_id as string,
489
+ skill_name: r.skill_name as string | undefined,
490
+ action: r.action as string,
491
+ details: r.details as string,
492
+ eval_snapshot: r.eval_snapshot_json
493
+ ? (safeParseJson(r.eval_snapshot_json as string) as Record<string, unknown>)
494
+ : undefined,
495
+ }));
496
+ }
497
+
498
+ /**
499
+ * Read all evolution evidence entries from SQLite.
500
+ * Replaces: readEvidenceTrail() / readJsonl<EvolutionEvidenceEntry>(EVOLUTION_EVIDENCE_LOG)
501
+ */
502
+ export function queryEvolutionEvidence(
503
+ db: Database,
504
+ skillName?: string,
505
+ ): Array<{
506
+ timestamp: string;
507
+ proposal_id: string;
508
+ skill_name: string;
509
+ skill_path: string;
510
+ target: string;
511
+ stage: string;
512
+ rationale?: string;
513
+ confidence?: number;
514
+ details?: string;
515
+ original_text?: string;
516
+ proposed_text?: string;
517
+ eval_set?: Record<string, unknown>[];
518
+ validation?: Record<string, unknown>;
519
+ }> {
520
+ const sql = skillName
521
+ ? `SELECT * FROM evolution_evidence WHERE skill_name = ? ORDER BY timestamp DESC`
522
+ : `SELECT * FROM evolution_evidence ORDER BY timestamp DESC`;
523
+ const rows = (skillName ? db.query(sql).all(skillName) : db.query(sql).all()) as Array<
524
+ Record<string, unknown>
525
+ >;
526
+ return rows.map((r) => ({
527
+ timestamp: r.timestamp as string,
528
+ proposal_id: r.proposal_id as string,
529
+ skill_name: r.skill_name as string,
530
+ skill_path: r.skill_path as string,
531
+ target: r.target as string,
532
+ stage: r.stage as string,
533
+ rationale: r.rationale as string | undefined,
534
+ confidence: r.confidence as number | undefined,
535
+ details: r.details as string | undefined,
536
+ original_text: r.original_text as string | undefined,
537
+ proposed_text: r.proposed_text as string | undefined,
538
+ eval_set: r.eval_set_json
539
+ ? safeParseJsonArray<Record<string, unknown>>(r.eval_set_json as string)
540
+ : undefined,
541
+ validation: r.validation_json
542
+ ? (safeParseJson(r.validation_json as string) as Record<string, unknown>)
543
+ : undefined,
544
+ }));
545
+ }
546
+
547
+ /**
548
+ * Read improvement signals from SQLite.
549
+ * Replaces: readJsonl<ImprovementSignalRecord>(SIGNAL_LOG)
550
+ */
551
+ export function queryImprovementSignals(
552
+ db: Database,
553
+ consumedOnly?: boolean,
554
+ ): Array<{
555
+ timestamp: string;
556
+ session_id: string;
557
+ query: string;
558
+ signal_type: string;
559
+ mentioned_skill?: string;
560
+ consumed: boolean;
561
+ consumed_at?: string;
562
+ consumed_by_run?: string;
563
+ }> {
564
+ const where =
565
+ consumedOnly === undefined ? "" : consumedOnly ? " WHERE consumed = 1" : " WHERE consumed = 0";
566
+ const rows = db
567
+ .query(`SELECT * FROM improvement_signals${where} ORDER BY timestamp DESC`)
568
+ .all() as Array<Record<string, unknown>>;
569
+ return rows.map((r) => ({
570
+ timestamp: r.timestamp as string,
571
+ session_id: r.session_id as string,
572
+ query: r.query as string,
573
+ signal_type: r.signal_type as string,
574
+ mentioned_skill: r.mentioned_skill as string | undefined,
575
+ consumed: (r.consumed as number) === 1,
576
+ consumed_at: r.consumed_at as string | undefined,
577
+ consumed_by_run: r.consumed_by_run as string | undefined,
578
+ }));
579
+ }
580
+
581
+ // -- Alpha upload query helpers -----------------------------------------------
582
+
583
+ /**
584
+ * Get the most recent failed queue item's error and timestamp.
585
+ * Returns null if no failed items exist.
586
+ */
587
+ export function getLastUploadError(
588
+ db: Database,
589
+ ): { last_error: string | null; updated_at: string } | null {
590
+ try {
591
+ const row = db
592
+ .query(
593
+ `SELECT last_error, updated_at
594
+ FROM upload_queue
595
+ WHERE status = 'failed'
596
+ ORDER BY updated_at DESC
597
+ LIMIT 1`,
598
+ )
599
+ .get() as { last_error: string | null; updated_at: string } | null;
600
+ return row ?? null;
601
+ } catch {
602
+ return null;
603
+ }
604
+ }
605
+
606
+ /**
607
+ * Get the most recent sent queue item's timestamp.
608
+ * Returns null if no sent items exist.
609
+ */
610
+ export function getLastUploadSuccess(db: Database): { updated_at: string } | null {
611
+ try {
612
+ const row = db
613
+ .query(
614
+ `SELECT updated_at
615
+ FROM upload_queue
616
+ WHERE status = 'sent'
617
+ ORDER BY updated_at DESC
618
+ LIMIT 1`,
619
+ )
620
+ .get() as { updated_at: string } | null;
621
+ return row ?? null;
622
+ } catch {
623
+ return null;
624
+ }
625
+ }
626
+
627
+ /**
628
+ * Get the age in seconds of the oldest pending queue item.
629
+ * Returns null if no pending items exist.
630
+ */
631
+ export function getOldestPendingAge(db: Database): number | null {
632
+ try {
633
+ const row = db
634
+ .query(
635
+ `SELECT created_at
636
+ FROM upload_queue
637
+ WHERE status = 'pending'
638
+ ORDER BY created_at ASC
639
+ LIMIT 1`,
640
+ )
641
+ .get() as { created_at: string } | null;
642
+ if (!row) return null;
643
+ const ageMs = Date.now() - new Date(row.created_at).getTime();
644
+ return Math.floor(ageMs / 1000);
645
+ } catch {
646
+ return null;
647
+ }
648
+ }
649
+
357
650
  // -- Helpers ------------------------------------------------------------------
358
651
 
359
- function safeParseJsonArray<T = string>(json: string | null): T[] {
652
+ export function safeParseJsonArray<T = string>(json: string | null): T[] {
360
653
  if (!json) return [];
361
654
  try {
362
655
  const parsed = JSON.parse(json);
@@ -366,7 +659,7 @@ function safeParseJsonArray<T = string>(json: string | null): T[] {
366
659
  }
367
660
  }
368
661
 
369
- function safeParseJson(json: string | null): Record<string, unknown> | null {
662
+ export function safeParseJson(json: string | null): Record<string, unknown> | null {
370
663
  if (!json) return null;
371
664
  try {
372
665
  return JSON.parse(json);
@@ -47,6 +47,11 @@ CREATE TABLE IF NOT EXISTS skill_invocations (
47
47
  confidence REAL,
48
48
  tool_name TEXT,
49
49
  matched_prompt_id TEXT,
50
+ agent_type TEXT,
51
+ query TEXT,
52
+ skill_path TEXT,
53
+ skill_scope TEXT,
54
+ source TEXT,
50
55
  FOREIGN KEY (session_id) REFERENCES sessions(session_id)
51
56
  )`;
52
57
 
@@ -151,6 +156,67 @@ CREATE TABLE IF NOT EXISTS orchestrate_runs (
151
156
  skill_actions_json TEXT NOT NULL
152
157
  )`;
153
158
 
159
+ // -- Query log table (from all_queries_log.jsonl) ----------------------------
160
+
161
+ export const CREATE_QUERIES = `
162
+ CREATE TABLE IF NOT EXISTS queries (
163
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
164
+ timestamp TEXT NOT NULL,
165
+ session_id TEXT NOT NULL,
166
+ query TEXT NOT NULL,
167
+ source TEXT
168
+ )`;
169
+
170
+ // -- Improvement signal table (from signal_log.jsonl) ------------------------
171
+
172
+ export const CREATE_IMPROVEMENT_SIGNALS = `
173
+ CREATE TABLE IF NOT EXISTS improvement_signals (
174
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
175
+ timestamp TEXT NOT NULL,
176
+ session_id TEXT NOT NULL,
177
+ query TEXT NOT NULL,
178
+ signal_type TEXT NOT NULL,
179
+ mentioned_skill TEXT,
180
+ consumed INTEGER NOT NULL DEFAULT 0,
181
+ consumed_at TEXT,
182
+ consumed_by_run TEXT
183
+ )`;
184
+
185
+ // -- Alpha upload queue -------------------------------------------------------
186
+
187
+ export const CREATE_UPLOAD_QUEUE = `
188
+ CREATE TABLE IF NOT EXISTS upload_queue (
189
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
190
+ payload_type TEXT NOT NULL,
191
+ payload_json TEXT NOT NULL,
192
+ status TEXT NOT NULL DEFAULT 'pending',
193
+ attempts INTEGER NOT NULL DEFAULT 0,
194
+ created_at TEXT NOT NULL,
195
+ updated_at TEXT NOT NULL,
196
+ last_error TEXT
197
+ )`;
198
+
199
+ // -- Canonical upload staging -------------------------------------------------
200
+
201
+ export const CREATE_CANONICAL_UPLOAD_STAGING = `
202
+ CREATE TABLE IF NOT EXISTS canonical_upload_staging (
203
+ local_seq INTEGER PRIMARY KEY AUTOINCREMENT,
204
+ record_kind TEXT NOT NULL,
205
+ record_id TEXT NOT NULL,
206
+ record_json TEXT NOT NULL,
207
+ session_id TEXT,
208
+ prompt_id TEXT,
209
+ normalized_at TEXT,
210
+ staged_at TEXT NOT NULL
211
+ )`;
212
+
213
+ export const CREATE_UPLOAD_WATERMARKS = `
214
+ CREATE TABLE IF NOT EXISTS upload_watermarks (
215
+ payload_type TEXT PRIMARY KEY,
216
+ last_uploaded_id INTEGER NOT NULL,
217
+ updated_at TEXT NOT NULL
218
+ )`;
219
+
154
220
  // -- Metadata table -----------------------------------------------------------
155
221
 
156
222
  export const CREATE_META = `
@@ -167,6 +233,7 @@ export const CREATE_INDEXES = [
167
233
  `CREATE INDEX IF NOT EXISTS idx_prompts_occurred ON prompts(occurred_at)`,
168
234
  `CREATE INDEX IF NOT EXISTS idx_skill_inv_session ON skill_invocations(session_id)`,
169
235
  `CREATE INDEX IF NOT EXISTS idx_skill_inv_name ON skill_invocations(skill_name)`,
236
+ `CREATE INDEX IF NOT EXISTS idx_skill_inv_ts ON skill_invocations(occurred_at)`,
170
237
  `CREATE INDEX IF NOT EXISTS idx_exec_facts_session ON execution_facts(session_id)`,
171
238
  `CREATE INDEX IF NOT EXISTS idx_evo_evidence_proposal ON evolution_evidence(proposal_id)`,
172
239
  `CREATE INDEX IF NOT EXISTS idx_evo_evidence_skill ON evolution_evidence(skill_name)`,
@@ -186,6 +253,43 @@ export const CREATE_INDEXES = [
186
253
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_evo_evidence_dedup ON evolution_evidence(proposal_id, stage, timestamp)`,
187
254
  // -- Orchestrate run indexes -----------------------------------------------
188
255
  `CREATE INDEX IF NOT EXISTS idx_orchestrate_runs_ts ON orchestrate_runs(timestamp)`,
256
+ // -- Query log indexes ------------------------------------------------------
257
+ `CREATE INDEX IF NOT EXISTS idx_queries_session ON queries(session_id)`,
258
+ `CREATE INDEX IF NOT EXISTS idx_queries_ts ON queries(timestamp)`,
259
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_queries_dedup ON queries(session_id, query, timestamp)`,
260
+ // -- Improvement signal indexes ---------------------------------------------
261
+ `CREATE INDEX IF NOT EXISTS idx_signals_session ON improvement_signals(session_id)`,
262
+ `CREATE INDEX IF NOT EXISTS idx_signals_consumed ON improvement_signals(consumed)`,
263
+ `CREATE INDEX IF NOT EXISTS idx_signals_ts ON improvement_signals(timestamp)`,
264
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_signals_dedup ON improvement_signals(session_id, query, signal_type, timestamp)`,
265
+ // -- Alpha upload queue indexes ---------------------------------------------
266
+ `CREATE INDEX IF NOT EXISTS idx_upload_queue_status ON upload_queue(status)`,
267
+ `CREATE INDEX IF NOT EXISTS idx_upload_queue_type_status ON upload_queue(payload_type, status)`,
268
+ // -- Canonical upload staging indexes ---------------------------------------
269
+ `CREATE INDEX IF NOT EXISTS idx_staging_kind ON canonical_upload_staging(record_kind)`,
270
+ `CREATE INDEX IF NOT EXISTS idx_staging_session ON canonical_upload_staging(session_id)`,
271
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_staging_dedup ON canonical_upload_staging(record_kind, record_id)`,
272
+ ];
273
+
274
+ /**
275
+ * Schema migrations — ALTER TABLE statements for columns added after initial release.
276
+ * Each is safe to re-run: SQLite throws "duplicate column" which openDb() catches.
277
+ */
278
+ export const MIGRATIONS = [
279
+ // skill_invocations consolidation (skill_usage columns merged in)
280
+ `ALTER TABLE skill_invocations ADD COLUMN query TEXT`,
281
+ `ALTER TABLE skill_invocations ADD COLUMN skill_path TEXT`,
282
+ `ALTER TABLE skill_invocations ADD COLUMN skill_scope TEXT`,
283
+ `ALTER TABLE skill_invocations ADD COLUMN source TEXT`,
284
+ // Track how many iteration loops each evolution run used
285
+ `ALTER TABLE evolution_audit ADD COLUMN iterations_used INTEGER`,
286
+ ];
287
+
288
+ /** Indexes that depend on migration columns — must run AFTER MIGRATIONS. */
289
+ export const POST_MIGRATION_INDEXES = [
290
+ `CREATE INDEX IF NOT EXISTS idx_skill_inv_query_triggered ON skill_invocations(query, triggered)`,
291
+ `CREATE INDEX IF NOT EXISTS idx_skill_inv_scope ON skill_invocations(skill_name, skill_scope, occurred_at)`,
292
+ `CREATE INDEX IF NOT EXISTS idx_skill_inv_dedup ON skill_invocations(session_id, skill_name, query, occurred_at, triggered)`,
189
293
  ];
190
294
 
191
295
  /** All DDL statements in creation order. */
@@ -199,6 +303,11 @@ export const ALL_DDL = [
199
303
  CREATE_SESSION_TELEMETRY,
200
304
  CREATE_SKILL_USAGE,
201
305
  CREATE_ORCHESTRATE_RUNS,
306
+ CREATE_QUERIES,
307
+ CREATE_IMPROVEMENT_SIGNALS,
308
+ CREATE_UPLOAD_QUEUE,
309
+ CREATE_UPLOAD_WATERMARKS,
310
+ CREATE_CANONICAL_UPLOAD_STAGING,
202
311
  CREATE_META,
203
312
  ...CREATE_INDEXES,
204
313
  ];
@@ -11,6 +11,12 @@ import { parseArgs } from "node:util";
11
11
  import { QUERY_LOG, SKILL_LOG, TELEMETRY_LOG } from "../constants.js";
12
12
  import { classifyInvocation } from "../eval/hooks-to-evals.js";
13
13
  import { getLastDeployedProposal } from "../evolution/audit.js";
14
+ import { getDb } from "../localdb/db.js";
15
+ import {
16
+ queryQueryLog,
17
+ querySessionTelemetry,
18
+ querySkillUsageRecords,
19
+ } from "../localdb/queries.js";
14
20
  import { updateContextAfterWatch } from "../memory/writer.js";
15
21
  import type { SyncResult } from "../sync.js";
16
22
  import type {
@@ -25,7 +31,6 @@ import {
25
31
  filterActionableQueryRecords,
26
32
  filterActionableSkillUsageRecords,
27
33
  } from "../utils/query-filter.js";
28
- import { readEffectiveSkillUsageRecords } from "../utils/skill-log.js";
29
34
 
30
35
  // ---------------------------------------------------------------------------
31
36
  // Public interfaces
@@ -207,13 +212,26 @@ export async function watch(options: WatchOptions): Promise<WatchResult> {
207
212
  );
208
213
  }
209
214
 
210
- // 1. Read log files
211
- const telemetry = readJsonl<SessionTelemetryRecord>(_telemetryLogPath);
212
- const skillRecords =
213
- _skillLogPath === SKILL_LOG
214
- ? readEffectiveSkillUsageRecords()
215
- : readJsonl<SkillUsageRecord>(_skillLogPath);
216
- const queryRecords = readJsonl<QueryLogRecord>(_queryLogPath);
215
+ // 1. Read log files from SQLite (fall back to JSONL for custom paths)
216
+ let telemetry: SessionTelemetryRecord[];
217
+ let skillRecords: SkillUsageRecord[];
218
+ let queryRecords: QueryLogRecord[];
219
+ if (
220
+ _telemetryLogPath === TELEMETRY_LOG &&
221
+ _skillLogPath === SKILL_LOG &&
222
+ _queryLogPath === QUERY_LOG
223
+ ) {
224
+ const db = getDb();
225
+ telemetry = querySessionTelemetry(db) as SessionTelemetryRecord[];
226
+ // SQLite queries return DESC order; computeMonitoringSnapshot expects chronological (ASC)
227
+ telemetry.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
228
+ skillRecords = querySkillUsageRecords(db) as SkillUsageRecord[];
229
+ queryRecords = queryQueryLog(db) as QueryLogRecord[];
230
+ } else {
231
+ telemetry = readJsonl<SessionTelemetryRecord>(_telemetryLogPath);
232
+ skillRecords = readJsonl<SkillUsageRecord>(_skillLogPath);
233
+ queryRecords = readJsonl<QueryLogRecord>(_queryLogPath);
234
+ }
217
235
 
218
236
  // 2. Determine baseline pass rate from last deployed audit entry
219
237
  const lastDeployed = getLastDeployedProposal(skillName, _auditLogPath);