task-agent 1.3.10__tar.gz → 1.3.11__tar.gz
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.
- {task_agent-1.3.10 → task_agent-1.3.11}/.gitignore +2 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/PKG-INFO +1 -1
- {task_agent-1.3.10 → task_agent-1.3.11}/pyproject.toml +1 -1
- task_agent-1.3.11/scripts/check_task_worktree_cleanup.py +240 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/cli.py +180 -28
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_cli.py +103 -1
- {task_agent-1.3.10 → task_agent-1.3.11}/uv.lock +1 -1
- task_agent-1.3.10/src/taskagent/theme.py +0 -17
- {task_agent-1.3.10 → task_agent-1.3.11}/.agents/skills/next/SKILL.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/.gemini/settings.json +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/.github/workflows/ci.yml +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/.github/workflows/publish.yml +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/.mise.toml +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/.pre-commit-config.yaml +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/.python-version +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/.ta/agents/agy-cli/meta.toml +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/.ta/agents/antigravity/meta.toml +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/.ta/agents/gh/meta.toml +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/.ta/agents/minimal/meta.toml +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/.ta/agents/uat-aws/meta.toml +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/.ta/worker +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/.ta-config.json +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/CLAUDE.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/GEMINI.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/Makefile +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/README.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/agent.env +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/docs/README.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/docs/STATION-MAP.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/docs/architecture/README.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/docs/chats/gemini-conversation-1773283034591.json +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/docs/chats/project-init-to-v0.1.46.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/docs/cli/README.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/docs/development/README.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/docs/task-agents.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/docs/workflow/README.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/docs/workflow/trivial_worker_policy.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/opencode.json +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/.gemini-plugin/plugin.json +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/.mcp.json +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/README.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/hooks/hooks.json +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/hooks/session_start.sh +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/rules/task_agent.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/scripts/statusline.sh +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/skills/README.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/skills/agent-import/SKILL.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/skills/complete-task/SKILL.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/skills/mission-workflow/SKILL.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/skills/next/SKILL.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/skills/next-task/SKILL.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/claude-code/.mcp.json +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/claude-code/README.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/claude-code/hooks/hooks.json +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/claude-code/hooks/session_start.sh +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/claude-code/scripts/statusline.sh +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/claude-code/skills/agent-import/SKILL.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/claude-code/skills/complete-task/SKILL.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/claude-code/skills/mission-workflow/SKILL.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/claude-code/skills/next-task/SKILL.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/codex/.mcp.json +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/codex/README.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/copilot/.mcp.json +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/copilot/README.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/grok/.mcp.json +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/plugins/grok/README.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/scripts/clean_duplicates.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/scripts/migrate_tasks.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/scripts/recover_dates.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/adk-worker/.env.example +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/adk-worker/README.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/adk-worker/instructions/manager.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/adk-worker/instructions/validator.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/adk-worker/instructions/worker.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/adk-worker/profiles/default.json +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/adk-worker/pyproject.toml +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/adk-worker/uv.lock +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/adk-worker/worker.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/keet-bridge/README.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/keet-bridge/bridge.js +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/keet-bridge/package-lock.json +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/keet-bridge/package.json +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/keet-bridge/run.sh +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/keet-bridge/test-e2e.js +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/matrix-bridge/README.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/matrix-bridge/matrix_bridge.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/skills/README.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/skills/agent-import/SKILL.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/skills/complete-task/SKILL.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/skills/mission-workflow/SKILL.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/skills/next/SKILL.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/skills/next-task/SKILL.md +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/__init__.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/__main__.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/agent.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/agent_registry.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/audit.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/chat/__init__.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/chat/config.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/chat/discovery.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/chat/last_used.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/config.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/discovery.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/importers/__init__.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/importers/antigravity.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/importers/base.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/importers/claude_code.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/importers/generic.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/inbox.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/manager.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/mcp.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/models/__init__.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/models/issue.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/models/metric.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/perf.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/plugins/__init__.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/plugins/github.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/runner.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/store_registry.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/templates.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/test-dir/opencode.json +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/conftest.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/e2e/test_agent_sandboxing.sh +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_agent.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_agent_registry.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_agy_cli_worker.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_atomic_mutations.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_auto_commit.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_chat_config.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_chat_discovery.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_chat_last_used.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_cli_windows.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_discovery.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_github_token.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_importers.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_inbox.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_inbox_watch.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_lease.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_manager.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_manager_git.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_mcp.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_metrics.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_migration.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_perf.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_plugin_antigravity.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_plugin_claude.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_project_host_root.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_rebind.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_remote_attach.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_remote_create.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_repo_resolve.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_runner.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_store_registry.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_store_symlink.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_templates.py +0 -0
- {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_yazi.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: task-agent
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.11
|
|
4
4
|
Summary: A prioritized, file-based task queue for autonomous agentic workers
|
|
5
5
|
Project-URL: Homepage, https://github.com/InTEGr8or/task-agent
|
|
6
6
|
Project-URL: Repository, https://github.com/InTEGr8or/task-agent.git
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Classify task-agent worktrees whose branch is merged and whose task is closed.
|
|
2
|
+
|
|
3
|
+
A worktree is safe to remove when three local signals all agree:
|
|
4
|
+
|
|
5
|
+
1. Branch merged: ``issue/<slug>`` is an ancestor of the integration branch
|
|
6
|
+
(--base, default uat). ``git cherry`` additionally detects content that
|
|
7
|
+
landed via cherry-pick/rebase (patch-merged); those branches need ``-D``
|
|
8
|
+
because ``git branch -d`` only trusts ancestry.
|
|
9
|
+
2. Task closed: the task-agent store encodes status as directory location, so
|
|
10
|
+
the slug under ``completed/YYYY/MM/`` (or ``deleted/``) means closed, while
|
|
11
|
+
``active/``, ``pending/``, ``draft/`` mean the worktree may still be a
|
|
12
|
+
runner staging area.
|
|
13
|
+
3. Nothing to lose: ``git status --porcelain`` in the worktree is empty. A
|
|
14
|
+
worktree whose directory is gone (prunable registration) counts as clean;
|
|
15
|
+
removal uses ``git worktree prune`` for those.
|
|
16
|
+
|
|
17
|
+
Run from any project repo registered in the task-agent registry.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
python scripts/check_task_worktree_cleanup.py # report only
|
|
21
|
+
python scripts/check_task_worktree_cleanup.py --remove # remove safe ones
|
|
22
|
+
python scripts/check_task_worktree_cleanup.py --remove --allow-patch-merged
|
|
23
|
+
|
|
24
|
+
--base defaults to the current branch of the repo it runs in.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import json
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Optional
|
|
36
|
+
|
|
37
|
+
REGISTRY_PATH = Path.home() / ".local/share/task-agent/registry.json"
|
|
38
|
+
OPEN_STATUSES = ("active", "pending", "draft")
|
|
39
|
+
CLOSED_STATUSES = ("completed", "deleted")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def run_git(
|
|
43
|
+
args: list[str], cwd: Optional[Path] = None
|
|
44
|
+
) -> subprocess.CompletedProcess[str]:
|
|
45
|
+
return subprocess.run(
|
|
46
|
+
["git", *args], capture_output=True, text=True, cwd=cwd, check=False
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def moniker_from_remote(url: str) -> Optional[str]:
|
|
51
|
+
tail = url.removesuffix(".git")
|
|
52
|
+
tail = tail.rsplit(":", 1)[-1] if ":" in tail else tail
|
|
53
|
+
parts = [p for p in tail.split("/") if p]
|
|
54
|
+
if len(parts) >= 2:
|
|
55
|
+
return f"{parts[-2]}/{parts[-1]}"
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def resolve_store(repo: Path, override: Optional[str]) -> Optional[Path]:
|
|
60
|
+
if override:
|
|
61
|
+
return Path(override).expanduser()
|
|
62
|
+
if not REGISTRY_PATH.is_file():
|
|
63
|
+
return None
|
|
64
|
+
try:
|
|
65
|
+
registry = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
|
|
66
|
+
except (OSError, ValueError):
|
|
67
|
+
return None
|
|
68
|
+
entries = registry.get("stores", {})
|
|
69
|
+
for entry in entries.values():
|
|
70
|
+
if str(repo) in entry.get("host_paths", []):
|
|
71
|
+
return Path(entry["store_path"])
|
|
72
|
+
remote = run_git(["remote", "get-url", "origin"], cwd=repo).stdout.strip()
|
|
73
|
+
moniker = moniker_from_remote(remote)
|
|
74
|
+
if moniker and moniker in entries:
|
|
75
|
+
return Path(entries[moniker]["store_path"])
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def task_status(store: Path, slug: str) -> str:
|
|
80
|
+
for status in OPEN_STATUSES:
|
|
81
|
+
if (store / status / slug).is_dir():
|
|
82
|
+
return status
|
|
83
|
+
if (store / "deleted" / slug).is_dir():
|
|
84
|
+
return "deleted"
|
|
85
|
+
for match in (store / "completed").glob(f"*/*/{slug}"):
|
|
86
|
+
if match.is_dir():
|
|
87
|
+
return "completed"
|
|
88
|
+
return "not-found"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def merged_state(repo: Path, branch: str, base: str) -> str:
|
|
92
|
+
if run_git(["merge-base", "--is-ancestor", branch, base], cwd=repo).returncode == 0:
|
|
93
|
+
return "ancestor"
|
|
94
|
+
cherry = run_git(["cherry", base, branch], cwd=repo)
|
|
95
|
+
if cherry.returncode == 0:
|
|
96
|
+
lines = [line for line in cherry.stdout.splitlines() if line.strip()]
|
|
97
|
+
if lines and all(line.startswith("-") for line in lines):
|
|
98
|
+
return "patch"
|
|
99
|
+
return "no"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def dirty_count(worktree: Path) -> Optional[int]:
|
|
103
|
+
if not worktree.is_dir():
|
|
104
|
+
return None
|
|
105
|
+
proc = run_git(["status", "--porcelain"], cwd=worktree)
|
|
106
|
+
return len([line for line in proc.stdout.splitlines() if line.strip()])
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def issue_worktrees(repo: Path) -> list[tuple[Path, str]]:
|
|
110
|
+
proc = run_git(["worktree", "list", "--porcelain"], cwd=repo)
|
|
111
|
+
pairs: list[tuple[Path, str]] = []
|
|
112
|
+
worktree: Optional[Path] = None
|
|
113
|
+
for line in proc.stdout.splitlines():
|
|
114
|
+
if line.startswith("worktree "):
|
|
115
|
+
worktree = Path(line.split(" ", 1)[1])
|
|
116
|
+
elif line.startswith("branch refs/heads/issue/"):
|
|
117
|
+
branch = line.split(" ", 1)[1][len("refs/heads/") :]
|
|
118
|
+
if worktree is not None:
|
|
119
|
+
pairs.append((worktree, branch))
|
|
120
|
+
return pairs
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class Row:
|
|
125
|
+
slug: str
|
|
126
|
+
worktree: Path
|
|
127
|
+
branch: str
|
|
128
|
+
merged: str
|
|
129
|
+
status: str
|
|
130
|
+
dirty: Optional[int]
|
|
131
|
+
verdict: str
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def classify(repo: Path, store: Optional[Path], base: str) -> list[Row]:
|
|
135
|
+
rows: list[Row] = []
|
|
136
|
+
for worktree, branch in issue_worktrees(repo):
|
|
137
|
+
slug = branch[len("issue/") :]
|
|
138
|
+
merged = merged_state(repo, branch, base)
|
|
139
|
+
status = task_status(store, slug) if store else "unknown-store"
|
|
140
|
+
dirty = dirty_count(worktree)
|
|
141
|
+
if status not in CLOSED_STATUSES:
|
|
142
|
+
verdict = f"KEEP (task {status})"
|
|
143
|
+
elif merged == "no":
|
|
144
|
+
verdict = "REVIEW (unmerged)"
|
|
145
|
+
elif dirty:
|
|
146
|
+
verdict = "REVIEW (dirty)"
|
|
147
|
+
elif merged == "patch":
|
|
148
|
+
verdict = "REMOVE (patch-merged, -D)"
|
|
149
|
+
else:
|
|
150
|
+
verdict = "REMOVE"
|
|
151
|
+
rows.append(Row(slug, worktree, branch, merged, status, dirty, verdict))
|
|
152
|
+
return rows
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def remove_row(repo: Path, row: Row, force_branch: bool) -> bool:
|
|
156
|
+
if row.worktree.is_dir():
|
|
157
|
+
proc = run_git(["worktree", "remove", str(row.worktree)], cwd=repo)
|
|
158
|
+
else:
|
|
159
|
+
proc = run_git(["worktree", "prune"], cwd=repo)
|
|
160
|
+
if proc.returncode != 0:
|
|
161
|
+
print(f" worktree remove FAILED: {row.worktree}")
|
|
162
|
+
return False
|
|
163
|
+
delete_flag = "-D" if force_branch else "-d"
|
|
164
|
+
proc = run_git(["branch", delete_flag, row.branch], cwd=repo)
|
|
165
|
+
if proc.returncode != 0:
|
|
166
|
+
print(f" branch {delete_flag} FAILED: {row.branch}: {proc.stderr.strip()}")
|
|
167
|
+
return False
|
|
168
|
+
print(f" removed {row.branch} ({row.worktree.name})")
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def main(argv: Optional[list[str]] = None) -> int:
|
|
173
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
174
|
+
parser.add_argument("--base", help="integration branch (default: current branch)")
|
|
175
|
+
parser.add_argument("--store", help="task-agent store path override")
|
|
176
|
+
parser.add_argument(
|
|
177
|
+
"--remove",
|
|
178
|
+
action="store_true",
|
|
179
|
+
help="remove worktrees/branches verdicted REMOVE",
|
|
180
|
+
)
|
|
181
|
+
parser.add_argument(
|
|
182
|
+
"--allow-patch-merged",
|
|
183
|
+
action="store_true",
|
|
184
|
+
help="also remove patch-merged branches (deleted with -D)",
|
|
185
|
+
)
|
|
186
|
+
args = parser.parse_args(argv)
|
|
187
|
+
|
|
188
|
+
toplevel = run_git(["rev-parse", "--show-toplevel"]).stdout.strip()
|
|
189
|
+
if not toplevel:
|
|
190
|
+
print("not a git repository")
|
|
191
|
+
return 1
|
|
192
|
+
repo = Path(toplevel)
|
|
193
|
+
store = resolve_store(repo, args.store)
|
|
194
|
+
base = (
|
|
195
|
+
args.base
|
|
196
|
+
or run_git(["rev-parse", "--abbrev-ref", "HEAD"], cwd=repo).stdout.strip()
|
|
197
|
+
)
|
|
198
|
+
if not base:
|
|
199
|
+
print("could not determine integration branch; pass --base")
|
|
200
|
+
return 1
|
|
201
|
+
if store is None:
|
|
202
|
+
print("warning: task-agent store not found; task status unavailable")
|
|
203
|
+
else:
|
|
204
|
+
print(f"store: {store}")
|
|
205
|
+
|
|
206
|
+
rows = sorted(classify(repo, store, base), key=lambda r: (r.verdict, r.slug))
|
|
207
|
+
for row in rows:
|
|
208
|
+
if row.dirty is None:
|
|
209
|
+
dirty = "missing"
|
|
210
|
+
elif row.dirty:
|
|
211
|
+
dirty = f"{row.dirty} changed"
|
|
212
|
+
else:
|
|
213
|
+
dirty = "clean"
|
|
214
|
+
print(
|
|
215
|
+
f"{row.verdict:<26} merged={row.merged:<8} "
|
|
216
|
+
f"task={row.status:<10} {dirty:<12} {row.branch}"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
removable = [r for r in rows if r.verdict == "REMOVE"]
|
|
220
|
+
patchable = [r for r in rows if r.verdict == "REMOVE (patch-merged, -D)"]
|
|
221
|
+
print(
|
|
222
|
+
f"\n{len(removable)} removable, {len(patchable)} patch-merged, "
|
|
223
|
+
f"{len(rows) - len(removable) - len(patchable)} to keep/review"
|
|
224
|
+
)
|
|
225
|
+
if patchable and not args.allow_patch_merged:
|
|
226
|
+
print("patch-merged branches need --allow-patch-merged (or manual -D)")
|
|
227
|
+
|
|
228
|
+
if args.remove:
|
|
229
|
+
ok = True
|
|
230
|
+
for row in removable:
|
|
231
|
+
ok = remove_row(repo, row, force_branch=False) and ok
|
|
232
|
+
if args.allow_patch_merged:
|
|
233
|
+
for row in patchable:
|
|
234
|
+
ok = remove_row(repo, row, force_branch=True) and ok
|
|
235
|
+
return 0 if ok else 1
|
|
236
|
+
return 0
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
if __name__ == "__main__":
|
|
240
|
+
sys.exit(main())
|
|
@@ -7,7 +7,7 @@ from rich.panel import Panel
|
|
|
7
7
|
from rich.table import Table
|
|
8
8
|
import sys
|
|
9
9
|
|
|
10
|
-
from
|
|
10
|
+
from verkit.theme import DEFAULT as theme # type: ignore[import-untyped]
|
|
11
11
|
|
|
12
12
|
# Removed invalid NO_BORDER definition
|
|
13
13
|
import argparse
|
|
@@ -949,7 +949,7 @@ def cmd_history(console: Console, manager: TaskAgent, limit: int = 20):
|
|
|
949
949
|
str(absolute_idx + 1), date_str, f"{prefix}{slug}", style=style
|
|
950
950
|
)
|
|
951
951
|
|
|
952
|
-
help_text = "[dim]v/l: view | c: copy slug | q: exit[/dim]"
|
|
952
|
+
help_text = "[dim]v/l: view | c/y: copy slug | q: exit[/dim]"
|
|
953
953
|
|
|
954
954
|
live.update(
|
|
955
955
|
Panel(table, subtitle=help_text, box=theme.panel_box), refresh=True
|
|
@@ -976,7 +976,7 @@ def cmd_history(console: Console, manager: TaskAgent, limit: int = 20):
|
|
|
976
976
|
except Exception:
|
|
977
977
|
pass
|
|
978
978
|
live.start()
|
|
979
|
-
elif key in ["c"]:
|
|
979
|
+
elif key in ["c", "y"]:
|
|
980
980
|
# Copy slug to clipboard
|
|
981
981
|
_, slug = all_completed[cursor]
|
|
982
982
|
try:
|
|
@@ -1843,6 +1843,44 @@ def cmd_done(
|
|
|
1843
1843
|
f"[yellow]Run 'git branch -D {branch_name}' to force delete it.[/yellow]"
|
|
1844
1844
|
)
|
|
1845
1845
|
|
|
1846
|
+
# After standard cleanup, check for any orphaned loosely matching branches or worktrees
|
|
1847
|
+
if not is_cwd_inside_worktree:
|
|
1848
|
+
try:
|
|
1849
|
+
loose_wts = subprocess.run(
|
|
1850
|
+
["git", "worktree", "list", "--porcelain"],
|
|
1851
|
+
capture_output=True,
|
|
1852
|
+
text=True,
|
|
1853
|
+
).stdout
|
|
1854
|
+
for line in loose_wts.splitlines():
|
|
1855
|
+
if line.startswith("worktree "):
|
|
1856
|
+
wt_path = line[len("worktree ") :].strip()
|
|
1857
|
+
if slug in Path(wt_path).name and (
|
|
1858
|
+
not worktree_path or str(worktree_path) != wt_path
|
|
1859
|
+
):
|
|
1860
|
+
console.print(
|
|
1861
|
+
f"[yellow]Found loosely matching orphaned worktree: {wt_path}[/yellow]"
|
|
1862
|
+
)
|
|
1863
|
+
console.print(
|
|
1864
|
+
f" [yellow]Consider removing it: git worktree remove {wt_path}[/yellow]"
|
|
1865
|
+
)
|
|
1866
|
+
|
|
1867
|
+
loose_brs = subprocess.run(
|
|
1868
|
+
["git", "branch", "--list", "--format=%(refname:short)"],
|
|
1869
|
+
capture_output=True,
|
|
1870
|
+
text=True,
|
|
1871
|
+
).stdout
|
|
1872
|
+
for br in loose_brs.splitlines():
|
|
1873
|
+
br = br.strip()
|
|
1874
|
+
if slug in br and br != f"issue/{slug}":
|
|
1875
|
+
console.print(
|
|
1876
|
+
f"[yellow]Found loosely matching orphaned branch: {br}[/yellow]"
|
|
1877
|
+
)
|
|
1878
|
+
console.print(
|
|
1879
|
+
f" [yellow]Consider deleting it: git branch -D {br}[/yellow]"
|
|
1880
|
+
)
|
|
1881
|
+
except Exception:
|
|
1882
|
+
pass
|
|
1883
|
+
|
|
1846
1884
|
|
|
1847
1885
|
def cmd_push(console: Console, manager: TaskAgent):
|
|
1848
1886
|
"""Push the mission repository."""
|
|
@@ -3423,7 +3461,7 @@ def cmd_list(
|
|
|
3423
3461
|
)
|
|
3424
3462
|
table.add_column("Pri", justify="right", style="cyan", width=3)
|
|
3425
3463
|
table.add_column("Date", style="dim", width=5)
|
|
3426
|
-
table.add_column("Status", width=
|
|
3464
|
+
table.add_column("Status", width=7)
|
|
3427
3465
|
table.add_column("Blocked", style="yellow", width=8)
|
|
3428
3466
|
table.add_column("Slug")
|
|
3429
3467
|
|
|
@@ -3446,8 +3484,8 @@ def cmd_list(
|
|
|
3446
3484
|
prefix_plain = "└─ " if depth > 0 else ""
|
|
3447
3485
|
prefix = "[dim cyan]└─[/dim cyan] " if depth > 0 else ""
|
|
3448
3486
|
|
|
3449
|
-
# Fixed columns width: Pri (~4) + Date (5) + Status (
|
|
3450
|
-
available_width = max(10, term_width -
|
|
3487
|
+
# Fixed columns width: Pri (~4) + Date (5) + Status (7) + Blocked (8) + borders/padding (~12)
|
|
3488
|
+
available_width = max(10, term_width - 36)
|
|
3451
3489
|
prefix_len = len(indent) + len(prefix_plain)
|
|
3452
3490
|
max_slug_len = max(5, available_width - prefix_len)
|
|
3453
3491
|
|
|
@@ -5166,6 +5204,77 @@ def cmd_worktree(console: Console, manager: TaskAgent, args):
|
|
|
5166
5204
|
except subprocess.CalledProcessError as e:
|
|
5167
5205
|
console.print(f"[red]Error pruning worktrees: {e.stderr}[/red]")
|
|
5168
5206
|
|
|
5207
|
+
elif args.action == "cleanup":
|
|
5208
|
+
console.print("[blue]Finding orphaned task worktrees/branches...[/blue]")
|
|
5209
|
+
try:
|
|
5210
|
+
active_issues = {i.slug for i in manager.load_mission()}
|
|
5211
|
+
|
|
5212
|
+
# 1. Clean up orphaned .gwt/ worktrees
|
|
5213
|
+
wt_res = subprocess.run(
|
|
5214
|
+
["git", "worktree", "list", "--porcelain"],
|
|
5215
|
+
capture_output=True,
|
|
5216
|
+
text=True,
|
|
5217
|
+
check=True,
|
|
5218
|
+
)
|
|
5219
|
+
worktrees = []
|
|
5220
|
+
for line in wt_res.stdout.splitlines():
|
|
5221
|
+
if line.startswith("worktree "):
|
|
5222
|
+
worktrees.append(Path(line[len("worktree ") :].strip()))
|
|
5223
|
+
|
|
5224
|
+
cwd_gwt = Path.cwd().resolve() / ".gwt"
|
|
5225
|
+
for wt in worktrees:
|
|
5226
|
+
try:
|
|
5227
|
+
resolved_wt = wt.resolve()
|
|
5228
|
+
if resolved_wt.is_relative_to(cwd_gwt):
|
|
5229
|
+
slug = resolved_wt.name
|
|
5230
|
+
if slug not in active_issues:
|
|
5231
|
+
console.print(
|
|
5232
|
+
f" [yellow]Found orphaned worktree: {wt}[/yellow]"
|
|
5233
|
+
)
|
|
5234
|
+
subprocess.run(
|
|
5235
|
+
["git", "worktree", "remove", "--force", str(wt)],
|
|
5236
|
+
capture_output=True,
|
|
5237
|
+
text=True,
|
|
5238
|
+
check=True,
|
|
5239
|
+
)
|
|
5240
|
+
console.print(" [green]Removed[/green]")
|
|
5241
|
+
except ValueError:
|
|
5242
|
+
pass # Not relative to .gwt
|
|
5243
|
+
except subprocess.CalledProcessError as e:
|
|
5244
|
+
console.print(
|
|
5245
|
+
f" [red]Failed to remove: {e.stderr.strip() if hasattr(e, 'stderr') and e.stderr else str(e)}[/red]"
|
|
5246
|
+
)
|
|
5247
|
+
|
|
5248
|
+
# 2. Clean up orphaned issue/ branches
|
|
5249
|
+
br_res = subprocess.run(
|
|
5250
|
+
["git", "branch", "--list", "--format=%(refname:short)"],
|
|
5251
|
+
capture_output=True,
|
|
5252
|
+
text=True,
|
|
5253
|
+
check=True,
|
|
5254
|
+
)
|
|
5255
|
+
for br in br_res.stdout.splitlines():
|
|
5256
|
+
br = br.strip()
|
|
5257
|
+
if br.startswith("issue/"):
|
|
5258
|
+
slug = br[len("issue/") :]
|
|
5259
|
+
if slug not in active_issues:
|
|
5260
|
+
console.print(f" [yellow]Found orphaned branch: {br}[/yellow]")
|
|
5261
|
+
try:
|
|
5262
|
+
subprocess.run(
|
|
5263
|
+
["git", "branch", "-D", br],
|
|
5264
|
+
capture_output=True,
|
|
5265
|
+
text=True,
|
|
5266
|
+
check=True,
|
|
5267
|
+
)
|
|
5268
|
+
console.print(" [green]Deleted[/green]")
|
|
5269
|
+
except subprocess.CalledProcessError as e:
|
|
5270
|
+
console.print(
|
|
5271
|
+
f" [red]Failed to delete: {e.stderr.strip() if hasattr(e, 'stderr') and e.stderr else str(e)}[/red]"
|
|
5272
|
+
)
|
|
5273
|
+
|
|
5274
|
+
console.print("[green]Cleanup complete.[/green]")
|
|
5275
|
+
except Exception as e:
|
|
5276
|
+
console.print(f"[red]Error during cleanup: {e}[/red]")
|
|
5277
|
+
|
|
5169
5278
|
|
|
5170
5279
|
def cmd_github(console: Console, manager: TaskAgent, args):
|
|
5171
5280
|
"""Sync with GitHub Issues."""
|
|
@@ -5586,6 +5695,7 @@ def cmd_triage(
|
|
|
5586
5695
|
indexed_issues = [(issue, depth) for issue, depth in hierarchy]
|
|
5587
5696
|
cursor = 0
|
|
5588
5697
|
last_key = ""
|
|
5698
|
+
status_message: Optional[str] = None
|
|
5589
5699
|
|
|
5590
5700
|
with Live(auto_refresh=False, console=console, screen=True) as live:
|
|
5591
5701
|
while True:
|
|
@@ -5672,10 +5782,24 @@ def cmd_triage(
|
|
|
5672
5782
|
style=style,
|
|
5673
5783
|
)
|
|
5674
5784
|
|
|
5675
|
-
|
|
5785
|
+
if show_completed:
|
|
5786
|
+
if term_width >= 100:
|
|
5787
|
+
help_text = "[dim]j/k: move | r: restore | y/c: copy | v/e: view/edit | C: active-list | /: search | q: exit[/dim]"
|
|
5788
|
+
else:
|
|
5789
|
+
help_text = "[dim]j/k: move | r: restore | y/c: copy | v: view | C: back | q: exit[/dim]"
|
|
5790
|
+
else:
|
|
5791
|
+
if term_width >= 130:
|
|
5792
|
+
help_text = "[dim]j/k: move | ctrl+k/j: prio | A: active | y/c: copy | D: done | p/d: prom/dem | v/e: view/edit | a: add | l/h: link | /: search | C: done-list | q: exit[/dim]"
|
|
5793
|
+
elif term_width >= 100:
|
|
5794
|
+
help_text = "[dim]j/k: move | ctrl+k/j: prio | A: active | y/c: copy | D: done | v/e: view/edit | a: add | q: exit[/dim]"
|
|
5795
|
+
else:
|
|
5796
|
+
help_text = "[dim]j/k: move | ctrl+k/j: prio | A: active | y/c: copy | D: done | v: view | q: exit[/dim]"
|
|
5676
5797
|
|
|
5677
5798
|
live.update(
|
|
5678
|
-
Panel(
|
|
5799
|
+
Panel(
|
|
5800
|
+
table, title=status_message, subtitle=help_text, box=theme.panel_box
|
|
5801
|
+
),
|
|
5802
|
+
refresh=True,
|
|
5679
5803
|
)
|
|
5680
5804
|
|
|
5681
5805
|
# Input
|
|
@@ -5688,36 +5812,47 @@ def cmd_triage(
|
|
|
5688
5812
|
elif key == "\r": # enter (return)
|
|
5689
5813
|
break
|
|
5690
5814
|
elif key == "G": # shift+g: jump to bottom
|
|
5815
|
+
status_message = None
|
|
5691
5816
|
cursor = len(indexed_issues) - 1
|
|
5692
5817
|
elif key == "g": # gg: jump to top
|
|
5818
|
+
status_message = None
|
|
5693
5819
|
if prev_key == "g":
|
|
5694
5820
|
cursor = 0
|
|
5695
5821
|
else:
|
|
5696
5822
|
last_key = "g"
|
|
5697
5823
|
continue
|
|
5698
5824
|
elif key in ["k", "\x1b[A"]: # up
|
|
5825
|
+
status_message = None
|
|
5699
5826
|
cursor = max(0, cursor - 1)
|
|
5700
5827
|
elif key in ["j", "\x1b[B"]: # down
|
|
5828
|
+
status_message = None
|
|
5701
5829
|
cursor = min(len(indexed_issues) - 1, cursor + 1)
|
|
5702
5830
|
elif key == "/":
|
|
5831
|
+
status_message = None
|
|
5703
5832
|
live.stop()
|
|
5704
5833
|
search_query = questionary.text("Search slug:").ask()
|
|
5705
5834
|
issues = get_display_issues(search_query, show_completed)
|
|
5706
5835
|
indexed_issues = build_hierarchy(issues)
|
|
5707
5836
|
cursor = 0
|
|
5708
5837
|
live.start()
|
|
5709
|
-
elif key
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5838
|
+
elif key in ["y", "c"]:
|
|
5839
|
+
if indexed_issues and 0 <= cursor < len(indexed_issues):
|
|
5840
|
+
slug = indexed_issues[cursor][0].slug
|
|
5841
|
+
try:
|
|
5842
|
+
pyperclip.copy(slug)
|
|
5843
|
+
status_message = (
|
|
5844
|
+
f"[bold green]Copied slug to clipboard: {slug}[/bold green]"
|
|
5845
|
+
)
|
|
5846
|
+
except Exception as e:
|
|
5847
|
+
status_message = (
|
|
5848
|
+
f"[yellow]Failed to copy to clipboard: {e}[/yellow]"
|
|
5849
|
+
)
|
|
5850
|
+
elif key == "C":
|
|
5717
5851
|
show_completed = not show_completed
|
|
5718
5852
|
issues = get_display_issues(search_query, show_completed)
|
|
5719
5853
|
indexed_issues = build_hierarchy(issues)
|
|
5720
5854
|
cursor = 0
|
|
5855
|
+
status_message = f"[dim]Showing {'completed' if show_completed else 'active/pending'} tasks[/dim]"
|
|
5721
5856
|
|
|
5722
5857
|
elif key == "v":
|
|
5723
5858
|
live.stop()
|
|
@@ -5829,17 +5964,28 @@ def cmd_triage(
|
|
|
5829
5964
|
cursor = min(len(indexed_issues) - 1, cursor)
|
|
5830
5965
|
except Exception:
|
|
5831
5966
|
pass
|
|
5832
|
-
elif key == "A"
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5967
|
+
elif key == "A": # active
|
|
5968
|
+
if show_completed:
|
|
5969
|
+
status_message = "[yellow]Completed tasks cannot be marked active directly (press 'r' to restore)[/yellow]"
|
|
5970
|
+
elif indexed_issues and 0 <= cursor < len(indexed_issues):
|
|
5971
|
+
issue = indexed_issues[cursor][0]
|
|
5972
|
+
if issue.status == "active":
|
|
5973
|
+
status_message = (
|
|
5974
|
+
f"[yellow]Task '{issue.slug}' is already active[/yellow]"
|
|
5975
|
+
)
|
|
5976
|
+
else:
|
|
5977
|
+
try:
|
|
5978
|
+
manager.move_to_active(issue.slug)
|
|
5979
|
+
issues = get_display_issues(search_query, show_completed)
|
|
5980
|
+
indexed_issues = build_hierarchy(issues)
|
|
5981
|
+
new_idx = find_slug_index(issue.slug, indexed_issues)
|
|
5982
|
+
if new_idx is not None:
|
|
5983
|
+
cursor = new_idx
|
|
5984
|
+
status_message = f"[bold green]Task '{issue.slug}' marked as active[/bold green]"
|
|
5985
|
+
except Exception as e:
|
|
5986
|
+
status_message = (
|
|
5987
|
+
f"[red]Failed to activate '{issue.slug}': {e}[/red]"
|
|
5988
|
+
)
|
|
5843
5989
|
elif key == "D" and not show_completed: # done
|
|
5844
5990
|
live.stop()
|
|
5845
5991
|
issue = indexed_issues[cursor][0]
|
|
@@ -6350,7 +6496,7 @@ def main():
|
|
|
6350
6496
|
worktree_parser.add_argument(
|
|
6351
6497
|
"action",
|
|
6352
6498
|
nargs="?",
|
|
6353
|
-
choices=["add", "list", "remove", "prune"],
|
|
6499
|
+
choices=["add", "list", "remove", "prune", "cleanup"],
|
|
6354
6500
|
help="Worktree action to perform (shows help if omitted)",
|
|
6355
6501
|
)
|
|
6356
6502
|
worktree_parser.add_argument(
|
|
@@ -6362,6 +6508,12 @@ def main():
|
|
|
6362
6508
|
worktree_parser.add_argument(
|
|
6363
6509
|
"--commit", action="store_true", help="Create worktree from specific commit SHA"
|
|
6364
6510
|
)
|
|
6511
|
+
worktree_parser.add_argument(
|
|
6512
|
+
"--days",
|
|
6513
|
+
type=int,
|
|
6514
|
+
default=30,
|
|
6515
|
+
help="Days back to check for completed tasks (for cleanup action)",
|
|
6516
|
+
)
|
|
6365
6517
|
worktree_parser.add_argument(
|
|
6366
6518
|
"--copy",
|
|
6367
6519
|
action="append",
|