文件

803 行
31 KiB
Python
可执行文件

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Task Management Script.
Usage:
python3 task.py create "<title>" --description "<desc>" [--slug <name>] [--assignee <dev>] [--priority P0|P1|P2|P3] [--parent <dir>] [--package <pkg>] [--no-start] [--force]
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 active task, record current branch
python3 task.py current [--source] [--json] # Show active task
python3 task.py finish # Clear active 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 set-meta <dir> <key> <value> # Set a task metadata key
python3 task.py rename <dir> <new-slug> [--dry-run] # Rename task + references
python3 task.py archive <task-dir> [--skip-branch-validation] # 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 json
import sys
from pathlib import Path
from common.log import Colors, colored
from common.config import get_codex_dispatch_mode
from common.paths import (
DEVELOPER_HINT,
DIR_WORKFLOW,
DIR_TASKS,
FILE_TASK_JSON,
get_repo_root,
get_developer,
get_tasks_dir,
get_current_task,
)
from common.active_task import (
clear_active_task,
resolve_active_task,
resolve_context_key,
set_active_task,
)
from common.git import current_branch_name
from common.io import (
describe_json_read_failure,
read_json_checked,
write_json,
)
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_rename,
cmd_archive,
cmd_set_branch,
cmd_set_base_branch,
cmd_set_scope,
cmd_set_meta,
cmd_add_subtask,
cmd_remove_subtask,
)
from common.task_context import (
cmd_add_context,
cmd_validate,
cmd_list_context,
curated_entry_count,
)
# =============================================================================
# Command: start / finish
# =============================================================================
def _record_start_state(
task_json_path: Path,
repo_root: Path,
label: str = "",
) -> None:
"""Move a freshly started task to in_progress and record its branch.
Both updates share one read/write: the status flip from planning, and the
checked-out branch when `branch` is still empty. Recording at start is what
keeps `branch` trustworthy at archive time — a task whose branch is only
ever set by hand tends to reach archive with `branch: null`.
Tolerant on purpose — a broken task.json does not fail `start`, because the
session pointer is the point of the command. But the read overwrites the
file it just read, so no failure may be silent: without a message the
absent status line looks like the task simply was not in planning.
"""
data, reason = read_json_checked(task_json_path)
if data is None:
problem, hint = describe_json_read_failure(task_json_path, reason)
print(
colored(f"Warning: {problem}; task.json not updated.", Colors.YELLOW),
file=sys.stderr,
)
print(hint, file=sys.stderr)
return
applied: list[str] = []
if data.get("status") == "planning":
data["status"] = "in_progress"
applied.append(f"✓ Status: planning → in_progress{label}")
# Only fill an empty field: an explicit `set-branch` must survive a later
# `start` (re-starting a task after a checkout is a normal thing to do).
base_branch_conflict: str | None = None
if not data.get("branch"):
branch = current_branch_name(repo_root)
if branch:
data["branch"] = branch
applied.append(f"✓ Branch recorded: {branch}{label}")
if branch == data.get("base_branch"):
base_branch_conflict = branch
else:
print(
colored(
"Note: no checked-out branch (detached HEAD, or not a git "
"repository); task branch not recorded.",
Colors.YELLOW,
),
file=sys.stderr,
)
if not applied:
return
if not write_json(task_json_path, data):
print(
colored(
f"Warning: Failed to write {task_json_path}; "
"status and branch are unchanged.",
Colors.YELLOW,
),
file=sys.stderr,
)
return
for line in applied:
print(colored(line, Colors.GREEN))
if base_branch_conflict:
# Recorded anyway — the value is true, it just cannot describe a PR.
# Archive refuses this shape, so say so now rather than at the gate.
print(
colored(
f"Warning: '{base_branch_conflict}' is also this task's base_branch; "
"a PR cannot target its own branch, and archive will refuse it.",
Colors.YELLOW,
),
file=sys.stderr,
)
print(
f"Once you branch off, run: python3 {DIR_WORKFLOW}/scripts/task.py "
"set-branch <task> <feature-branch>",
file=sys.stderr,
)
def cmd_start(args: argparse.Namespace) -> int:
"""Set active 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 full_path is None:
# resolve_task_dir already named the exact reason on stderr. A second,
# generic line on stdout would split one diagnosis across two streams
# and bury the specific message.
return 1
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
# Context-manifest gate (#573): a seeded-but-uncurated implement/check
# manifest means every sub-agent dispatched for this task runs with zero
# spec context, and nothing downstream surfaces that to the main session.
# An absent manifest is not gated — create seeds the files only on
# sub-agent-capable platforms, so absence means no sub-agent reads them.
if get_codex_dispatch_mode(repo_root) != "inline" and not getattr(args, "allow_empty_context", False):
empty_manifests = [
name
for name in ("implement.jsonl", "check.jsonl")
if curated_entry_count(full_path / name) == 0
]
if empty_manifests:
print(colored(
f"Error: {' and '.join(empty_manifests)} "
f"{'has' if len(empty_manifests) == 1 else 'have'} no curated entries",
Colors.RED,
))
print("Sub-agents (implement/check) would run with zero spec context.")
print(f" Curate: python3 .trellis/scripts/task.py add-context {task_input} implement <path> \"<why>\"")
print(f" Verify: python3 .trellis/scripts/task.py validate {task_input}")
print(" Intentionally empty? Re-run start with --allow-empty-context")
return 1
# Convert to relative path for storage. repo_root is resolved because
# full_path already is (resolve_task_dir only returns paths inside the
# resolved root), so an unresolved repo_root would mismatch under a
# symlink (e.g. /tmp on macOS) and reject a perfectly normal task.
try:
task_dir = full_path.relative_to(repo_root.resolve()).as_posix()
except ValueError:
# resolve_task_dir already refused everything outside the repo, so
# this is unreachable in practice. Refuse rather than fall back to
# str(full_path) — that fallback (a lexical relative_to() paired with
# an absolute-path fallback) is exactly the pattern that let a `..`
# ref escape into storage before this fix.
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
task_json_path = full_path / FILE_TASK_JSON
if not resolve_context_key():
# Degraded mode: no session identity available.
# Hook didn't inject TRELLIS_CONTEXT_ID (common on Windows + Claude Code,
# --continue resume path, fork distribution, hooks disabled, etc.). Skip
# per-session pointer write; AI continues based on conversation context.
print(colored(
"ℹ Session identity not available; active-task pointer not persisted "
"this session (degraded mode). AI continues based on conversation context.",
Colors.YELLOW,
))
print(colored(
"Hint: run inside an AI IDE/session that exposes session identity, "
"or set TRELLIS_CONTEXT_ID before running task.py start.",
Colors.YELLOW,
))
# Still flip task.json status: planning → in_progress so downstream phases proceed.
if task_json_path.is_file():
_record_start_state(task_json_path, repo_root, " (degraded)")
run_task_hooks("after_start", task_json_path, repo_root)
return 0
active = set_active_task(task_dir, repo_root)
if active:
print(colored(f"✓ Current task set to: {task_dir}", Colors.GREEN))
print(f"Source: {active.source}")
if task_json_path.is_file():
_record_start_state(task_json_path, repo_root)
print()
print(colored("The hook will now inject context from this task's jsonl files.", Colors.BLUE))
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 active task."""
repo_root = get_repo_root()
active = clear_active_task(repo_root)
current = active.task_path
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
print(colored(f"✓ Cleared current task (was: {current})", Colors.GREEN))
print(f"Source: {active.source}")
if task_json_path.is_file():
run_task_hooks("after_finish", task_json_path, repo_root)
return 0
def cmd_current(args: argparse.Namespace) -> int:
"""Show active task."""
repo_root = get_repo_root()
active = resolve_active_task(repo_root)
if getattr(args, "json", False):
task_obj = None
read_error = None
if active.task_path:
task_json_path = repo_root / active.task_path / FILE_TASK_JSON
data, reason = read_json_checked(task_json_path)
if data is None:
# Without this, a corrupt task.json emits null for every field
# — indistinguishable from a task whose fields really are null.
problem, hint = describe_json_read_failure(task_json_path, reason)
read_error = {
"file": str(task_json_path),
"reason": reason,
"message": f"{problem}. {hint}",
}
data = {}
task_obj = {
"dir": active.task_path,
"id": data.get("id") or data.get("name"),
"title": data.get("title"),
"status": data.get("status"),
"parent": data.get("parent"),
"children": data.get("children", []),
"branch": data.get("branch"),
"base_branch": data.get("base_branch"),
}
payload = {
"current_task": task_obj,
"source": active.source,
"stale": active.stale,
}
# Only present when the read failed, so the healthy shape is unchanged.
if read_error:
payload["error"] = read_error
print(json.dumps(payload, ensure_ascii=False))
return 0 if active.task_path else 1
if args.source:
print(f"Current task: {active.task_path or '(none)'}")
print(f"Source: {active.source}")
if active.stale:
print("State: stale")
return 0 if active.task_path else 1
if active.task_path:
print(active.task_path)
return 0
return 1
# =============================================================================
# Command: list
# =============================================================================
def _display_status(t, all_statuses: dict) -> str:
"""Return the status label to show for a task in `list` output.
A parent task's stored status stays "planning" until someone runs
`task.py start` on the parent directly, even while its children are
actively being worked — a misleading label for anyone scanning the
list (#399 item 3). Show "active" instead when at least one child is
past planning; the stored status.json value is left untouched.
"""
if t.status == "planning" and t.children:
child_in_flight = any(
all_statuses.get(c) not in (None, "planning") for c in t.children
)
if child_in_flight:
return "active"
return t.status
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
as_json = getattr(args, "json", False)
# 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()}
if as_json:
if filter_mine and not developer:
print(
json.dumps({"error": "No developer set", "hint": DEVELOPER_HINT}),
file=sys.stderr,
)
return 1
items = []
for dir_name in sorted(all_tasks.keys()):
t = all_tasks[dir_name]
if filter_mine and (t.assignee or "-") != developer:
continue
if filter_status and t.status != filter_status:
continue
items.append({
"dir": f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}",
"id": t.raw.get("id") or dir_name,
"title": t.title,
"status": t.status,
"display_status": _display_status(t, all_statuses),
"priority": t.priority,
"assignee": t.assignee or None,
"parent": t.parent,
"children": list(t.children),
"package": t.package,
})
print(json.dumps({"tasks": items}, ensure_ascii=False))
return 0
if filter_mine:
if not developer:
print(colored("Error: No developer set. Run init_developer.py first", Colors.RED), file=sys.stderr)
print(DEVELOPER_HINT, file=sys.stderr)
return 1
print(colored(f"My tasks (assignee: {developer}):", Colors.BLUE))
else:
print(colored("All active tasks:", Colors.BLUE))
print()
# 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)
status_label = _display_status(t, all_statuses)
# Package tag
pkg_tag = f" @{t.package}" if t.package else ""
prefix = " " * indent + " - "
if filter_mine:
print(f"{prefix}{dir_name}/ ({status_label}){pkg_tag}{progress}{marker}")
else:
print(f"{prefix}{dir_name}/ ({status_label}){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, plus orphans
# whose recorded parent is not (or no longer) in the active set — a
# dangling parent ref must still render flat instead of disappearing.
for dir_name in sorted(all_tasks.keys()):
parent = all_tasks[dir_name].parent
if not parent or parent not in all_tasks:
_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
# =============================================================================
# Help
# =============================================================================
def show_usage() -> None:
"""Show usage help."""
print("""Task Management Script
Usage:
python3 task.py create <title> --description <desc> Create new task directory (both required, non-empty)
python3 task.py create <title> --description <desc> --package <pkg> Create task for a specific package
python3 task.py create <title> --description <desc> --parent <dir> Create task as child of parent
python3 task.py create <title> --description <desc> --no-start Create without making it active in this session
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 active task; records the checked-out branch when unset
python3 task.py current [--source] Show active task
python3 task.py finish Clear active 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 set-meta <dir> <key> <value> Set/overwrite a task metadata key
python3 task.py rename <dir> <new-slug> Rename task, identity fields and references
python3 task.py archive <task-dir> 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>] [--json] List tasks
python3 task.py list-archive [YYYY-MM] List archived tasks
Monorepo options:
--package <pkg> Package name (validated against config.yaml packages)
Rename options:
--dry-run Print the change set without writing anything
Archive options:
--no-commit Skip the auto git commit after archiving
--skip-branch-validation Archive despite missing or self-referential branch metadata.
Archive normally refuses a task with no `branch` when it has a
`base_branch` and the repo has a remote, or with
`branch == base_branch`; repair those with `set-branch` /
`set-base-branch` instead. Use this flag only for tasks that
were never PR-backed. A recorded branch that was merged and
deleted is only a warning and needs no flag.
List options:
--mine, -m Show only tasks assigned to current developer
--status, -s <s> Filter by status (planning, in_progress, review, completed)
--json Output machine-readable JSON (also available on `current`)
Examples:
python3 task.py create "Add login feature" --description "Email + password sign-in" --slug add-login
python3 task.py create "Add login feature" --description "Email + password sign-in" --slug add-login --package cli
python3 task.py create "Add login feature" --description "Email + password sign-in" --meta linear=ENG-123 --meta epic=auth
python3 task.py create "Child task" --description "Session cookie handling" --slug child --parent .trellis/tasks/01-21-parent
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 current --source
python3 task.py finish
python3 task.py rename add-login add-sso --dry-run # Preview the change set
python3 task.py rename add-login add-sso
python3 task.py archive add-login
python3 task.py archive add-login --skip-branch-validation # Task never had a branch of its own
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."""
# Deprecation guard: `init-context` was removed in v0.5.0-beta.12.
# Detect early so argparse doesn't mask the real reason with a generic
# "invalid choice" error.
if len(sys.argv) >= 2 and sys.argv[1] == "init-context":
print(
colored(
"Error: `task.py init-context` was removed in v0.5.0-beta.12.",
Colors.RED,
),
file=sys.stderr,
)
print(
"implement.jsonl / check.jsonl are now seeded on `task.py create` for",
file=sys.stderr,
)
print(
"sub-agent-capable platforms and curated by the AI during planning when needed.",
file=sys.stderr,
)
print("See .trellis/workflow.md planning artifact guidance or run:", file=sys.stderr)
print(
" python3 ./.trellis/scripts/get_context.py --mode phase --step 1",
file=sys.stderr,
)
print(
"Use `task.py add-context <dir> implement|check <path> <reason>` to append entries.",
file=sys.stderr,
)
return 2
parser = argparse.ArgumentParser(
description="Task Management Script",
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 (required, non-empty)")
p_create.add_argument("--slug", "-s", help="Task slug without the MM-DD date prefix")
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 (required, non-empty — an empty one is refused at archive)",
)
p_create.add_argument("--parent", help="Parent task directory (establishes subtask link)")
p_create.add_argument("--package", help="Package name for monorepo projects")
p_create.add_argument(
"--base-branch",
help="PR target branch (overrides origin/HEAD detection and the checked-out-branch fallback)",
)
p_create.add_argument(
"--meta",
action="append",
help="Task metadata key=value (repeatable)",
)
p_create.add_argument(
"--no-start",
action="store_true",
help="Create the task without making it active in this session",
)
p_create.add_argument(
"--force",
action="store_true",
help="Overwrite task.json when the task directory already exists",
)
# 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)")
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 active task")
p_start.add_argument("dir", help="Task directory")
p_start.add_argument(
"--allow-empty-context",
action="store_true",
help="Start even when implement.jsonl / check.jsonl have no curated entries",
)
# current
p_current = subparsers.add_parser("current", help="Show active task")
p_current.add_argument("--source", action="store_true",
help="Show active task source")
p_current.add_argument("--json", action="store_true",
help="Output machine-readable JSON")
# finish
subparsers.add_parser("finish", help="Clear active 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")
# set-meta
p_setmeta = subparsers.add_parser("set-meta", help="Set/overwrite a task metadata key")
p_setmeta.add_argument("dir", help="Task directory")
p_setmeta.add_argument("key", help="Metadata key")
p_setmeta.add_argument("value", help="Metadata value")
# rename
p_rename = subparsers.add_parser("rename", help="Rename task and its references")
p_rename.add_argument("name", help="Task directory or name")
p_rename.add_argument("new_slug", help="New slug without the MM-DD date prefix")
p_rename.add_argument(
"--dry-run",
action="store_true",
help="Print the change set without writing anything",
)
# archive
p_archive = subparsers.add_parser("archive", help="Archive task")
p_archive.add_argument("name", help="Task directory or name")
p_archive.add_argument("--no-commit", action="store_true", help="Skip auto git commit after archive")
p_archive.add_argument(
"--skip-branch-validation",
action="store_true",
help=(
"Archive even when branch metadata is missing or self-referential "
"(for tasks that were never PR-backed)"
),
)
# 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")
p_list.add_argument("--json", action="store_true", help="Output machine-readable JSON")
# 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,
"add-context": cmd_add_context,
"validate": cmd_validate,
"list-context": cmd_list_context,
"start": cmd_start,
"current": cmd_current,
"finish": cmd_finish,
"set-branch": cmd_set_branch,
"set-base-branch": cmd_set_base_branch,
"set-scope": cmd_set_scope,
"set-meta": cmd_set_meta,
"rename": cmd_rename,
"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())