junhaocheng
Sync post-credential gate hardening
f54fafa
Raw
History Blame Contribute Delete
5.83 kB
#!/usr/bin/env python3
"""Small task ledger helper for ProofFrame."""
from __future__ import annotations
import argparse
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
TASKS_PATH = ROOT / "tasks.json"
VALID_STATUSES = {"todo", "doing", "blocked", "done"}
def load_data() -> dict[str, Any]:
with TASKS_PATH.open("r", encoding="utf-8") as f:
return json.load(f)
def save_data(data: dict[str, Any]) -> None:
data["updated_at"] = (
datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
)
tmp_path = TASKS_PATH.with_suffix(".json.tmp")
with tmp_path.open("w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
f.write("\n")
tmp_path.replace(TASKS_PATH)
def find_task(data: dict[str, Any], task_id: str) -> dict[str, Any]:
task_id = task_id.upper()
for task in data["tasks"]:
if task["id"].upper() == task_id:
return task
raise SystemExit(f"Task not found: {task_id}")
def print_task(task: dict[str, Any]) -> None:
note = f" | {task.get('notes', '')}" if task.get("notes") else ""
print(f"{task['id']} [{task['status']}] {task['phase']} / {task['owner']} - {task['title']}{note}")
def task_search_text(task: dict[str, Any]) -> str:
values = [
task.get("id", ""),
task.get("title", ""),
task.get("phase", ""),
task.get("owner", ""),
task.get("status", ""),
task.get("done_criteria", ""),
task.get("notes", ""),
]
return "\n".join(str(value) for value in values).lower()
def insert_task(data: dict[str, Any], task: dict[str, Any], after: str | None = None) -> None:
tasks = data["tasks"]
if any(existing["id"].upper() == task["id"].upper() for existing in tasks):
raise SystemExit(f"Task already exists: {task['id']}")
if not after:
tasks.append(task)
return
after = after.upper()
for index, existing in enumerate(tasks):
if existing["id"].upper() == after:
tasks.insert(index + 1, task)
return
raise SystemExit(f"Task not found for --after: {after}")
def cmd_add(args: argparse.Namespace) -> None:
data = load_data()
task = {
"id": args.task_id.upper(),
"title": args.title,
"phase": args.phase,
"owner": args.owner,
"status": args.status,
"done_criteria": args.done_criteria,
"notes": args.note,
}
insert_task(data, task, args.after)
save_data(data)
print_task(task)
def cmd_list(args: argparse.Namespace) -> None:
data = load_data()
tasks = data["tasks"]
if args.status:
tasks = [task for task in tasks if task["status"] == args.status]
if args.owner:
tasks = [task for task in tasks if task["owner"] == args.owner]
for task in tasks:
print_task(task)
def cmd_show(args: argparse.Namespace) -> None:
data = load_data()
task = find_task(data, args.task_id)
print(json.dumps(task, indent=2, ensure_ascii=False))
def cmd_search(args: argparse.Namespace) -> None:
data = load_data()
terms = [term.lower() for term in args.query]
tasks = data["tasks"]
if args.status:
tasks = [task for task in tasks if task["status"] == args.status]
if args.owner:
tasks = [task for task in tasks if task["owner"] == args.owner]
for task in tasks:
text = task_search_text(task)
if all(term in text for term in terms):
print_task(task)
def update_status(args: argparse.Namespace, status: str) -> None:
data = load_data()
task = find_task(data, args.task_id)
task["status"] = status
if args.note:
task["notes"] = args.note
save_data(data)
print_task(task)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Query and update ProofFrame tasks.")
subparsers = parser.add_subparsers(dest="command", required=True)
add_parser = subparsers.add_parser("add", help="Add a task.")
add_parser.add_argument("task_id")
add_parser.add_argument("title")
add_parser.add_argument("--phase", required=True)
add_parser.add_argument("--owner", required=True)
add_parser.add_argument("--status", choices=sorted(VALID_STATUSES), default="todo")
add_parser.add_argument("--done-criteria", default="")
add_parser.add_argument("--note", default="")
add_parser.add_argument("--after", help="Insert after an existing task id.")
add_parser.set_defaults(func=cmd_add)
list_parser = subparsers.add_parser("list", help="List tasks.")
list_parser.add_argument("--status", choices=sorted(VALID_STATUSES))
list_parser.add_argument("--owner")
list_parser.set_defaults(func=cmd_list)
show_parser = subparsers.add_parser("show", help="Show one task as JSON.")
show_parser.add_argument("task_id")
show_parser.set_defaults(func=cmd_show)
search_parser = subparsers.add_parser("search", help="Search tasks.")
search_parser.add_argument("query", nargs="+")
search_parser.add_argument("--status", choices=sorted(VALID_STATUSES))
search_parser.add_argument("--owner")
search_parser.set_defaults(func=cmd_search)
for status in sorted(VALID_STATUSES):
status_parser = subparsers.add_parser(status, help=f"Mark a task as {status}.")
status_parser.add_argument("task_id")
status_parser.add_argument("--note", default="")
status_parser.set_defaults(func=lambda args, status=status: update_status(args, status))
return parser
def main() -> None:
parser = build_parser()
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()