up-cli 0.2.0__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 (46) hide show
  1. up/__init__.py +1 -1
  2. up/ai_cli.py +229 -0
  3. up/cli.py +54 -9
  4. up/commands/agent.py +521 -0
  5. up/commands/bisect.py +343 -0
  6. up/commands/branch.py +350 -0
  7. up/commands/init.py +195 -6
  8. up/commands/learn.py +1392 -32
  9. up/commands/memory.py +545 -0
  10. up/commands/provenance.py +267 -0
  11. up/commands/review.py +239 -0
  12. up/commands/start.py +752 -42
  13. up/commands/status.py +173 -18
  14. up/commands/sync.py +317 -0
  15. up/commands/vibe.py +304 -0
  16. up/context.py +64 -10
  17. up/core/__init__.py +69 -0
  18. up/core/checkpoint.py +479 -0
  19. up/core/provenance.py +364 -0
  20. up/core/state.py +678 -0
  21. up/events.py +512 -0
  22. up/git/__init__.py +37 -0
  23. up/git/utils.py +270 -0
  24. up/git/worktree.py +331 -0
  25. up/learn/__init__.py +155 -0
  26. up/learn/analyzer.py +227 -0
  27. up/learn/plan.py +374 -0
  28. up/learn/research.py +511 -0
  29. up/learn/utils.py +117 -0
  30. up/memory.py +1096 -0
  31. up/parallel.py +551 -0
  32. up/templates/config/__init__.py +1 -1
  33. up/templates/docs/SKILL.md +28 -0
  34. up/templates/docs/__init__.py +341 -0
  35. up/templates/docs/standards/HEADERS.md +24 -0
  36. up/templates/docs/standards/STRUCTURE.md +18 -0
  37. up/templates/docs/standards/TEMPLATES.md +19 -0
  38. up/templates/loop/__init__.py +92 -32
  39. up/ui/__init__.py +14 -0
  40. up/ui/loop_display.py +650 -0
  41. up/ui/theme.py +137 -0
  42. {up_cli-0.2.0.dist-info → up_cli-0.5.0.dist-info}/METADATA +160 -15
  43. up_cli-0.5.0.dist-info/RECORD +55 -0
  44. up_cli-0.2.0.dist-info/RECORD +0 -23
  45. {up_cli-0.2.0.dist-info → up_cli-0.5.0.dist-info}/WHEEL +0 -0
  46. {up_cli-0.2.0.dist-info → up_cli-0.5.0.dist-info}/entry_points.txt +0 -0
up/commands/vibe.py ADDED
@@ -0,0 +1,304 @@
1
+ """up save/reset/diff - Vibe coding safety commands.
2
+
3
+ These commands provide the core safety rails for AI-assisted development:
4
+ - up save: Create checkpoint before AI work
5
+ - up reset: Restore to checkpoint when AI fails
6
+ - up diff: Review AI changes before accepting
7
+ """
8
+
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ import click
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+ from rich.syntax import Syntax
17
+ from rich.table import Table
18
+
19
+ from up.core.checkpoint import (
20
+ get_checkpoint_manager,
21
+ CheckpointManager,
22
+ CheckpointNotFoundError,
23
+ NotAGitRepoError,
24
+ )
25
+ from up.core.state import get_state_manager
26
+
27
+ console = Console()
28
+
29
+
30
+ # =============================================================================
31
+ # up save - Create checkpoint
32
+ # =============================================================================
33
+
34
+ @click.command("save")
35
+ @click.argument("message", required=False)
36
+ @click.option("--task", "-t", help="Associated task ID")
37
+ @click.option("--quiet", "-q", is_flag=True, help="Minimal output")
38
+ def save_cmd(message: str, task: str, quiet: bool):
39
+ """Create a checkpoint before AI work.
40
+
41
+ Automatically commits any dirty files and creates a tag for easy recovery.
42
+ Use 'up reset' to restore if AI generation goes wrong.
43
+
44
+ \b
45
+ Examples:
46
+ up save # Auto-named checkpoint
47
+ up save "before auth" # Named checkpoint
48
+ up save -t US-004 # Link to task
49
+ """
50
+ cwd = Path.cwd()
51
+
52
+ try:
53
+ manager = get_checkpoint_manager(cwd)
54
+
55
+ # Check for changes
56
+ has_changes = manager._has_changes()
57
+
58
+ if not has_changes and not quiet:
59
+ console.print("[dim]No changes to checkpoint (working tree clean)[/]")
60
+
61
+ # Create checkpoint
62
+ metadata = manager.save(
63
+ message=message,
64
+ task_id=task,
65
+ auto_commit=True
66
+ )
67
+
68
+ if quiet:
69
+ console.print(metadata.id)
70
+ else:
71
+ console.print(f"[green]✓[/] Checkpoint created: [cyan]{metadata.id}[/]")
72
+ if metadata.files_changed > 0:
73
+ console.print(f" Committed {metadata.files_changed} file(s)")
74
+ console.print(f" Commit: {metadata.commit_sha[:8]}")
75
+ console.print(f" Tag: {metadata.tag_name}")
76
+ console.print(f"\nTo restore: [cyan]up reset {metadata.id}[/]")
77
+
78
+ except NotAGitRepoError:
79
+ console.print("[red]Error:[/] Not a git repository")
80
+ console.print("Initialize with: [cyan]git init[/]")
81
+ sys.exit(1)
82
+ except Exception as e:
83
+ console.print(f"[red]Error:[/] {e}")
84
+ sys.exit(1)
85
+
86
+
87
+ # =============================================================================
88
+ # up reset - Restore checkpoint
89
+ # =============================================================================
90
+
91
+ @click.command("reset")
92
+ @click.argument("checkpoint_id", required=False)
93
+ @click.option("--hard", is_flag=True, default=True, help="Hard reset (discard changes)")
94
+ @click.option("--soft", is_flag=True, help="Soft reset (keep changes staged)")
95
+ @click.option("--list", "list_checkpoints", is_flag=True, help="List available checkpoints")
96
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
97
+ def reset_cmd(checkpoint_id: str, hard: bool, soft: bool, list_checkpoints: bool, yes: bool):
98
+ """Reset to a checkpoint.
99
+
100
+ Instantly restores your code to a previous checkpoint state.
101
+ Use when AI generation produces bad results.
102
+
103
+ \b
104
+ Examples:
105
+ up reset # Reset to last checkpoint
106
+ up reset cp-20260204-1234 # Reset to specific checkpoint
107
+ up reset --list # Show available checkpoints
108
+ """
109
+ cwd = Path.cwd()
110
+
111
+ try:
112
+ manager = get_checkpoint_manager(cwd)
113
+
114
+ # List mode
115
+ if list_checkpoints:
116
+ checkpoints = manager.list_checkpoints(limit=20)
117
+
118
+ if not checkpoints:
119
+ console.print("[dim]No checkpoints available[/]")
120
+ return
121
+
122
+ table = Table(title="Available Checkpoints")
123
+ table.add_column("ID", style="cyan")
124
+ table.add_column("Message")
125
+ table.add_column("Branch")
126
+ table.add_column("Time")
127
+
128
+ for cp in checkpoints:
129
+ table.add_row(
130
+ cp.id,
131
+ cp.message[:40] + "..." if len(cp.message) > 40 else cp.message,
132
+ cp.branch,
133
+ cp.created_at[:19]
134
+ )
135
+
136
+ console.print(table)
137
+ return
138
+
139
+ # Get checkpoint info for confirmation
140
+ if checkpoint_id:
141
+ target = checkpoint_id
142
+ else:
143
+ last = manager.get_last_checkpoint()
144
+ if not last:
145
+ console.print("[yellow]No checkpoints available[/]")
146
+ console.print("Create one with: [cyan]up save[/]")
147
+ return
148
+ target = last.id
149
+
150
+ # Show what will be reset
151
+ stats = manager.diff_stats(target)
152
+
153
+ console.print(f"[bold]Reset to checkpoint:[/] {target}")
154
+ if stats["files"] > 0:
155
+ console.print(f" Changes to discard: {stats['files']} files, "
156
+ f"+{stats['insertions']} -{stats['deletions']}")
157
+
158
+ # Confirm
159
+ if not yes:
160
+ if not click.confirm("Proceed with reset?"):
161
+ console.print("[dim]Cancelled[/]")
162
+ return
163
+
164
+ # Perform reset
165
+ use_hard = not soft
166
+ metadata = manager.restore(checkpoint_id=target, hard=use_hard)
167
+
168
+ console.print(f"\n[green]✓[/] Reset to [cyan]{metadata.id}[/]")
169
+ console.print(f" Commit: {metadata.commit_sha[:8]}")
170
+
171
+ # Update state
172
+ state_manager = get_state_manager(cwd)
173
+ state_manager.state.loop.consecutive_failures = 0 # Reset doom loop counter
174
+ state_manager.save()
175
+
176
+ except NotAGitRepoError:
177
+ console.print("[red]Error:[/] Not a git repository")
178
+ sys.exit(1)
179
+ except CheckpointNotFoundError as e:
180
+ console.print(f"[red]Error:[/] {e}")
181
+ console.print("List available: [cyan]up reset --list[/]")
182
+ sys.exit(1)
183
+ except Exception as e:
184
+ console.print(f"[red]Error:[/] {e}")
185
+ sys.exit(1)
186
+
187
+
188
+ # =============================================================================
189
+ # up diff - Review AI changes
190
+ # =============================================================================
191
+
192
+ @click.command("diff")
193
+ @click.argument("checkpoint_id", required=False)
194
+ @click.option("--stat", is_flag=True, help="Show only stats")
195
+ @click.option("--accept", "-a", is_flag=True, help="Accept changes and commit")
196
+ @click.option("--reject", "-r", is_flag=True, help="Reject changes and reset")
197
+ @click.option("--message", "-m", help="Commit message (with --accept)")
198
+ def diff_cmd(checkpoint_id: str, stat: bool, accept: bool, reject: bool, message: str):
199
+ """Review AI changes before accepting.
200
+
201
+ Shows a syntax-highlighted diff of changes since the last checkpoint.
202
+ Can accept (commit) or reject (reset) the changes.
203
+
204
+ \b
205
+ Examples:
206
+ up diff # Show diff from last checkpoint
207
+ up diff --stat # Show stats only
208
+ up diff --accept # Accept and commit changes
209
+ up diff --reject # Reject and reset to checkpoint
210
+ """
211
+ cwd = Path.cwd()
212
+
213
+ try:
214
+ manager = get_checkpoint_manager(cwd)
215
+
216
+ # Get diff
217
+ diff_output = manager.diff_from_checkpoint(checkpoint_id)
218
+ stats = manager.diff_stats(checkpoint_id)
219
+
220
+ if not diff_output and stats["files"] == 0:
221
+ console.print("[dim]No changes since checkpoint[/]")
222
+ return
223
+
224
+ # Stats mode
225
+ if stat:
226
+ console.print(Panel.fit(
227
+ f"[bold]Changes since checkpoint[/]\n\n"
228
+ f"Files: {stats['files']}\n"
229
+ f"Insertions: [green]+{stats['insertions']}[/]\n"
230
+ f"Deletions: [red]-{stats['deletions']}[/]",
231
+ border_style="blue"
232
+ ))
233
+ return
234
+
235
+ # Reject mode
236
+ if reject:
237
+ console.print("[yellow]Rejecting changes...[/]")
238
+ manager.restore(checkpoint_id=checkpoint_id)
239
+ console.print("[green]✓[/] Changes rejected, reset to checkpoint")
240
+ return
241
+
242
+ # Accept mode
243
+ if accept:
244
+ # Commit the changes
245
+ result = subprocess.run(
246
+ ["git", "add", "-A"],
247
+ cwd=cwd,
248
+ capture_output=True
249
+ )
250
+
251
+ commit_msg = message or "Accept AI changes"
252
+ result = subprocess.run(
253
+ ["git", "commit", "-m", commit_msg],
254
+ cwd=cwd,
255
+ capture_output=True,
256
+ text=True
257
+ )
258
+
259
+ if result.returncode == 0:
260
+ console.print(f"[green]✓[/] Changes accepted and committed")
261
+ console.print(f" Message: {commit_msg}")
262
+ else:
263
+ console.print("[yellow]No changes to commit[/]")
264
+ return
265
+
266
+ # Show diff
267
+ console.print(Panel.fit(
268
+ f"[bold]Changes since checkpoint[/] "
269
+ f"({stats['files']} files, +{stats['insertions']} -{stats['deletions']})",
270
+ border_style="blue"
271
+ ))
272
+ console.print()
273
+
274
+ # Syntax-highlighted diff
275
+ syntax = Syntax(diff_output, "diff", theme="monokai", line_numbers=False)
276
+ console.print(syntax)
277
+
278
+ # Interactive prompt
279
+ console.print()
280
+ console.print("[bold]Actions:[/]")
281
+ console.print(" [cyan]up diff --accept[/] Accept changes")
282
+ console.print(" [cyan]up diff --reject[/] Reject and reset")
283
+
284
+ except NotAGitRepoError:
285
+ console.print("[red]Error:[/] Not a git repository")
286
+ sys.exit(1)
287
+ except Exception as e:
288
+ console.print(f"[red]Error:[/] {e}")
289
+ sys.exit(1)
290
+
291
+
292
+ # =============================================================================
293
+ # Command Group (for future expansion)
294
+ # =============================================================================
295
+
296
+ @click.group()
297
+ def vibe():
298
+ """Vibe coding safety commands."""
299
+ pass
300
+
301
+
302
+ vibe.add_command(save_cmd, name="save")
303
+ vibe.add_command(reset_cmd, name="reset")
304
+ vibe.add_command(diff_cmd, name="diff")
up/context.py CHANGED
@@ -127,7 +127,11 @@ def estimate_file_tokens(path: Path) -> int:
127
127
 
128
128
 
129
129
  class ContextManager:
130
- """Manages context window budget for AI sessions."""
130
+ """Manages context window budget for AI sessions.
131
+
132
+ Now uses the unified StateManager for storage while maintaining
133
+ backwards compatibility with the existing API.
134
+ """
131
135
 
132
136
  def __init__(
133
137
  self,
@@ -135,19 +139,46 @@ class ContextManager:
135
139
  budget: int = DEFAULT_BUDGET
136
140
  ):
137
141
  self.workspace = workspace or Path.cwd()
138
- self.state_file = self.workspace / ".claude" / "context_budget.json"
142
+ # Old location for migration
143
+ self._old_state_file = self.workspace / ".claude" / "context_budget.json"
144
+ # New unified state
145
+ self._use_unified_state = True
139
146
  self.budget = ContextBudget(budget=budget)
140
147
  self._load_state()
141
148
 
142
149
  def _load_state(self) -> None:
143
- """Load state from file."""
144
- if self.state_file.exists():
150
+ """Load state from unified state manager or migrate from old file."""
151
+ try:
152
+ from up.core.state import get_state_manager
153
+ manager = get_state_manager(self.workspace)
154
+ ctx = manager.state.context
155
+
156
+ # Sync from unified state
157
+ self.budget.budget = ctx.budget
158
+ self.budget.total_tokens = ctx.total_tokens
159
+ self.budget.warning_threshold = ctx.warning_threshold
160
+ self.budget.critical_threshold = ctx.critical_threshold
161
+ self.budget.session_start = ctx.session_start
162
+
163
+ # Convert entries
164
+ self.budget.entries = [
165
+ ContextEntry(**e) if isinstance(e, dict) else e
166
+ for e in ctx.entries
167
+ ]
168
+
169
+ except ImportError:
170
+ # Fallback to old file-based storage
171
+ self._use_unified_state = False
172
+ self._load_state_legacy()
173
+
174
+ def _load_state_legacy(self) -> None:
175
+ """Load state from old file location (for backwards compatibility)."""
176
+ if self._old_state_file.exists():
145
177
  try:
146
- data = json.loads(self.state_file.read_text())
178
+ data = json.loads(self._old_state_file.read_text())
147
179
  self.budget.budget = data.get("budget", DEFAULT_BUDGET)
148
180
  self.budget.total_tokens = data.get("total_tokens", 0)
149
181
  self.budget.session_start = data.get("session_start", datetime.now().isoformat())
150
- # Reconstruct entries
151
182
  entries_data = data.get("entries", [])
152
183
  self.budget.entries = [
153
184
  ContextEntry(**e) for e in entries_data
@@ -156,9 +187,31 @@ class ContextManager:
156
187
  pass
157
188
 
158
189
  def _save_state(self) -> None:
159
- """Save state to file."""
160
- self.state_file.parent.mkdir(parents=True, exist_ok=True)
161
- self.state_file.write_text(json.dumps(self.budget.to_dict(), indent=2))
190
+ """Save state to unified state manager."""
191
+ if self._use_unified_state:
192
+ try:
193
+ from up.core.state import get_state_manager
194
+ manager = get_state_manager(self.workspace)
195
+
196
+ # Sync to unified state
197
+ manager.state.context.budget = self.budget.budget
198
+ manager.state.context.total_tokens = self.budget.total_tokens
199
+ manager.state.context.warning_threshold = self.budget.warning_threshold
200
+ manager.state.context.critical_threshold = self.budget.critical_threshold
201
+ manager.state.context.session_start = self.budget.session_start
202
+ manager.state.context.entries = [
203
+ e.to_dict() if hasattr(e, 'to_dict') else e
204
+ for e in self.budget.entries[-50:] # Keep last 50
205
+ ]
206
+
207
+ manager.save()
208
+ return
209
+ except ImportError:
210
+ pass
211
+
212
+ # Fallback to old file-based storage
213
+ self._old_state_file.parent.mkdir(parents=True, exist_ok=True)
214
+ self._old_state_file.write_text(json.dumps(self.budget.to_dict(), indent=2))
162
215
 
163
216
  def record_file_read(self, path: Path) -> ContextEntry:
164
217
  """Record a file being read into context.
@@ -327,7 +380,8 @@ def create_context_budget_file(target_dir: Path, budget: int = DEFAULT_BUDGET) -
327
380
  """
328
381
  manager = ContextManager(workspace=target_dir, budget=budget)
329
382
  manager.reset()
330
- return manager.state_file
383
+ # Return the unified state file path
384
+ return target_dir / ".up" / "state.json"
331
385
 
332
386
 
333
387
  # CLI integration
up/core/__init__.py ADDED
@@ -0,0 +1,69 @@
1
+ """Core modules for up-cli.
2
+
3
+ This package contains the foundational modules used across all commands:
4
+ - state: Unified state management
5
+ - checkpoint: Git checkpoint operations
6
+ """
7
+
8
+ from up.core.state import (
9
+ UnifiedState,
10
+ LoopState,
11
+ ContextState,
12
+ AgentState,
13
+ CircuitBreakerState,
14
+ StateManager,
15
+ get_state_manager,
16
+ get_state,
17
+ save_state,
18
+ )
19
+
20
+ from up.core.checkpoint import (
21
+ CheckpointManager,
22
+ CheckpointMetadata,
23
+ CheckpointError,
24
+ GitError,
25
+ NotAGitRepoError,
26
+ CheckpointNotFoundError,
27
+ get_checkpoint_manager,
28
+ save_checkpoint,
29
+ restore_checkpoint,
30
+ get_diff,
31
+ )
32
+
33
+ from up.core.provenance import (
34
+ ProvenanceEntry,
35
+ ProvenanceManager,
36
+ get_provenance_manager,
37
+ track_ai_operation,
38
+ complete_ai_operation,
39
+ )
40
+
41
+ __all__ = [
42
+ # State
43
+ "UnifiedState",
44
+ "LoopState",
45
+ "ContextState",
46
+ "AgentState",
47
+ "CircuitBreakerState",
48
+ "StateManager",
49
+ "get_state_manager",
50
+ "get_state",
51
+ "save_state",
52
+ # Checkpoint
53
+ "CheckpointManager",
54
+ "CheckpointMetadata",
55
+ "CheckpointError",
56
+ "GitError",
57
+ "NotAGitRepoError",
58
+ "CheckpointNotFoundError",
59
+ "get_checkpoint_manager",
60
+ "save_checkpoint",
61
+ "restore_checkpoint",
62
+ "get_diff",
63
+ # Provenance
64
+ "ProvenanceEntry",
65
+ "ProvenanceManager",
66
+ "get_provenance_manager",
67
+ "track_ai_operation",
68
+ "complete_ai_operation",
69
+ ]