selftune 0.2.16 → 0.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +24 -19
  2. package/cli/selftune/alpha-upload/build-payloads.ts +14 -1
  3. package/cli/selftune/alpha-upload/client.ts +51 -1
  4. package/cli/selftune/alpha-upload/flush.ts +46 -5
  5. package/cli/selftune/alpha-upload/stage-canonical.ts +25 -4
  6. package/cli/selftune/alpha-upload-contract.ts +9 -0
  7. package/cli/selftune/constants.ts +82 -5
  8. package/cli/selftune/contribute/sanitize.ts +52 -5
  9. package/cli/selftune/dashboard-contract.ts +100 -0
  10. package/cli/selftune/dashboard-server.ts +2 -2
  11. package/cli/selftune/evolution/description-quality.ts +12 -11
  12. package/cli/selftune/evolution/evolve.ts +214 -51
  13. package/cli/selftune/evolution/validate-proposal.ts +9 -6
  14. package/cli/selftune/grading/grade-session.ts +20 -0
  15. package/cli/selftune/hooks/commit-track.ts +188 -0
  16. package/cli/selftune/hooks/prompt-log.ts +10 -1
  17. package/cli/selftune/hooks/session-stop.ts +2 -2
  18. package/cli/selftune/hooks/skill-eval.ts +15 -1
  19. package/cli/selftune/hooks/stdin-preview.ts +32 -0
  20. package/cli/selftune/localdb/direct-write.ts +69 -6
  21. package/cli/selftune/localdb/queries.ts +552 -7
  22. package/cli/selftune/localdb/schema.ts +46 -0
  23. package/cli/selftune/orchestrate.ts +32 -4
  24. package/cli/selftune/routes/overview.ts +41 -3
  25. package/cli/selftune/routes/skill-report.ts +88 -17
  26. package/cli/selftune/types.ts +31 -0
  27. package/cli/selftune/utils/transcript.ts +210 -1
  28. package/node_modules/@selftune/telemetry-contract/src/types.ts +11 -0
  29. package/package.json +1 -1
  30. package/packages/telemetry-contract/src/types.ts +11 -0
  31. package/skill/SKILL.md +29 -1
  32. package/skill/Workflows/Evolve.md +31 -13
  33. package/skill/Workflows/ExportCanonical.md +121 -0
  34. package/skill/Workflows/Hook.md +131 -0
  35. package/skill/Workflows/Initialize.md +9 -8
  36. package/skill/Workflows/Orchestrate.md +27 -5
  37. package/skill/Workflows/Quickstart.md +94 -0
  38. package/skill/Workflows/RepairSkillUsage.md +87 -0
  39. package/skill/Workflows/Uninstall.md +82 -0
  40. package/skill/settings_snippet.json +11 -0
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { EvalEntry, EvolutionProposal, InvocationTypeScores } from "../types.js";
10
- import { callLlm } from "../utils/llm-call.js";
10
+ import { callLlm, type EffortLevel } from "../utils/llm-call.js";
11
11
  import {
12
12
  buildBatchTriggerCheckPrompt,
13
13
  buildTriggerCheckPrompt,
@@ -52,6 +52,7 @@ export async function validateProposalSequential(
52
52
  evalSet: EvalEntry[],
53
53
  agent: string,
54
54
  modelFlag?: string,
55
+ effort?: EffortLevel,
55
56
  ): Promise<ValidationResult> {
56
57
  if (evalSet.length === 0) {
57
58
  return {
@@ -76,14 +77,14 @@ export async function validateProposalSequential(
76
77
  for (const entry of evalSet) {
77
78
  // Check with original description
78
79
  const beforePrompt = buildTriggerCheckPrompt(proposal.original_description, entry.query);
79
- const beforeRaw = await callLlm(systemPrompt, beforePrompt, agent, modelFlag);
80
+ const beforeRaw = await callLlm(systemPrompt, beforePrompt, agent, modelFlag, effort);
80
81
  const beforeTriggered = parseTriggerResponse(beforeRaw);
81
82
  const beforePass =
82
83
  (entry.should_trigger && beforeTriggered) || (!entry.should_trigger && !beforeTriggered);
83
84
 
84
85
  // Check with proposed description
85
86
  const afterPrompt = buildTriggerCheckPrompt(proposal.proposed_description, entry.query);
86
- const afterRaw = await callLlm(systemPrompt, afterPrompt, agent, modelFlag);
87
+ const afterRaw = await callLlm(systemPrompt, afterPrompt, agent, modelFlag, effort);
87
88
  const afterTriggered = parseTriggerResponse(afterRaw);
88
89
  const afterPass =
89
90
  (entry.should_trigger && afterTriggered) || (!entry.should_trigger && !afterTriggered);
@@ -208,6 +209,7 @@ export async function validateProposalBatched(
208
209
  evalSet: EvalEntry[],
209
210
  agent: string,
210
211
  modelFlag?: string,
212
+ effort?: EffortLevel,
211
213
  ): Promise<ValidationResult> {
212
214
  if (evalSet.length === 0) {
213
215
  return {
@@ -242,8 +244,8 @@ export async function validateProposalBatched(
242
244
  // Run VALIDATION_RUNS times in parallel and majority-vote to reduce LLM variance
243
245
  const allCalls: Promise<string>[] = [];
244
246
  for (let r = 0; r < VALIDATION_RUNS; r++) {
245
- allCalls.push(callLlm(systemPrompt, beforePrompt, agent, modelFlag));
246
- allCalls.push(callLlm(systemPrompt, afterPrompt, agent, modelFlag));
247
+ allCalls.push(callLlm(systemPrompt, beforePrompt, agent, modelFlag, effort));
248
+ allCalls.push(callLlm(systemPrompt, afterPrompt, agent, modelFlag, effort));
247
249
  }
248
250
  const allRaw = await Promise.all(allCalls);
249
251
 
@@ -353,6 +355,7 @@ export async function validateProposal(
353
355
  evalSet: EvalEntry[],
354
356
  agent: string,
355
357
  modelFlag?: string,
358
+ effort?: EffortLevel,
356
359
  ): Promise<ValidationResult> {
357
- return validateProposalBatched(proposal, evalSet, agent, modelFlag);
360
+ return validateProposalBatched(proposal, evalSet, agent, modelFlag, effort);
358
361
  }
@@ -26,6 +26,7 @@ import type {
26
26
  GradingExpectation,
27
27
  GradingResult,
28
28
  SessionTelemetryRecord,
29
+ SessionType,
29
30
  SkillUsageRecord,
30
31
  } from "../types.js";
31
32
  import { CLIError, handleCLIError } from "../utils/cli-error.js";
@@ -420,6 +421,8 @@ export function buildExecutionMetrics(telemetry: SessionTelemetryRecord): Execut
420
421
  errors_encountered: telemetry.errors_encountered ?? 0,
421
422
  skills_triggered: telemetry.skills_triggered ?? [],
422
423
  transcript_chars: telemetry.transcript_chars ?? 0,
424
+ artifact_count: telemetry.artifact_count,
425
+ session_type: telemetry.session_type,
423
426
  };
424
427
  }
425
428
 
@@ -481,13 +484,30 @@ export function buildGradingPrompt(
481
484
  ? transcriptExcerpt.slice(0, MAX_TRANSCRIPT_LENGTH)
482
485
  : transcriptExcerpt;
483
486
 
487
+ const sessionType: SessionType = (telemetry.session_type as SessionType) ?? "mixed";
488
+ const SESSION_TYPE_CONTEXT: Record<SessionType, string> = {
489
+ dev: "This is a development session — code output and commits are expected productivity signals.",
490
+ research:
491
+ "This is a research session — information gathering and synthesis are the primary outputs, not code changes.",
492
+ content:
493
+ "This is a content/writing session — document creation is the primary output, not code commits.",
494
+ mixed:
495
+ "This is a mixed session — evaluate based on what was actually accomplished, not code-specific metrics.",
496
+ };
497
+ const sessionTypeContext = SESSION_TYPE_CONTEXT[sessionType] ?? SESSION_TYPE_CONTEXT.mixed;
498
+
484
499
  return `Skill: ${skillName}
485
500
 
501
+ === SESSION CONTEXT ===
502
+ Session type: ${sessionType}
503
+ ${sessionTypeContext}
504
+
486
505
  === PROCESS TELEMETRY ===
487
506
  Skills triggered: ${JSON.stringify(telemetry.skills_triggered ?? [])}
488
507
  Assistant turns: ${telemetry.assistant_turns ?? "?"}
489
508
  Errors: ${telemetry.errors_encountered ?? "?"}
490
509
  Total tool calls: ${telemetry.total_tool_calls ?? "?"}
510
+ Artifacts produced: ${telemetry.artifact_count ?? "?"}
491
511
 
492
512
  Tool breakdown:
493
513
  ${toolSummary}
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * PostToolUse hook: commit-track.ts
4
+ *
5
+ * Detects git commits in Bash tool output and records commit SHA, title,
6
+ * branch, and session ID for session-to-commit traceability.
7
+ *
8
+ * Fail-open: exits 0 on all errors. Never blocks the host agent.
9
+ */
10
+
11
+ import { execSync } from "node:child_process";
12
+
13
+ import type { PostToolUsePayload } from "../types.js";
14
+
15
+ // -- Regex patterns (pre-compiled at module load) ----------------------------
16
+
17
+ /** Matches git commands that produce commits. */
18
+ const GIT_COMMIT_CMD_RE = /\bgit\s+(commit|merge|cherry-pick|revert)\b/;
19
+
20
+ /**
21
+ * Matches the standard git commit output format: [branch SHA] title
22
+ * Supports optional parenthetical like (root-commit).
23
+ * Branch names can contain word chars, slashes, dots, hyphens, plus signs.
24
+ */
25
+ const COMMIT_OUTPUT_RE = /\[([\w/.+-]+)(?:\s+\([^)]+\))?\s+([a-f0-9]{7,40})\]\s+(.+)/;
26
+
27
+ // -- Pure extraction functions (exported for testability) ---------------------
28
+
29
+ /** Check if a command string contains a git commit/merge/cherry-pick/revert. */
30
+ export function containsGitCommitCommand(command: string): boolean {
31
+ return GIT_COMMIT_CMD_RE.test(command);
32
+ }
33
+
34
+ /** Extract commit SHA from git output. */
35
+ export function parseCommitSha(output: string): string | undefined {
36
+ const match = output.match(COMMIT_OUTPUT_RE);
37
+ return match ? match[2] : undefined;
38
+ }
39
+
40
+ /** Extract commit title from git output. */
41
+ export function parseCommitTitle(output: string): string | undefined {
42
+ const match = output.match(COMMIT_OUTPUT_RE);
43
+ return match ? match[3].trim() : undefined;
44
+ }
45
+
46
+ /** Extract branch name from git output. */
47
+ export function parseBranchFromOutput(output: string): string | undefined {
48
+ const match = output.match(COMMIT_OUTPUT_RE);
49
+ return match ? match[1] : undefined;
50
+ }
51
+
52
+ /** Scrub credentials from a remote URL. Returns undefined for empty input. */
53
+ export function scrubRemoteUrl(rawUrl: string): string | undefined {
54
+ if (!rawUrl) return undefined;
55
+ try {
56
+ const parsed = new URL(rawUrl);
57
+ parsed.username = "";
58
+ parsed.password = "";
59
+ return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
60
+ } catch {
61
+ // SSH or non-URL format — safe as-is
62
+ return rawUrl;
63
+ }
64
+ }
65
+
66
+ // -- Commit tracking record shape --------------------------------------------
67
+
68
+ export interface CommitTrackRecord {
69
+ session_id: string;
70
+ commit_sha: string;
71
+ commit_title?: string;
72
+ branch?: string;
73
+ repo_remote?: string;
74
+ timestamp: string;
75
+ }
76
+
77
+ // -- Core processing logic ---------------------------------------------------
78
+
79
+ /**
80
+ * Process a PostToolUse payload for git commit tracking.
81
+ * Returns the record that was written, or null if skipped.
82
+ * Exported for testability.
83
+ */
84
+ export async function processCommitTrack(
85
+ payload: PostToolUsePayload,
86
+ ): Promise<CommitTrackRecord | null> {
87
+ // Fast-path: only care about Bash tool
88
+ if (payload.tool_name !== "Bash") return null;
89
+
90
+ // Fast-path: if cwd is known and not inside a git repo, skip
91
+ const cwd = payload.cwd ?? "";
92
+ if (cwd) {
93
+ try {
94
+ execSync("git rev-parse --is-inside-work-tree", {
95
+ cwd,
96
+ timeout: 1000,
97
+ stdio: ["ignore", "ignore", "ignore"],
98
+ });
99
+ } catch {
100
+ return null; // Not inside a git repo
101
+ }
102
+ }
103
+
104
+ // Fast-path: check if the command is a git commit-producing operation
105
+ const command = typeof payload.tool_input?.command === "string" ? payload.tool_input.command : "";
106
+ if (!containsGitCommitCommand(command)) return null;
107
+
108
+ // Extract stdout from tool_response
109
+ const response = payload.tool_response ?? {};
110
+ const stdout = typeof response.stdout === "string" ? response.stdout : "";
111
+ if (!stdout) return null;
112
+
113
+ // Parse commit SHA — if we can't find one, nothing to track
114
+ const commitSha = parseCommitSha(stdout);
115
+ if (!commitSha) return null;
116
+
117
+ const commitTitle = parseCommitTitle(stdout);
118
+ const outputBranch = parseBranchFromOutput(stdout);
119
+ const sessionId = payload.session_id;
120
+ if (!sessionId) return null; // No session ID — skip insert (fail-open)
121
+
122
+ // Try to get branch from git if not parsed from output
123
+ let branch = outputBranch;
124
+ if (!branch && cwd) {
125
+ try {
126
+ branch =
127
+ execSync("git rev-parse --abbrev-ref HEAD", {
128
+ cwd,
129
+ timeout: 3000,
130
+ stdio: ["ignore", "pipe", "ignore"],
131
+ })
132
+ .toString()
133
+ .trim() || undefined;
134
+ } catch {
135
+ /* not a git repo or git not available */
136
+ }
137
+ }
138
+
139
+ // Try to get remote URL (scrub credentials)
140
+ let repoRemote: string | undefined;
141
+ if (cwd) {
142
+ try {
143
+ const rawRemote =
144
+ execSync("git remote get-url origin", {
145
+ cwd,
146
+ timeout: 3000,
147
+ stdio: ["ignore", "pipe", "ignore"],
148
+ })
149
+ .toString()
150
+ .trim() || undefined;
151
+ if (rawRemote) {
152
+ repoRemote = scrubRemoteUrl(rawRemote);
153
+ }
154
+ } catch {
155
+ /* no remote configured */
156
+ }
157
+ }
158
+
159
+ const record: CommitTrackRecord = {
160
+ session_id: sessionId,
161
+ commit_sha: commitSha,
162
+ commit_title: commitTitle,
163
+ branch,
164
+ repo_remote: repoRemote,
165
+ timestamp: new Date().toISOString(),
166
+ };
167
+
168
+ // Write to SQLite (dynamic import to reduce hook startup cost)
169
+ try {
170
+ const { writeCommitTracking } = await import("../localdb/direct-write.js");
171
+ writeCommitTracking(record);
172
+ } catch {
173
+ /* hooks must never block */
174
+ }
175
+
176
+ return record;
177
+ }
178
+
179
+ // --- stdin main (only when executed directly, not when imported) ---
180
+ if (import.meta.main) {
181
+ try {
182
+ const payload: PostToolUsePayload = JSON.parse(await Bun.stdin.text());
183
+ await processCommitTrack(payload);
184
+ } catch {
185
+ // silent — hooks must never block Claude
186
+ }
187
+ process.exit(0);
188
+ }
@@ -226,7 +226,16 @@ export async function processPrompt(
226
226
  // --- stdin main (only when executed directly, not when imported) ---
227
227
  if (import.meta.main) {
228
228
  try {
229
- const payload: PromptSubmitPayload = JSON.parse(await Bun.stdin.text());
229
+ const { readStdinWithPreview } = await import("./stdin-preview.js");
230
+ const { preview, full } = await readStdinWithPreview();
231
+
232
+ // Fast-path: prompt-log only handles UserPromptSubmit events.
233
+ // If the keyword is absent from the first 4 KiB we can skip JSON.parse entirely.
234
+ if (!preview.includes('"UserPromptSubmit"')) {
235
+ process.exit(0);
236
+ }
237
+
238
+ const payload: PromptSubmitPayload = JSON.parse(full);
230
239
  await processPrompt(payload);
231
240
  } catch {
232
241
  // silent — hooks must never block Claude
@@ -11,7 +11,7 @@
11
11
  import { execSync } from "node:child_process";
12
12
  import { readFileSync } from "node:fs";
13
13
 
14
- import { CANONICAL_LOG, ORCHESTRATE_LOCK, TELEMETRY_LOG } from "../constants.js";
14
+ import { CANONICAL_LOG, getOrchestrateLockPath, TELEMETRY_LOG } from "../constants.js";
15
15
  import {
16
16
  appendCanonicalRecords,
17
17
  buildCanonicalExecutionFact,
@@ -47,7 +47,7 @@ function hasFreshOrchestrateLock(lockPath: string): boolean {
47
47
  * Returns true if a process was spawned, false otherwise.
48
48
  */
49
49
  export async function maybeSpawnReactiveOrchestrate(
50
- lockPath: string = ORCHESTRATE_LOCK,
50
+ lockPath: string = getOrchestrateLockPath(),
51
51
  deps: ReactiveSpawnDeps = {},
52
52
  ): Promise<boolean> {
53
53
  try {
@@ -359,7 +359,21 @@ async function processSkillToolUse(
359
359
  // --- stdin main (only when executed directly, not when imported) ---
360
360
  if (import.meta.main) {
361
361
  try {
362
- const payload: PostToolUsePayload = JSON.parse(await Bun.stdin.text());
362
+ const { readStdinWithPreview } = await import("./stdin-preview.js");
363
+ const { preview, full } = await readStdinWithPreview();
364
+
365
+ // Fast-path: skill-eval only handles PostToolUse events.
366
+ if (!preview.includes('"PostToolUse"')) {
367
+ process.exit(0);
368
+ }
369
+
370
+ // Secondary fast-path: only Read and Skill tools are relevant.
371
+ // Most PostToolUse events are for Bash/Write/Edit — skip those entirely.
372
+ if (!preview.includes('"Read"') && !preview.includes('"Skill"')) {
373
+ process.exit(0);
374
+ }
375
+
376
+ const payload: PostToolUsePayload = JSON.parse(full);
363
377
  await processToolUse(payload);
364
378
  } catch {
365
379
  // silent — hooks must never block Claude
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shared stdin preview utility for hook fast-path optimization.
3
+ *
4
+ * Reads all of stdin once, then exposes a small preview slice so callers
5
+ * can do cheap `.includes()` keyword checks before paying for JSON.parse().
6
+ * When the keyword is absent the hook can exit in <1ms.
7
+ */
8
+
9
+ const STDIN_PREVIEW_BYTES = 4096;
10
+
11
+ /**
12
+ * Read stdin and return both a preview slice and the full text.
13
+ *
14
+ * Bun's `stdin.text()` consumes the entire stream in one call, so this is
15
+ * not a true streaming preview. The win comes from avoiding `JSON.parse()`
16
+ * entirely when the preview slice already proves the payload is irrelevant.
17
+ *
18
+ * @param previewBytes Number of leading characters to expose in `preview`.
19
+ * Defaults to 4096, which comfortably covers the
20
+ * envelope fields (`hook_event_name`, `tool_name`, etc.)
21
+ * of any Claude Code hook payload.
22
+ */
23
+ export async function readStdinWithPreview(previewBytes: number = STDIN_PREVIEW_BYTES): Promise<{
24
+ preview: string;
25
+ full: string;
26
+ }> {
27
+ const raw = await Bun.stdin.text();
28
+ return {
29
+ preview: raw.slice(0, previewBytes),
30
+ full: raw,
31
+ };
32
+ }
@@ -191,14 +191,17 @@ export function writeSessionTelemetryToDb(record: SessionTelemetryRecord): boole
191
191
  return safeWrite("session-telemetry", (db) => {
192
192
  getStmt(
193
193
  db,
194
- "session-telemetry",
194
+ "session-telemetry-v4",
195
195
  `
196
196
  INSERT INTO session_telemetry
197
197
  (session_id, timestamp, cwd, transcript_path, tool_calls_json,
198
198
  total_tool_calls, bash_commands_json, skills_triggered_json,
199
199
  skills_invoked_json, assistant_turns, errors_encountered,
200
- transcript_chars, last_user_query, source, input_tokens, output_tokens)
201
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
200
+ transcript_chars, last_user_query, source, input_tokens, output_tokens,
201
+ cached_input_tokens, reasoning_output_tokens, cost_usd,
202
+ files_changed, lines_added, lines_removed, lines_modified,
203
+ artifact_count, session_type, agent_summary)
204
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
202
205
  ON CONFLICT(session_id) DO UPDATE SET
203
206
  timestamp = excluded.timestamp,
204
207
  tool_calls_json = excluded.tool_calls_json,
@@ -211,7 +214,17 @@ export function writeSessionTelemetryToDb(record: SessionTelemetryRecord): boole
211
214
  transcript_chars = excluded.transcript_chars,
212
215
  last_user_query = excluded.last_user_query,
213
216
  input_tokens = COALESCE(excluded.input_tokens, session_telemetry.input_tokens),
214
- output_tokens = COALESCE(excluded.output_tokens, session_telemetry.output_tokens)
217
+ output_tokens = COALESCE(excluded.output_tokens, session_telemetry.output_tokens),
218
+ cached_input_tokens = COALESCE(excluded.cached_input_tokens, session_telemetry.cached_input_tokens),
219
+ reasoning_output_tokens = COALESCE(excluded.reasoning_output_tokens, session_telemetry.reasoning_output_tokens),
220
+ cost_usd = COALESCE(excluded.cost_usd, session_telemetry.cost_usd),
221
+ files_changed = COALESCE(excluded.files_changed, session_telemetry.files_changed),
222
+ lines_added = COALESCE(excluded.lines_added, session_telemetry.lines_added),
223
+ lines_removed = COALESCE(excluded.lines_removed, session_telemetry.lines_removed),
224
+ lines_modified = COALESCE(excluded.lines_modified, session_telemetry.lines_modified),
225
+ artifact_count = COALESCE(excluded.artifact_count, session_telemetry.artifact_count),
226
+ session_type = COALESCE(excluded.session_type, session_telemetry.session_type),
227
+ agent_summary = COALESCE(excluded.agent_summary, session_telemetry.agent_summary)
215
228
  `,
216
229
  ).run(
217
230
  record.session_id,
@@ -230,6 +243,16 @@ export function writeSessionTelemetryToDb(record: SessionTelemetryRecord): boole
230
243
  record.source ?? null,
231
244
  record.input_tokens ?? null,
232
245
  record.output_tokens ?? null,
246
+ record.cached_input_tokens ?? null,
247
+ record.reasoning_output_tokens ?? null,
248
+ record.cost_usd ?? null,
249
+ record.files_changed ?? null,
250
+ record.lines_added ?? null,
251
+ record.lines_removed ?? null,
252
+ record.lines_modified ?? null,
253
+ record.artifact_count ?? null,
254
+ record.session_type ?? null,
255
+ record.agent_summary ?? null,
233
256
  );
234
257
  });
235
258
  }
@@ -444,6 +467,34 @@ export function updateSignalConsumed(
444
467
  return result?.changes > 0;
445
468
  }
446
469
 
470
+ export function writeCommitTracking(record: {
471
+ session_id: string;
472
+ commit_sha: string;
473
+ commit_title?: string;
474
+ branch?: string;
475
+ repo_remote?: string;
476
+ timestamp: string;
477
+ }): boolean {
478
+ return safeWrite("commit-tracking", (db) => {
479
+ getStmt(
480
+ db,
481
+ "commit-tracking",
482
+ `
483
+ INSERT INTO commit_tracking
484
+ (session_id, commit_sha, commit_title, branch, repo_remote, timestamp)
485
+ VALUES (?, ?, ?, ?, ?, ?)
486
+ `,
487
+ ).run(
488
+ record.session_id,
489
+ record.commit_sha,
490
+ record.commit_title ?? null,
491
+ record.branch ?? null,
492
+ record.repo_remote ?? null,
493
+ record.timestamp,
494
+ );
495
+ });
496
+ }
497
+
447
498
  // -- Internal insert helpers (used by cached statements) ----------------------
448
499
 
449
500
  function insertSession(db: Database, s: CanonicalSessionRecord): void {
@@ -581,14 +632,17 @@ function insertSkillInvocation(
581
632
  function insertExecutionFact(db: Database, ef: CanonicalExecutionFactRecord): void {
582
633
  getStmt(
583
634
  db,
584
- "execution-fact",
635
+ "execution-fact-v3",
585
636
  `
586
637
  INSERT INTO execution_facts
587
638
  (session_id, occurred_at, prompt_id, tool_calls_json, total_tool_calls,
588
639
  assistant_turns, errors_encountered, input_tokens, output_tokens,
640
+ cached_input_tokens, reasoning_output_tokens, cost_usd,
641
+ files_changed, lines_added, lines_removed, lines_modified,
642
+ artifact_count, session_type,
589
643
  duration_ms, completion_status,
590
644
  schema_version, platform, normalized_at, normalizer_version, capture_mode, raw_source_ref)
591
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
645
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
592
646
  `,
593
647
  ).run(
594
648
  ef.session_id,
@@ -600,6 +654,15 @@ function insertExecutionFact(db: Database, ef: CanonicalExecutionFactRecord): vo
600
654
  ef.errors_encountered,
601
655
  ef.input_tokens ?? null,
602
656
  ef.output_tokens ?? null,
657
+ ef.cached_input_tokens ?? null,
658
+ ef.reasoning_output_tokens ?? null,
659
+ ef.cost_usd ?? null,
660
+ ef.files_changed ?? null,
661
+ ef.lines_added ?? null,
662
+ ef.lines_removed ?? null,
663
+ ef.lines_modified ?? null,
664
+ ef.artifact_count ?? null,
665
+ ef.session_type ?? null,
603
666
  ef.duration_ms ?? null,
604
667
  ef.completion_status ?? null,
605
668
  ef.schema_version ?? null,