task-agent 0.1.8__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.
- task_agent-0.1.8.dist-info/METADATA +53 -0
- task_agent-0.1.8.dist-info/RECORD +9 -0
- task_agent-0.1.8.dist-info/WHEEL +4 -0
- task_agent-0.1.8.dist-info/entry_points.txt +2 -0
- taskagent/__init__.py +0 -0
- taskagent/__main__.py +4 -0
- taskagent/cli.py +335 -0
- taskagent/models/__init__.py +0 -0
- taskagent/models/issue.py +12 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: task-agent
|
|
3
|
+
Version: 0.1.8
|
|
4
|
+
Summary: A prioritized, file-based task queue for autonomous agentic workers
|
|
5
|
+
Author-email: Mark Stouffer <1802850+InTEGr8or@users.noreply.github.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: pydantic>=2.10.6
|
|
9
|
+
Requires-Dist: rich>=13.9.4
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# Task Agent 🤖
|
|
13
|
+
|
|
14
|
+
A prioritized, file-based task queue for autonomous agentic workers. This system uses a "Mission Control" approach to manage multiple agents working on git-tracked improvements across various branches and worktrees.
|
|
15
|
+
|
|
16
|
+
## 📂 System Architecture
|
|
17
|
+
|
|
18
|
+
The project follows a specific folder structure to manage the lifecycle of an improvement:
|
|
19
|
+
|
|
20
|
+
- `docs/issues/`: The core queue.
|
|
21
|
+
- `mission.usv`: A prioritized list of issues using Unit Separator Value (`\x1f`) format. This serves as the source of truth for task priority.
|
|
22
|
+
- `datapackage.json`: Frictionless Data metadata for the `mission.usv` schema.
|
|
23
|
+
- `pending/`: New issues awaiting triage or assignment.
|
|
24
|
+
- `draft/`: Issues currently being refined or planned.
|
|
25
|
+
- `active/`: Issues actively being worked on by an agent.
|
|
26
|
+
- `completed/{year}/`: Successfully implemented and verified improvements.
|
|
27
|
+
- `.gwt/`: Git Worktree directory where active branches are checked out for isolated agent execution.
|
|
28
|
+
|
|
29
|
+
## 🛠️ Tooling: `ta` (Task Agent CLI)
|
|
30
|
+
|
|
31
|
+
The `ta` tool automates the transition of issues through the queue and manages the underlying git infrastructure.
|
|
32
|
+
|
|
33
|
+
### Commands
|
|
34
|
+
|
|
35
|
+
| Command | Action |
|
|
36
|
+
| :--- | :--- |
|
|
37
|
+
| `ta next` | Displays the top prioritized issue from `mission.usv`. |
|
|
38
|
+
| `ta new` | Creates a new issue file and adds it to the queue. |
|
|
39
|
+
| `ta done` | Moves issue to `completed/`, and removes it from the queue. |
|
|
40
|
+
| `ta start <slug>` | (Planned) Moves issue to `active/`, creates a git branch, and sets up a `.gwt/` worktree. |
|
|
41
|
+
| `ta run <slug>` | (Planned) Invokes Gemini CLI in **headless mode** within the issue's specific worktree. |
|
|
42
|
+
|
|
43
|
+
## 🚀 Workflow
|
|
44
|
+
|
|
45
|
+
1. **Prioritize**: Use `ta new -t "Title"` to add a task.
|
|
46
|
+
2. **Review**: Run `ta next` to see what is currently at the top of the queue.
|
|
47
|
+
3. **Dispatch**: (Planned) Run `ta start <slug>` to prepare the workspace.
|
|
48
|
+
4. **Execute**: (Planned) The agent (Gemini CLI) processes the task in its isolated worktree via `ta run <slug>`.
|
|
49
|
+
5. **Finalize**: Once verified, `ta done` moves the task to the finished state.
|
|
50
|
+
|
|
51
|
+
## 🤖 Gemini CLI Integration
|
|
52
|
+
|
|
53
|
+
By utilizing the Gemini CLI's headless mode, this system can orchestrate multiple agents simultaneously without requiring manual terminal management, significantly improving portability and scalability between Windows and WSL environments.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
taskagent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
taskagent/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
|
|
3
|
+
taskagent/cli.py,sha256=fk7eAR0NncXwXqai8S5DLK9XGM6rWL23q2qxV0pltw4,11810
|
|
4
|
+
taskagent/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
taskagent/models/issue.py,sha256=Zud_FNJhmYqQxByMS7tu2lsoQYZkLHTpotohUNW5hgk,250
|
|
6
|
+
task_agent-0.1.8.dist-info/METADATA,sha256=YzQhRHT2mwaw5FF1HDPCQmWLfEqSOYCJN26zhPaHFcU,2671
|
|
7
|
+
task_agent-0.1.8.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
8
|
+
task_agent-0.1.8.dist-info/entry_points.txt,sha256=8lLYabXhYFb2kO0JgfdlgMS9_9WugxrUdCso_vnE81c,42
|
|
9
|
+
task_agent-0.1.8.dist-info/RECORD,,
|
taskagent/__init__.py
ADDED
|
File without changes
|
taskagent/__main__.py
ADDED
taskagent/cli.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.markdown import Markdown
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
import sys
|
|
8
|
+
import argparse
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
|
|
13
|
+
from taskagent.models.issue import Issue, USV_DELIM
|
|
14
|
+
|
|
15
|
+
# Constants
|
|
16
|
+
ISSUES_ROOT = Path("docs/issues")
|
|
17
|
+
MISSION_PATH = ISSUES_ROOT / "mission.usv"
|
|
18
|
+
|
|
19
|
+
def slugify(text: str) -> str:
|
|
20
|
+
"""Convert text to a slug."""
|
|
21
|
+
text = text.lower()
|
|
22
|
+
text = re.sub(r'[^\w\s-]', '', text)
|
|
23
|
+
text = re.sub(r'[\s_-]+', '-', text)
|
|
24
|
+
return text.strip('-')
|
|
25
|
+
|
|
26
|
+
def load_mission() -> List[Issue]:
|
|
27
|
+
if not MISSION_PATH.exists():
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
issues = []
|
|
31
|
+
with MISSION_PATH.open("r", encoding="utf-8") as f:
|
|
32
|
+
for line in f:
|
|
33
|
+
line = line.strip()
|
|
34
|
+
if not line:
|
|
35
|
+
continue
|
|
36
|
+
parts = line.split(USV_DELIM)
|
|
37
|
+
if len(parts) >= 3:
|
|
38
|
+
try:
|
|
39
|
+
issues.append(Issue(
|
|
40
|
+
slug=parts[0],
|
|
41
|
+
priority=int(parts[1]),
|
|
42
|
+
status=parts[2]
|
|
43
|
+
))
|
|
44
|
+
except (ValueError, IndexError):
|
|
45
|
+
continue
|
|
46
|
+
return issues
|
|
47
|
+
|
|
48
|
+
def save_mission(issues: List[Issue]):
|
|
49
|
+
"""Save the list of issues back to mission.usv."""
|
|
50
|
+
with MISSION_PATH.open("w", encoding="utf-8", newline='\r\n') as f:
|
|
51
|
+
for issue in issues:
|
|
52
|
+
f.write(issue.to_usv() + "\r\n")
|
|
53
|
+
|
|
54
|
+
def find_issue_file(slug: str) -> Optional[Path]:
|
|
55
|
+
"""Find the issue markdown file by slug in docs/issues/ excluding completed/."""
|
|
56
|
+
search_dirs = [d for d in ISSUES_ROOT.iterdir() if d.is_dir() and d.name != "completed"]
|
|
57
|
+
|
|
58
|
+
for directory in search_dirs:
|
|
59
|
+
issue_file = directory / f"{slug}.md"
|
|
60
|
+
if issue_file.exists():
|
|
61
|
+
return issue_file
|
|
62
|
+
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def get_git_commit() -> str:
|
|
66
|
+
"""Get the short git commit hash."""
|
|
67
|
+
try:
|
|
68
|
+
return subprocess.check_output(
|
|
69
|
+
["git", "rev-parse", "--short", "HEAD"],
|
|
70
|
+
stderr=subprocess.DEVNULL,
|
|
71
|
+
text=True
|
|
72
|
+
).strip()
|
|
73
|
+
except subprocess.CalledProcessError:
|
|
74
|
+
return "unknown"
|
|
75
|
+
|
|
76
|
+
def get_current_version() -> str:
|
|
77
|
+
"""Read the current version from pyproject.toml."""
|
|
78
|
+
try:
|
|
79
|
+
with open("pyproject.toml", "r") as f:
|
|
80
|
+
content = f.read()
|
|
81
|
+
match = re.search(r'version = "(.*?)"', content)
|
|
82
|
+
return match.group(1) if match else "unknown"
|
|
83
|
+
except Exception:
|
|
84
|
+
return "unknown"
|
|
85
|
+
|
|
86
|
+
def cmd_next(console: Console):
|
|
87
|
+
"""Show the top issue."""
|
|
88
|
+
issues = load_mission()
|
|
89
|
+
if not issues:
|
|
90
|
+
console.print("[yellow]No issues found in mission.usv[/yellow]")
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
next_issue = issues[0]
|
|
94
|
+
issue_file = find_issue_file(next_issue.slug)
|
|
95
|
+
|
|
96
|
+
if not issue_file:
|
|
97
|
+
console.print(f"[red]Issue file not found for slug: {next_issue.slug}[/red]")
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
|
|
100
|
+
with issue_file.open("r", encoding="utf-8") as f:
|
|
101
|
+
content = f.read()
|
|
102
|
+
|
|
103
|
+
console.print(Panel(
|
|
104
|
+
f"[bold blue]NEXT ISSUE:[/bold blue] [cyan]{next_issue.slug}[/cyan]\n"
|
|
105
|
+
f"[bold blue]PRIORITY:[/bold blue] {next_issue.priority} | "
|
|
106
|
+
f"[bold blue]STATUS:[/bold blue] {next_issue.status}\n"
|
|
107
|
+
f"[bold blue]FILE:[/bold blue] {issue_file}",
|
|
108
|
+
title="Task Agent",
|
|
109
|
+
expand=False
|
|
110
|
+
))
|
|
111
|
+
|
|
112
|
+
md = Markdown(content)
|
|
113
|
+
console.print(md)
|
|
114
|
+
|
|
115
|
+
def cmd_done(console: Console, slug: Optional[str] = None):
|
|
116
|
+
"""Mark an issue as done."""
|
|
117
|
+
issues = load_mission()
|
|
118
|
+
if not issues:
|
|
119
|
+
console.print("[yellow]No issues found in mission.usv[/yellow]")
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
if slug is None:
|
|
123
|
+
target_issue: Optional[Issue] = issues[0] if issues else None
|
|
124
|
+
else:
|
|
125
|
+
target_issue = next((i for i in issues if i.slug == slug), None)
|
|
126
|
+
|
|
127
|
+
if not target_issue:
|
|
128
|
+
if slug:
|
|
129
|
+
console.print(f"[red]Issue with slug '{slug}' not found in mission.usv[/red]")
|
|
130
|
+
sys.exit(1)
|
|
131
|
+
|
|
132
|
+
issue_file = find_issue_file(target_issue.slug)
|
|
133
|
+
if not issue_file:
|
|
134
|
+
console.print(f"[red]Issue file not found for slug: {target_issue.slug}[/red]")
|
|
135
|
+
sys.exit(1)
|
|
136
|
+
|
|
137
|
+
# Get git commit hash
|
|
138
|
+
commit_hash = get_git_commit()
|
|
139
|
+
|
|
140
|
+
# Move to completed/{year}/
|
|
141
|
+
year = datetime.now().year
|
|
142
|
+
completed_dir = ISSUES_ROOT / "completed" / str(year)
|
|
143
|
+
completed_dir.mkdir(parents=True, exist_ok=True)
|
|
144
|
+
|
|
145
|
+
dest_path = completed_dir / f"{target_issue.slug}.md"
|
|
146
|
+
|
|
147
|
+
console.print(f"[green]Moving {issue_file} to {dest_path}...[/green]")
|
|
148
|
+
|
|
149
|
+
# Read, append commit hash, and write to destination
|
|
150
|
+
with issue_file.open("r", encoding="utf-8") as f:
|
|
151
|
+
content = f.read()
|
|
152
|
+
|
|
153
|
+
if not content.endswith("\n"):
|
|
154
|
+
content += "\n"
|
|
155
|
+
content += f"\n---\n**Completed in commit:** `{commit_hash}`\n"
|
|
156
|
+
|
|
157
|
+
with dest_path.open("w", encoding="utf-8") as f:
|
|
158
|
+
f.write(content)
|
|
159
|
+
|
|
160
|
+
# Remove original file
|
|
161
|
+
issue_file.unlink()
|
|
162
|
+
|
|
163
|
+
new_issues = [i for i in issues if i.slug != target_issue.slug]
|
|
164
|
+
save_mission(new_issues)
|
|
165
|
+
console.print(f"[bold green]Issue '{target_issue.slug}' marked as done and removed from mission.usv[/bold green]")
|
|
166
|
+
|
|
167
|
+
# Auto-promote patch version
|
|
168
|
+
console.print("[blue]Auto-promoting patch version...[/blue]")
|
|
169
|
+
try:
|
|
170
|
+
# We need to make sure the repo is clean for bump-my-version if it's configured to commit
|
|
171
|
+
# But here we just want it to update the file.
|
|
172
|
+
# Actually, we should probably just use the cmd_version logic.
|
|
173
|
+
cmd_version(console, promote="patch")
|
|
174
|
+
except Exception as e:
|
|
175
|
+
console.print(f"[yellow]Warning: Could not auto-promote version: {e}[/yellow]")
|
|
176
|
+
|
|
177
|
+
def cmd_new(console: Console, title: str, body: str, draft: bool):
|
|
178
|
+
"""Create a new issue."""
|
|
179
|
+
slug = slugify(title)
|
|
180
|
+
status = "draft" if draft else "pending"
|
|
181
|
+
target_dir = ISSUES_ROOT / status
|
|
182
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
183
|
+
|
|
184
|
+
issue_file = target_dir / f"{slug}.md"
|
|
185
|
+
if issue_file.exists():
|
|
186
|
+
console.print(f"[red]Error: Issue file already exists: {issue_file}[/red]")
|
|
187
|
+
sys.exit(1)
|
|
188
|
+
|
|
189
|
+
# Write the markdown file
|
|
190
|
+
with issue_file.open("w", encoding="utf-8") as f:
|
|
191
|
+
f.write(f"# {title}\n\n{body}\n")
|
|
192
|
+
|
|
193
|
+
# Update mission.usv
|
|
194
|
+
issues = load_mission()
|
|
195
|
+
|
|
196
|
+
# Determine priority: max + 1
|
|
197
|
+
max_priority = max([i.priority for i in issues], default=0)
|
|
198
|
+
new_priority = max_priority + 1
|
|
199
|
+
|
|
200
|
+
new_issue = Issue(
|
|
201
|
+
slug=slug,
|
|
202
|
+
priority=new_priority,
|
|
203
|
+
status=status
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
issues.append(new_issue)
|
|
207
|
+
save_mission(issues)
|
|
208
|
+
|
|
209
|
+
console.print(f"[bold green]Created new issue: {slug}[/bold green]")
|
|
210
|
+
console.print(f"File: {issue_file}")
|
|
211
|
+
console.print(f"Priority: {new_priority}")
|
|
212
|
+
|
|
213
|
+
def cmd_list(console: Console):
|
|
214
|
+
"""List all issues in mission.usv, sorted by status (pending first) then priority."""
|
|
215
|
+
issues = load_mission()
|
|
216
|
+
if not issues:
|
|
217
|
+
console.print("[yellow]No issues found in mission.usv[/yellow]")
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
# Sort: pending first, then by priority
|
|
221
|
+
sorted_issues = sorted(issues, key=lambda x: (x.status != "pending", x.priority))
|
|
222
|
+
|
|
223
|
+
table = Table(title="Task Queue")
|
|
224
|
+
table.add_column("Priority", justify="right", style="cyan")
|
|
225
|
+
table.add_column("Status", style="magenta")
|
|
226
|
+
table.add_column("Slug", style="green")
|
|
227
|
+
table.add_column("Location", style="dim")
|
|
228
|
+
|
|
229
|
+
for issue in sorted_issues:
|
|
230
|
+
issue_file = find_issue_file(issue.slug)
|
|
231
|
+
location = str(issue_file) if issue_file else "[red]MISSING[/red]"
|
|
232
|
+
|
|
233
|
+
# Color code status
|
|
234
|
+
status_str = issue.status
|
|
235
|
+
if status_str == "pending":
|
|
236
|
+
status_str = f"[bold yellow]{status_str}[/bold yellow]"
|
|
237
|
+
elif status_str == "draft":
|
|
238
|
+
status_str = f"[dim]{status_str}[/dim]"
|
|
239
|
+
elif status_str == "active":
|
|
240
|
+
status_str = f"[bold green]{status_str}[/bold green]"
|
|
241
|
+
|
|
242
|
+
table.add_row(
|
|
243
|
+
str(issue.priority),
|
|
244
|
+
status_str,
|
|
245
|
+
issue.slug,
|
|
246
|
+
location
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
console.print(table)
|
|
250
|
+
|
|
251
|
+
def cmd_version(console: Console, promote: Optional[str] = None, tag: bool = False):
|
|
252
|
+
"""Show version, promote it, or tag it."""
|
|
253
|
+
try:
|
|
254
|
+
if tag:
|
|
255
|
+
v = get_current_version()
|
|
256
|
+
tag_name = f"v{v}"
|
|
257
|
+
console.print(f"[blue]Tagging current commit as {tag_name}...[/blue]")
|
|
258
|
+
subprocess.run(["git", "tag", tag_name], check=True)
|
|
259
|
+
console.print(f"[bold green]Tagged commit as {tag_name}[/bold green]")
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
if promote:
|
|
263
|
+
# Validate promote part
|
|
264
|
+
if promote not in ["major", "minor", "patch"]:
|
|
265
|
+
console.print(f"[red]Invalid version part: {promote}. Use major, minor, or patch.[/red]")
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
console.print(f"[blue]Promoting {promote} version...[/blue]")
|
|
269
|
+
# Use bump-my-version
|
|
270
|
+
# Note: bump-my-version might fail if there are uncommitted changes.
|
|
271
|
+
# We use --no-commit --no-tag to just update the file.
|
|
272
|
+
subprocess.run(["uv", "run", "bump-my-version", "bump", promote, "--no-commit", "--no-tag"], check=True)
|
|
273
|
+
|
|
274
|
+
new_v = get_current_version()
|
|
275
|
+
console.print(f"[bold green]Promoted to version {new_v}[/bold green]")
|
|
276
|
+
else:
|
|
277
|
+
v = get_current_version()
|
|
278
|
+
console.print(f"[bold blue]Current Version:[/bold blue] [cyan]{v}[/cyan]")
|
|
279
|
+
console.print("\nSubcommands:")
|
|
280
|
+
console.print(" [bold]ta version promote [major|minor|patch][/bold]")
|
|
281
|
+
console.print(" [bold]ta version tag[/bold]")
|
|
282
|
+
|
|
283
|
+
except Exception as e:
|
|
284
|
+
console.print(f"[red]Error managing version: {e}[/red]")
|
|
285
|
+
|
|
286
|
+
def main():
|
|
287
|
+
parser = argparse.ArgumentParser(description="Task Agent CLI")
|
|
288
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
289
|
+
|
|
290
|
+
# next
|
|
291
|
+
subparsers.add_parser("next", help="Show the top issue")
|
|
292
|
+
|
|
293
|
+
# list
|
|
294
|
+
subparsers.add_parser("list", help="List all issues")
|
|
295
|
+
|
|
296
|
+
# done
|
|
297
|
+
done_parser = subparsers.add_parser("done", help="Mark an issue as done")
|
|
298
|
+
done_parser.add_argument("slug", nargs="?", help="Slug of the issue to mark as done (defaults to top issue)")
|
|
299
|
+
|
|
300
|
+
# new
|
|
301
|
+
new_parser = subparsers.add_parser("new", help="Create a new issue")
|
|
302
|
+
new_parser.add_argument("-t", "--title", required=True, help="Title of the issue")
|
|
303
|
+
new_parser.add_argument("-b", "--body", default="", help="Body of the issue")
|
|
304
|
+
new_parser.add_argument("-d", "--draft", action="store_true", help="Create as a draft")
|
|
305
|
+
|
|
306
|
+
# version
|
|
307
|
+
version_parser = subparsers.add_parser("version", help="Show or promote version")
|
|
308
|
+
version_subparsers = version_parser.add_subparsers(dest="version_command")
|
|
309
|
+
promote_parser = version_subparsers.add_parser("promote", help="Promote semantic version")
|
|
310
|
+
promote_parser.add_argument("part", choices=["major", "minor", "patch"], help="Part of the version to promote")
|
|
311
|
+
version_subparsers.add_parser("tag", help="Tag current commit with current version")
|
|
312
|
+
|
|
313
|
+
args = parser.parse_args()
|
|
314
|
+
console = Console()
|
|
315
|
+
|
|
316
|
+
if args.command == "next":
|
|
317
|
+
cmd_next(console)
|
|
318
|
+
elif args.command == "list":
|
|
319
|
+
cmd_list(console)
|
|
320
|
+
elif args.command == "done":
|
|
321
|
+
cmd_done(console, args.slug)
|
|
322
|
+
elif args.command == "new":
|
|
323
|
+
cmd_new(console, args.title, args.body, args.draft)
|
|
324
|
+
elif args.command == "version":
|
|
325
|
+
if args.version_command == "promote":
|
|
326
|
+
cmd_version(console, args.part)
|
|
327
|
+
elif args.version_command == "tag":
|
|
328
|
+
cmd_version(console, tag=True)
|
|
329
|
+
else:
|
|
330
|
+
cmd_version(console)
|
|
331
|
+
else:
|
|
332
|
+
parser.print_help()
|
|
333
|
+
|
|
334
|
+
if __name__ == "__main__":
|
|
335
|
+
main()
|
|
File without changes
|