selftune 0.2.18 → 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 +9 -4
- 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/stage-canonical.ts +7 -6
- package/cli/selftune/constants.ts +10 -0
- package/cli/selftune/contribute/contribute.ts +30 -2
- 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 +205 -1
- package/cli/selftune/dashboard-server.ts +45 -11
- 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/export.ts +2 -2
- 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/queries.ts +701 -30
- package/cli/selftune/localdb/schema.ts +20 -0
- 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 +112 -4
- package/cli/selftune/routes/skill-report.ts +569 -10
- 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 +49 -0
- package/cli/selftune/utils/skill-detection.ts +43 -0
- package/cli/selftune/watchlist.ts +65 -0
- package/package.json +1 -1
- 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 +11 -1
- 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/Ingest.md +7 -0
- package/skill/Workflows/Initialize.md +20 -1
- package/skill/Workflows/Recover.md +84 -0
- package/skill/Workflows/RepairSkillUsage.md +12 -4
- package/skill/Workflows/Sync.md +18 -12
- 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
|
@@ -239,6 +239,21 @@ CREATE TABLE IF NOT EXISTS upload_queue (
|
|
|
239
239
|
last_error TEXT
|
|
240
240
|
)`;
|
|
241
241
|
|
|
242
|
+
// -- Creator contribution staging --------------------------------------------
|
|
243
|
+
|
|
244
|
+
export const CREATE_CREATOR_CONTRIBUTION_STAGING = `
|
|
245
|
+
CREATE TABLE IF NOT EXISTS creator_contribution_staging (
|
|
246
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
247
|
+
dedupe_key TEXT NOT NULL,
|
|
248
|
+
skill_name TEXT NOT NULL,
|
|
249
|
+
creator_id TEXT NOT NULL,
|
|
250
|
+
payload_json TEXT NOT NULL,
|
|
251
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
252
|
+
staged_at TEXT NOT NULL,
|
|
253
|
+
updated_at TEXT NOT NULL,
|
|
254
|
+
last_error TEXT
|
|
255
|
+
)`;
|
|
256
|
+
|
|
242
257
|
// -- Canonical upload staging -------------------------------------------------
|
|
243
258
|
|
|
244
259
|
export const CREATE_CANONICAL_UPLOAD_STAGING = `
|
|
@@ -327,6 +342,10 @@ export const CREATE_INDEXES = [
|
|
|
327
342
|
// -- Alpha upload queue indexes ---------------------------------------------
|
|
328
343
|
`CREATE INDEX IF NOT EXISTS idx_upload_queue_status ON upload_queue(status)`,
|
|
329
344
|
`CREATE INDEX IF NOT EXISTS idx_upload_queue_type_status ON upload_queue(payload_type, status)`,
|
|
345
|
+
// -- Creator contribution staging indexes -----------------------------------
|
|
346
|
+
`CREATE INDEX IF NOT EXISTS idx_creator_contrib_status ON creator_contribution_staging(status)`,
|
|
347
|
+
`CREATE INDEX IF NOT EXISTS idx_creator_contrib_skill ON creator_contribution_staging(skill_name)`,
|
|
348
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_creator_contrib_dedup ON creator_contribution_staging(dedupe_key)`,
|
|
330
349
|
// -- Canonical upload staging indexes ---------------------------------------
|
|
331
350
|
`CREATE INDEX IF NOT EXISTS idx_staging_kind ON canonical_upload_staging(record_kind)`,
|
|
332
351
|
`CREATE INDEX IF NOT EXISTS idx_staging_session ON canonical_upload_staging(session_id)`,
|
|
@@ -422,6 +441,7 @@ export const ALL_DDL = [
|
|
|
422
441
|
CREATE_GRADING_RESULTS,
|
|
423
442
|
CREATE_IMPROVEMENT_SIGNALS,
|
|
424
443
|
CREATE_UPLOAD_QUEUE,
|
|
444
|
+
CREATE_CREATOR_CONTRIBUTION_STAGING,
|
|
425
445
|
CREATE_UPLOAD_WATERMARKS,
|
|
426
446
|
CREATE_CANONICAL_UPLOAD_STAGING,
|
|
427
447
|
CREATE_COMMIT_TRACKING,
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
CANONICAL_LOG,
|
|
7
|
+
EVOLUTION_AUDIT_LOG,
|
|
8
|
+
EVOLUTION_EVIDENCE_LOG,
|
|
9
|
+
ORCHESTRATE_RUN_LOG,
|
|
10
|
+
TELEMETRY_LOG,
|
|
11
|
+
} from "./constants.js";
|
|
12
|
+
import { getDb } from "./localdb/db.js";
|
|
13
|
+
import {
|
|
14
|
+
materializeFull,
|
|
15
|
+
materializeIncremental,
|
|
16
|
+
type MaterializeOptions,
|
|
17
|
+
type MaterializeResult,
|
|
18
|
+
} from "./localdb/materialize.js";
|
|
19
|
+
import { CLIError, handleCLIError } from "./utils/cli-error.js";
|
|
20
|
+
|
|
21
|
+
interface RecoverSummary {
|
|
22
|
+
mode: "incremental" | "full";
|
|
23
|
+
source: "legacy_jsonl_or_export_snapshot";
|
|
24
|
+
since: string | null;
|
|
25
|
+
force: boolean;
|
|
26
|
+
result: MaterializeResult;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildMaterializeOptions(values: Record<string, unknown>): MaterializeOptions {
|
|
30
|
+
return {
|
|
31
|
+
canonicalLogPath: (values["canonical-log"] as string | undefined) ?? CANONICAL_LOG,
|
|
32
|
+
telemetryLogPath: (values["telemetry-log"] as string | undefined) ?? TELEMETRY_LOG,
|
|
33
|
+
evolutionAuditPath:
|
|
34
|
+
(values["evolution-audit-log"] as string | undefined) ?? EVOLUTION_AUDIT_LOG,
|
|
35
|
+
evolutionEvidencePath:
|
|
36
|
+
(values["evolution-evidence-log"] as string | undefined) ?? EVOLUTION_EVIDENCE_LOG,
|
|
37
|
+
orchestrateRunLogPath:
|
|
38
|
+
(values["orchestrate-run-log"] as string | undefined) ?? ORCHESTRATE_RUN_LOG,
|
|
39
|
+
force: (values.force as boolean | undefined) ?? false,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function printHumanSummary(summary: RecoverSummary): void {
|
|
44
|
+
const rows = [
|
|
45
|
+
`mode: ${summary.mode}`,
|
|
46
|
+
"source: legacy JSONL or explicit export snapshot",
|
|
47
|
+
`sessions: ${summary.result.sessions}`,
|
|
48
|
+
`prompts: ${summary.result.prompts}`,
|
|
49
|
+
`skill invocations: ${summary.result.skillInvocations}`,
|
|
50
|
+
`execution facts: ${summary.result.executionFacts}`,
|
|
51
|
+
`session telemetry: ${summary.result.sessionTelemetry}`,
|
|
52
|
+
`legacy skill usage: ${summary.result.skillUsage}`,
|
|
53
|
+
`evolution audit: ${summary.result.evolutionAudit}`,
|
|
54
|
+
`evolution evidence: ${summary.result.evolutionEvidence}`,
|
|
55
|
+
`orchestrate runs: ${summary.result.orchestrateRuns}`,
|
|
56
|
+
];
|
|
57
|
+
console.log(`selftune recover\n${rows.map((row) => ` ${row}`).join("\n")}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function cliMain(): void {
|
|
61
|
+
const { values } = parseArgs({
|
|
62
|
+
args: process.argv.slice(2),
|
|
63
|
+
options: {
|
|
64
|
+
full: { type: "boolean", default: false },
|
|
65
|
+
force: { type: "boolean", default: false },
|
|
66
|
+
since: { type: "string" },
|
|
67
|
+
json: { type: "boolean", default: false },
|
|
68
|
+
"canonical-log": { type: "string", default: CANONICAL_LOG },
|
|
69
|
+
"telemetry-log": { type: "string", default: TELEMETRY_LOG },
|
|
70
|
+
"evolution-audit-log": { type: "string", default: EVOLUTION_AUDIT_LOG },
|
|
71
|
+
"evolution-evidence-log": { type: "string", default: EVOLUTION_EVIDENCE_LOG },
|
|
72
|
+
"orchestrate-run-log": { type: "string", default: ORCHESTRATE_RUN_LOG },
|
|
73
|
+
help: { type: "boolean", short: "h", default: false },
|
|
74
|
+
},
|
|
75
|
+
strict: true,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (values.help) {
|
|
79
|
+
console.log(`selftune recover — Recover SQLite from legacy/exported JSONL
|
|
80
|
+
|
|
81
|
+
Usage:
|
|
82
|
+
selftune recover [options]
|
|
83
|
+
|
|
84
|
+
Use this only for legacy backfill or explicit export-based recovery. Normal
|
|
85
|
+
operation should use \`selftune sync\`, which replays native source data into
|
|
86
|
+
SQLite and preserves alpha-upload compatibility.
|
|
87
|
+
|
|
88
|
+
Options:
|
|
89
|
+
--full Rebuild SQLite tables from scratch
|
|
90
|
+
--force Skip preflight rebuild guard for SQLite-only rows
|
|
91
|
+
--since <date> Incrementally materialize records on/after date
|
|
92
|
+
--canonical-log <path> Canonical JSONL path
|
|
93
|
+
--telemetry-log <path> Session telemetry JSONL path
|
|
94
|
+
--evolution-audit-log <path> Evolution audit JSONL path
|
|
95
|
+
--evolution-evidence-log <path> Evolution evidence JSONL path
|
|
96
|
+
--orchestrate-run-log <path> Orchestrate runs JSONL path
|
|
97
|
+
--json Output JSON summary
|
|
98
|
+
-h, --help Show this help`);
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (values.full && values.since) {
|
|
103
|
+
throw new CLIError(
|
|
104
|
+
"Cannot combine --full with --since.",
|
|
105
|
+
"INVALID_FLAG",
|
|
106
|
+
"Use either `selftune recover --full` or `selftune recover --since 2026-01-01`.",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let sinceIso: string | null = null;
|
|
111
|
+
if (values.since) {
|
|
112
|
+
const parsed = new Date(values.since as string);
|
|
113
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
114
|
+
throw new CLIError(
|
|
115
|
+
`Invalid --since date: ${values.since}`,
|
|
116
|
+
"INVALID_FLAG",
|
|
117
|
+
"selftune recover --since 2026-01-01",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
sinceIso = parsed.toISOString();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const db = getDb();
|
|
124
|
+
const materializeOptions = buildMaterializeOptions(values);
|
|
125
|
+
if (!values.full) materializeOptions.since = sinceIso;
|
|
126
|
+
|
|
127
|
+
const result = values.full
|
|
128
|
+
? materializeFull(db, materializeOptions)
|
|
129
|
+
: materializeIncremental(db, materializeOptions);
|
|
130
|
+
|
|
131
|
+
const summary: RecoverSummary = {
|
|
132
|
+
mode: values.full ? "full" : "incremental",
|
|
133
|
+
source: "legacy_jsonl_or_export_snapshot",
|
|
134
|
+
since: sinceIso,
|
|
135
|
+
force: (values.force as boolean | undefined) ?? false,
|
|
136
|
+
result,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
if (values.json || !process.stdout.isTTY) {
|
|
140
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
printHumanSummary(summary);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (import.meta.main) {
|
|
148
|
+
try {
|
|
149
|
+
cliMain();
|
|
150
|
+
} catch (error) {
|
|
151
|
+
handleCLIError(error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -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
|
+
}
|