selftune 0.2.16 → 0.2.19
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.
- package/README.md +32 -22
- package/apps/local-dashboard/dist/assets/index-DnhnXQm6.js +60 -0
- package/apps/local-dashboard/dist/assets/index-_EcLywDg.css +1 -0
- package/apps/local-dashboard/dist/assets/vendor-table-BIiI3YhS.js +1 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-CGEmUayx.js +12 -0
- package/apps/local-dashboard/dist/index.html +5 -5
- package/cli/selftune/alpha-upload/build-payloads.ts +14 -1
- package/cli/selftune/alpha-upload/client.ts +51 -1
- package/cli/selftune/alpha-upload/flush.ts +46 -5
- package/cli/selftune/alpha-upload/stage-canonical.ts +32 -10
- package/cli/selftune/alpha-upload-contract.ts +9 -0
- package/cli/selftune/constants.ts +92 -5
- package/cli/selftune/contribute/contribute.ts +30 -2
- package/cli/selftune/contribute/sanitize.ts +52 -5
- package/cli/selftune/contribution-config.ts +249 -0
- package/cli/selftune/contribution-relay.ts +177 -0
- package/cli/selftune/contribution-signals.ts +219 -0
- package/cli/selftune/contribution-staging.ts +147 -0
- package/cli/selftune/contributions.ts +532 -0
- package/cli/selftune/creator-contributions.ts +333 -0
- package/cli/selftune/dashboard-contract.ts +305 -1
- package/cli/selftune/dashboard-server.ts +47 -13
- package/cli/selftune/eval/family-overlap.ts +395 -0
- package/cli/selftune/eval/hooks-to-evals.ts +182 -28
- package/cli/selftune/eval/synthetic-evals.ts +298 -11
- package/cli/selftune/evolution/description-quality.ts +12 -11
- package/cli/selftune/evolution/evolve.ts +214 -51
- package/cli/selftune/evolution/validate-proposal.ts +9 -6
- package/cli/selftune/export.ts +2 -2
- package/cli/selftune/grading/grade-session.ts +20 -0
- package/cli/selftune/hooks/commit-track.ts +188 -0
- package/cli/selftune/hooks/prompt-log.ts +10 -1
- package/cli/selftune/hooks/session-stop.ts +2 -2
- package/cli/selftune/hooks/skill-eval.ts +15 -1
- package/cli/selftune/hooks/stdin-preview.ts +32 -0
- package/cli/selftune/index.ts +41 -5
- package/cli/selftune/ingestors/codex-rollout.ts +31 -35
- package/cli/selftune/ingestors/codex-wrapper.ts +32 -24
- package/cli/selftune/localdb/db.ts +2 -2
- package/cli/selftune/localdb/direct-write.ts +69 -6
- package/cli/selftune/localdb/queries.ts +1253 -37
- package/cli/selftune/localdb/schema.ts +66 -0
- package/cli/selftune/orchestrate.ts +32 -4
- package/cli/selftune/recover.ts +153 -0
- package/cli/selftune/repair/skill-usage.ts +363 -4
- package/cli/selftune/routes/actions.ts +35 -1
- package/cli/selftune/routes/analytics.ts +14 -0
- package/cli/selftune/routes/index.ts +1 -0
- package/cli/selftune/routes/overview.ts +150 -4
- package/cli/selftune/routes/skill-report.ts +648 -18
- package/cli/selftune/status.ts +81 -2
- package/cli/selftune/sync.ts +56 -2
- package/cli/selftune/trust-model.ts +66 -0
- package/cli/selftune/types.ts +80 -0
- package/cli/selftune/utils/skill-detection.ts +43 -0
- package/cli/selftune/utils/transcript.ts +210 -1
- package/cli/selftune/watchlist.ts +65 -0
- package/node_modules/@selftune/telemetry-contract/src/types.ts +11 -0
- package/package.json +1 -1
- package/packages/telemetry-contract/src/types.ts +11 -0
- package/packages/ui/src/components/ActivityTimeline.tsx +165 -150
- package/packages/ui/src/components/EvidenceViewer.tsx +335 -144
- package/packages/ui/src/components/EvolutionTimeline.tsx +58 -28
- package/packages/ui/src/components/OrchestrateRunsPanel.tsx +33 -16
- package/packages/ui/src/components/RecentActivityFeed.tsx +72 -41
- package/packages/ui/src/components/section-cards.tsx +12 -9
- package/packages/ui/src/primitives/card.tsx +1 -1
- package/skill/SKILL.md +40 -2
- package/skill/Workflows/AlphaUpload.md +4 -0
- package/skill/Workflows/Composability.md +64 -0
- package/skill/Workflows/Contribute.md +6 -3
- package/skill/Workflows/Contributions.md +97 -0
- package/skill/Workflows/CreatorContributions.md +74 -0
- package/skill/Workflows/Dashboard.md +31 -0
- package/skill/Workflows/Evals.md +57 -8
- package/skill/Workflows/Evolve.md +31 -13
- package/skill/Workflows/ExportCanonical.md +121 -0
- package/skill/Workflows/Hook.md +131 -0
- package/skill/Workflows/Ingest.md +7 -0
- package/skill/Workflows/Initialize.md +29 -9
- package/skill/Workflows/Orchestrate.md +27 -5
- package/skill/Workflows/Quickstart.md +94 -0
- package/skill/Workflows/Recover.md +84 -0
- package/skill/Workflows/RepairSkillUsage.md +95 -0
- package/skill/Workflows/Sync.md +18 -12
- package/skill/Workflows/Uninstall.md +82 -0
- package/skill/settings_snippet.json +11 -0
- package/apps/local-dashboard/dist/assets/index-BMIS6uUh.css +0 -2
- package/apps/local-dashboard/dist/assets/index-DOu3iLD9.js +0 -16
- package/apps/local-dashboard/dist/assets/vendor-table-pHbDxq36.js +0 -8
- package/apps/local-dashboard/dist/assets/vendor-ui-DIwlrGlb.js +0 -12
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
+
import type { Database } from "bun:sqlite";
|
|
3
4
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
4
5
|
import { basename, dirname, join } from "node:path";
|
|
5
6
|
import { parseArgs } from "node:util";
|
|
@@ -19,7 +20,9 @@ import {
|
|
|
19
20
|
parseRolloutFile,
|
|
20
21
|
} from "../ingestors/codex-rollout.js";
|
|
21
22
|
import { getDb } from "../localdb/db.js";
|
|
23
|
+
import { writeSkillCheckToDb } from "../localdb/direct-write.js";
|
|
22
24
|
import { queryQueryLog, querySkillUsageRecords } from "../localdb/queries.js";
|
|
25
|
+
import { buildCanonicalSkillInvocation, deriveInvocationMode } from "../normalization.js";
|
|
23
26
|
import type { QueryLogRecord, SkillUsageRecord } from "../types.js";
|
|
24
27
|
import { readJsonl } from "../utils/jsonl.js";
|
|
25
28
|
import { isActionableQueryText } from "../utils/query-filter.js";
|
|
@@ -60,6 +63,47 @@ interface ResolvedSkillPath {
|
|
|
60
63
|
resolutionSource: NonNullable<SkillUsageRecord["skill_path_resolution_source"]>;
|
|
61
64
|
}
|
|
62
65
|
|
|
66
|
+
export interface RepairSQLiteResult {
|
|
67
|
+
deleted_legacy_rows: number;
|
|
68
|
+
deleted_prior_repair_rows: number;
|
|
69
|
+
inserted_repair_rows: number;
|
|
70
|
+
skipped_pairs_with_canonical: number;
|
|
71
|
+
repaired_pairs_inserted: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function deleteRedundantLegacyRows(db: Database): number {
|
|
75
|
+
const deleteTriggered = db.run(`
|
|
76
|
+
DELETE FROM skill_invocations
|
|
77
|
+
WHERE skill_invocation_id LIKE '%:su:%'
|
|
78
|
+
AND triggered = 1
|
|
79
|
+
AND EXISTS (
|
|
80
|
+
SELECT 1
|
|
81
|
+
FROM skill_invocations current
|
|
82
|
+
WHERE current.session_id = skill_invocations.session_id
|
|
83
|
+
AND lower(current.skill_name) = lower(skill_invocations.skill_name)
|
|
84
|
+
AND current.skill_invocation_id NOT LIKE '%:su:%'
|
|
85
|
+
AND current.triggered = 1
|
|
86
|
+
)
|
|
87
|
+
`);
|
|
88
|
+
|
|
89
|
+
const deleteMisses = db.run(`
|
|
90
|
+
DELETE FROM skill_invocations
|
|
91
|
+
WHERE skill_invocation_id LIKE '%:su:%'
|
|
92
|
+
AND triggered = 0
|
|
93
|
+
AND EXISTS (
|
|
94
|
+
SELECT 1
|
|
95
|
+
FROM skill_invocations current
|
|
96
|
+
WHERE current.session_id = skill_invocations.session_id
|
|
97
|
+
AND lower(current.skill_name) = lower(skill_invocations.skill_name)
|
|
98
|
+
AND COALESCE(current.query, '') = COALESCE(skill_invocations.query, '')
|
|
99
|
+
AND current.skill_invocation_id NOT LIKE '%:su:%'
|
|
100
|
+
AND current.triggered = 0
|
|
101
|
+
)
|
|
102
|
+
`);
|
|
103
|
+
|
|
104
|
+
return deleteTriggered.changes + deleteMisses.changes;
|
|
105
|
+
}
|
|
106
|
+
|
|
63
107
|
function isEphemeralLauncherProjectRoot(projectRoot: string): boolean {
|
|
64
108
|
return projectRoot.startsWith("/tmp/") || projectRoot.startsWith("/private/tmp/");
|
|
65
109
|
}
|
|
@@ -238,6 +282,7 @@ function extractSessionSkillUsage(
|
|
|
238
282
|
let lastUserMessage: ActionableUserMessage | null = null;
|
|
239
283
|
let sessionCwd: string | undefined;
|
|
240
284
|
const seen = new Set<string>();
|
|
285
|
+
const pendingContextualReads = new Map<string, SkillUsageRecord>();
|
|
241
286
|
const pendingSkillCalls = new Map<string, { skillName: string; recordIndex?: number }>();
|
|
242
287
|
const repaired: SkillUsageRecord[] = [];
|
|
243
288
|
|
|
@@ -336,9 +381,25 @@ function extractSessionSkillUsage(
|
|
|
336
381
|
if (toolName === "Read") {
|
|
337
382
|
const filePath = (input.file_path as string) ?? "";
|
|
338
383
|
if (filePath.endsWith("SKILL.md")) {
|
|
339
|
-
const inferredSkillName = basename(dirname(filePath)).trim()
|
|
384
|
+
const inferredSkillName = basename(dirname(filePath)).trim();
|
|
340
385
|
if (inferredSkillName && !skillPathLookup.has(inferredSkillName)) {
|
|
341
|
-
skillPathLookup.set(inferredSkillName, filePath);
|
|
386
|
+
skillPathLookup.set(inferredSkillName.toLowerCase(), filePath);
|
|
387
|
+
}
|
|
388
|
+
if (lastUserMessage && inferredSkillName) {
|
|
389
|
+
const dedupeKey = invocationKey(sessionId, inferredSkillName, lastUserMessage.query);
|
|
390
|
+
if (!seen.has(dedupeKey) && !pendingContextualReads.has(dedupeKey)) {
|
|
391
|
+
pendingContextualReads.set(dedupeKey, {
|
|
392
|
+
timestamp: timestamp || lastUserMessage.timestamp || fallbackTimestamp,
|
|
393
|
+
session_id: sessionId,
|
|
394
|
+
skill_name: inferredSkillName,
|
|
395
|
+
skill_path: filePath,
|
|
396
|
+
...classifySkillPath(filePath, homeDir, codexHome),
|
|
397
|
+
skill_path_resolution_source: "raw_log",
|
|
398
|
+
query: lastUserMessage.query,
|
|
399
|
+
triggered: false,
|
|
400
|
+
source: "claude_code_repair",
|
|
401
|
+
});
|
|
402
|
+
}
|
|
342
403
|
}
|
|
343
404
|
}
|
|
344
405
|
continue;
|
|
@@ -350,9 +411,10 @@ function extractSessionSkillUsage(
|
|
|
350
411
|
if (!skillName) continue;
|
|
351
412
|
const toolUseId = optionalString(toolUse.id);
|
|
352
413
|
|
|
353
|
-
const dedupeKey =
|
|
414
|
+
const dedupeKey = invocationKey(sessionId, skillName, lastUserMessage.query);
|
|
354
415
|
if (seen.has(dedupeKey)) continue;
|
|
355
416
|
seen.add(dedupeKey);
|
|
417
|
+
pendingContextualReads.delete(dedupeKey);
|
|
356
418
|
|
|
357
419
|
const knownSkillPath = skillPathLookup.get(skillName.toLowerCase());
|
|
358
420
|
const { skillPath, resolutionSource } = knownSkillPath
|
|
@@ -378,6 +440,10 @@ function extractSessionSkillUsage(
|
|
|
378
440
|
}
|
|
379
441
|
}
|
|
380
442
|
|
|
443
|
+
if (pendingContextualReads.size > 0) {
|
|
444
|
+
repaired.push(...pendingContextualReads.values());
|
|
445
|
+
}
|
|
446
|
+
|
|
381
447
|
return { processed: true, records: repaired };
|
|
382
448
|
}
|
|
383
449
|
|
|
@@ -465,6 +531,297 @@ export function rebuildSkillUsageFromTranscripts(
|
|
|
465
531
|
return { repairedRecords, repairedSessionIds };
|
|
466
532
|
}
|
|
467
533
|
|
|
534
|
+
function inferRepairPlatform(source: string | undefined): "claude_code" | "codex" {
|
|
535
|
+
return source?.includes("codex") ? "codex" : "claude_code";
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function normalizeRepairSkillName(skillName: string): string {
|
|
539
|
+
return skillName.trim().toLowerCase();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function pairKey(sessionId: string, skillName: string): string {
|
|
543
|
+
return `${sessionId}\u0000${normalizeRepairSkillName(skillName)}`;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function splitPairKey(key: string): { sessionId: string; skillName: string } {
|
|
547
|
+
const [sessionId, skillName] = key.split("\u0000");
|
|
548
|
+
return { sessionId, skillName: normalizeRepairSkillName(skillName) };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function compareRepairRecords(a: SkillUsageRecord, b: SkillUsageRecord): number {
|
|
552
|
+
return (
|
|
553
|
+
a.timestamp.localeCompare(b.timestamp) ||
|
|
554
|
+
a.query.localeCompare(b.query) ||
|
|
555
|
+
a.skill_path.localeCompare(b.skill_path) ||
|
|
556
|
+
Number(a.triggered) - Number(b.triggered)
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function invocationKey(sessionId: string, skillName: string, query: string): string {
|
|
561
|
+
return `${sessionId}\u0000${skillName.trim().toLowerCase()}\u0000${query}`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function stableKeyHash(value: string): string {
|
|
565
|
+
let hash = 2166136261;
|
|
566
|
+
for (let i = 0; i < value.length; i++) {
|
|
567
|
+
hash ^= value.charCodeAt(i);
|
|
568
|
+
hash = Math.imul(hash, 16777619);
|
|
569
|
+
}
|
|
570
|
+
return (hash >>> 0).toString(16);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Persist repaired skill usage into the canonical skill_invocations table.
|
|
575
|
+
*
|
|
576
|
+
* Strategy:
|
|
577
|
+
* - delete legacy triggered :su: rows for repaired session/skill pairs
|
|
578
|
+
* - delete prior capture_mode=repair rows for those pairs (idempotent reruns)
|
|
579
|
+
* - insert repaired rows only when the pair has no canonical triggered rows
|
|
580
|
+
*
|
|
581
|
+
* This lets repair improve SQLite without overriding source-truth canonical
|
|
582
|
+
* replay/hook rows, while also removing duplicate legacy trigger rows from
|
|
583
|
+
* mixed historical sessions.
|
|
584
|
+
*/
|
|
585
|
+
export function persistRepairedSkillUsageToDb(
|
|
586
|
+
db: Database,
|
|
587
|
+
records: SkillUsageRecord[],
|
|
588
|
+
): RepairSQLiteResult {
|
|
589
|
+
const triggeredRecords = records.filter((record) => record.triggered);
|
|
590
|
+
const missedRecords = records.filter((record) => !record.triggered);
|
|
591
|
+
const recordsByPair = new Map<string, SkillUsageRecord[]>();
|
|
592
|
+
const missedRecordsByKey = new Map<string, SkillUsageRecord>();
|
|
593
|
+
|
|
594
|
+
for (const record of triggeredRecords) {
|
|
595
|
+
const key = pairKey(record.session_id, record.skill_name);
|
|
596
|
+
const bucket = recordsByPair.get(key);
|
|
597
|
+
if (bucket) {
|
|
598
|
+
bucket.push(record);
|
|
599
|
+
} else {
|
|
600
|
+
recordsByPair.set(key, [record]);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
for (const record of missedRecords) {
|
|
604
|
+
const key = invocationKey(record.session_id, record.skill_name, record.query);
|
|
605
|
+
const existing = missedRecordsByKey.get(key);
|
|
606
|
+
if (!existing || compareRepairRecords(record, existing) < 0) {
|
|
607
|
+
missedRecordsByKey.set(key, record);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (recordsByPair.size === 0 && missedRecordsByKey.size === 0) {
|
|
612
|
+
return {
|
|
613
|
+
deleted_legacy_rows: 0,
|
|
614
|
+
deleted_prior_repair_rows: 0,
|
|
615
|
+
inserted_repair_rows: 0,
|
|
616
|
+
skipped_pairs_with_canonical: 0,
|
|
617
|
+
repaired_pairs_inserted: 0,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const selectExisting = db.prepare(`
|
|
622
|
+
SELECT
|
|
623
|
+
COALESCE(SUM(CASE WHEN skill_invocation_id LIKE '%:su:%' AND triggered = 1 THEN 1 ELSE 0 END), 0) AS legacy_rows,
|
|
624
|
+
COALESCE(SUM(CASE WHEN capture_mode = 'repair' AND triggered = 1 THEN 1 ELSE 0 END), 0) AS repair_rows,
|
|
625
|
+
COALESCE(
|
|
626
|
+
SUM(
|
|
627
|
+
CASE
|
|
628
|
+
WHEN skill_invocation_id NOT LIKE '%:su:%'
|
|
629
|
+
AND COALESCE(capture_mode, '') != 'repair'
|
|
630
|
+
AND triggered = 1
|
|
631
|
+
THEN 1 ELSE 0
|
|
632
|
+
END
|
|
633
|
+
),
|
|
634
|
+
0
|
|
635
|
+
) AS canonical_rows
|
|
636
|
+
FROM skill_invocations
|
|
637
|
+
WHERE session_id = ? AND LOWER(skill_name) = ?
|
|
638
|
+
`);
|
|
639
|
+
const deleteLegacyTriggered = db.prepare(`
|
|
640
|
+
DELETE FROM skill_invocations
|
|
641
|
+
WHERE session_id = ? AND LOWER(skill_name) = ? AND skill_invocation_id LIKE '%:su:%' AND triggered = 1
|
|
642
|
+
`);
|
|
643
|
+
const deleteRepairTriggered = db.prepare(`
|
|
644
|
+
DELETE FROM skill_invocations
|
|
645
|
+
WHERE session_id = ? AND LOWER(skill_name) = ? AND capture_mode = 'repair' AND triggered = 1
|
|
646
|
+
`);
|
|
647
|
+
const selectExistingMiss = db.prepare(`
|
|
648
|
+
SELECT
|
|
649
|
+
COALESCE(SUM(CASE WHEN skill_invocation_id LIKE '%:su:%' AND triggered = 0 THEN 1 ELSE 0 END), 0) AS legacy_rows,
|
|
650
|
+
COALESCE(SUM(CASE WHEN capture_mode = 'repair' AND triggered = 0 THEN 1 ELSE 0 END), 0) AS repair_rows,
|
|
651
|
+
COALESCE(
|
|
652
|
+
SUM(
|
|
653
|
+
CASE
|
|
654
|
+
WHEN skill_invocation_id NOT LIKE '%:su:%'
|
|
655
|
+
AND COALESCE(capture_mode, '') != 'repair'
|
|
656
|
+
AND triggered = 0
|
|
657
|
+
THEN 1 ELSE 0
|
|
658
|
+
END
|
|
659
|
+
),
|
|
660
|
+
0
|
|
661
|
+
) AS canonical_rows
|
|
662
|
+
FROM skill_invocations
|
|
663
|
+
WHERE session_id = ? AND LOWER(skill_name) = ? AND query = ?
|
|
664
|
+
`);
|
|
665
|
+
const deleteLegacyMiss = db.prepare(`
|
|
666
|
+
DELETE FROM skill_invocations
|
|
667
|
+
WHERE session_id = ? AND LOWER(skill_name) = ? AND query = ? AND skill_invocation_id LIKE '%:su:%' AND triggered = 0
|
|
668
|
+
`);
|
|
669
|
+
const deleteRepairMiss = db.prepare(`
|
|
670
|
+
DELETE FROM skill_invocations
|
|
671
|
+
WHERE session_id = ? AND LOWER(skill_name) = ? AND query = ? AND capture_mode = 'repair' AND triggered = 0
|
|
672
|
+
`);
|
|
673
|
+
|
|
674
|
+
const result: RepairSQLiteResult = {
|
|
675
|
+
deleted_legacy_rows: 0,
|
|
676
|
+
deleted_prior_repair_rows: 0,
|
|
677
|
+
inserted_repair_rows: 0,
|
|
678
|
+
skipped_pairs_with_canonical: 0,
|
|
679
|
+
repaired_pairs_inserted: 0,
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
db.run("BEGIN IMMEDIATE");
|
|
683
|
+
try {
|
|
684
|
+
const redundantLegacyRows = deleteRedundantLegacyRows(db);
|
|
685
|
+
result.deleted_legacy_rows += redundantLegacyRows;
|
|
686
|
+
|
|
687
|
+
for (const [key, pairRecords] of recordsByPair.entries()) {
|
|
688
|
+
const { sessionId, skillName } = splitPairKey(key);
|
|
689
|
+
const existing = selectExisting.get(sessionId, skillName) as
|
|
690
|
+
| { legacy_rows: number; repair_rows: number; canonical_rows: number }
|
|
691
|
+
| undefined;
|
|
692
|
+
|
|
693
|
+
const legacyRows = existing?.legacy_rows ?? 0;
|
|
694
|
+
const repairRows = existing?.repair_rows ?? 0;
|
|
695
|
+
const canonicalRows = existing?.canonical_rows ?? 0;
|
|
696
|
+
|
|
697
|
+
if (repairRows > 0) {
|
|
698
|
+
deleteRepairTriggered.run(sessionId, skillName);
|
|
699
|
+
result.deleted_prior_repair_rows += repairRows;
|
|
700
|
+
}
|
|
701
|
+
if (legacyRows > 0) {
|
|
702
|
+
deleteLegacyTriggered.run(sessionId, skillName);
|
|
703
|
+
result.deleted_legacy_rows += legacyRows;
|
|
704
|
+
}
|
|
705
|
+
if (canonicalRows > 0) {
|
|
706
|
+
result.skipped_pairs_with_canonical += 1;
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const sortedRecords = [...pairRecords].sort(compareRepairRecords);
|
|
711
|
+
const canonicalSkillName = sortedRecords[0]?.skill_name.trim() || skillName;
|
|
712
|
+
const { invocation_mode, confidence } = deriveInvocationMode({ is_repaired: true });
|
|
713
|
+
|
|
714
|
+
for (let index = 0; index < sortedRecords.length; index++) {
|
|
715
|
+
const record = sortedRecords[index];
|
|
716
|
+
const platform = inferRepairPlatform(record.source);
|
|
717
|
+
const canonical = buildCanonicalSkillInvocation({
|
|
718
|
+
platform,
|
|
719
|
+
capture_mode: "repair",
|
|
720
|
+
source_session_kind: "repaired",
|
|
721
|
+
session_id: record.session_id,
|
|
722
|
+
raw_source_ref: {
|
|
723
|
+
event_type: "repair-skill-usage",
|
|
724
|
+
metadata: {
|
|
725
|
+
source: record.source ?? null,
|
|
726
|
+
skill_path_resolution_source: record.skill_path_resolution_source ?? null,
|
|
727
|
+
skill_project_root: record.skill_project_root ?? null,
|
|
728
|
+
skill_registry_dir: record.skill_registry_dir ?? null,
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
skill_invocation_id: `${record.session_id}:r:${canonicalSkillName}:${index}`,
|
|
732
|
+
occurred_at: record.timestamp,
|
|
733
|
+
skill_name: canonicalSkillName,
|
|
734
|
+
skill_path: record.skill_path,
|
|
735
|
+
invocation_mode,
|
|
736
|
+
triggered: true,
|
|
737
|
+
confidence,
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
writeSkillCheckToDb({
|
|
741
|
+
...canonical,
|
|
742
|
+
query: record.query,
|
|
743
|
+
skill_path: record.skill_path,
|
|
744
|
+
skill_scope: record.skill_scope,
|
|
745
|
+
source: record.source,
|
|
746
|
+
});
|
|
747
|
+
result.inserted_repair_rows += 1;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
result.repaired_pairs_inserted += 1;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
for (const record of missedRecordsByKey.values()) {
|
|
754
|
+
const canonicalSkillName = record.skill_name.trim();
|
|
755
|
+
const normalizedSkillName = normalizeRepairSkillName(canonicalSkillName);
|
|
756
|
+
const existing = selectExistingMiss.get(
|
|
757
|
+
record.session_id,
|
|
758
|
+
normalizedSkillName,
|
|
759
|
+
record.query,
|
|
760
|
+
) as { legacy_rows: number; repair_rows: number; canonical_rows: number } | undefined;
|
|
761
|
+
|
|
762
|
+
const legacyRows = existing?.legacy_rows ?? 0;
|
|
763
|
+
const repairRows = existing?.repair_rows ?? 0;
|
|
764
|
+
const canonicalRows = existing?.canonical_rows ?? 0;
|
|
765
|
+
|
|
766
|
+
if (repairRows > 0) {
|
|
767
|
+
deleteRepairMiss.run(record.session_id, normalizedSkillName, record.query);
|
|
768
|
+
result.deleted_prior_repair_rows += repairRows;
|
|
769
|
+
}
|
|
770
|
+
if (legacyRows > 0) {
|
|
771
|
+
deleteLegacyMiss.run(record.session_id, normalizedSkillName, record.query);
|
|
772
|
+
result.deleted_legacy_rows += legacyRows;
|
|
773
|
+
}
|
|
774
|
+
if (canonicalRows > 0) {
|
|
775
|
+
result.skipped_pairs_with_canonical += 1;
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const platform = inferRepairPlatform(record.source);
|
|
780
|
+
const { invocation_mode, confidence } = deriveInvocationMode({ is_repaired: true });
|
|
781
|
+
const canonical = buildCanonicalSkillInvocation({
|
|
782
|
+
platform,
|
|
783
|
+
capture_mode: "repair",
|
|
784
|
+
source_session_kind: "repaired",
|
|
785
|
+
session_id: record.session_id,
|
|
786
|
+
raw_source_ref: {
|
|
787
|
+
event_type: "repair-skill-usage",
|
|
788
|
+
metadata: {
|
|
789
|
+
source: record.source ?? null,
|
|
790
|
+
skill_path_resolution_source: record.skill_path_resolution_source ?? null,
|
|
791
|
+
skill_project_root: record.skill_project_root ?? null,
|
|
792
|
+
skill_registry_dir: record.skill_registry_dir ?? null,
|
|
793
|
+
miss_type: "contextual_read",
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
skill_invocation_id: `${record.session_id}:rmiss:${canonicalSkillName}:${stableKeyHash(record.query)}`,
|
|
797
|
+
occurred_at: record.timestamp,
|
|
798
|
+
skill_name: canonicalSkillName,
|
|
799
|
+
skill_path: record.skill_path,
|
|
800
|
+
invocation_mode,
|
|
801
|
+
triggered: false,
|
|
802
|
+
confidence,
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
writeSkillCheckToDb({
|
|
806
|
+
...canonical,
|
|
807
|
+
query: record.query,
|
|
808
|
+
skill_path: record.skill_path,
|
|
809
|
+
skill_scope: record.skill_scope,
|
|
810
|
+
source: record.source,
|
|
811
|
+
});
|
|
812
|
+
result.inserted_repair_rows += 1;
|
|
813
|
+
result.repaired_pairs_inserted += 1;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
db.run("COMMIT");
|
|
817
|
+
} catch (error) {
|
|
818
|
+
db.run("ROLLBACK");
|
|
819
|
+
throw error;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return result;
|
|
823
|
+
}
|
|
824
|
+
|
|
468
825
|
export function cliMain(): void {
|
|
469
826
|
try {
|
|
470
827
|
const { values } = parseArgs({
|
|
@@ -571,13 +928,15 @@ Options:
|
|
|
571
928
|
return;
|
|
572
929
|
}
|
|
573
930
|
|
|
931
|
+
const sqlite = persistRepairedSkillUsageToDb(getDb(), repairedRecords);
|
|
932
|
+
|
|
574
933
|
writeRepairedSkillUsageRecords(
|
|
575
934
|
repairedRecords,
|
|
576
935
|
repairedSessionIds,
|
|
577
936
|
values.out ?? REPAIRED_SKILL_LOG,
|
|
578
937
|
values["sessions-marker"] ?? REPAIRED_SKILL_SESSIONS_MARKER,
|
|
579
938
|
);
|
|
580
|
-
console.log(JSON.stringify(summary, null, 2));
|
|
939
|
+
console.log(JSON.stringify({ ...summary, sqlite }, null, 2));
|
|
581
940
|
} catch (error) {
|
|
582
941
|
const message = error instanceof Error ? error.message : String(error);
|
|
583
942
|
console.error(`[ERROR] Failed to repair skill usage: ${message}`);
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Route handler: POST /api/actions/{watch,evolve,rollback}
|
|
2
|
+
* Route handler: POST /api/actions/{watch,evolve,rollback,watchlist}
|
|
3
3
|
*
|
|
4
4
|
* Triggers selftune CLI commands as child processes and returns the result.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
|
|
9
|
+
import { saveWatchedSkills } from "../watchlist.js";
|
|
10
|
+
|
|
9
11
|
export type ActionRunner = (
|
|
10
12
|
command: string,
|
|
11
13
|
args: string[],
|
|
@@ -41,6 +43,38 @@ export async function handleAction(
|
|
|
41
43
|
body: Record<string, unknown>,
|
|
42
44
|
executeAction: ActionRunner = runAction,
|
|
43
45
|
): Promise<Response> {
|
|
46
|
+
if (action === "watchlist") {
|
|
47
|
+
const skills = body.skills;
|
|
48
|
+
if (skills === undefined || skills === null) {
|
|
49
|
+
return Response.json(
|
|
50
|
+
{ success: false, error: "Missing required field: skills[]" },
|
|
51
|
+
{ status: 400 },
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (!Array.isArray(skills) || !skills.every((skill) => typeof skill === "string")) {
|
|
55
|
+
return Response.json(
|
|
56
|
+
{
|
|
57
|
+
success: false,
|
|
58
|
+
error: "Invalid type for skills: expected array of strings",
|
|
59
|
+
},
|
|
60
|
+
{ status: 400 },
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const saved = saveWatchedSkills(skills);
|
|
65
|
+
return Response.json({ success: true, watched_skills: saved, error: null });
|
|
66
|
+
} catch (error: unknown) {
|
|
67
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
68
|
+
return Response.json(
|
|
69
|
+
{
|
|
70
|
+
success: false,
|
|
71
|
+
error: `Failed to save watched skills. Check your selftune config directory and try again. ${message}`,
|
|
72
|
+
},
|
|
73
|
+
{ status: 500 },
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
44
78
|
if (action === "watch" || action === "evolve") {
|
|
45
79
|
const skill = body.skill as string | undefined;
|
|
46
80
|
const skillPath = body.skillPath as string | undefined;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route handler: GET /api/v2/analytics
|
|
3
|
+
*
|
|
4
|
+
* Returns performance analytics payload from SQLite.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Database } from "bun:sqlite";
|
|
8
|
+
|
|
9
|
+
import { getAnalyticsPayload } from "../localdb/queries.js";
|
|
10
|
+
|
|
11
|
+
export function handleAnalytics(db: Database): Response {
|
|
12
|
+
const analytics = getAnalyticsPayload(db);
|
|
13
|
+
return Response.json(analytics);
|
|
14
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
export type { ActionRunner } from "./actions.js";
|
|
8
8
|
export { handleAction, runAction } from "./actions.js";
|
|
9
|
+
export { handleAnalytics } from "./analytics.js";
|
|
9
10
|
export { handleBadge } from "./badge.js";
|
|
10
11
|
export { handleDoctor } from "./doctor.js";
|
|
11
12
|
export { handleOrchestrateRuns } from "./orchestrate-runs.js";
|
|
@@ -2,14 +2,160 @@
|
|
|
2
2
|
* Route handler: GET /api/v2/overview
|
|
3
3
|
*
|
|
4
4
|
* Returns SQLite-backed overview payload with skill listing and version info.
|
|
5
|
+
* Supports optional cursor-based pagination via query params:
|
|
6
|
+
* ?telemetry_cursor=<json>&telemetry_limit=N&skills_cursor=<json>&skills_limit=N
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import type { Database } from "bun:sqlite";
|
|
8
10
|
|
|
9
|
-
import {
|
|
11
|
+
import type {
|
|
12
|
+
AttentionItem,
|
|
13
|
+
AutonomousDecision,
|
|
14
|
+
AutonomyStatus,
|
|
15
|
+
AutonomyStatusLevel,
|
|
16
|
+
OverviewResponse,
|
|
17
|
+
} from "../dashboard-contract.js";
|
|
18
|
+
import { parseCursorParam, parseIntParam } from "../dashboard-contract.js";
|
|
19
|
+
import {
|
|
20
|
+
getAttentionQueue,
|
|
21
|
+
getOverviewPayload,
|
|
22
|
+
getOverviewPayloadPaginated,
|
|
23
|
+
getRecentDecisions,
|
|
24
|
+
getSkillTrustSummaries,
|
|
25
|
+
getSkillsList,
|
|
26
|
+
} from "../localdb/queries.js";
|
|
27
|
+
import { buildTrustWatchlist } from "../trust-model.js";
|
|
28
|
+
import { loadWatchedSkills } from "../watchlist.js";
|
|
10
29
|
|
|
11
|
-
export function handleOverview(
|
|
12
|
-
|
|
30
|
+
export function handleOverview(
|
|
31
|
+
db: Database,
|
|
32
|
+
version: string,
|
|
33
|
+
searchParams?: URLSearchParams,
|
|
34
|
+
): Response {
|
|
13
35
|
const skills = getSkillsList(db);
|
|
14
|
-
|
|
36
|
+
|
|
37
|
+
// -- Autonomy-first enrichment fields ----------------------------------------
|
|
38
|
+
const attentionQueue = getAttentionQueue(db);
|
|
39
|
+
const recentDecisions = getRecentDecisions(db);
|
|
40
|
+
const trustSummaries = getSkillTrustSummaries(db);
|
|
41
|
+
const pendingReviews = attentionQueue.filter((a) => a.category === "needs_review").length;
|
|
42
|
+
|
|
43
|
+
const trustWatchlist = buildTrustWatchlist(trustSummaries);
|
|
44
|
+
const autonomyStatus = buildAutonomyStatus(
|
|
45
|
+
db,
|
|
46
|
+
attentionQueue,
|
|
47
|
+
recentDecisions,
|
|
48
|
+
skills.length,
|
|
49
|
+
pendingReviews,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const enrichment = {
|
|
53
|
+
watched_skills: loadWatchedSkills(),
|
|
54
|
+
autonomy_status: autonomyStatus,
|
|
55
|
+
attention_queue: attentionQueue,
|
|
56
|
+
trust_watchlist: trustWatchlist,
|
|
57
|
+
recent_decisions: recentDecisions,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// -- Standard overview payload -----------------------------------------------
|
|
61
|
+
const hasPaginationParams =
|
|
62
|
+
searchParams &&
|
|
63
|
+
(searchParams.has("telemetry_cursor") ||
|
|
64
|
+
searchParams.has("telemetry_limit") ||
|
|
65
|
+
searchParams.has("skills_cursor") ||
|
|
66
|
+
searchParams.has("skills_limit"));
|
|
67
|
+
const hasSkillsPagination =
|
|
68
|
+
searchParams && (searchParams.has("skills_cursor") || searchParams.has("skills_limit"));
|
|
69
|
+
|
|
70
|
+
if (!hasPaginationParams) {
|
|
71
|
+
const overview = getOverviewPayload(db);
|
|
72
|
+
const response: OverviewResponse = { overview, skills, version, ...enrichment };
|
|
73
|
+
return Response.json(response);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Parse pagination params
|
|
77
|
+
const telemetryCursor = parseCursorParam(searchParams.get("telemetry_cursor"));
|
|
78
|
+
const telemetryLimit = parseIntParam(searchParams.get("telemetry_limit"), 1000);
|
|
79
|
+
const skillsCursor = parseCursorParam(searchParams.get("skills_cursor"));
|
|
80
|
+
const skillsLimit = parseIntParam(searchParams.get("skills_limit"), 2000);
|
|
81
|
+
|
|
82
|
+
const overview = getOverviewPayloadPaginated(db, {
|
|
83
|
+
telemetry_cursor: telemetryCursor,
|
|
84
|
+
telemetry_limit: telemetryLimit,
|
|
85
|
+
skills_cursor: skillsCursor,
|
|
86
|
+
skills_limit: skillsLimit,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const paginatedSkillNames = new Set(overview.skills_page.items.map((row) => row.skill_name));
|
|
90
|
+
const paginatedSkills = hasSkillsPagination
|
|
91
|
+
? skills.filter((skill) => paginatedSkillNames.has(skill.skill_name))
|
|
92
|
+
: skills;
|
|
93
|
+
|
|
94
|
+
return Response.json({ overview, skills: paginatedSkills, version, ...enrichment });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// -- Internal helpers ----------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
function buildAutonomyStatus(
|
|
100
|
+
db: Database,
|
|
101
|
+
attentionQueue: AttentionItem[],
|
|
102
|
+
recentDecisions: AutonomousDecision[],
|
|
103
|
+
skillsObserved: number,
|
|
104
|
+
pendingReviews: number,
|
|
105
|
+
): AutonomyStatus {
|
|
106
|
+
let lastRun: string | null = null;
|
|
107
|
+
try {
|
|
108
|
+
const row = db
|
|
109
|
+
.query(`SELECT timestamp FROM orchestrate_runs ORDER BY timestamp DESC LIMIT 1`)
|
|
110
|
+
.get() as { timestamp: string } | null;
|
|
111
|
+
lastRun = row?.timestamp ?? null;
|
|
112
|
+
} catch {
|
|
113
|
+
// Table may not exist
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const hasCritical = attentionQueue.some((a) => a.severity === "critical");
|
|
117
|
+
|
|
118
|
+
// "watching" means recent autonomous activity — last run within 24 hours
|
|
119
|
+
// or recent decisions within the 7-day freshness window
|
|
120
|
+
const hasRecentActivity =
|
|
121
|
+
(lastRun != null && Date.now() - new Date(lastRun).getTime() < 24 * 60 * 60 * 1000) ||
|
|
122
|
+
recentDecisions.length > 0;
|
|
123
|
+
|
|
124
|
+
let level: AutonomyStatusLevel;
|
|
125
|
+
if (hasCritical) {
|
|
126
|
+
level = "blocked";
|
|
127
|
+
} else if (pendingReviews > 0) {
|
|
128
|
+
level = "needs_review";
|
|
129
|
+
} else if (hasRecentActivity) {
|
|
130
|
+
level = "watching";
|
|
131
|
+
} else {
|
|
132
|
+
level = "healthy";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let summary: string;
|
|
136
|
+
switch (level) {
|
|
137
|
+
case "healthy":
|
|
138
|
+
summary = "No action needed. System is healthy.";
|
|
139
|
+
break;
|
|
140
|
+
case "blocked": {
|
|
141
|
+
const critCount = attentionQueue.filter((a) => a.severity === "critical").length;
|
|
142
|
+
summary = `${critCount} skill${critCount !== 1 ? "s" : ""} need${critCount === 1 ? "s" : ""} urgent attention after rollback.`;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case "needs_review":
|
|
146
|
+
summary = `selftune is watching ${skillsObserved} skill${skillsObserved !== 1 ? "s" : ""} and needs review on ${pendingReviews} proposal${pendingReviews !== 1 ? "s" : ""}.`;
|
|
147
|
+
break;
|
|
148
|
+
case "watching":
|
|
149
|
+
summary = `selftune is actively watching ${skillsObserved} skill${skillsObserved !== 1 ? "s" : ""}. No action needed.`;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
level,
|
|
155
|
+
summary,
|
|
156
|
+
last_run: lastRun,
|
|
157
|
+
skills_observed: skillsObserved,
|
|
158
|
+
pending_reviews: pendingReviews,
|
|
159
|
+
attention_required: attentionQueue.length,
|
|
160
|
+
};
|
|
15
161
|
}
|