forked from mengyxu/noob-components
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
445 lines
17 KiB
445 lines
17 KiB
#!/usr/bin/env python3 |
|
# -*- coding: utf-8 -*- |
|
""" |
|
Task Management Script for Multi-Agent Pipeline. |
|
|
|
Usage: |
|
python3 task.py create "<title>" [--slug <name>] [--assignee <dev>] [--priority P0|P1|P2|P3] [--parent <dir>] [--package <pkg>] |
|
python3 task.py init-context <dir> <type> [--package <pkg>] # Initialize jsonl files |
|
python3 task.py add-context <dir> <file> <path> [reason] # Add jsonl entry |
|
python3 task.py validate <dir> # Validate jsonl files |
|
python3 task.py list-context <dir> # List jsonl entries |
|
python3 task.py start <dir> # Set as current task |
|
python3 task.py finish # Clear current task |
|
python3 task.py set-branch <dir> <branch> # Set git branch |
|
python3 task.py set-base-branch <dir> <branch> # Set PR target branch |
|
python3 task.py set-scope <dir> <scope> # Set scope for PR title |
|
python3 task.py create-pr [dir] [--dry-run] # Create PR from task |
|
python3 task.py archive <task-name> # Archive completed task |
|
python3 task.py list # List active tasks |
|
python3 task.py list-archive [month] # List archived tasks |
|
python3 task.py add-subtask <parent-dir> <child-dir> # Link child to parent |
|
python3 task.py remove-subtask <parent-dir> <child-dir> # Unlink child from parent |
|
""" |
|
|
|
from __future__ import annotations |
|
|
|
import argparse |
|
import sys |
|
from pathlib import Path |
|
|
|
from common.log import Colors, colored |
|
from common.paths import ( |
|
DIR_WORKFLOW, |
|
DIR_TASKS, |
|
FILE_TASK_JSON, |
|
get_repo_root, |
|
get_developer, |
|
get_tasks_dir, |
|
get_current_task, |
|
set_current_task, |
|
clear_current_task, |
|
) |
|
from common.task_utils import resolve_task_dir, run_task_hooks |
|
from common.tasks import iter_active_tasks, children_progress |
|
|
|
# Import command handlers from split modules (also re-exports for plan.py compatibility) |
|
from common.task_store import ( |
|
cmd_create, |
|
cmd_archive, |
|
cmd_set_branch, |
|
cmd_set_base_branch, |
|
cmd_set_scope, |
|
cmd_add_subtask, |
|
cmd_remove_subtask, |
|
) |
|
from common.task_context import ( |
|
cmd_init_context, |
|
cmd_add_context, |
|
cmd_validate, |
|
cmd_list_context, |
|
) |
|
|
|
|
|
# ============================================================================= |
|
# Command: start / finish |
|
# ============================================================================= |
|
|
|
def cmd_start(args: argparse.Namespace) -> int: |
|
"""Set current task.""" |
|
repo_root = get_repo_root() |
|
task_input = args.dir |
|
|
|
if not task_input: |
|
print(colored("Error: task directory or name required", Colors.RED)) |
|
return 1 |
|
|
|
# Resolve task directory (supports task name, relative path, or absolute path) |
|
full_path = resolve_task_dir(task_input, repo_root) |
|
|
|
if not full_path.is_dir(): |
|
print(colored(f"Error: Task not found: {task_input}", Colors.RED)) |
|
print("Hint: Use task name (e.g., 'my-task') or full path (e.g., '.trellis/tasks/01-31-my-task')") |
|
return 1 |
|
|
|
# Convert to relative path for storage |
|
try: |
|
task_dir = str(full_path.relative_to(repo_root)) |
|
except ValueError: |
|
task_dir = str(full_path) |
|
|
|
if set_current_task(task_dir, repo_root): |
|
print(colored(f"✓ Current task set to: {task_dir}", Colors.GREEN)) |
|
print() |
|
print(colored("The hook will now inject context from this task's jsonl files.", Colors.BLUE)) |
|
|
|
task_json_path = full_path / FILE_TASK_JSON |
|
run_task_hooks("after_start", task_json_path, repo_root) |
|
return 0 |
|
else: |
|
print(colored("Error: Failed to set current task", Colors.RED)) |
|
return 1 |
|
|
|
|
|
def cmd_finish(args: argparse.Namespace) -> int: |
|
"""Clear current task.""" |
|
repo_root = get_repo_root() |
|
current = get_current_task(repo_root) |
|
|
|
if not current: |
|
print(colored("No current task set", Colors.YELLOW)) |
|
return 0 |
|
|
|
# Resolve task.json path before clearing |
|
task_json_path = repo_root / current / FILE_TASK_JSON |
|
|
|
clear_current_task(repo_root) |
|
print(colored(f"✓ Cleared current task (was: {current})", Colors.GREEN)) |
|
|
|
if task_json_path.is_file(): |
|
run_task_hooks("after_finish", task_json_path, repo_root) |
|
return 0 |
|
|
|
|
|
# ============================================================================= |
|
# Command: list |
|
# ============================================================================= |
|
|
|
def cmd_list(args: argparse.Namespace) -> int: |
|
"""List active tasks.""" |
|
repo_root = get_repo_root() |
|
tasks_dir = get_tasks_dir(repo_root) |
|
current_task = get_current_task(repo_root) |
|
developer = get_developer(repo_root) |
|
filter_mine = args.mine |
|
filter_status = args.status |
|
|
|
if filter_mine: |
|
if not developer: |
|
print(colored("Error: No developer set. Run init_developer.py first", Colors.RED), file=sys.stderr) |
|
return 1 |
|
print(colored(f"My tasks (assignee: {developer}):", Colors.BLUE)) |
|
else: |
|
print(colored("All active tasks:", Colors.BLUE)) |
|
print() |
|
|
|
# Single pass: collect all tasks via shared iterator |
|
all_tasks = {t.dir_name: t for t in iter_active_tasks(tasks_dir)} |
|
all_statuses = {name: t.status for name, t in all_tasks.items()} |
|
|
|
# Display tasks hierarchically |
|
count = 0 |
|
|
|
def _print_task(dir_name: str, indent: int = 0) -> None: |
|
nonlocal count |
|
t = all_tasks[dir_name] |
|
|
|
# Apply --mine filter |
|
if filter_mine and (t.assignee or "-") != developer: |
|
return |
|
|
|
# Apply --status filter |
|
if filter_status and t.status != filter_status: |
|
return |
|
|
|
relative_path = f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}" |
|
marker = "" |
|
if relative_path == current_task: |
|
marker = f" {colored('<- current', Colors.GREEN)}" |
|
|
|
# Children progress |
|
progress = children_progress(t.children, all_statuses) |
|
|
|
# Package tag |
|
pkg_tag = f" @{t.package}" if t.package else "" |
|
|
|
prefix = " " * indent + " - " |
|
|
|
if filter_mine: |
|
print(f"{prefix}{dir_name}/ ({t.status}){pkg_tag}{progress}{marker}") |
|
else: |
|
print(f"{prefix}{dir_name}/ ({t.status}){pkg_tag}{progress} [{colored(t.assignee or '-', Colors.CYAN)}]{marker}") |
|
count += 1 |
|
|
|
# Print children indented |
|
for child_name in t.children: |
|
if child_name in all_tasks: |
|
_print_task(child_name, indent + 1) |
|
|
|
# Display only top-level tasks (those without a parent) |
|
for dir_name in sorted(all_tasks.keys()): |
|
if not all_tasks[dir_name].parent: |
|
_print_task(dir_name) |
|
|
|
if count == 0: |
|
if filter_mine: |
|
print(" (no tasks assigned to you)") |
|
else: |
|
print(" (no active tasks)") |
|
|
|
print() |
|
print(f"Total: {count} task(s)") |
|
return 0 |
|
|
|
|
|
# ============================================================================= |
|
# Command: list-archive |
|
# ============================================================================= |
|
|
|
def cmd_list_archive(args: argparse.Namespace) -> int: |
|
"""List archived tasks.""" |
|
repo_root = get_repo_root() |
|
tasks_dir = get_tasks_dir(repo_root) |
|
archive_dir = tasks_dir / "archive" |
|
month = args.month |
|
|
|
print(colored("Archived tasks:", Colors.BLUE)) |
|
print() |
|
|
|
if month: |
|
month_dir = archive_dir / month |
|
if month_dir.is_dir(): |
|
print(f"[{month}]") |
|
for d in sorted(month_dir.iterdir()): |
|
if d.is_dir(): |
|
print(f" - {d.name}/") |
|
else: |
|
print(f" No archives for {month}") |
|
else: |
|
if archive_dir.is_dir(): |
|
for month_dir in sorted(archive_dir.iterdir()): |
|
if month_dir.is_dir(): |
|
month_name = month_dir.name |
|
count = sum(1 for d in month_dir.iterdir() if d.is_dir()) |
|
print(f"[{month_name}] - {count} task(s)") |
|
|
|
return 0 |
|
|
|
|
|
# ============================================================================= |
|
# Command: create-pr (delegates to multi-agent script) |
|
# ============================================================================= |
|
|
|
def cmd_create_pr(args: argparse.Namespace) -> int: |
|
"""Create PR from task - delegates to multi_agent/create_pr.py.""" |
|
import subprocess |
|
script_dir = Path(__file__).parent |
|
create_pr_script = script_dir / "multi_agent" / "create_pr.py" |
|
|
|
cmd = [sys.executable, str(create_pr_script)] |
|
if args.dir: |
|
cmd.append(args.dir) |
|
if args.dry_run: |
|
cmd.append("--dry-run") |
|
|
|
result = subprocess.run(cmd) |
|
return result.returncode |
|
|
|
|
|
# ============================================================================= |
|
# Help |
|
# ============================================================================= |
|
|
|
def show_usage() -> None: |
|
"""Show usage help.""" |
|
print("""Task Management Script for Multi-Agent Pipeline |
|
|
|
Usage: |
|
python3 task.py create <title> Create new task directory |
|
python3 task.py create <title> --package <pkg> Create task for a specific package |
|
python3 task.py create <title> --parent <dir> Create task as child of parent |
|
python3 task.py init-context <dir> <dev_type> Initialize jsonl files |
|
python3 task.py init-context <dir> <type> --package <pkg> With explicit package |
|
python3 task.py add-context <dir> <jsonl> <path> [reason] Add entry to jsonl |
|
python3 task.py validate <dir> Validate jsonl files |
|
python3 task.py list-context <dir> List jsonl entries |
|
python3 task.py start <dir> Set as current task |
|
python3 task.py finish Clear current task |
|
python3 task.py set-branch <dir> <branch> Set git branch for multi-agent |
|
python3 task.py set-scope <dir> <scope> Set scope for PR title |
|
python3 task.py create-pr [dir] [--dry-run] Create PR from task |
|
python3 task.py archive <task-name> Archive completed task |
|
python3 task.py add-subtask <parent> <child> Link child task to parent |
|
python3 task.py remove-subtask <parent> <child> Unlink child from parent |
|
python3 task.py list [--mine] [--status <status>] List tasks |
|
python3 task.py list-archive [YYYY-MM] List archived tasks |
|
|
|
Arguments: |
|
dev_type: backend | frontend | fullstack | test | docs |
|
|
|
Monorepo options: |
|
--package <pkg> Package name (validated against config.yaml packages) |
|
|
|
List options: |
|
--mine, -m Show only tasks assigned to current developer |
|
--status, -s <s> Filter by status (planning, in_progress, review, completed) |
|
|
|
Examples: |
|
python3 task.py create "Add login feature" --slug add-login |
|
python3 task.py create "Add login feature" --slug add-login --package cli |
|
python3 task.py create "Child task" --slug child --parent .trellis/tasks/01-21-parent |
|
python3 task.py init-context .trellis/tasks/01-21-add-login backend |
|
python3 task.py init-context .trellis/tasks/01-21-add-login backend --package cli |
|
python3 task.py add-context <dir> implement .trellis/spec/cli/backend/auth.md "Auth guidelines" |
|
python3 task.py set-branch <dir> task/add-login |
|
python3 task.py start .trellis/tasks/01-21-add-login |
|
python3 task.py create-pr # Uses current task |
|
python3 task.py create-pr <dir> --dry-run # Preview without changes |
|
python3 task.py finish |
|
python3 task.py archive add-login |
|
python3 task.py add-subtask parent-task child-task # Link existing tasks |
|
python3 task.py remove-subtask parent-task child-task |
|
python3 task.py list # List all active tasks |
|
python3 task.py list --mine # List my tasks only |
|
python3 task.py list --mine --status in_progress # List my in-progress tasks |
|
""") |
|
|
|
|
|
# ============================================================================= |
|
# Main Entry |
|
# ============================================================================= |
|
|
|
def main() -> int: |
|
"""CLI entry point.""" |
|
parser = argparse.ArgumentParser( |
|
description="Task Management Script for Multi-Agent Pipeline", |
|
formatter_class=argparse.RawDescriptionHelpFormatter, |
|
) |
|
subparsers = parser.add_subparsers(dest="command", help="Commands") |
|
|
|
# create |
|
p_create = subparsers.add_parser("create", help="Create new task") |
|
p_create.add_argument("title", help="Task title") |
|
p_create.add_argument("--slug", "-s", help="Task slug") |
|
p_create.add_argument("--assignee", "-a", help="Assignee developer") |
|
p_create.add_argument("--priority", "-p", default="P2", help="Priority (P0-P3)") |
|
p_create.add_argument("--description", "-d", help="Task description") |
|
p_create.add_argument("--parent", help="Parent task directory (establishes subtask link)") |
|
p_create.add_argument("--package", help="Package name for monorepo projects") |
|
|
|
# init-context |
|
p_init = subparsers.add_parser("init-context", help="Initialize context files") |
|
p_init.add_argument("dir", help="Task directory") |
|
p_init.add_argument("type", help="Dev type: backend|frontend|fullstack|test|docs") |
|
p_init.add_argument("--package", help="Package name for monorepo projects") |
|
|
|
# add-context |
|
p_add = subparsers.add_parser("add-context", help="Add context entry") |
|
p_add.add_argument("dir", help="Task directory") |
|
p_add.add_argument("file", help="JSONL file (implement|check|debug)") |
|
p_add.add_argument("path", help="File path to add") |
|
p_add.add_argument("reason", nargs="?", help="Reason for adding") |
|
|
|
# validate |
|
p_validate = subparsers.add_parser("validate", help="Validate context files") |
|
p_validate.add_argument("dir", help="Task directory") |
|
|
|
# list-context |
|
p_listctx = subparsers.add_parser("list-context", help="List context entries") |
|
p_listctx.add_argument("dir", help="Task directory") |
|
|
|
# start |
|
p_start = subparsers.add_parser("start", help="Set current task") |
|
p_start.add_argument("dir", help="Task directory") |
|
|
|
# finish |
|
subparsers.add_parser("finish", help="Clear current task") |
|
|
|
# set-branch |
|
p_branch = subparsers.add_parser("set-branch", help="Set git branch") |
|
p_branch.add_argument("dir", help="Task directory") |
|
p_branch.add_argument("branch", help="Branch name") |
|
|
|
# set-base-branch |
|
p_base = subparsers.add_parser("set-base-branch", help="Set PR target branch") |
|
p_base.add_argument("dir", help="Task directory") |
|
p_base.add_argument("base_branch", help="Base branch name (PR target)") |
|
|
|
# set-scope |
|
p_scope = subparsers.add_parser("set-scope", help="Set scope") |
|
p_scope.add_argument("dir", help="Task directory") |
|
p_scope.add_argument("scope", help="Scope name") |
|
|
|
# create-pr |
|
p_pr = subparsers.add_parser("create-pr", help="Create PR") |
|
p_pr.add_argument("dir", nargs="?", help="Task directory") |
|
p_pr.add_argument("--dry-run", action="store_true", help="Dry run mode") |
|
|
|
# archive |
|
p_archive = subparsers.add_parser("archive", help="Archive task") |
|
p_archive.add_argument("name", help="Task name") |
|
p_archive.add_argument("--no-commit", action="store_true", help="Skip auto git commit after archive") |
|
|
|
# list |
|
p_list = subparsers.add_parser("list", help="List tasks") |
|
p_list.add_argument("--mine", "-m", action="store_true", help="My tasks only") |
|
p_list.add_argument("--status", "-s", help="Filter by status") |
|
|
|
# add-subtask |
|
p_addsub = subparsers.add_parser("add-subtask", help="Link child task to parent") |
|
p_addsub.add_argument("parent_dir", help="Parent task directory") |
|
p_addsub.add_argument("child_dir", help="Child task directory") |
|
|
|
# remove-subtask |
|
p_rmsub = subparsers.add_parser("remove-subtask", help="Unlink child task from parent") |
|
p_rmsub.add_argument("parent_dir", help="Parent task directory") |
|
p_rmsub.add_argument("child_dir", help="Child task directory") |
|
|
|
# list-archive |
|
p_listarch = subparsers.add_parser("list-archive", help="List archived tasks") |
|
p_listarch.add_argument("month", nargs="?", help="Month (YYYY-MM)") |
|
|
|
args = parser.parse_args() |
|
|
|
if not args.command: |
|
show_usage() |
|
return 1 |
|
|
|
commands = { |
|
"create": cmd_create, |
|
"init-context": cmd_init_context, |
|
"add-context": cmd_add_context, |
|
"validate": cmd_validate, |
|
"list-context": cmd_list_context, |
|
"start": cmd_start, |
|
"finish": cmd_finish, |
|
"set-branch": cmd_set_branch, |
|
"set-base-branch": cmd_set_base_branch, |
|
"set-scope": cmd_set_scope, |
|
"create-pr": cmd_create_pr, |
|
"archive": cmd_archive, |
|
"add-subtask": cmd_add_subtask, |
|
"remove-subtask": cmd_remove_subtask, |
|
"list": cmd_list, |
|
"list-archive": cmd_list_archive, |
|
} |
|
|
|
if args.command in commands: |
|
return commands[args.command](args) |
|
else: |
|
show_usage() |
|
return 1 |
|
|
|
|
|
if __name__ == "__main__": |
|
sys.exit(main())
|
|
|