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/agent.py ADDED
@@ -0,0 +1,521 @@
1
+ """up agent - Multi-agent worktree management.
2
+
3
+ These commands enable parallel AI development using Git worktrees:
4
+ - up agent spawn: Create isolated agent environment
5
+ - up agent status: Monitor all active agents
6
+ - up agent merge: Squash and merge agent work
7
+ - up agent cleanup: Remove completed worktrees
8
+ """
9
+
10
+ import json
11
+ import shutil
12
+ import subprocess
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+
16
+ import click
17
+ from rich.console import Console
18
+ from rich.panel import Panel
19
+ from rich.table import Table
20
+
21
+ from up.core.state import get_state_manager, AgentState
22
+ from up.core.checkpoint import get_checkpoint_manager, NotAGitRepoError
23
+ from up.git.utils import is_git_repo, get_current_branch, count_commits_since, make_branch_name, preview_merge
24
+
25
+ console = Console()
26
+
27
+
28
+ # =============================================================================
29
+ # up agent spawn - Create agent worktree
30
+ # =============================================================================
31
+
32
+ @click.command("spawn")
33
+ @click.argument("name")
34
+ @click.option("--task", "-t", help="Task ID to implement")
35
+ @click.option("--branch", "-b", default="main", help="Base branch (default: main)")
36
+ @click.option("--title", help="Task title/description")
37
+ def spawn_cmd(name: str, task: str, branch: str, title: str):
38
+ """Create an isolated agent environment.
39
+
40
+ Creates a Git worktree for parallel AI development. Each agent
41
+ works in isolation, preventing code conflicts.
42
+
43
+ \b
44
+ Examples:
45
+ up agent spawn frontend --task US-007
46
+ up agent spawn auth --task US-008 --title "Add authentication"
47
+ up agent spawn api -b develop
48
+ """
49
+ cwd = Path.cwd()
50
+
51
+ if not is_git_repo(cwd):
52
+ console.print("[red]Error:[/] Not a git repository")
53
+ return
54
+
55
+ # Create worktree directory
56
+ worktree_dir = cwd / ".worktrees"
57
+ worktree_path = worktree_dir / name
58
+ agent_branch = make_branch_name(name)
59
+
60
+ if worktree_path.exists():
61
+ console.print(f"[yellow]Warning:[/] Agent '{name}' already exists")
62
+ console.print(f" Path: {worktree_path}")
63
+ console.print(f"\nTo remove: [cyan]up agent cleanup {name}[/]")
64
+ return
65
+
66
+ # Create worktree
67
+ console.print(f"Creating agent worktree: [cyan]{name}[/]")
68
+
69
+ worktree_dir.mkdir(exist_ok=True)
70
+
71
+ # Create branch and worktree
72
+ result = subprocess.run(
73
+ ["git", "worktree", "add", "-b", agent_branch, str(worktree_path), branch],
74
+ cwd=cwd,
75
+ capture_output=True,
76
+ text=True
77
+ )
78
+
79
+ if result.returncode != 0:
80
+ # Branch might exist, try without -b
81
+ result = subprocess.run(
82
+ ["git", "worktree", "add", str(worktree_path), agent_branch],
83
+ cwd=cwd,
84
+ capture_output=True,
85
+ text=True
86
+ )
87
+ if result.returncode != 0:
88
+ console.print(f"[red]Error:[/] Failed to create worktree")
89
+ console.print(f"[dim]{result.stderr}[/]")
90
+ return
91
+
92
+ # Copy environment files
93
+ env_files = [".env", ".env.local", ".env.development"]
94
+ copied = []
95
+ for env_file in env_files:
96
+ src = cwd / env_file
97
+ if src.exists():
98
+ shutil.copy(src, worktree_path / env_file)
99
+ copied.append(env_file)
100
+
101
+ # Create agent state
102
+ agent = AgentState(
103
+ task_id=task or name,
104
+ task_title=title or f"Agent: {name}",
105
+ branch=agent_branch,
106
+ worktree_path=str(worktree_path),
107
+ status="created",
108
+ phase="READY",
109
+ )
110
+
111
+ # Save to unified state
112
+ state_manager = get_state_manager(cwd)
113
+ state_manager.add_agent(agent)
114
+
115
+ # Also save state in worktree for standalone access
116
+ agent_state_file = worktree_path / ".agent_state.json"
117
+ agent_state_file.write_text(json.dumps({
118
+ "task_id": agent.task_id,
119
+ "task_title": agent.task_title,
120
+ "branch": agent.branch,
121
+ "status": agent.status,
122
+ "phase": agent.phase,
123
+ "started_at": agent.started_at,
124
+ "parent_workspace": str(cwd),
125
+ }, indent=2))
126
+
127
+ # Display success
128
+ console.print(f"\n[green]✓[/] Agent '[cyan]{name}[/]' created")
129
+ console.print()
130
+
131
+ table = Table(show_header=False, box=None)
132
+ table.add_column("Key", style="dim")
133
+ table.add_column("Value")
134
+
135
+ table.add_row("Path", str(worktree_path))
136
+ table.add_row("Branch", agent_branch)
137
+ if task:
138
+ table.add_row("Task", task)
139
+ if copied:
140
+ table.add_row("Env files", ", ".join(copied))
141
+
142
+ console.print(table)
143
+
144
+ console.print(f"\n[bold]To work in this agent:[/]")
145
+ console.print(f" cd {worktree_path}")
146
+ console.print(f"\n[bold]When done:[/]")
147
+ console.print(f" up agent merge {name}")
148
+
149
+
150
+ # =============================================================================
151
+ # up agent status - Monitor all agents
152
+ # =============================================================================
153
+
154
+ @click.command("status")
155
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
156
+ def status_cmd(as_json: bool):
157
+ """Show status of all active agents.
158
+
159
+ Lists all agent worktrees with their current status,
160
+ commits, and health indicators.
161
+ """
162
+ cwd = Path.cwd()
163
+
164
+ if not is_git_repo(cwd):
165
+ console.print("[red]Error:[/] Not a git repository")
166
+ return
167
+
168
+ # Get agents from state
169
+ state_manager = get_state_manager(cwd)
170
+ agents = state_manager.state.agents
171
+
172
+ # Also check for worktrees not in state
173
+ worktree_dir = cwd / ".worktrees"
174
+ if worktree_dir.exists():
175
+ for wt_path in worktree_dir.iterdir():
176
+ if wt_path.is_dir():
177
+ agent_id = wt_path.name
178
+ if agent_id not in agents:
179
+ # Found orphan worktree
180
+ state_file = wt_path / ".agent_state.json"
181
+ if state_file.exists():
182
+ try:
183
+ data = json.loads(state_file.read_text())
184
+ agents[agent_id] = AgentState(
185
+ task_id=data.get("task_id", agent_id),
186
+ task_title=data.get("task_title", ""),
187
+ branch=data.get("branch", make_branch_name(agent_id)),
188
+ worktree_path=str(wt_path),
189
+ status=data.get("status", "unknown"),
190
+ phase=data.get("phase", "UNKNOWN"),
191
+ )
192
+ except json.JSONDecodeError:
193
+ pass
194
+
195
+ if not agents:
196
+ console.print("[dim]No active agents[/]")
197
+ console.print("\nCreate one with: [cyan]up agent spawn <name>[/]")
198
+ return
199
+
200
+ # JSON output
201
+ if as_json:
202
+ output = {
203
+ task_id: {
204
+ "task_title": agent.task_title,
205
+ "branch": agent.branch,
206
+ "worktree": agent.worktree_path,
207
+ "status": agent.status,
208
+ "phase": agent.phase,
209
+ "started_at": agent.started_at,
210
+ }
211
+ for task_id, agent in agents.items()
212
+ }
213
+ console.print(json.dumps(output, indent=2))
214
+ return
215
+
216
+ # Table output
217
+ table = Table(title="Active Agents")
218
+ table.add_column("Name", style="cyan")
219
+ table.add_column("Task")
220
+ table.add_column("Status")
221
+ table.add_column("Commits")
222
+ table.add_column("Branch")
223
+
224
+ for task_id, agent in agents.items():
225
+ # Count commits
226
+ wt_path = Path(agent.worktree_path)
227
+ commits = 0
228
+ if wt_path.exists():
229
+ commits = count_commits_since(wt_path, "main")
230
+
231
+ # Status icon
232
+ status_icons = {
233
+ "created": "🟡",
234
+ "executing": "🔵",
235
+ "verifying": "🟠",
236
+ "passed": "🟢",
237
+ "failed": "🔴",
238
+ "merged": "✅",
239
+ }
240
+ icon = status_icons.get(agent.status, "⚪")
241
+
242
+ table.add_row(
243
+ task_id,
244
+ agent.task_title[:30] + "..." if len(agent.task_title) > 30 else agent.task_title,
245
+ f"{icon} {agent.status}",
246
+ str(commits),
247
+ agent.branch,
248
+ )
249
+
250
+ console.print(table)
251
+
252
+ console.print(f"\n[dim]Total agents: {len(agents)}[/]")
253
+
254
+
255
+ # =============================================================================
256
+ # up agent merge - Squash and merge agent work
257
+ # =============================================================================
258
+
259
+ @click.command("merge")
260
+ @click.argument("name")
261
+ @click.option("--target", "-t", default="main", help="Target branch (default: main)")
262
+ @click.option("--no-squash", is_flag=True, help="Don't squash commits")
263
+ @click.option("--message", "-m", help="Custom commit message")
264
+ @click.option("--keep", "-k", is_flag=True, help="Keep worktree after merge")
265
+ def merge_cmd(name: str, target: str, no_squash: bool, message: str, keep: bool):
266
+ """Merge agent work into target branch.
267
+
268
+ Squashes all agent commits into a single clean commit
269
+ and merges into the target branch.
270
+
271
+ \b
272
+ Examples:
273
+ up agent merge frontend # Merge to main
274
+ up agent merge auth --target develop # Merge to develop
275
+ up agent merge api --no-squash # Keep individual commits
276
+ """
277
+ cwd = Path.cwd()
278
+
279
+ if not is_git_repo(cwd):
280
+ console.print("[red]Error:[/] Not a git repository")
281
+ return
282
+
283
+ state_manager = get_state_manager(cwd)
284
+ agents = state_manager.state.agents
285
+
286
+ # Find agent
287
+ agent = agents.get(name)
288
+ worktree_path = cwd / ".worktrees" / name
289
+
290
+ if not agent and not worktree_path.exists():
291
+ console.print(f"[red]Error:[/] Agent '{name}' not found")
292
+ console.print(f"\nList agents: [cyan]up agent status[/]")
293
+ return
294
+
295
+ # Get branch name
296
+ agent_branch = agent.branch if agent else make_branch_name(name)
297
+
298
+ # Check for commits
299
+ commits = count_commits_since(worktree_path, target)
300
+
301
+ console.print(f"[bold]Merging agent:[/] {name}")
302
+ console.print(f" Branch: {agent_branch}")
303
+ console.print(f" Commits: {commits}")
304
+ console.print(f" Target: {target}")
305
+
306
+ # Preview merge for conflicts
307
+ can_merge, conflicts = preview_merge(agent_branch, target, cwd)
308
+ if not can_merge:
309
+ console.print(f"\n[red]Merge conflicts detected![/]")
310
+ if conflicts:
311
+ console.print("[yellow]Conflicting files:[/]")
312
+ for f in conflicts:
313
+ console.print(f" • {f}")
314
+ console.print(f"\nResolve conflicts manually or use:")
315
+ console.print(f" cd {worktree_path}")
316
+ console.print(f" git merge {target}")
317
+ return
318
+
319
+ if commits == 0:
320
+ console.print("\n[yellow]No commits to merge[/]")
321
+ if not keep:
322
+ if click.confirm("Remove worktree anyway?"):
323
+ _remove_worktree(cwd, name, agent_branch)
324
+ return
325
+
326
+ # Create checkpoint before merge
327
+ try:
328
+ checkpoint_manager = get_checkpoint_manager(cwd)
329
+ checkpoint_manager.save(message=f"Before merge: {name}", task_id=name)
330
+ console.print("[dim]Checkpoint created[/]")
331
+ except NotAGitRepoError:
332
+ pass
333
+
334
+ # Checkout target branch
335
+ result = subprocess.run(
336
+ ["git", "checkout", target],
337
+ cwd=cwd,
338
+ capture_output=True,
339
+ text=True
340
+ )
341
+ if result.returncode != 0:
342
+ console.print(f"[red]Error:[/] Failed to checkout {target}")
343
+ console.print(f"[dim]{result.stderr}[/]")
344
+ return
345
+
346
+ # Merge
347
+ squash = not no_squash
348
+
349
+ if squash:
350
+ # Squash merge
351
+ result = subprocess.run(
352
+ ["git", "merge", "--squash", agent_branch],
353
+ cwd=cwd,
354
+ capture_output=True,
355
+ text=True
356
+ )
357
+ if result.returncode != 0:
358
+ console.print(f"[red]Error:[/] Merge failed")
359
+ console.print(f"[dim]{result.stderr}[/]")
360
+ return
361
+
362
+ # Commit
363
+ commit_msg = message or f"feat({name}): {agent.task_title if agent else 'Agent work'}"
364
+ result = subprocess.run(
365
+ ["git", "commit", "-m", commit_msg],
366
+ cwd=cwd,
367
+ capture_output=True,
368
+ text=True
369
+ )
370
+ if result.returncode != 0:
371
+ console.print(f"[yellow]Warning:[/] Commit may have failed")
372
+ console.print(f"[dim]{result.stderr}[/]")
373
+ else:
374
+ # Regular merge
375
+ commit_msg = message or f"Merge {agent_branch} into {target}"
376
+ result = subprocess.run(
377
+ ["git", "merge", agent_branch, "-m", commit_msg],
378
+ cwd=cwd,
379
+ capture_output=True,
380
+ text=True
381
+ )
382
+ if result.returncode != 0:
383
+ console.print(f"[red]Error:[/] Merge failed")
384
+ console.print(f"[dim]{result.stderr}[/]")
385
+ return
386
+
387
+ console.print(f"\n[green]✓[/] Merged {commits} commit(s) to {target}")
388
+
389
+ # Update state
390
+ if agent:
391
+ agent.status = "merged"
392
+ agent.completed_at = datetime.now().isoformat()
393
+ state_manager.save()
394
+
395
+ # Cleanup
396
+ if not keep:
397
+ _remove_worktree(cwd, name, agent_branch)
398
+ console.print(f"[green]✓[/] Removed worktree and branch")
399
+
400
+ # Remove from state
401
+ state_manager.remove_agent(name)
402
+
403
+
404
+ def _remove_worktree(cwd: Path, name: str, branch: str):
405
+ """Remove worktree and branch."""
406
+ worktree_path = cwd / ".worktrees" / name
407
+
408
+ # Remove worktree
409
+ if worktree_path.exists():
410
+ subprocess.run(
411
+ ["git", "worktree", "remove", str(worktree_path), "--force"],
412
+ cwd=cwd,
413
+ capture_output=True
414
+ )
415
+
416
+ # Delete branch
417
+ subprocess.run(
418
+ ["git", "branch", "-D", branch],
419
+ cwd=cwd,
420
+ capture_output=True
421
+ )
422
+
423
+
424
+ # =============================================================================
425
+ # up agent cleanup - Remove completed worktrees
426
+ # =============================================================================
427
+
428
+ @click.command("cleanup")
429
+ @click.argument("name", required=False)
430
+ @click.option("--all", "cleanup_all", is_flag=True, help="Remove all agents")
431
+ @click.option("--merged", is_flag=True, help="Remove only merged agents")
432
+ @click.option("--force", "-f", is_flag=True, help="Force removal")
433
+ def cleanup_cmd(name: str, cleanup_all: bool, merged: bool, force: bool):
434
+ """Remove agent worktrees.
435
+
436
+ Cleans up completed or abandoned agent environments.
437
+
438
+ \b
439
+ Examples:
440
+ up agent cleanup frontend # Remove specific agent
441
+ up agent cleanup --all # Remove all agents
442
+ up agent cleanup --merged # Remove only merged agents
443
+ """
444
+ cwd = Path.cwd()
445
+
446
+ if not is_git_repo(cwd):
447
+ console.print("[red]Error:[/] Not a git repository")
448
+ return
449
+
450
+ state_manager = get_state_manager(cwd)
451
+ agents = state_manager.state.agents.copy()
452
+
453
+ removed = []
454
+
455
+ if name:
456
+ # Remove specific agent
457
+ agent = agents.get(name)
458
+ branch = agent.branch if agent else make_branch_name(name)
459
+
460
+ if not force:
461
+ if not click.confirm(f"Remove agent '{name}'?"):
462
+ return
463
+
464
+ _remove_worktree(cwd, name, branch)
465
+ state_manager.remove_agent(name)
466
+ removed.append(name)
467
+
468
+ elif cleanup_all:
469
+ # Remove all agents
470
+ if not force:
471
+ if not click.confirm(f"Remove all {len(agents)} agents?"):
472
+ return
473
+
474
+ for task_id, agent in agents.items():
475
+ _remove_worktree(cwd, task_id, agent.branch)
476
+ removed.append(task_id)
477
+
478
+ state_manager.state.agents.clear()
479
+ state_manager.state.parallel.agents.clear()
480
+ state_manager.save()
481
+
482
+ elif merged:
483
+ # Remove only merged agents
484
+ for task_id, agent in agents.items():
485
+ if agent.status == "merged":
486
+ _remove_worktree(cwd, task_id, agent.branch)
487
+ state_manager.remove_agent(task_id)
488
+ removed.append(task_id)
489
+
490
+ else:
491
+ console.print("Specify an agent name or use --all/--merged")
492
+ console.print("\nUsage:")
493
+ console.print(" up agent cleanup <name>")
494
+ console.print(" up agent cleanup --all")
495
+ console.print(" up agent cleanup --merged")
496
+ return
497
+
498
+ if removed:
499
+ console.print(f"[green]✓[/] Removed {len(removed)} agent(s): {', '.join(removed)}")
500
+ else:
501
+ console.print("[dim]No agents to remove[/]")
502
+
503
+
504
+ # =============================================================================
505
+ # Command Group
506
+ # =============================================================================
507
+
508
+ @click.group()
509
+ def agent():
510
+ """Multi-agent worktree management.
511
+
512
+ Enable parallel AI development by creating isolated
513
+ Git worktrees for each task.
514
+ """
515
+ pass
516
+
517
+
518
+ agent.add_command(spawn_cmd, name="spawn")
519
+ agent.add_command(status_cmd, name="status")
520
+ agent.add_command(merge_cmd, name="merge")
521
+ agent.add_command(cleanup_cmd, name="cleanup")