openrunner-sdk 2.7.1__tar.gz → 2.8.0__tar.gz

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 (118) hide show
  1. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/PKG-INFO +2 -1
  2. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/__init__.py +1 -1
  3. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/install_commands.py +65 -43
  4. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/session.py +110 -34
  5. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/pyproject.toml +2 -2
  6. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/.gitignore +0 -0
  7. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/=6.0 +0 -0
  8. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/=8.1 +0 -0
  9. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/README.md +0 -0
  10. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/api_client.py +0 -0
  11. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/artifact.py +0 -0
  12. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/buffer.py +0 -0
  13. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/cache.py +0 -0
  14. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/cli.py +0 -0
  15. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/config.py +0 -0
  16. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/cost.py +0 -0
  17. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/dataset.py +0 -0
  18. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/environment.py +0 -0
  19. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/evaluation.py +0 -0
  20. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/feedback.py +0 -0
  21. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/git_info.py +0 -0
  22. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/guardrails.py +0 -0
  23. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/__init__.py +0 -0
  24. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/accelerate.py +0 -0
  25. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/anthropic_tracer.py +0 -0
  26. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/catboost.py +0 -0
  27. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/diffusers.py +0 -0
  28. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/fastai.py +0 -0
  29. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/forced_alignment.py +0 -0
  30. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/gladia.py +0 -0
  31. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/gymnasium.py +0 -0
  32. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/huggingface.py +0 -0
  33. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/hydra.py +0 -0
  34. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/ignite.py +0 -0
  35. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/jax.py +0 -0
  36. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/keras.py +0 -0
  37. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/langchain.py +0 -0
  38. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/lightgbm.py +0 -0
  39. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/lightning.py +0 -0
  40. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/llamaindex.py +0 -0
  41. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/openai_finetune.py +0 -0
  42. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/openai_tracer.py +0 -0
  43. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/optuna.py +0 -0
  44. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/pytorch.py +0 -0
  45. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/sb3.py +0 -0
  46. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/sklearn.py +0 -0
  47. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/tensorflow.py +0 -0
  48. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/trl.py +0 -0
  49. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/tts.py +0 -0
  50. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/ultralytics.py +0 -0
  51. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/voice_agent.py +0 -0
  52. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/whisper.py +0 -0
  53. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/integration/xgboost.py +0 -0
  54. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/launch.py +0 -0
  55. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/media.py +0 -0
  56. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/migrate.py +0 -0
  57. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/model.py +0 -0
  58. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/offline.py +0 -0
  59. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/pii.py +0 -0
  60. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/plot.py +0 -0
  61. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/prompt.py +0 -0
  62. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/query_api.py +0 -0
  63. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/redact.py +0 -0
  64. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/run.py +0 -0
  65. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/scorers.py +0 -0
  66. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/sender.py +0 -0
  67. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/settings.py +0 -0
  68. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/summary.py +0 -0
  69. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/sweep.py +0 -0
  70. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/system_metrics.py +0 -0
  71. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/tensorboard.py +0 -0
  72. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/trace.py +0 -0
  73. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/transcript_formatter.py +0 -0
  74. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/wal.py +0 -0
  75. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/wandb_compat/__init__.py +0 -0
  76. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/wandb_compat/_shim.py +0 -0
  77. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/openrunner/wer.py +0 -0
  78. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/__init__.py +0 -0
  79. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/conftest.py +0 -0
  80. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_alert.py +0 -0
  81. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_aliases.py +0 -0
  82. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_api_client.py +0 -0
  83. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_artifact.py +0 -0
  84. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_buffer.py +0 -0
  85. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_cache.py +0 -0
  86. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_class_scorers.py +0 -0
  87. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_cli.py +0 -0
  88. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_config.py +0 -0
  89. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_evaluation.py +0 -0
  90. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_finish.py +0 -0
  91. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_git_info.py +0 -0
  92. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_init.py +0 -0
  93. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_integration_fastai.py +0 -0
  94. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_integration_huggingface.py +0 -0
  95. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_integration_keras.py +0 -0
  96. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_integration_langchain.py +0 -0
  97. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_integration_lightning.py +0 -0
  98. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_integration_pytorch.py +0 -0
  99. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_integration_sklearn.py +0 -0
  100. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_integration_xgboost.py +0 -0
  101. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_launch.py +0 -0
  102. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_log.py +0 -0
  103. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_log_code.py +0 -0
  104. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_media.py +0 -0
  105. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_migrate.py +0 -0
  106. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_offline.py +0 -0
  107. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_offline_sync.py +0 -0
  108. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_pii.py +0 -0
  109. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_plot.py +0 -0
  110. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_query_api.py +0 -0
  111. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_resume.py +0 -0
  112. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_sdk_features.py +0 -0
  113. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_sender.py +0 -0
  114. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_summary.py +0 -0
  115. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_sweep.py +0 -0
  116. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_system_metrics.py +0 -0
  117. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_trace.py +0 -0
  118. {openrunner_sdk-2.7.1 → openrunner_sdk-2.8.0}/tests/test_wandb_compat.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openrunner-sdk
3
- Version: 2.7.1
3
+ Version: 2.8.0
4
4
  Summary: OpenRunner SDK - W&B-compatible ML experiment tracking client
5
5
  Project-URL: Homepage, https://github.com/jqueguiner/openrunner
6
6
  Project-URL: Repository, https://github.com/jqueguiner/openrunner
@@ -33,6 +33,7 @@ Requires-Dist: catboost>=1.2; extra == 'catboost'
33
33
  Provides-Extra: dev
34
34
  Requires-Dist: numpy>=1.24; extra == 'dev'
35
35
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
36
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
36
37
  Requires-Dist: pytest>=8.0; extra == 'dev'
37
38
  Provides-Extra: diffusers
38
39
  Requires-Dist: diffusers>=0.25; extra == 'diffusers'
@@ -120,7 +120,7 @@ launch.from_run = _launch_from_run # type: ignore[attr-defined]
120
120
  # openrunner.trace.patch_openai() syntax
121
121
  trace.patch_openai = _patch_openai # type: ignore[attr-defined]
122
122
 
123
- __version__ = "2.7.1"
123
+ __version__ = "2.8.0"
124
124
 
125
125
  logger = logging.getLogger("openrunner")
126
126
 
@@ -26,68 +26,90 @@ from pathlib import Path
26
26
  SYNC_SESSION_CMD = """---
27
27
  name: {prefix}sync-session
28
28
  description: Sync current coding session to OpenRunner as a research log
29
+ argument-hint: "[org/project]"
30
+ allowed-tools:
31
+ - Bash
32
+ - Read
33
+ - AskUserQuestion
29
34
  ---
30
35
 
31
- Sync the current session to OpenRunner. Run:
36
+ Sync the current Claude Code session to OpenRunner.
37
+
38
+ ## Process
39
+
40
+ ### Step 1: Check configuration
41
+
42
+ Run this to check if session config exists:
32
43
 
33
44
  ```bash
34
45
  python3 -c "
35
- import sys, os
46
+ import json
36
47
  from pathlib import Path
48
+ config_file = Path.home() / '.openrunner' / 'session_config.json'
49
+ if config_file.exists():
50
+ config = json.loads(config_file.read_text())
51
+ if config.get('api_key') and config.get('project'):
52
+ print('CONFIGURED')
53
+ print(f'project={{config[\"project\"]}}')
54
+ else:
55
+ print('INCOMPLETE')
56
+ else:
57
+ print('NOT_CONFIGURED')
58
+ "
59
+ ```
37
60
 
38
- # Ensure openrunner is importable
39
- for p in [os.path.expanduser('~/.local/lib/python3.12/site-packages'),
40
- os.path.expanduser('~/.local/lib/python3.11/site-packages'),
41
- os.path.expanduser('~/.local/lib/python3.10/site-packages')]:
42
- if os.path.isdir(p):
43
- sys.path.insert(0, p)
61
+ ### Step 2a: If NOT_CONFIGURED or INCOMPLETE
44
62
 
45
- from openrunner.session import discover_in_directory, parse_claude_session, parse_generic_session, sync_session_to_openrunner
46
-
47
- # Find session dir for current tool
48
- session_dirs = [
49
- Path.home() / '.claude' / 'projects',
50
- Path.home() / '.codex' / 'sessions',
51
- Path.home() / '.qwen-code' / 'sessions',
52
- Path.home() / '.opencode' / 'sessions',
53
- ]
54
-
55
- # Find most recent session file across all sources
56
- all_sessions = []
57
- for d in session_dirs:
58
- if d.exists():
59
- for f in d.rglob('*.jsonl'):
60
- if f.stat().st_size > 100 and '.meta.' not in f.name:
61
- all_sessions.append(f)
62
- for f in d.rglob('*.json'):
63
- if f.stat().st_size > 100 and '.meta.' not in f.name:
64
- all_sessions.append(f)
65
-
66
- if not all_sessions:
67
- print('No sessions found.')
68
- sys.exit(1)
63
+ Ask the user for their OpenRunner API key and base URL. Then list projects and ask them to pick one. Save to ~/.openrunner/session_config.json.
69
64
 
70
- latest = max(all_sessions, key=lambda f: f.stat().st_mtime)
71
- print(f'Syncing: {{latest.name}} ({{latest.stat().st_size // 1024}} KB)')
65
+ ### Step 2b: If CONFIGURED (or after setup)
72
66
 
73
- if latest.suffix == '.jsonl' and '.claude' in str(latest):
74
- parsed = parse_claude_session(latest)
75
- else:
76
- source = 'codex' if '.codex' in str(latest) else 'qwen' if '.qwen' in str(latest) else 'opencode'
77
- parsed = parse_generic_session(latest, source)
67
+ Sync the current session. Use $ARGUMENTS as project override if provided:
68
+
69
+ ```bash
70
+ python3 -c "
71
+ import sys, os
72
+ from pathlib import Path
73
+ for p in [os.path.expanduser(f'~/.local/lib/python3.{{v}}/site-packages') for v in (12,11,10)]:
74
+ if os.path.isdir(p): sys.path.insert(0, p)
75
+ from openrunner.session import parse_claude_session, sync_session_to_openrunner, get_session_config
76
+
77
+ cwd = Path.cwd()
78
+ cwd_key = '-' + str(cwd).replace('/', '-').lstrip('-')
79
+ project_dir = Path.home() / '.claude' / 'projects' / cwd_key
80
+ if not project_dir.exists():
81
+ for d in (Path.home() / '.claude' / 'projects').iterdir():
82
+ if d.is_dir() and cwd_key in d.name:
83
+ project_dir = d
84
+ break
85
+
86
+ sessions = sorted(
87
+ [f for f in project_dir.rglob('*.jsonl') if f.stat().st_size > 100 and '.meta.' not in f.name],
88
+ key=lambda f: f.stat().st_mtime, reverse=True,
89
+ ) if project_dir.exists() else []
90
+
91
+ if not sessions:
92
+ print('No session files found.')
93
+ sys.exit(1)
78
94
 
95
+ latest = sessions[0]
96
+ print(f'Syncing: {{latest.name}} ({{latest.stat().st_size // 1024}} KB)')
97
+ parsed = parse_claude_session(latest)
79
98
  print(f' Messages: {{parsed[\"message_count\"]}} ({{parsed[\"user_message_count\"]}} user)')
80
99
  print(f' Tokens: {{parsed.get(\"total_tokens\", 0):,}}')
81
100
 
82
- project = os.environ.get('OPENRUNNER_SESSION_PROJECT', 'research-sessions')
83
- run_id = sync_session_to_openrunner(parsed, project=project)
101
+ project_override = '$ARGUMENTS'.strip() or None
102
+ run_id = sync_session_to_openrunner(parsed, project=project_override)
84
103
  if run_id:
85
- base = os.environ.get('OPENRUNNER_BASE_URL', 'https://openrun.gladia.io')
104
+ config = get_session_config()
105
+ base = config.get('base_url', 'https://openrun.gladia.io')
86
106
  print(f'Synced -> {{base}}/runs/{{run_id}}')
87
107
  else:
88
- print('Failed. Run: openrunner login')
108
+ print('Sync failed. Run: openrunner session setup')
89
109
  "
90
110
  ```
111
+
112
+ ### Step 3: Report the run URL and stats to the user.
91
113
  """
92
114
 
93
115
  LOG_NOTE_CMD = """---
@@ -137,8 +137,15 @@ def parse_claude_session(path: Path) -> dict[str, Any]:
137
137
  total_tokens += usage.get("input_tokens", 0) + usage.get("output_tokens", 0)
138
138
 
139
139
  # Extract project context from path
140
- # ~/.claude/projects/-home-user-myproject/session.jsonl
141
- project_hint = path.parent.name.replace("-", "/").lstrip("/")
140
+ # ~/.claude/projects/-home-user-myproject/session-id.jsonl
141
+ # or ~/.claude/projects/-home-user-myproject/subagents/agent-xxx.jsonl
142
+ parts = path.parts
143
+ # Find the project dir (child of "projects")
144
+ project_hint = ""
145
+ for i, p in enumerate(parts):
146
+ if p == "projects" and i + 1 < len(parts):
147
+ project_hint = parts[i + 1].replace("-", "/").lstrip("/")
148
+ break
142
149
 
143
150
  # Build summary
144
151
  user_messages = [m for m in messages if m["role"] == "user"]
@@ -170,19 +177,41 @@ def _estimate_duration(path: Path) -> float:
170
177
 
171
178
 
172
179
  def _summarize_session(messages: list[dict]) -> str:
173
- """Generate a text summary from messages."""
174
- user_msgs = [m["content"] for m in messages if m["role"] == "user"]
180
+ """Generate a structured summary from messages.
181
+
182
+ Extracts: topic, key actions, outcomes.
183
+ """
184
+ user_msgs = [m["content"] for m in messages if m["role"] == "user" and m.get("content")]
185
+ assistant_msgs = [m["content"] for m in messages if m["role"] == "assistant" and m.get("content")]
186
+
175
187
  if not user_msgs:
176
188
  return "Empty session"
177
189
 
178
- # Use first and last user messages as summary
179
190
  parts = []
180
- if user_msgs:
181
- parts.append(f"Started: {user_msgs[0][:100]}")
191
+
192
+ # Topic from first message
193
+ parts.append(f"**Topic:** {user_msgs[0][:150]}")
194
+
195
+ # Key user requests (sample up to 5 distinct short ones)
196
+ if len(user_msgs) > 2:
197
+ key_requests = []
198
+ for msg in user_msgs[1:]:
199
+ # Skip very short messages (confirmations) and very long ones (pastes)
200
+ if 10 < len(msg) < 200:
201
+ key_requests.append(msg.strip()[:80])
202
+ if len(key_requests) >= 5:
203
+ break
204
+ if key_requests:
205
+ parts.append("**Key requests:** " + " → ".join(key_requests))
206
+
207
+ # Last request
182
208
  if len(user_msgs) > 1:
183
- parts.append(f"Last: {user_msgs[-1][:100]}")
184
- parts.append(f"({len(user_msgs)} user messages)")
185
- return " | ".join(parts)
209
+ parts.append(f"**Last:** {user_msgs[-1][:100]}")
210
+
211
+ # Stats
212
+ parts.append(f"**Stats:** {len(user_msgs)} requests, {len(assistant_msgs)} responses")
213
+
214
+ return "\n".join(parts)
186
215
 
187
216
 
188
217
  # ---------------------------------------------------------------------------
@@ -480,21 +509,33 @@ def sync_session_to_openrunner(
480
509
  # Create run
481
510
  source = parsed["source"]
482
511
  timestamp = parsed.get("ended_at", "")[:16].replace("T", " ")
483
- run_name = f"{source}/{timestamp}"
512
+ project_hint = parsed.get("project_hint", "")
513
+
514
+ # Build display name: include worktree/project context
515
+ if project_hint:
516
+ # Shorten: /home/ubuntu/openrun → openrun
517
+ short_hint = project_hint.rsplit("/", 1)[-1] if "/" in project_hint else project_hint
518
+ run_name = f"{source}/{short_hint}/{timestamp}"
519
+ else:
520
+ run_name = f"{source}/{timestamp}"
521
+
522
+ # Group sessions by source project (worktree)
523
+ group_name = f"{source}:{project_hint}" if project_hint else f"{source}:default"
484
524
 
485
525
  run_data = {
486
526
  "project": project,
487
527
  "display_name": run_name,
528
+ "group_name": group_name,
488
529
  "config": {
489
530
  "source": source,
490
531
  "session_file": parsed.get("session_file", ""),
491
- "project_hint": parsed.get("project_hint", ""),
532
+ "project_hint": project_hint,
492
533
  "message_count": parsed.get("message_count", 0),
493
534
  "user_message_count": parsed.get("user_message_count", 0),
494
535
  "tools_used": parsed.get("tools_used", []),
495
536
  "total_tokens": parsed.get("total_tokens", 0),
496
537
  },
497
- "tags": [f"source:{source}", "ai-session", f"visibility:{visibility}"],
538
+ "tags": [f"source:{source}", "ai-session", f"visibility:{visibility}", f"worktree:{group_name}"],
498
539
  "notes": _format_session_notes(parsed),
499
540
  "state": "finished",
500
541
  }
@@ -544,30 +585,45 @@ def sync_session_to_openrunner(
544
585
  def _format_session_notes(parsed: dict) -> str:
545
586
  """Format parsed session into readable notes."""
546
587
  lines = []
547
- lines.append(f"# {parsed['source'].title()} Session")
588
+ source = parsed.get("source", "unknown").replace("-", " ").title()
589
+ lines.append(f"# {source} Session")
548
590
  lines.append(f"**Time:** {parsed.get('started_at', '?')[:16]} → {parsed.get('ended_at', '?')[:16]}")
591
+ if parsed.get("project_hint"):
592
+ lines.append(f"**Project:** `{parsed['project_hint']}`")
593
+ lines.append(f"**Tokens:** {parsed.get('total_tokens', 0):,}")
549
594
  lines.append("")
550
595
 
551
596
  if parsed.get("first_message"):
552
- lines.append(f"## First Request")
597
+ lines.append("## Initial Request")
553
598
  lines.append(parsed["first_message"])
554
599
  lines.append("")
555
600
 
601
+ if parsed.get("summary"):
602
+ lines.append("## Summary")
603
+ lines.append(parsed["summary"])
604
+ lines.append("")
605
+
556
606
  if parsed.get("tools_used"):
557
607
  lines.append(f"## Tools Used ({len(parsed['tools_used'])})")
558
- for t in parsed["tools_used"][:20]:
559
- lines.append(f"- {t}")
608
+ lines.append(", ".join(parsed["tools_used"][:20]))
560
609
  lines.append("")
561
610
 
562
611
  if parsed.get("files_touched"):
563
- lines.append(f"## Files Touched ({len(parsed['files_touched'])})")
612
+ lines.append(f"## Files Modified ({len(parsed['files_touched'])})")
564
613
  for f in parsed["files_touched"][:30]:
565
614
  lines.append(f"- `{f}`")
566
615
  lines.append("")
567
616
 
568
- if parsed.get("summary"):
569
- lines.append(f"## Summary")
570
- lines.append(parsed["summary"])
617
+ # Extract key conversation flow (first 10 user messages as bullet points)
618
+ messages = parsed.get("messages", [])
619
+ user_msgs = [m["content"] for m in messages if m.get("role") == "user" and m.get("content") and len(m["content"]) > 10]
620
+ if len(user_msgs) > 2:
621
+ lines.append("## Conversation Flow")
622
+ for msg in user_msgs[:15]:
623
+ lines.append(f"- {msg[:120]}")
624
+ if len(user_msgs) > 15:
625
+ lines.append(f"- ... ({len(user_msgs) - 15} more)")
626
+ lines.append("")
571
627
 
572
628
  return "\n".join(lines)
573
629
 
@@ -680,26 +736,46 @@ def sync_all(
680
736
  HOOK_SCRIPT = '''#!/usr/bin/env python3
681
737
  """Auto-log Claude Code session to OpenRunner on exit."""
682
738
  import sys
739
+ import os
683
740
  from pathlib import Path
684
741
 
685
742
  def main():
686
- # Find the most recent session file that was just written
687
- from openrunner.session import discover_claude_sessions, parse_claude_session, sync_session_to_openrunner
743
+ try:
744
+ from openrunner.session import discover_claude_sessions, parse_claude_session, sync_session_to_openrunner
745
+
746
+ # Find the session for the CWD project (not just any recent session)
747
+ cwd = Path.cwd()
748
+ cwd_key = "-" + str(cwd).replace("/", "-").lstrip("-")
749
+ project_dir = Path.home() / ".claude" / "projects" / cwd_key
750
+
751
+ if project_dir.exists():
752
+ # Find most recent .jsonl in this project
753
+ sessions = sorted(
754
+ [f for f in project_dir.glob("*.jsonl") if f.stat().st_size > 100 and ".meta." not in f.name],
755
+ key=lambda f: f.stat().st_mtime,
756
+ reverse=True,
757
+ )
758
+ else:
759
+ # Fallback: most recent globally
760
+ sessions_info = discover_claude_sessions(since_hours=0.1)
761
+ sessions = [s["path"] for s in sessions_info]
688
762
 
689
- sessions = discover_claude_sessions(since_hours=0.1) # last 6 minutes
690
- if not sessions:
691
- return
763
+ if not sessions:
764
+ return
692
765
 
693
- latest = sessions[0]
694
- parsed = parse_claude_session(latest["path"])
766
+ latest = sessions[0]
767
+ parsed = parse_claude_session(latest)
695
768
 
696
- # Only sync if meaningful (>2 user messages)
697
- if parsed.get("user_message_count", 0) < 2:
698
- return
769
+ # Only sync if meaningful (>2 user messages)
770
+ if parsed.get("user_message_count", 0) < 2:
771
+ return
699
772
 
700
- run_id = sync_session_to_openrunner(parsed)
701
- if run_id:
702
- print(f"openrunner: Session logged as run {run_id}", file=sys.stderr)
773
+ run_id = sync_session_to_openrunner(parsed)
774
+ if run_id:
775
+ print(f"openrunner: Session logged as run {run_id}", file=sys.stderr)
776
+ except Exception as e:
777
+ # Never crash Claude Code on hook failure
778
+ print(f"openrunner hook: {e}", file=sys.stderr)
703
779
 
704
780
  if __name__ == "__main__":
705
781
  main()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openrunner-sdk"
3
- version = "2.7.1"
3
+ version = "2.8.0"
4
4
  description = "OpenRunner SDK - W&B-compatible ML experiment tracking client"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -58,7 +58,7 @@ whisper = ["openai-whisper>=20231117"]
58
58
  tts = ["matplotlib>=3.7", "numpy>=1.24"]
59
59
  voice-agent = []
60
60
  forced-alignment = ["matplotlib>=3.7", "numpy>=1.24"]
61
- dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "numpy>=1.24"]
61
+ dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "pytest-cov>=5.0", "numpy>=1.24"]
62
62
 
63
63
  [tool.hatch.build.targets.wheel]
64
64
  packages = ["openrunner"]
File without changes
File without changes
File without changes