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.
Files changed (55) hide show
  1. up/__init__.py +1 -1
  2. up/ai_cli.py +229 -0
  3. up/cli.py +75 -4
  4. up/commands/agent.py +521 -0
  5. up/commands/bisect.py +343 -0
  6. up/commands/branch.py +350 -0
  7. up/commands/dashboard.py +248 -0
  8. up/commands/init.py +195 -6
  9. up/commands/learn.py +1741 -0
  10. up/commands/memory.py +545 -0
  11. up/commands/new.py +108 -10
  12. up/commands/provenance.py +267 -0
  13. up/commands/review.py +239 -0
  14. up/commands/start.py +1124 -0
  15. up/commands/status.py +360 -0
  16. up/commands/summarize.py +122 -0
  17. up/commands/sync.py +317 -0
  18. up/commands/vibe.py +304 -0
  19. up/context.py +421 -0
  20. up/core/__init__.py +69 -0
  21. up/core/checkpoint.py +479 -0
  22. up/core/provenance.py +364 -0
  23. up/core/state.py +678 -0
  24. up/events.py +512 -0
  25. up/git/__init__.py +37 -0
  26. up/git/utils.py +270 -0
  27. up/git/worktree.py +331 -0
  28. up/learn/__init__.py +155 -0
  29. up/learn/analyzer.py +227 -0
  30. up/learn/plan.py +374 -0
  31. up/learn/research.py +511 -0
  32. up/learn/utils.py +117 -0
  33. up/memory.py +1096 -0
  34. up/parallel.py +551 -0
  35. up/summarizer.py +407 -0
  36. up/templates/__init__.py +70 -2
  37. up/templates/config/__init__.py +502 -20
  38. up/templates/docs/SKILL.md +28 -0
  39. up/templates/docs/__init__.py +341 -0
  40. up/templates/docs/standards/HEADERS.md +24 -0
  41. up/templates/docs/standards/STRUCTURE.md +18 -0
  42. up/templates/docs/standards/TEMPLATES.md +19 -0
  43. up/templates/learn/__init__.py +567 -14
  44. up/templates/loop/__init__.py +546 -27
  45. up/templates/mcp/__init__.py +474 -0
  46. up/templates/projects/__init__.py +786 -0
  47. up/ui/__init__.py +14 -0
  48. up/ui/loop_display.py +650 -0
  49. up/ui/theme.py +137 -0
  50. up_cli-0.5.0.dist-info/METADATA +519 -0
  51. up_cli-0.5.0.dist-info/RECORD +55 -0
  52. up_cli-0.1.1.dist-info/METADATA +0 -186
  53. up_cli-0.1.1.dist-info/RECORD +0 -14
  54. {up_cli-0.1.1.dist-info → up_cli-0.5.0.dist-info}/WHEEL +0 -0
  55. {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
+ ]