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.
Files changed (91) hide show
  1. package/README.md +32 -22
  2. package/apps/local-dashboard/dist/assets/index-DnhnXQm6.js +60 -0
  3. package/apps/local-dashboard/dist/assets/index-_EcLywDg.css +1 -0
  4. package/apps/local-dashboard/dist/assets/vendor-table-BIiI3YhS.js +1 -0
  5. package/apps/local-dashboard/dist/assets/vendor-ui-CGEmUayx.js +12 -0
  6. package/apps/local-dashboard/dist/index.html +5 -5
  7. package/cli/selftune/alpha-upload/build-payloads.ts +14 -1
  8. package/cli/selftune/alpha-upload/client.ts +51 -1
  9. package/cli/selftune/alpha-upload/flush.ts +46 -5
  10. package/cli/selftune/alpha-upload/stage-canonical.ts +32 -10
  11. package/cli/selftune/alpha-upload-contract.ts +9 -0
  12. package/cli/selftune/constants.ts +92 -5
  13. package/cli/selftune/contribute/contribute.ts +30 -2
  14. package/cli/selftune/contribute/sanitize.ts +52 -5
  15. package/cli/selftune/contribution-config.ts +249 -0
  16. package/cli/selftune/contribution-relay.ts +177 -0
  17. package/cli/selftune/contribution-signals.ts +219 -0
  18. package/cli/selftune/contribution-staging.ts +147 -0
  19. package/cli/selftune/contributions.ts +532 -0
  20. package/cli/selftune/creator-contributions.ts +333 -0
  21. package/cli/selftune/dashboard-contract.ts +305 -1
  22. package/cli/selftune/dashboard-server.ts +47 -13
  23. package/cli/selftune/eval/family-overlap.ts +395 -0
  24. package/cli/selftune/eval/hooks-to-evals.ts +182 -28
  25. package/cli/selftune/eval/synthetic-evals.ts +298 -11
  26. package/cli/selftune/evolution/description-quality.ts +12 -11
  27. package/cli/selftune/evolution/evolve.ts +214 -51
  28. package/cli/selftune/evolution/validate-proposal.ts +9 -6
  29. package/cli/selftune/export.ts +2 -2
  30. package/cli/selftune/grading/grade-session.ts +20 -0
  31. package/cli/selftune/hooks/commit-track.ts +188 -0
  32. package/cli/selftune/hooks/prompt-log.ts +10 -1
  33. package/cli/selftune/hooks/session-stop.ts +2 -2
  34. package/cli/selftune/hooks/skill-eval.ts +15 -1
  35. package/cli/selftune/hooks/stdin-preview.ts +32 -0
  36. package/cli/selftune/index.ts +41 -5
  37. package/cli/selftune/ingestors/codex-rollout.ts +31 -35
  38. package/cli/selftune/ingestors/codex-wrapper.ts +32 -24
  39. package/cli/selftune/localdb/db.ts +2 -2
  40. package/cli/selftune/localdb/direct-write.ts +69 -6
  41. package/cli/selftune/localdb/queries.ts +1253 -37
  42. package/cli/selftune/localdb/schema.ts +66 -0
  43. package/cli/selftune/orchestrate.ts +32 -4
  44. package/cli/selftune/recover.ts +153 -0
  45. package/cli/selftune/repair/skill-usage.ts +363 -4
  46. package/cli/selftune/routes/actions.ts +35 -1
  47. package/cli/selftune/routes/analytics.ts +14 -0
  48. package/cli/selftune/routes/index.ts +1 -0
  49. package/cli/selftune/routes/overview.ts +150 -4
  50. package/cli/selftune/routes/skill-report.ts +648 -18
  51. package/cli/selftune/status.ts +81 -2
  52. package/cli/selftune/sync.ts +56 -2
  53. package/cli/selftune/trust-model.ts +66 -0
  54. package/cli/selftune/types.ts +80 -0
  55. package/cli/selftune/utils/skill-detection.ts +43 -0
  56. package/cli/selftune/utils/transcript.ts +210 -1
  57. package/cli/selftune/watchlist.ts +65 -0
  58. package/node_modules/@selftune/telemetry-contract/src/types.ts +11 -0
  59. package/package.json +1 -1
  60. package/packages/telemetry-contract/src/types.ts +11 -0
  61. package/packages/ui/src/components/ActivityTimeline.tsx +165 -150
  62. package/packages/ui/src/components/EvidenceViewer.tsx +335 -144
  63. package/packages/ui/src/components/EvolutionTimeline.tsx +58 -28
  64. package/packages/ui/src/components/OrchestrateRunsPanel.tsx +33 -16
  65. package/packages/ui/src/components/RecentActivityFeed.tsx +72 -41
  66. package/packages/ui/src/components/section-cards.tsx +12 -9
  67. package/packages/ui/src/primitives/card.tsx +1 -1
  68. package/skill/SKILL.md +40 -2
  69. package/skill/Workflows/AlphaUpload.md +4 -0
  70. package/skill/Workflows/Composability.md +64 -0
  71. package/skill/Workflows/Contribute.md +6 -3
  72. package/skill/Workflows/Contributions.md +97 -0
  73. package/skill/Workflows/CreatorContributions.md +74 -0
  74. package/skill/Workflows/Dashboard.md +31 -0
  75. package/skill/Workflows/Evals.md +57 -8
  76. package/skill/Workflows/Evolve.md +31 -13
  77. package/skill/Workflows/ExportCanonical.md +121 -0
  78. package/skill/Workflows/Hook.md +131 -0
  79. package/skill/Workflows/Ingest.md +7 -0
  80. package/skill/Workflows/Initialize.md +29 -9
  81. package/skill/Workflows/Orchestrate.md +27 -5
  82. package/skill/Workflows/Quickstart.md +94 -0
  83. package/skill/Workflows/Recover.md +84 -0
  84. package/skill/Workflows/RepairSkillUsage.md +95 -0
  85. package/skill/Workflows/Sync.md +18 -12
  86. package/skill/Workflows/Uninstall.md +82 -0
  87. package/skill/settings_snippet.json +11 -0
  88. package/apps/local-dashboard/dist/assets/index-BMIS6uUh.css +0 -2
  89. package/apps/local-dashboard/dist/assets/index-DOu3iLD9.js +0 -16
  90. package/apps/local-dashboard/dist/assets/vendor-table-pHbDxq36.js +0 -8
  91. package/apps/local-dashboard/dist/assets/vendor-ui-DIwlrGlb.js +0 -12
@@ -6,9 +6,15 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
6
6
  import { basename, dirname } from "node:path";
7
7
 
8
8
  import { CLAUDE_CODE_PROJECTS_DIR } from "../constants.js";
9
- import type { SessionTelemetryRecord, TranscriptMetrics } from "../types.js";
9
+ import type { SessionTelemetryRecord, SessionType, TranscriptMetrics } from "../types.js";
10
10
  import { isActionableQueryText } from "./query-filter.js";
11
11
 
12
+ /** Tools that produce durable output artifacts (not reads or exploration). */
13
+ const ARTIFACT_TOOLS = new Set(["Write", "Edit", "WebFetch", "WebSearch", "Skill", "Agent"]);
14
+
15
+ /** Matches any bash command containing a git invocation. */
16
+ const GIT_CMD_RE = /\bgit\b/;
17
+
12
18
  /**
13
19
  * Parse a Claude Code transcript JSONL and extract process metrics.
14
20
  *
@@ -32,10 +38,18 @@ export function parseTranscript(transcriptPath: string): TranscriptMetrics {
32
38
  let lastUserQuery = "";
33
39
  let inputTokens = 0;
34
40
  let outputTokens = 0;
41
+ let cachedInputTokens = 0;
42
+ let reasoningOutputTokens = 0;
35
43
  let firstTimestamp: string | null = null;
36
44
  let lastTimestamp: string | null = null;
37
45
  let model: string | undefined;
38
46
 
47
+ // File change tracking (Win 2)
48
+ const changedFiles = new Set<string>();
49
+ let linesAdded = 0;
50
+ let linesRemoved = 0;
51
+ let linesModified = 0;
52
+
39
53
  for (const raw of lines) {
40
54
  const line = raw.trim();
41
55
  if (!line) continue;
@@ -61,6 +75,14 @@ export function parseTranscript(transcriptPath: string): TranscriptMetrics {
61
75
  if (usage && typeof usage === "object") {
62
76
  if (typeof usage.input_tokens === "number") inputTokens += usage.input_tokens;
63
77
  if (typeof usage.output_tokens === "number") outputTokens += usage.output_tokens;
78
+ // Win 3: Token granularity — cached input tokens
79
+ if (typeof usage.cache_read_input_tokens === "number")
80
+ cachedInputTokens += usage.cache_read_input_tokens;
81
+ if (typeof usage.cache_creation_input_tokens === "number")
82
+ cachedInputTokens += usage.cache_creation_input_tokens;
83
+ // Win 3: Reasoning output tokens
84
+ if (typeof usage.reasoning_output_tokens === "number")
85
+ reasoningOutputTokens += usage.reasoning_output_tokens;
64
86
  }
65
87
 
66
88
  // Normalise: unwrap nested message if present
@@ -119,6 +141,26 @@ export function parseTranscript(transcriptPath: string): TranscriptMetrics {
119
141
  const cmd = ((inp.command as string) ?? "").trim();
120
142
  if (cmd) bashCommands.push(cmd);
121
143
  }
144
+
145
+ // Win 2: Track file changes from Write and Edit tools
146
+ if (toolName === "Write" || toolName === "Edit") {
147
+ const fp = (inp.file_path as string) ?? "";
148
+ if (fp) changedFiles.add(fp);
149
+ }
150
+ if (toolName === "Write" && typeof inp.content === "string") {
151
+ linesAdded += inp.content.split("\n").length;
152
+ }
153
+ if (toolName === "Edit") {
154
+ const oldStr = inp.old_string;
155
+ const newStr = inp.new_string;
156
+ if (typeof oldStr === "string" && typeof newStr === "string") {
157
+ const oldLines = oldStr.split("\n").length;
158
+ const newLines = newStr.split("\n").length;
159
+ linesModified += Math.min(oldLines, newLines);
160
+ linesAdded += Math.max(0, newLines - oldLines);
161
+ linesRemoved += Math.max(0, oldLines - newLines);
162
+ }
163
+ }
122
164
  }
123
165
  }
124
166
  }
@@ -143,6 +185,12 @@ export function parseTranscript(transcriptPath: string): TranscriptMetrics {
143
185
  }
144
186
  }
145
187
 
188
+ // Compute artifact count: output-producing tool calls
189
+ let artifactCount = 0;
190
+ for (const [tool, count] of Object.entries(toolCalls)) {
191
+ if (ARTIFACT_TOOLS.has(tool)) artifactCount += count;
192
+ }
193
+
146
194
  // Compute duration from first to last timestamp
147
195
  let durationMs: number | undefined;
148
196
  if (firstTimestamp && lastTimestamp && firstTimestamp !== lastTimestamp) {
@@ -153,6 +201,12 @@ export function parseTranscript(transcriptPath: string): TranscriptMetrics {
153
201
  }
154
202
  }
155
203
 
204
+ // Win 3: Calculate cost from model and token counts
205
+ const costUsd = calculateCost(model, inputTokens, outputTokens);
206
+
207
+ // Infer session type from tool distribution
208
+ const sessionType = inferSessionType(toolCalls, bashCommands);
209
+
156
210
  return {
157
211
  tool_calls: toolCalls,
158
212
  total_tool_calls: Object.values(toolCalls).reduce((a, b) => a + b, 0),
@@ -163,8 +217,18 @@ export function parseTranscript(transcriptPath: string): TranscriptMetrics {
163
217
  errors_encountered: errors,
164
218
  transcript_chars: totalChars,
165
219
  last_user_query: lastUserQuery,
220
+ // Win 2: File change metrics
221
+ files_changed: changedFiles.size,
222
+ lines_added: linesAdded,
223
+ lines_removed: linesRemoved,
224
+ lines_modified: linesModified,
225
+ artifact_count: artifactCount,
226
+ session_type: sessionType,
166
227
  ...(inputTokens > 0 ? { input_tokens: inputTokens } : {}),
167
228
  ...(outputTokens > 0 ? { output_tokens: outputTokens } : {}),
229
+ ...(cachedInputTokens > 0 ? { cached_input_tokens: cachedInputTokens } : {}),
230
+ ...(reasoningOutputTokens > 0 ? { reasoning_output_tokens: reasoningOutputTokens } : {}),
231
+ ...(costUsd !== undefined ? { cost_usd: costUsd } : {}),
168
232
  ...(durationMs !== undefined ? { duration_ms: durationMs } : {}),
169
233
  ...(model ? { model } : {}),
170
234
  ...(firstTimestamp ? { started_at: firstTimestamp } : {}),
@@ -307,6 +371,16 @@ export function buildTelemetryFromTranscript(
307
371
  source,
308
372
  input_tokens: metrics.input_tokens,
309
373
  output_tokens: metrics.output_tokens,
374
+ cached_input_tokens: metrics.cached_input_tokens,
375
+ reasoning_output_tokens: metrics.reasoning_output_tokens,
376
+ cost_usd: metrics.cost_usd,
377
+ files_changed: metrics.files_changed,
378
+ lines_added: metrics.lines_added,
379
+ lines_removed: metrics.lines_removed,
380
+ lines_modified: metrics.lines_modified,
381
+ artifact_count: metrics.artifact_count,
382
+ session_type: metrics.session_type,
383
+ agent_summary: generateSessionSummary(metrics),
310
384
  };
311
385
  }
312
386
 
@@ -518,6 +592,141 @@ export function extractTokenUsage(transcriptPath: string): { input: number; outp
518
592
  return { input, output };
519
593
  }
520
594
 
595
+ // ---------------------------------------------------------------------------
596
+ // Win 3: Model cost lookup (USD per million tokens)
597
+ // ---------------------------------------------------------------------------
598
+
599
+ const MODEL_COSTS: Record<string, { input: number; output: number }> = {
600
+ "claude-sonnet-4-20250514": { input: 3.0, output: 15.0 },
601
+ "claude-opus-4-20250514": { input: 15.0, output: 75.0 },
602
+ "claude-haiku-3-5-20241022": { input: 0.8, output: 4.0 },
603
+ "claude-3-5-sonnet-20241022": { input: 3.0, output: 15.0 },
604
+ "claude-3-5-haiku-20241022": { input: 0.8, output: 4.0 },
605
+ "claude-3-opus-20240229": { input: 15.0, output: 75.0 },
606
+ "claude-3-sonnet-20240229": { input: 3.0, output: 15.0 },
607
+ "claude-3-haiku-20240307": { input: 0.25, output: 1.25 },
608
+ };
609
+
610
+ /**
611
+ * Calculate estimated cost in USD from model name and token counts.
612
+ * Returns undefined if the model is unknown or not provided.
613
+ */
614
+ export function calculateCost(
615
+ model: string | undefined,
616
+ inputTokens: number,
617
+ outputTokens: number,
618
+ ): number | undefined {
619
+ if (!model) return undefined;
620
+ const costs =
621
+ MODEL_COSTS[model] ??
622
+ Object.entries(MODEL_COSTS).find(([k]) =>
623
+ model.startsWith(k.split("-").slice(0, -1).join("-")),
624
+ )?.[1];
625
+ if (!costs) return undefined;
626
+ return (inputTokens * costs.input + outputTokens * costs.output) / 1_000_000;
627
+ }
628
+
629
+ /**
630
+ * Infer session type from tool call distribution.
631
+ *
632
+ * - "dev": majority of output tools are Write/Edit/Bash with git commands
633
+ * - "research": majority are WebFetch/WebSearch/Read
634
+ * - "content": majority are Write/Edit but no git commands
635
+ * - "mixed": no clear majority
636
+ */
637
+ export function inferSessionType(
638
+ toolCalls: Record<string, number>,
639
+ bashCommands: string[],
640
+ ): "dev" | "research" | "content" | "mixed" {
641
+ const total = Object.values(toolCalls).reduce((a, b) => a + b, 0);
642
+ if (total === 0) return "mixed";
643
+
644
+ const writeEdit = (toolCalls.Write ?? 0) + (toolCalls.Edit ?? 0);
645
+ const research = (toolCalls.WebFetch ?? 0) + (toolCalls.WebSearch ?? 0);
646
+ const bash = toolCalls.Bash ?? 0;
647
+ const read = toolCalls.Read ?? 0;
648
+ const hasGit = bashCommands.some((cmd) => GIT_CMD_RE.test(cmd));
649
+
650
+ // Dev: file mutations + git commands OR bash-heavy with git
651
+ if (hasGit && (writeEdit + bash) / total > 0.3) return "dev";
652
+
653
+ // Research: web tools + read-heavy, low file mutations
654
+ if (research > 0 && research / total > 0.2 && writeEdit / total < 0.15) return "research";
655
+ if (read / total > 0.5 && writeEdit / total < 0.1) return "research";
656
+
657
+ // Content: file mutations but no git
658
+ if (writeEdit / total > 0.2 && !hasGit) return "content";
659
+
660
+ return "mixed";
661
+ }
662
+
663
+ /**
664
+ * Generate a short heuristic session summary from transcript metrics.
665
+ * No LLM call — pure template-based approach. Kept under 120 chars.
666
+ */
667
+ export function generateSessionSummary(metrics: TranscriptMetrics): string {
668
+ const MAX_LEN = 120;
669
+ const sessionType: SessionType = metrics.session_type ?? "mixed";
670
+ const lastQuery = truncateQuery(metrics.last_user_query, 60);
671
+
672
+ if (metrics.total_tool_calls === 0 && !lastQuery) {
673
+ return "Empty session — no tool calls or queries";
674
+ }
675
+
676
+ const topTools = getTopTools(metrics.tool_calls, 2);
677
+
678
+ let summary: string;
679
+ switch (sessionType) {
680
+ case "dev": {
681
+ const filesChanged = metrics.files_changed ?? 0;
682
+ const toolStr = topTools.length > 0 ? ` via ${topTools.join(", ")}` : "";
683
+ const queryStr = lastQuery ? ` — ${lastQuery}` : "";
684
+ summary = `${filesChanged} files changed${toolStr}${queryStr}`;
685
+ break;
686
+ }
687
+ case "research": {
688
+ const searches = (metrics.tool_calls.WebSearch ?? 0) + (metrics.tool_calls.WebFetch ?? 0);
689
+ const reads = metrics.tool_calls.Read ?? 0;
690
+ const queryStr = lastQuery ? ` — ${lastQuery}` : "";
691
+ summary = `${searches} searches + ${reads} reads${queryStr}`;
692
+ break;
693
+ }
694
+ case "content": {
695
+ const filesChanged = metrics.files_changed ?? 0;
696
+ const queryStr = lastQuery ? ` — ${lastQuery}` : "";
697
+ summary = `${filesChanged} files created/edited${queryStr}`;
698
+ break;
699
+ }
700
+ default: {
701
+ const toolCount = Object.keys(metrics.tool_calls).length;
702
+ const queryStr = lastQuery ? ` — ${lastQuery}` : "";
703
+ summary = `${metrics.total_tool_calls} tool calls across ${toolCount} tools${queryStr}`;
704
+ break;
705
+ }
706
+ }
707
+
708
+ if (summary.length > MAX_LEN) {
709
+ return `${summary.slice(0, MAX_LEN - 3)}...`;
710
+ }
711
+ return summary;
712
+ }
713
+
714
+ /** Get the top N tools by call count. */
715
+ function getTopTools(toolCalls: Record<string, number>, n: number): string[] {
716
+ return Object.entries(toolCalls)
717
+ .sort((a, b) => b[1] - a[1])
718
+ .slice(0, n)
719
+ .map(([name]) => name);
720
+ }
721
+
722
+ /** Truncate a query string to maxLen, adding ellipsis if needed. */
723
+ function truncateQuery(query: string, maxLen: number): string {
724
+ const trimmed = query.trim();
725
+ if (!trimmed) return "";
726
+ if (trimmed.length <= maxLen) return trimmed;
727
+ return `${trimmed.slice(0, maxLen - 3)}...`;
728
+ }
729
+
521
730
  function emptyMetrics(): TranscriptMetrics {
522
731
  return {
523
732
  tool_calls: {},
@@ -0,0 +1,65 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ renameSync,
6
+ unlinkSync,
7
+ writeFileSync,
8
+ } from "node:fs";
9
+
10
+ import { SELFTUNE_CONFIG_DIR, WATCHED_SKILLS_PATH } from "./constants.js";
11
+
12
+ const CURRENT_WATCHLIST_VERSION = 1;
13
+
14
+ interface WatchlistPayload {
15
+ version: typeof CURRENT_WATCHLIST_VERSION;
16
+ skills: string[];
17
+ }
18
+
19
+ function normalizeSkills(skills: string[]): string[] {
20
+ const seen = new Set<string>();
21
+ const normalized: string[] = [];
22
+ for (const skill of skills) {
23
+ const trimmed = skill.trim();
24
+ if (!trimmed || seen.has(trimmed)) continue;
25
+ seen.add(trimmed);
26
+ normalized.push(trimmed);
27
+ }
28
+ return normalized;
29
+ }
30
+
31
+ export function loadWatchedSkills(): string[] {
32
+ try {
33
+ if (!existsSync(WATCHED_SKILLS_PATH)) return [];
34
+ const parsed = JSON.parse(
35
+ readFileSync(WATCHED_SKILLS_PATH, "utf-8"),
36
+ ) as Partial<WatchlistPayload>;
37
+ return parsed.version === CURRENT_WATCHLIST_VERSION && Array.isArray(parsed.skills)
38
+ ? normalizeSkills(parsed.skills.filter((skill): skill is string => typeof skill === "string"))
39
+ : [];
40
+ } catch {
41
+ return [];
42
+ }
43
+ }
44
+
45
+ export function saveWatchedSkills(skills: string[]): string[] {
46
+ const normalized = normalizeSkills(skills);
47
+ mkdirSync(SELFTUNE_CONFIG_DIR, { recursive: true });
48
+ const tempPath = `${WATCHED_SKILLS_PATH}.${process.pid}.${Date.now()}.tmp`;
49
+ try {
50
+ writeFileSync(
51
+ tempPath,
52
+ JSON.stringify({ version: CURRENT_WATCHLIST_VERSION, skills: normalized }, null, 2),
53
+ "utf-8",
54
+ );
55
+ renameSync(tempPath, WATCHED_SKILLS_PATH);
56
+ } catch (error) {
57
+ try {
58
+ if (existsSync(tempPath)) unlinkSync(tempPath);
59
+ } catch {
60
+ // Best-effort cleanup for interrupted temp writes.
61
+ }
62
+ throw error;
63
+ }
64
+ return normalized;
65
+ }
@@ -143,7 +143,18 @@ export interface CanonicalExecutionFactRecord extends CanonicalSessionRecordBase
143
143
  errors_encountered: number;
144
144
  input_tokens?: number;
145
145
  output_tokens?: number;
146
+ cached_input_tokens?: number;
147
+ reasoning_output_tokens?: number;
148
+ cost_usd?: number;
146
149
  duration_ms?: number;
150
+ files_changed?: number;
151
+ lines_added?: number;
152
+ lines_removed?: number;
153
+ lines_modified?: number;
154
+ /** Count of output-producing tool calls (Write, Edit, WebFetch, WebSearch, Skill, Agent). */
155
+ artifact_count?: number;
156
+ /** Inferred session type based on tool distribution. */
157
+ session_type?: "dev" | "research" | "content" | "mixed";
147
158
  completion_status?: CanonicalCompletionStatus;
148
159
  end_reason?: string;
149
160
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "selftune",
3
- "version": "0.2.16",
3
+ "version": "0.2.19",
4
4
  "description": "Self-improving skills CLI for AI agents",
5
5
  "keywords": [
6
6
  "agent",
@@ -143,7 +143,18 @@ export interface CanonicalExecutionFactRecord extends CanonicalSessionRecordBase
143
143
  errors_encountered: number;
144
144
  input_tokens?: number;
145
145
  output_tokens?: number;
146
+ cached_input_tokens?: number;
147
+ reasoning_output_tokens?: number;
148
+ cost_usd?: number;
146
149
  duration_ms?: number;
150
+ files_changed?: number;
151
+ lines_added?: number;
152
+ lines_removed?: number;
153
+ lines_modified?: number;
154
+ /** Count of output-producing tool calls (Write, Edit, WebFetch, WebSearch, Skill, Agent). */
155
+ artifact_count?: number;
156
+ /** Inferred session type based on tool distribution. */
157
+ session_type?: "dev" | "research" | "content" | "mixed";
147
158
  completion_status?: CanonicalCompletionStatus;
148
159
  end_reason?: string;
149
160
  }