#!/usr/bin/env python3 """ update-task Modifies metadata, title, and/or description of a specific task in Documentation/Tasks.md and automatically synchronizes the document tables and sorting. Usage: ./scripts/update-task OT-018 --status pending ./scripts/update-task OT-018 --title "Updated Title" ./scripts/update-task OT-018 --description "New description..." ./scripts/update-task OT-018 --append-description "- [ ] Additional checklist item" ./scripts/update-task OT-018 --check-all """ import argparse import importlib.machinery import importlib.util import re import sys from pathlib import Path def load_update_tasks_module(): script_dir = Path(__file__).resolve().parent update_script = script_dir / "update-tasks" if not update_script.exists(): raise FileNotFoundError(f"Could not find update-tasks at {update_script}") loader = importlib.machinery.SourceFileLoader("update_tasks", str(update_script)) spec = importlib.util.spec_from_loader("update_tasks", loader) if spec is None: raise ImportError(f"Could not load spec for {update_script}") mod = importlib.util.module_from_spec(spec) loader.exec_module(mod) return mod def parse_task_id(id_str): return load_update_tasks_module().parse_task_id(id_str) def update_task_detail( task_id_input, title=None, project=None, subproject=None, clear_subproject=False, status=None, task_type=None, description=None, append_description=None, check_items=None, uncheck_items=None, check_all=False, uncheck_all=False, sync=True, tasks_file=None, epic=None, clear_epic=False, ): update_mod = load_update_tasks_module() tasks_path = update_mod.find_tasks_file(tasks_file) with open(tasks_path, "r", encoding="utf-8") as f: text = f.read() fm_data = update_mod.read_frontmatter(text) prefix = update_mod.get_task_id_prefix(fm_data) target_num = update_mod.parse_task_id(task_id_input) detail_header_marker = "## Detailed Tasks\n" detail_start = text.find(detail_header_marker) if detail_start == -1: raise ValueError("## Detailed Tasks heading not found") notes_marker = "\n---\n\n## Notes\n" notes_start = text.find(notes_marker) if notes_start == -1: notes_start = len(text) detail_body = text[detail_start + len(detail_header_marker) : notes_start] task_chunks = re.split(r"(?=]*\bid=|' subproj_line = f"**Subproject:** {new_subproj.capitalize()}\n" else: anchor_tag_str = f'' subproj_line = "" if new_epic: anchor_tag_str = anchor_tag_str.replace('>', f' data-epic="{new_epic}">') epic_line = f"**Epic:** {epic_label}\n" if new_epic else "" updated_chunk = ( f'{anchor_tag_str}\n' f"### {new_title}\n" f"**ID:** {normalized_id}\n" f"**Project:** {new_proj.capitalize()}\n" f"{subproj_line}" f"{epic_line}" f"**Status:** {new_status.capitalize()}\n" f"**Type:** {new_type.capitalize()}\n\n" f"**Description:**\n" f"{new_desc}" ) new_chunks.append(updated_chunk) else: new_chunks.append(chunk_clean) if not found: raise KeyError(f"Task {task_id_input} ({update_mod.format_task_id(target_num, prefix)}) was not found in {tasks_path}") # Reconstruct document new_detail_body = "\n\n".join(new_chunks) + "\n" updated_text = text[: detail_start + len(detail_header_marker)] + new_detail_body + text[notes_start:] with open(tasks_path, "w", encoding="utf-8") as f: f.write(updated_text) if sync: update_mod.update_tasks_document(tasks_path) return update_mod.format_task_id(target_num, prefix, digit_width), tasks_path def main(): parser = argparse.ArgumentParser(description="Modify task details and synchronize tasks.md") parser.add_argument("task_id", help="Task number or prefixed ID (e.g. 18, PREFIX-018)") parser.add_argument("--epic", default=None, help="Update epic using a frontmatter value or label") parser.add_argument("--clear-epic", action="store_true", help="Remove epic from task") parser.add_argument("-T", "--title", default=None, help="Update task title") parser.add_argument("-p", "--project", default=None, help="Update project (e.g. android, editor, shared, story-engine)") parser.add_argument("-S", "--subproject", default=None, help="Update subproject (e.g. actions, expressions, schema)") parser.add_argument("--clear-subproject", action="store_true", help="Remove subproject from task") parser.add_argument("-s", "--status", default=None, help="Update status (e.g. triage, pending, in_progress, done, blocked, cancelled)") parser.add_argument("-t", "--type", default=None, help="Update task type (e.g. feature, bug, lint, chore)") parser.add_argument("-d", "--description", default=None, help="Replace entire description body") parser.add_argument("--desc-file", default=None, help="File containing new description body") parser.add_argument("-a", "--append-description", default=None, help="Append text to existing description") parser.add_argument("--check", action="append", default=[], help="Checkbox index (1-based) or substring to check [x]") parser.add_argument("--uncheck", action="append", default=[], help="Checkbox index (1-based) or substring to uncheck [ ]") parser.add_argument("--check-all", action="store_true", help="Mark all checkboxes in this task as checked [x]") parser.add_argument("--uncheck-all", action="store_true", help="Mark all checkboxes in this task as unchecked [ ]") parser.add_argument("--no-sync", action="store_true", help="Do not run update-tasks table sync") parser.add_argument("-f", "--file", default=None, help="Path to tasks.md (optional)") args = parser.parse_args() desc_content = args.description if args.desc_file: with open(args.desc_file, "r", encoding="utf-8") as df: desc_content = df.read() try: formatted_id, path = update_task_detail( task_id_input=args.task_id, title=args.title, project=args.project, subproject=args.subproject, clear_subproject=args.clear_subproject, status=args.status, task_type=args.type, description=desc_content, append_description=args.append_description, check_items=args.check, uncheck_items=args.uncheck, check_all=args.check_all, uncheck_all=args.uncheck_all, sync=not args.no_sync, tasks_file=args.file, epic=args.epic, clear_epic=args.clear_epic, ) print(f"Successfully updated task {formatted_id} in {path}") except Exception as e: sys.stderr.write(f"Error: {e}\n") sys.exit(1) if __name__ == "__main__": main()