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.
Files changed (157) hide show
  1. {task_agent-1.3.10 → task_agent-1.3.11}/.gitignore +2 -0
  2. {task_agent-1.3.10 → task_agent-1.3.11}/PKG-INFO +1 -1
  3. {task_agent-1.3.10 → task_agent-1.3.11}/pyproject.toml +1 -1
  4. task_agent-1.3.11/scripts/check_task_worktree_cleanup.py +240 -0
  5. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/cli.py +180 -28
  6. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_cli.py +103 -1
  7. {task_agent-1.3.10 → task_agent-1.3.11}/uv.lock +1 -1
  8. task_agent-1.3.10/src/taskagent/theme.py +0 -17
  9. {task_agent-1.3.10 → task_agent-1.3.11}/.agents/skills/next/SKILL.md +0 -0
  10. {task_agent-1.3.10 → task_agent-1.3.11}/.gemini/settings.json +0 -0
  11. {task_agent-1.3.10 → task_agent-1.3.11}/.github/workflows/ci.yml +0 -0
  12. {task_agent-1.3.10 → task_agent-1.3.11}/.github/workflows/publish.yml +0 -0
  13. {task_agent-1.3.10 → task_agent-1.3.11}/.mise.toml +0 -0
  14. {task_agent-1.3.10 → task_agent-1.3.11}/.pre-commit-config.yaml +0 -0
  15. {task_agent-1.3.10 → task_agent-1.3.11}/.python-version +0 -0
  16. {task_agent-1.3.10 → task_agent-1.3.11}/.ta/agents/agy-cli/meta.toml +0 -0
  17. {task_agent-1.3.10 → task_agent-1.3.11}/.ta/agents/antigravity/meta.toml +0 -0
  18. {task_agent-1.3.10 → task_agent-1.3.11}/.ta/agents/gh/meta.toml +0 -0
  19. {task_agent-1.3.10 → task_agent-1.3.11}/.ta/agents/minimal/meta.toml +0 -0
  20. {task_agent-1.3.10 → task_agent-1.3.11}/.ta/agents/uat-aws/meta.toml +0 -0
  21. {task_agent-1.3.10 → task_agent-1.3.11}/.ta/worker +0 -0
  22. {task_agent-1.3.10 → task_agent-1.3.11}/.ta-config.json +0 -0
  23. {task_agent-1.3.10 → task_agent-1.3.11}/CLAUDE.md +0 -0
  24. {task_agent-1.3.10 → task_agent-1.3.11}/GEMINI.md +0 -0
  25. {task_agent-1.3.10 → task_agent-1.3.11}/Makefile +0 -0
  26. {task_agent-1.3.10 → task_agent-1.3.11}/README.md +0 -0
  27. {task_agent-1.3.10 → task_agent-1.3.11}/agent.env +0 -0
  28. {task_agent-1.3.10 → task_agent-1.3.11}/docs/README.md +0 -0
  29. {task_agent-1.3.10 → task_agent-1.3.11}/docs/STATION-MAP.md +0 -0
  30. {task_agent-1.3.10 → task_agent-1.3.11}/docs/architecture/README.md +0 -0
  31. {task_agent-1.3.10 → task_agent-1.3.11}/docs/chats/gemini-conversation-1773283034591.json +0 -0
  32. {task_agent-1.3.10 → task_agent-1.3.11}/docs/chats/project-init-to-v0.1.46.md +0 -0
  33. {task_agent-1.3.10 → task_agent-1.3.11}/docs/cli/README.md +0 -0
  34. {task_agent-1.3.10 → task_agent-1.3.11}/docs/development/README.md +0 -0
  35. {task_agent-1.3.10 → task_agent-1.3.11}/docs/task-agents.md +0 -0
  36. {task_agent-1.3.10 → task_agent-1.3.11}/docs/workflow/README.md +0 -0
  37. {task_agent-1.3.10 → task_agent-1.3.11}/docs/workflow/trivial_worker_policy.md +0 -0
  38. {task_agent-1.3.10 → task_agent-1.3.11}/opencode.json +0 -0
  39. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/.gemini-plugin/plugin.json +0 -0
  40. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/.mcp.json +0 -0
  41. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/README.md +0 -0
  42. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/hooks/hooks.json +0 -0
  43. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/hooks/session_start.sh +0 -0
  44. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/rules/task_agent.md +0 -0
  45. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/scripts/statusline.sh +0 -0
  46. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/skills/README.md +0 -0
  47. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/skills/agent-import/SKILL.md +0 -0
  48. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/skills/complete-task/SKILL.md +0 -0
  49. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/skills/mission-workflow/SKILL.md +0 -0
  50. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/skills/next/SKILL.md +0 -0
  51. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/antigravity/skills/next-task/SKILL.md +0 -0
  52. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
  53. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/claude-code/.mcp.json +0 -0
  54. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/claude-code/README.md +0 -0
  55. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/claude-code/hooks/hooks.json +0 -0
  56. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/claude-code/hooks/session_start.sh +0 -0
  57. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/claude-code/scripts/statusline.sh +0 -0
  58. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/claude-code/skills/agent-import/SKILL.md +0 -0
  59. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/claude-code/skills/complete-task/SKILL.md +0 -0
  60. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/claude-code/skills/mission-workflow/SKILL.md +0 -0
  61. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/claude-code/skills/next-task/SKILL.md +0 -0
  62. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/codex/.mcp.json +0 -0
  63. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/codex/README.md +0 -0
  64. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/copilot/.mcp.json +0 -0
  65. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/copilot/README.md +0 -0
  66. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/grok/.mcp.json +0 -0
  67. {task_agent-1.3.10 → task_agent-1.3.11}/plugins/grok/README.md +0 -0
  68. {task_agent-1.3.10 → task_agent-1.3.11}/scripts/clean_duplicates.py +0 -0
  69. {task_agent-1.3.10 → task_agent-1.3.11}/scripts/migrate_tasks.py +0 -0
  70. {task_agent-1.3.10 → task_agent-1.3.11}/scripts/recover_dates.py +0 -0
  71. {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/adk-worker/.env.example +0 -0
  72. {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/adk-worker/README.md +0 -0
  73. {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/adk-worker/instructions/manager.md +0 -0
  74. {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/adk-worker/instructions/validator.md +0 -0
  75. {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/adk-worker/instructions/worker.md +0 -0
  76. {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/adk-worker/profiles/default.json +0 -0
  77. {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/adk-worker/pyproject.toml +0 -0
  78. {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/adk-worker/uv.lock +0 -0
  79. {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/adk-worker/worker.py +0 -0
  80. {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/keet-bridge/README.md +0 -0
  81. {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/keet-bridge/bridge.js +0 -0
  82. {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/keet-bridge/package-lock.json +0 -0
  83. {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/keet-bridge/package.json +0 -0
  84. {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/keet-bridge/run.sh +0 -0
  85. {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/keet-bridge/test-e2e.js +0 -0
  86. {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/matrix-bridge/README.md +0 -0
  87. {task_agent-1.3.10 → task_agent-1.3.11}/sidecars/matrix-bridge/matrix_bridge.py +0 -0
  88. {task_agent-1.3.10 → task_agent-1.3.11}/skills/README.md +0 -0
  89. {task_agent-1.3.10 → task_agent-1.3.11}/skills/agent-import/SKILL.md +0 -0
  90. {task_agent-1.3.10 → task_agent-1.3.11}/skills/complete-task/SKILL.md +0 -0
  91. {task_agent-1.3.10 → task_agent-1.3.11}/skills/mission-workflow/SKILL.md +0 -0
  92. {task_agent-1.3.10 → task_agent-1.3.11}/skills/next/SKILL.md +0 -0
  93. {task_agent-1.3.10 → task_agent-1.3.11}/skills/next-task/SKILL.md +0 -0
  94. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/__init__.py +0 -0
  95. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/__main__.py +0 -0
  96. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/agent.py +0 -0
  97. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/agent_registry.py +0 -0
  98. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/audit.py +0 -0
  99. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/chat/__init__.py +0 -0
  100. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/chat/config.py +0 -0
  101. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/chat/discovery.py +0 -0
  102. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/chat/last_used.py +0 -0
  103. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/config.py +0 -0
  104. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/discovery.py +0 -0
  105. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/importers/__init__.py +0 -0
  106. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/importers/antigravity.py +0 -0
  107. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/importers/base.py +0 -0
  108. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/importers/claude_code.py +0 -0
  109. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/importers/generic.py +0 -0
  110. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/inbox.py +0 -0
  111. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/manager.py +0 -0
  112. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/mcp.py +0 -0
  113. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/models/__init__.py +0 -0
  114. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/models/issue.py +0 -0
  115. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/models/metric.py +0 -0
  116. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/perf.py +0 -0
  117. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/plugins/__init__.py +0 -0
  118. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/plugins/github.py +0 -0
  119. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/runner.py +0 -0
  120. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/store_registry.py +0 -0
  121. {task_agent-1.3.10 → task_agent-1.3.11}/src/taskagent/templates.py +0 -0
  122. {task_agent-1.3.10 → task_agent-1.3.11}/test-dir/opencode.json +0 -0
  123. {task_agent-1.3.10 → task_agent-1.3.11}/tests/conftest.py +0 -0
  124. {task_agent-1.3.10 → task_agent-1.3.11}/tests/e2e/test_agent_sandboxing.sh +0 -0
  125. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_agent.py +0 -0
  126. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_agent_registry.py +0 -0
  127. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_agy_cli_worker.py +0 -0
  128. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_atomic_mutations.py +0 -0
  129. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_auto_commit.py +0 -0
  130. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_chat_config.py +0 -0
  131. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_chat_discovery.py +0 -0
  132. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_chat_last_used.py +0 -0
  133. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_cli_windows.py +0 -0
  134. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_discovery.py +0 -0
  135. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_github_token.py +0 -0
  136. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_importers.py +0 -0
  137. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_inbox.py +0 -0
  138. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_inbox_watch.py +0 -0
  139. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_lease.py +0 -0
  140. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_manager.py +0 -0
  141. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_manager_git.py +0 -0
  142. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_mcp.py +0 -0
  143. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_metrics.py +0 -0
  144. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_migration.py +0 -0
  145. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_perf.py +0 -0
  146. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_plugin_antigravity.py +0 -0
  147. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_plugin_claude.py +0 -0
  148. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_project_host_root.py +0 -0
  149. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_rebind.py +0 -0
  150. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_remote_attach.py +0 -0
  151. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_remote_create.py +0 -0
  152. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_repo_resolve.py +0 -0
  153. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_runner.py +0 -0
  154. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_store_registry.py +0 -0
  155. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_store_symlink.py +0 -0
  156. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_templates.py +0 -0
  157. {task_agent-1.3.10 → task_agent-1.3.11}/tests/test_yazi.py +0 -0
@@ -20,3 +20,5 @@ docks/tasks
20
20
 
21
21
  # task-agent: human-facing store symlink
22
22
  docs/tasks
23
+ .map/
24
+ .ma/prompt/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: task-agent
3
- Version: 1.3.10
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "task-agent"
3
- version = "1.3.10"
3
+ version = "1.3.11"
4
4
  description = "A prioritized, file-based task queue for autonomous agentic workers"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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 taskagent.theme import DEFAULT as theme
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=6)
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 (6) + Blocked (8) + borders/padding (~12)
3450
- available_width = max(10, term_width - 35)
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
- help_text = "[dim]j/k: move | G/gg: bottom/top | ctrl+k/j: prio | p: prom | d: dem | v: view | e: edit | a: add | D: done | A: active | l/h: link/unlink | /: search | y: copy | q: exit[/dim]"
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(table, subtitle=help_text, box=theme.panel_box), refresh=True
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 == "y":
5710
- slug = indexed_issues[cursor][0].slug
5711
- pyperclip.copy(slug)
5712
- console.print(
5713
- f"[bold green]Copied slug to clipboard: {slug}[/bold green]"
5714
- )
5715
- questionary.press_any_key_to_continue().ask()
5716
- elif key == "c":
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" and not show_completed: # active
5833
- issue = indexed_issues[cursor][0]
5834
- try:
5835
- manager.move_to_active(issue.slug)
5836
- issues = get_display_issues(search_query, show_completed)
5837
- indexed_issues = build_hierarchy(issues)
5838
- new_idx = find_slug_index(issue.slug, indexed_issues)
5839
- if new_idx is not None:
5840
- cursor = new_idx
5841
- except Exception:
5842
- pass
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",