升级 Trellis 并完成项目审查(07-26-trellis-upgrade-project-audit)
这个提交包含在:
+117
-15
@@ -4,16 +4,17 @@
|
||||
Task Management Script.
|
||||
|
||||
Usage:
|
||||
python3 task.py create "<title>" [--slug <name>] [--assignee <dev>] [--priority P0|P1|P2|P3] [--parent <dir>] [--package <pkg>]
|
||||
python3 task.py create "<title>" [--slug <name>] [--assignee <dev>] [--priority P0|P1|P2|P3] [--parent <dir>] [--package <pkg>] [--no-start]
|
||||
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
|
||||
python3 task.py current [--source] # Show active task
|
||||
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 archive <task-dir> # Archive completed task
|
||||
python3 task.py list # List active tasks
|
||||
python3 task.py list-archive [month] # List archived tasks
|
||||
@@ -24,6 +25,7 @@ Usage:
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
|
||||
from common.log import Colors, colored
|
||||
@@ -53,6 +55,7 @@ from common.task_store import (
|
||||
cmd_set_branch,
|
||||
cmd_set_base_branch,
|
||||
cmd_set_scope,
|
||||
cmd_set_meta,
|
||||
cmd_add_subtask,
|
||||
cmd_remove_subtask,
|
||||
)
|
||||
@@ -166,6 +169,27 @@ def cmd_current(args: argparse.Namespace) -> int:
|
||||
repo_root = get_repo_root()
|
||||
active = resolve_active_task(repo_root)
|
||||
|
||||
if getattr(args, "json", False):
|
||||
task_obj = None
|
||||
if active.task_path:
|
||||
data = read_json(repo_root / active.task_path / FILE_TASK_JSON) or {}
|
||||
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"),
|
||||
}
|
||||
print(json.dumps({
|
||||
"current_task": task_obj,
|
||||
"source": active.source,
|
||||
"stale": active.stale,
|
||||
}, 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}")
|
||||
@@ -184,6 +208,24 @@ def cmd_current(args: argparse.Namespace) -> int:
|
||||
# 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()
|
||||
@@ -192,6 +234,38 @@ def cmd_list(args: argparse.Namespace) -> int:
|
||||
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"}), 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:
|
||||
@@ -202,10 +276,6 @@ def cmd_list(args: argparse.Namespace) -> int:
|
||||
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
|
||||
|
||||
@@ -228,6 +298,7 @@ def cmd_list(args: argparse.Namespace) -> int:
|
||||
|
||||
# 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 ""
|
||||
@@ -235,9 +306,9 @@ def cmd_list(args: argparse.Namespace) -> int:
|
||||
prefix = " " * indent + " - "
|
||||
|
||||
if filter_mine:
|
||||
print(f"{prefix}{dir_name}/ ({t.status}){pkg_tag}{progress}{marker}")
|
||||
print(f"{prefix}{dir_name}/ ({status_label}){pkg_tag}{progress}{marker}")
|
||||
else:
|
||||
print(f"{prefix}{dir_name}/ ({t.status}){pkg_tag}{progress} [{colored(t.assignee or '-', Colors.CYAN)}]{marker}")
|
||||
print(f"{prefix}{dir_name}/ ({status_label}){pkg_tag}{progress} [{colored(t.assignee or '-', Colors.CYAN)}]{marker}")
|
||||
count += 1
|
||||
|
||||
# Print children indented
|
||||
@@ -245,9 +316,12 @@ def cmd_list(args: argparse.Namespace) -> int:
|
||||
if child_name in all_tasks:
|
||||
_print_task(child_name, indent + 1)
|
||||
|
||||
# Display only top-level tasks (those without a parent)
|
||||
# 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()):
|
||||
if not all_tasks[dir_name].parent:
|
||||
parent = all_tasks[dir_name].parent
|
||||
if not parent or parent not in all_tasks:
|
||||
_print_task(dir_name)
|
||||
|
||||
if count == 0:
|
||||
@@ -307,6 +381,7 @@ 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 create <title> --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
|
||||
@@ -316,10 +391,11 @@ Usage:
|
||||
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 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>] List tasks
|
||||
python3 task.py list [--mine] [--status <status>] [--json] List tasks
|
||||
python3 task.py list-archive [YYYY-MM] List archived tasks
|
||||
|
||||
Monorepo options:
|
||||
@@ -328,10 +404,12 @@ Monorepo options:
|
||||
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" --slug add-login
|
||||
python3 task.py create "Add login feature" --slug add-login --package cli
|
||||
python3 task.py create "Add login feature" --meta linear=ENG-123 --meta epic=auth
|
||||
python3 task.py create "Child task" --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
|
||||
@@ -369,12 +447,12 @@ def main() -> int:
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
"sub-agent-capable platforms and curated by the AI during Phase 1.3.",
|
||||
"sub-agent-capable platforms and curated by the AI during planning when needed.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print("See .trellis/workflow.md Phase 1.3 or run:", 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.3",
|
||||
" python3 ./.trellis/scripts/get_context.py --mode phase --step 1",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
@@ -392,12 +470,26 @@ def main() -> int:
|
||||
# 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("--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")
|
||||
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",
|
||||
)
|
||||
|
||||
# add-context
|
||||
p_add = subparsers.add_parser("add-context", help="Add context entry")
|
||||
@@ -422,6 +514,8 @@ def main() -> int:
|
||||
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")
|
||||
@@ -441,6 +535,12 @@ def main() -> int:
|
||||
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")
|
||||
|
||||
# archive
|
||||
p_archive = subparsers.add_parser("archive", help="Archive task")
|
||||
p_archive.add_argument("name", help="Task directory or name")
|
||||
@@ -450,6 +550,7 @@ def main() -> int:
|
||||
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")
|
||||
@@ -482,6 +583,7 @@ def main() -> int:
|
||||
"set-branch": cmd_set_branch,
|
||||
"set-base-branch": cmd_set_base_branch,
|
||||
"set-scope": cmd_set_scope,
|
||||
"set-meta": cmd_set_meta,
|
||||
"archive": cmd_archive,
|
||||
"add-subtask": cmd_add_subtask,
|
||||
"remove-subtask": cmd_remove_subtask,
|
||||
|
||||
在新工单中引用
屏蔽一个用户