selftune 0.2.18 → 0.2.20
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-D8O-RG1I.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 +209 -1
- package/cli/selftune/dashboard-server.ts +45 -11
- package/cli/selftune/eval/family-overlap.ts +714 -0
- package/cli/selftune/eval/hooks-to-evals.ts +182 -28
- package/cli/selftune/eval/synthetic-evals.ts +298 -11
- package/cli/selftune/evolution/evidence.ts +5 -0
- package/cli/selftune/evolution/evolve-body.ts +62 -2
- package/cli/selftune/evolution/evolve.ts +58 -1
- package/cli/selftune/evolution/validate-body.ts +10 -0
- package/cli/selftune/evolution/validate-host-replay.ts +236 -0
- package/cli/selftune/evolution/validate-proposal.ts +10 -0
- package/cli/selftune/evolution/validate-routing.ts +112 -5
- 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/direct-write.ts +8 -3
- package/cli/selftune/localdb/materialize.ts +7 -2
- package/cli/selftune/localdb/queries.ts +712 -31
- package/cli/selftune/localdb/schema.ts +30 -1
- 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 +575 -11
- 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 +103 -0
- package/cli/selftune/utils/skill-detection.ts +43 -0
- package/cli/selftune/utils/text-similarity.ts +73 -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 +419 -145
- package/packages/ui/src/components/EvolutionTimeline.tsx +81 -29
- 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/packages/ui/src/types.ts +4 -0
- package/skill/SKILL.md +11 -1
- package/skill/Workflows/AlphaUpload.md +4 -0
- package/skill/Workflows/Composability.md +78 -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 +23 -0
- 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
|
@@ -121,7 +121,12 @@ CREATE TABLE IF NOT EXISTS evolution_audit (
|
|
|
121
121
|
skill_name TEXT,
|
|
122
122
|
action TEXT NOT NULL,
|
|
123
123
|
details TEXT,
|
|
124
|
-
eval_snapshot_json TEXT
|
|
124
|
+
eval_snapshot_json TEXT,
|
|
125
|
+
iterations_used INTEGER,
|
|
126
|
+
validation_mode TEXT,
|
|
127
|
+
validation_agent TEXT,
|
|
128
|
+
validation_fixture_id TEXT,
|
|
129
|
+
validation_evidence_ref TEXT
|
|
125
130
|
)`;
|
|
126
131
|
|
|
127
132
|
// -- Local telemetry tables (from JSONL logs) ---------------------------------
|
|
@@ -239,6 +244,21 @@ CREATE TABLE IF NOT EXISTS upload_queue (
|
|
|
239
244
|
last_error TEXT
|
|
240
245
|
)`;
|
|
241
246
|
|
|
247
|
+
// -- Creator contribution staging --------------------------------------------
|
|
248
|
+
|
|
249
|
+
export const CREATE_CREATOR_CONTRIBUTION_STAGING = `
|
|
250
|
+
CREATE TABLE IF NOT EXISTS creator_contribution_staging (
|
|
251
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
252
|
+
dedupe_key TEXT NOT NULL,
|
|
253
|
+
skill_name TEXT NOT NULL,
|
|
254
|
+
creator_id TEXT NOT NULL,
|
|
255
|
+
payload_json TEXT NOT NULL,
|
|
256
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
257
|
+
staged_at TEXT NOT NULL,
|
|
258
|
+
updated_at TEXT NOT NULL,
|
|
259
|
+
last_error TEXT
|
|
260
|
+
)`;
|
|
261
|
+
|
|
242
262
|
// -- Canonical upload staging -------------------------------------------------
|
|
243
263
|
|
|
244
264
|
export const CREATE_CANONICAL_UPLOAD_STAGING = `
|
|
@@ -327,6 +347,10 @@ export const CREATE_INDEXES = [
|
|
|
327
347
|
// -- Alpha upload queue indexes ---------------------------------------------
|
|
328
348
|
`CREATE INDEX IF NOT EXISTS idx_upload_queue_status ON upload_queue(status)`,
|
|
329
349
|
`CREATE INDEX IF NOT EXISTS idx_upload_queue_type_status ON upload_queue(payload_type, status)`,
|
|
350
|
+
// -- Creator contribution staging indexes -----------------------------------
|
|
351
|
+
`CREATE INDEX IF NOT EXISTS idx_creator_contrib_status ON creator_contribution_staging(status)`,
|
|
352
|
+
`CREATE INDEX IF NOT EXISTS idx_creator_contrib_skill ON creator_contribution_staging(skill_name)`,
|
|
353
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_creator_contrib_dedup ON creator_contribution_staging(dedupe_key)`,
|
|
330
354
|
// -- Canonical upload staging indexes ---------------------------------------
|
|
331
355
|
`CREATE INDEX IF NOT EXISTS idx_staging_kind ON canonical_upload_staging(record_kind)`,
|
|
332
356
|
`CREATE INDEX IF NOT EXISTS idx_staging_session ON canonical_upload_staging(session_id)`,
|
|
@@ -350,6 +374,10 @@ export const MIGRATIONS = [
|
|
|
350
374
|
`ALTER TABLE skill_invocations ADD COLUMN source TEXT`,
|
|
351
375
|
// Track how many iteration loops each evolution run used
|
|
352
376
|
`ALTER TABLE evolution_audit ADD COLUMN iterations_used INTEGER`,
|
|
377
|
+
`ALTER TABLE evolution_audit ADD COLUMN validation_mode TEXT`,
|
|
378
|
+
`ALTER TABLE evolution_audit ADD COLUMN validation_agent TEXT`,
|
|
379
|
+
`ALTER TABLE evolution_audit ADD COLUMN validation_fixture_id TEXT`,
|
|
380
|
+
`ALTER TABLE evolution_audit ADD COLUMN validation_evidence_ref TEXT`,
|
|
353
381
|
// Canonical contract fields for upload staging (sessions already has schema_version, platform, normalized_at)
|
|
354
382
|
`ALTER TABLE sessions ADD COLUMN normalizer_version TEXT`,
|
|
355
383
|
`ALTER TABLE sessions ADD COLUMN capture_mode TEXT`,
|
|
@@ -422,6 +450,7 @@ export const ALL_DDL = [
|
|
|
422
450
|
CREATE_GRADING_RESULTS,
|
|
423
451
|
CREATE_IMPROVEMENT_SIGNALS,
|
|
424
452
|
CREATE_UPLOAD_QUEUE,
|
|
453
|
+
CREATE_CREATOR_CONTRIBUTION_STAGING,
|
|
425
454
|
CREATE_UPLOAD_WATERMARKS,
|
|
426
455
|
CREATE_CANONICAL_UPLOAD_STAGING,
|
|
427
456
|
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}`);
|