up-cli 0.1.1__py3-none-any.whl → 0.5.0__py3-none-any.whl
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.
- up/__init__.py +1 -1
- up/ai_cli.py +229 -0
- up/cli.py +75 -4
- up/commands/agent.py +521 -0
- up/commands/bisect.py +343 -0
- up/commands/branch.py +350 -0
- up/commands/dashboard.py +248 -0
- up/commands/init.py +195 -6
- up/commands/learn.py +1741 -0
- up/commands/memory.py +545 -0
- up/commands/new.py +108 -10
- up/commands/provenance.py +267 -0
- up/commands/review.py +239 -0
- up/commands/start.py +1124 -0
- up/commands/status.py +360 -0
- up/commands/summarize.py +122 -0
- up/commands/sync.py +317 -0
- up/commands/vibe.py +304 -0
- up/context.py +421 -0
- up/core/__init__.py +69 -0
- up/core/checkpoint.py +479 -0
- up/core/provenance.py +364 -0
- up/core/state.py +678 -0
- up/events.py +512 -0
- up/git/__init__.py +37 -0
- up/git/utils.py +270 -0
- up/git/worktree.py +331 -0
- up/learn/__init__.py +155 -0
- up/learn/analyzer.py +227 -0
- up/learn/plan.py +374 -0
- up/learn/research.py +511 -0
- up/learn/utils.py +117 -0
- up/memory.py +1096 -0
- up/parallel.py +551 -0
- up/summarizer.py +407 -0
- up/templates/__init__.py +70 -2
- up/templates/config/__init__.py +502 -20
- up/templates/docs/SKILL.md +28 -0
- up/templates/docs/__init__.py +341 -0
- up/templates/docs/standards/HEADERS.md +24 -0
- up/templates/docs/standards/STRUCTURE.md +18 -0
- up/templates/docs/standards/TEMPLATES.md +19 -0
- up/templates/learn/__init__.py +567 -14
- up/templates/loop/__init__.py +546 -27
- up/templates/mcp/__init__.py +474 -0
- up/templates/projects/__init__.py +786 -0
- up/ui/__init__.py +14 -0
- up/ui/loop_display.py +650 -0
- up/ui/theme.py +137 -0
- up_cli-0.5.0.dist-info/METADATA +519 -0
- up_cli-0.5.0.dist-info/RECORD +55 -0
- up_cli-0.1.1.dist-info/METADATA +0 -186
- up_cli-0.1.1.dist-info/RECORD +0 -14
- {up_cli-0.1.1.dist-info → up_cli-0.5.0.dist-info}/WHEEL +0 -0
- {up_cli-0.1.1.dist-info → up_cli-0.5.0.dist-info}/entry_points.txt +0 -0
up/events.py
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
"""Event system for up-cli lifecycle integration.
|
|
2
|
+
|
|
3
|
+
Provides event-driven communication between systems:
|
|
4
|
+
- Memory
|
|
5
|
+
- Docs
|
|
6
|
+
- Learn
|
|
7
|
+
- Product Loop
|
|
8
|
+
|
|
9
|
+
Events flow through a central bridge that dispatches to handlers.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from dataclasses import dataclass, field, asdict
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Callable, Dict, List, Any, Optional
|
|
17
|
+
from enum import Enum
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EventType(Enum):
|
|
21
|
+
"""Core event types in the lifecycle."""
|
|
22
|
+
|
|
23
|
+
# Git events
|
|
24
|
+
GIT_COMMIT = "git.commit"
|
|
25
|
+
GIT_PUSH = "git.push"
|
|
26
|
+
|
|
27
|
+
# File events
|
|
28
|
+
FILE_CHANGED = "file.changed"
|
|
29
|
+
FILE_CREATED = "file.created"
|
|
30
|
+
FILE_DELETED = "file.deleted"
|
|
31
|
+
|
|
32
|
+
# Session events
|
|
33
|
+
SESSION_START = "session.start"
|
|
34
|
+
SESSION_END = "session.end"
|
|
35
|
+
SESSION_ACTIVITY = "session.activity"
|
|
36
|
+
|
|
37
|
+
# Task events
|
|
38
|
+
TASK_START = "task.start"
|
|
39
|
+
TASK_COMPLETE = "task.complete"
|
|
40
|
+
TASK_FAILED = "task.failed"
|
|
41
|
+
TASK_BLOCKED = "task.blocked"
|
|
42
|
+
|
|
43
|
+
# Error events
|
|
44
|
+
ERROR_OCCURRED = "error.occurred"
|
|
45
|
+
ERROR_FIXED = "error.fixed"
|
|
46
|
+
|
|
47
|
+
# Learning events
|
|
48
|
+
LEARNING_DISCOVERED = "learning.discovered"
|
|
49
|
+
LEARNING_NEEDED = "learning.needed"
|
|
50
|
+
PATTERN_DETECTED = "pattern.detected"
|
|
51
|
+
|
|
52
|
+
# Decision events
|
|
53
|
+
DECISION_MADE = "decision.made"
|
|
54
|
+
|
|
55
|
+
# Milestone events
|
|
56
|
+
MILESTONE_REACHED = "milestone.reached"
|
|
57
|
+
|
|
58
|
+
# System events
|
|
59
|
+
SYNC_REQUESTED = "sync.requested"
|
|
60
|
+
CONTEXT_UPDATED = "context.updated"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class Event:
|
|
65
|
+
"""An event in the lifecycle system."""
|
|
66
|
+
|
|
67
|
+
type: EventType
|
|
68
|
+
data: Dict[str, Any] = field(default_factory=dict)
|
|
69
|
+
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
70
|
+
source: str = "unknown"
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> dict:
|
|
73
|
+
return {
|
|
74
|
+
"type": self.type.value,
|
|
75
|
+
"data": self.data,
|
|
76
|
+
"timestamp": self.timestamp,
|
|
77
|
+
"source": self.source,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Type alias for event handlers
|
|
82
|
+
EventHandler = Callable[[Event], None]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class EventBridge:
|
|
86
|
+
"""Central event bridge that dispatches events to handlers.
|
|
87
|
+
|
|
88
|
+
Implements a simple pub/sub pattern for loose coupling between systems.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
_instance = None
|
|
92
|
+
|
|
93
|
+
def __new__(cls, *args, **kwargs):
|
|
94
|
+
"""Singleton pattern - one bridge per process."""
|
|
95
|
+
if cls._instance is None:
|
|
96
|
+
cls._instance = super().__new__(cls)
|
|
97
|
+
cls._instance._initialized = False
|
|
98
|
+
return cls._instance
|
|
99
|
+
|
|
100
|
+
def __init__(self, workspace: Optional[Path] = None):
|
|
101
|
+
if self._initialized:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
self.workspace = workspace or Path.cwd()
|
|
105
|
+
self.handlers: Dict[EventType, List[EventHandler]] = {}
|
|
106
|
+
self.event_log: List[Event] = []
|
|
107
|
+
self.config = self._load_config()
|
|
108
|
+
self._initialized = True
|
|
109
|
+
|
|
110
|
+
def _load_config(self) -> dict:
|
|
111
|
+
"""Load automation configuration."""
|
|
112
|
+
config_file = self.workspace / ".up" / "config.json"
|
|
113
|
+
if config_file.exists():
|
|
114
|
+
try:
|
|
115
|
+
return json.loads(config_file.read_text())
|
|
116
|
+
except json.JSONDecodeError:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
# Default configuration
|
|
120
|
+
return {
|
|
121
|
+
"automation": {
|
|
122
|
+
"memory": {
|
|
123
|
+
"auto_index_commits": True,
|
|
124
|
+
"auto_record_tasks": True,
|
|
125
|
+
"auto_record_errors": True,
|
|
126
|
+
"session_timeout_minutes": 30,
|
|
127
|
+
},
|
|
128
|
+
"docs": {
|
|
129
|
+
"auto_update_context": True,
|
|
130
|
+
"auto_update_handoff": True,
|
|
131
|
+
"auto_changelog_on_milestone": True,
|
|
132
|
+
},
|
|
133
|
+
"learn": {
|
|
134
|
+
"auto_trigger_on_repeated_error": True,
|
|
135
|
+
"auto_trigger_threshold": 2,
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
def subscribe(self, event_type: EventType, handler: EventHandler) -> None:
|
|
141
|
+
"""Subscribe a handler to an event type."""
|
|
142
|
+
if event_type not in self.handlers:
|
|
143
|
+
self.handlers[event_type] = []
|
|
144
|
+
self.handlers[event_type].append(handler)
|
|
145
|
+
|
|
146
|
+
def unsubscribe(self, event_type: EventType, handler: EventHandler) -> None:
|
|
147
|
+
"""Unsubscribe a handler from an event type."""
|
|
148
|
+
if event_type in self.handlers:
|
|
149
|
+
self.handlers[event_type] = [
|
|
150
|
+
h for h in self.handlers[event_type] if h != handler
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
def emit(self, event: Event) -> None:
|
|
154
|
+
"""Emit an event to all subscribed handlers."""
|
|
155
|
+
# Log event
|
|
156
|
+
self.event_log.append(event)
|
|
157
|
+
if len(self.event_log) > 100:
|
|
158
|
+
self.event_log = self.event_log[-100:]
|
|
159
|
+
|
|
160
|
+
# Dispatch to handlers
|
|
161
|
+
if event.type in self.handlers:
|
|
162
|
+
for handler in self.handlers[event.type]:
|
|
163
|
+
try:
|
|
164
|
+
handler(event)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
# Log but don't fail on handler errors
|
|
167
|
+
print(f"Event handler error: {e}")
|
|
168
|
+
|
|
169
|
+
def emit_simple(
|
|
170
|
+
self,
|
|
171
|
+
event_type: EventType,
|
|
172
|
+
source: str = "unknown",
|
|
173
|
+
**data
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Convenience method to emit an event with data."""
|
|
176
|
+
event = Event(type=event_type, data=data, source=source)
|
|
177
|
+
self.emit(event)
|
|
178
|
+
|
|
179
|
+
def get_recent_events(self, limit: int = 20) -> List[Event]:
|
|
180
|
+
"""Get recent events."""
|
|
181
|
+
return self.event_log[-limit:]
|
|
182
|
+
|
|
183
|
+
def clear_handlers(self) -> None:
|
|
184
|
+
"""Clear all handlers (useful for testing)."""
|
|
185
|
+
self.handlers.clear()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# =============================================================================
|
|
189
|
+
# Default Event Handlers
|
|
190
|
+
# =============================================================================
|
|
191
|
+
|
|
192
|
+
def create_memory_handlers(bridge: EventBridge) -> None:
|
|
193
|
+
"""Register memory-related event handlers."""
|
|
194
|
+
from up.memory import MemoryManager
|
|
195
|
+
|
|
196
|
+
config = bridge.config.get("automation", {}).get("memory", {})
|
|
197
|
+
|
|
198
|
+
def on_task_complete(event: Event):
|
|
199
|
+
if not config.get("auto_record_tasks", True):
|
|
200
|
+
return
|
|
201
|
+
manager = MemoryManager(bridge.workspace)
|
|
202
|
+
task = event.data.get("task", "Unknown task")
|
|
203
|
+
manager.record_task(task)
|
|
204
|
+
|
|
205
|
+
files = event.data.get("files", [])
|
|
206
|
+
for f in files:
|
|
207
|
+
manager.record_file(f)
|
|
208
|
+
|
|
209
|
+
def on_error_occurred(event: Event):
|
|
210
|
+
if not config.get("auto_record_errors", True):
|
|
211
|
+
return
|
|
212
|
+
manager = MemoryManager(bridge.workspace)
|
|
213
|
+
error = event.data.get("error", "Unknown error")
|
|
214
|
+
solution = event.data.get("solution")
|
|
215
|
+
manager.record_error(error, solution)
|
|
216
|
+
|
|
217
|
+
def on_learning_discovered(event: Event):
|
|
218
|
+
manager = MemoryManager(bridge.workspace)
|
|
219
|
+
learning = event.data.get("learning", "")
|
|
220
|
+
if learning:
|
|
221
|
+
manager.record_learning(learning)
|
|
222
|
+
|
|
223
|
+
def on_decision_made(event: Event):
|
|
224
|
+
manager = MemoryManager(bridge.workspace)
|
|
225
|
+
decision = event.data.get("decision", "")
|
|
226
|
+
if decision:
|
|
227
|
+
manager.record_decision(decision)
|
|
228
|
+
|
|
229
|
+
def on_git_commit(event: Event):
|
|
230
|
+
if not config.get("auto_index_commits", True):
|
|
231
|
+
return
|
|
232
|
+
manager = MemoryManager(bridge.workspace)
|
|
233
|
+
manager.index_recent_commits(count=1)
|
|
234
|
+
|
|
235
|
+
def on_session_end(event: Event):
|
|
236
|
+
manager = MemoryManager(bridge.workspace)
|
|
237
|
+
summary = event.data.get("summary")
|
|
238
|
+
manager.end_session(summary)
|
|
239
|
+
|
|
240
|
+
# Register handlers
|
|
241
|
+
bridge.subscribe(EventType.TASK_COMPLETE, on_task_complete)
|
|
242
|
+
bridge.subscribe(EventType.ERROR_OCCURRED, on_error_occurred)
|
|
243
|
+
bridge.subscribe(EventType.LEARNING_DISCOVERED, on_learning_discovered)
|
|
244
|
+
bridge.subscribe(EventType.DECISION_MADE, on_decision_made)
|
|
245
|
+
bridge.subscribe(EventType.GIT_COMMIT, on_git_commit)
|
|
246
|
+
bridge.subscribe(EventType.SESSION_END, on_session_end)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def create_docs_handlers(bridge: EventBridge) -> None:
|
|
250
|
+
"""Register docs-related event handlers."""
|
|
251
|
+
config = bridge.config.get("automation", {}).get("docs", {})
|
|
252
|
+
|
|
253
|
+
def on_task_complete(event: Event):
|
|
254
|
+
if not config.get("auto_update_context", True):
|
|
255
|
+
return
|
|
256
|
+
_update_context_md(
|
|
257
|
+
bridge.workspace,
|
|
258
|
+
recent_change=event.data.get("task"),
|
|
259
|
+
files=event.data.get("files", [])
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
def on_session_end(event: Event):
|
|
263
|
+
if not config.get("auto_update_handoff", True):
|
|
264
|
+
return
|
|
265
|
+
_update_handoff_md(
|
|
266
|
+
bridge.workspace,
|
|
267
|
+
summary=event.data.get("summary"),
|
|
268
|
+
tasks=event.data.get("tasks", []),
|
|
269
|
+
files=event.data.get("files", [])
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def on_milestone_reached(event: Event):
|
|
273
|
+
if not config.get("auto_changelog_on_milestone", True):
|
|
274
|
+
return
|
|
275
|
+
_create_changelog_entry(
|
|
276
|
+
bridge.workspace,
|
|
277
|
+
milestone=event.data.get("milestone"),
|
|
278
|
+
changes=event.data.get("changes", [])
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Register handlers
|
|
282
|
+
bridge.subscribe(EventType.TASK_COMPLETE, on_task_complete)
|
|
283
|
+
bridge.subscribe(EventType.SESSION_END, on_session_end)
|
|
284
|
+
bridge.subscribe(EventType.MILESTONE_REACHED, on_milestone_reached)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _update_context_md(workspace: Path, recent_change: str = None, files: List[str] = None):
|
|
288
|
+
"""Update docs/CONTEXT.md with recent changes."""
|
|
289
|
+
context_file = workspace / "docs" / "CONTEXT.md"
|
|
290
|
+
if not context_file.exists():
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
content = context_file.read_text()
|
|
295
|
+
|
|
296
|
+
# Update the "Updated" date
|
|
297
|
+
from datetime import date
|
|
298
|
+
today = date.today().isoformat()
|
|
299
|
+
|
|
300
|
+
import re
|
|
301
|
+
content = re.sub(
|
|
302
|
+
r'\*\*Updated\*\*:\s*[\d-]+',
|
|
303
|
+
f'**Updated**: {today}',
|
|
304
|
+
content
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Update recent changes section if present
|
|
308
|
+
if recent_change and "## Recent Changes" in content:
|
|
309
|
+
# Find the section and prepend new change
|
|
310
|
+
lines = content.split("\n")
|
|
311
|
+
new_lines = []
|
|
312
|
+
in_recent = False
|
|
313
|
+
added = False
|
|
314
|
+
|
|
315
|
+
for line in lines:
|
|
316
|
+
new_lines.append(line)
|
|
317
|
+
if line.startswith("## Recent Changes"):
|
|
318
|
+
in_recent = True
|
|
319
|
+
elif in_recent and line.startswith("- ") and not added:
|
|
320
|
+
# Insert before first item
|
|
321
|
+
new_lines.insert(-1, f"- {recent_change}")
|
|
322
|
+
added = True
|
|
323
|
+
in_recent = False
|
|
324
|
+
|
|
325
|
+
content = "\n".join(new_lines)
|
|
326
|
+
|
|
327
|
+
context_file.write_text(content)
|
|
328
|
+
|
|
329
|
+
except Exception:
|
|
330
|
+
pass # Don't fail on docs update errors
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _update_handoff_md(
|
|
334
|
+
workspace: Path,
|
|
335
|
+
summary: str = None,
|
|
336
|
+
tasks: List[str] = None,
|
|
337
|
+
files: List[str] = None
|
|
338
|
+
):
|
|
339
|
+
"""Update docs/handoff/LATEST.md with session summary."""
|
|
340
|
+
handoff_file = workspace / "docs" / "handoff" / "LATEST.md"
|
|
341
|
+
handoff_file.parent.mkdir(parents=True, exist_ok=True)
|
|
342
|
+
|
|
343
|
+
from datetime import datetime
|
|
344
|
+
now = datetime.now()
|
|
345
|
+
|
|
346
|
+
content = f"""# Latest Session Handoff
|
|
347
|
+
|
|
348
|
+
**Date**: {now.strftime('%Y-%m-%d')}
|
|
349
|
+
**Time**: {now.strftime('%H:%M')}
|
|
350
|
+
**Status**: 🟢 Ready
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Session Summary
|
|
355
|
+
|
|
356
|
+
{summary or 'Session completed.'}
|
|
357
|
+
|
|
358
|
+
## What Was Done
|
|
359
|
+
|
|
360
|
+
"""
|
|
361
|
+
|
|
362
|
+
if tasks:
|
|
363
|
+
for task in tasks:
|
|
364
|
+
content += f"- {task}\n"
|
|
365
|
+
else:
|
|
366
|
+
content += "- Session work completed\n"
|
|
367
|
+
|
|
368
|
+
content += """
|
|
369
|
+
## Files Modified
|
|
370
|
+
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
if files:
|
|
374
|
+
for f in files[:10]: # Limit to 10
|
|
375
|
+
content += f"- `{f}`\n"
|
|
376
|
+
if len(files) > 10:
|
|
377
|
+
content += f"- ...and {len(files) - 10} more\n"
|
|
378
|
+
else:
|
|
379
|
+
content += "- No files recorded\n"
|
|
380
|
+
|
|
381
|
+
content += """
|
|
382
|
+
## Next Steps
|
|
383
|
+
|
|
384
|
+
1. Review changes
|
|
385
|
+
2. Continue with remaining tasks
|
|
386
|
+
3. Run tests
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
*Auto-generated by up-cli*
|
|
391
|
+
"""
|
|
392
|
+
|
|
393
|
+
handoff_file.write_text(content)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _create_changelog_entry(workspace: Path, milestone: str = None, changes: List[str] = None):
|
|
397
|
+
"""Create a changelog entry for a milestone."""
|
|
398
|
+
changelog_dir = workspace / "docs" / "changelog"
|
|
399
|
+
changelog_dir.mkdir(parents=True, exist_ok=True)
|
|
400
|
+
|
|
401
|
+
from datetime import date
|
|
402
|
+
today = date.today()
|
|
403
|
+
filename = f"{today.isoformat()}-{milestone or 'update'}.md"
|
|
404
|
+
filepath = changelog_dir / filename
|
|
405
|
+
|
|
406
|
+
content = f"""# {milestone or 'Update'}
|
|
407
|
+
|
|
408
|
+
**Date**: {today.isoformat()}
|
|
409
|
+
**Status**: ✅ Completed
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## Summary
|
|
414
|
+
|
|
415
|
+
Milestone completed.
|
|
416
|
+
|
|
417
|
+
## Changes
|
|
418
|
+
|
|
419
|
+
"""
|
|
420
|
+
|
|
421
|
+
if changes:
|
|
422
|
+
for change in changes:
|
|
423
|
+
content += f"- {change}\n"
|
|
424
|
+
else:
|
|
425
|
+
content += "- Changes implemented\n"
|
|
426
|
+
|
|
427
|
+
filepath.write_text(content)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
# =============================================================================
|
|
431
|
+
# Initialize Default Handlers
|
|
432
|
+
# =============================================================================
|
|
433
|
+
|
|
434
|
+
def initialize_event_system(workspace: Optional[Path] = None) -> EventBridge:
|
|
435
|
+
"""Initialize the event system with default handlers."""
|
|
436
|
+
bridge = EventBridge(workspace)
|
|
437
|
+
|
|
438
|
+
# Only register handlers once
|
|
439
|
+
if not bridge.handlers:
|
|
440
|
+
create_memory_handlers(bridge)
|
|
441
|
+
create_docs_handlers(bridge)
|
|
442
|
+
|
|
443
|
+
return bridge
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
# =============================================================================
|
|
447
|
+
# Convenience Functions
|
|
448
|
+
# =============================================================================
|
|
449
|
+
|
|
450
|
+
def emit_task_complete(task: str, files: List[str] = None, source: str = "loop"):
|
|
451
|
+
"""Emit task complete event."""
|
|
452
|
+
bridge = EventBridge()
|
|
453
|
+
bridge.emit_simple(
|
|
454
|
+
EventType.TASK_COMPLETE,
|
|
455
|
+
source=source,
|
|
456
|
+
task=task,
|
|
457
|
+
files=files or []
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def emit_error(error: str, solution: str = None, source: str = "loop"):
|
|
462
|
+
"""Emit error occurred event."""
|
|
463
|
+
bridge = EventBridge()
|
|
464
|
+
bridge.emit_simple(
|
|
465
|
+
EventType.ERROR_OCCURRED,
|
|
466
|
+
source=source,
|
|
467
|
+
error=error,
|
|
468
|
+
solution=solution
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def emit_learning(learning: str, source: str = "learn"):
|
|
473
|
+
"""Emit learning discovered event."""
|
|
474
|
+
bridge = EventBridge()
|
|
475
|
+
bridge.emit_simple(
|
|
476
|
+
EventType.LEARNING_DISCOVERED,
|
|
477
|
+
source=source,
|
|
478
|
+
learning=learning
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def emit_decision(decision: str, source: str = "user"):
|
|
483
|
+
"""Emit decision made event."""
|
|
484
|
+
bridge = EventBridge()
|
|
485
|
+
bridge.emit_simple(
|
|
486
|
+
EventType.DECISION_MADE,
|
|
487
|
+
source=source,
|
|
488
|
+
decision=decision
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def emit_session_end(summary: str = None, tasks: List[str] = None, files: List[str] = None):
|
|
493
|
+
"""Emit session end event."""
|
|
494
|
+
bridge = EventBridge()
|
|
495
|
+
bridge.emit_simple(
|
|
496
|
+
EventType.SESSION_END,
|
|
497
|
+
source="session",
|
|
498
|
+
summary=summary,
|
|
499
|
+
tasks=tasks or [],
|
|
500
|
+
files=files or []
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def emit_git_commit(commit_hash: str, message: str):
|
|
505
|
+
"""Emit git commit event."""
|
|
506
|
+
bridge = EventBridge()
|
|
507
|
+
bridge.emit_simple(
|
|
508
|
+
EventType.GIT_COMMIT,
|
|
509
|
+
source="git",
|
|
510
|
+
hash=commit_hash,
|
|
511
|
+
message=message
|
|
512
|
+
)
|
up/git/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Git utilities for up-cli."""
|
|
2
|
+
|
|
3
|
+
from up.git.worktree import (
|
|
4
|
+
create_worktree,
|
|
5
|
+
remove_worktree,
|
|
6
|
+
list_worktrees,
|
|
7
|
+
merge_worktree,
|
|
8
|
+
WorktreeState,
|
|
9
|
+
)
|
|
10
|
+
from up.git.utils import (
|
|
11
|
+
is_git_repo,
|
|
12
|
+
get_current_branch,
|
|
13
|
+
count_commits_since,
|
|
14
|
+
make_branch_name,
|
|
15
|
+
run_git,
|
|
16
|
+
migrate_legacy_branch,
|
|
17
|
+
preview_merge,
|
|
18
|
+
BRANCH_PREFIX,
|
|
19
|
+
LEGACY_BRANCH_PREFIX,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"create_worktree",
|
|
24
|
+
"remove_worktree",
|
|
25
|
+
"list_worktrees",
|
|
26
|
+
"merge_worktree",
|
|
27
|
+
"WorktreeState",
|
|
28
|
+
"is_git_repo",
|
|
29
|
+
"get_current_branch",
|
|
30
|
+
"count_commits_since",
|
|
31
|
+
"make_branch_name",
|
|
32
|
+
"run_git",
|
|
33
|
+
"migrate_legacy_branch",
|
|
34
|
+
"preview_merge",
|
|
35
|
+
"BRANCH_PREFIX",
|
|
36
|
+
"LEGACY_BRANCH_PREFIX",
|
|
37
|
+
]
|