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.
- up/__init__.py +1 -1
- up/ai_cli.py +229 -0
- up/cli.py +54 -9
- up/commands/agent.py +521 -0
- up/commands/bisect.py +343 -0
- up/commands/branch.py +350 -0
- up/commands/init.py +195 -6
- up/commands/learn.py +1392 -32
- up/commands/memory.py +545 -0
- up/commands/provenance.py +267 -0
- up/commands/review.py +239 -0
- up/commands/start.py +752 -42
- up/commands/status.py +173 -18
- up/commands/sync.py +317 -0
- up/commands/vibe.py +304 -0
- up/context.py +64 -10
- 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/templates/config/__init__.py +1 -1
- 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/loop/__init__.py +92 -32
- up/ui/__init__.py +14 -0
- up/ui/loop_display.py +650 -0
- up/ui/theme.py +137 -0
- {up_cli-0.2.0.dist-info → up_cli-0.5.0.dist-info}/METADATA +160 -15
- up_cli-0.5.0.dist-info/RECORD +55 -0
- up_cli-0.2.0.dist-info/RECORD +0 -23
- {up_cli-0.2.0.dist-info → up_cli-0.5.0.dist-info}/WHEEL +0 -0
- {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")
|