244 lines
9.0 KiB
Python
Executable File
244 lines
9.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
add-task
|
|
|
|
Appends a new task to Documentation/Tasks.md and re-synchronizes the task list.
|
|
Usage:
|
|
add-task "My Title Goes Here" --project=shared --type=chore
|
|
add-task "My Title Goes Here" -p editor -t feature -s triage -d "Description text"
|
|
"""
|
|
|
|
import argparse
|
|
import importlib.machinery
|
|
import importlib.util
|
|
import os
|
|
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 add_task(title, project="shared", subproject=None, task_type="feature", status="triage", description=None, tasks_file=None, epic=None):
|
|
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()
|
|
|
|
# 1. Parse frontmatter for valid labels and values
|
|
fm_match = re.match(r"^---\n(.*?)\n---", text, re.DOTALL)
|
|
if not fm_match:
|
|
raise ValueError("YAML frontmatter not found in tasks.md")
|
|
|
|
fm_data = update_mod.parse_yaml_frontmatter(fm_match.group(1))
|
|
prefix = update_mod.get_task_id_prefix(fm_data)
|
|
epic_key, epic_label = update_mod.resolve_epic(epic, fm_data)
|
|
|
|
project_list = fm_data.get("projects", [])
|
|
subproject_list = fm_data.get("subprojects", [])
|
|
status_list = fm_data.get("task-statuses", [])
|
|
type_list = fm_data.get("task-types", [])
|
|
|
|
project_map = {}
|
|
project_value_map = {}
|
|
for p in project_list:
|
|
val = p.get("value", "").strip()
|
|
lbl = p.get("label", "").strip()
|
|
if val:
|
|
project_map[val.lower()] = lbl or val.capitalize()
|
|
project_value_map[val.lower()] = val
|
|
if lbl:
|
|
project_map[lbl.lower()] = lbl
|
|
if val:
|
|
project_value_map[lbl.lower()] = val
|
|
|
|
subproject_map = {}
|
|
subproject_value_map = {}
|
|
for sp in subproject_list:
|
|
val = sp.get("value", "").strip()
|
|
lbl = sp.get("label", "").strip()
|
|
if val:
|
|
subproject_map[val.lower()] = lbl or val.replace("-", " ").title()
|
|
subproject_value_map[val.lower()] = val
|
|
if lbl:
|
|
subproject_map[lbl.lower()] = lbl
|
|
if val:
|
|
subproject_value_map[lbl.lower()] = val
|
|
|
|
status_map = {s.get("value", "").lower(): s.get("label", "") for s in status_list}
|
|
status_value_map = {s.get("value", "").lower(): s.get("value", "") for s in status_list}
|
|
for s in status_list:
|
|
lbl = s.get("label", "").strip()
|
|
val = s.get("value", "").strip()
|
|
if lbl:
|
|
status_map[lbl.lower()] = lbl
|
|
if val:
|
|
status_value_map[lbl.lower()] = val
|
|
status_map["fixed"] = "Fixed"
|
|
status_map["done"] = "Fixed"
|
|
status_value_map["fixed"] = "done"
|
|
status_value_map["done"] = "done"
|
|
|
|
type_map = {t.get("value", "").lower(): t.get("label", "") for t in type_list}
|
|
type_value_map = {t.get("value", "").lower(): t.get("value", "") for t in type_list}
|
|
for t in type_list:
|
|
lbl = t.get("label", "").strip()
|
|
val = t.get("value", "").strip()
|
|
if lbl:
|
|
type_map[lbl.lower()] = lbl
|
|
if val:
|
|
type_value_map[lbl.lower()] = val
|
|
|
|
# Normalize values
|
|
proj_input = project.strip().lower()
|
|
type_input = task_type.strip().lower()
|
|
status_input = status.strip().lower()
|
|
|
|
proj_key = project_value_map.get(proj_input, proj_input)
|
|
type_key = type_value_map.get(type_input, type_input)
|
|
status_key = status_value_map.get(status_input, status_input)
|
|
|
|
proj_label = project_map.get(proj_key, project_map.get(proj_input, project.strip().capitalize()))
|
|
type_label = type_map.get(type_key, type_map.get(type_input, task_type.strip().capitalize()))
|
|
status_label = status_map.get(status_key, status_map.get(status_input, status.strip().capitalize()))
|
|
|
|
subproj_key = None
|
|
subproj_label = None
|
|
if subproject and subproject.strip() and subproject.strip().lower() not in ("", "none", "null"):
|
|
sp_in = subproject.strip().lower()
|
|
subproj_key = subproject_value_map.get(sp_in, sp_in)
|
|
subproj_label = subproject_map.get(subproj_key, subproject_map.get(sp_in, subproject.strip().replace("-", " ").replace("_", " ").title()))
|
|
|
|
# 2. Find max task ID
|
|
detail_start = text.find("## Detailed Tasks\n")
|
|
if detail_start == -1:
|
|
raise ValueError("## Detailed Tasks heading not found")
|
|
ids = [int(m.group(2)) for m in re.finditer(update_mod.TASK_ID_PATTERN, text[detail_start:], re.IGNORECASE)]
|
|
next_num = (max(ids) + 1) if ids else 1
|
|
|
|
# Digit width calculation (minimum 3 digits)
|
|
digit_width = max(3, len(str(next_num)))
|
|
formatted_id = update_mod.format_task_id(next_num, prefix, digit_width)
|
|
formatted_aid = formatted_id.lower()
|
|
|
|
# 3. Format description
|
|
if description and description.strip():
|
|
desc_lines = description.strip().splitlines()
|
|
desc_body = "\n".join(desc_lines)
|
|
if not any(line.strip().startswith("- [") for line in desc_lines):
|
|
desc_body += "\n- [ ] "
|
|
else:
|
|
desc_body = "Describe task objectives and implementation requirements here.\n- [ ] "
|
|
|
|
if subproj_key:
|
|
anchor_attrs = f'id="{formatted_aid}" class="task" data-project="{proj_key}" data-subproject="{subproj_key}" data-status="{status_key}" data-task-type="{type_key}"'
|
|
subproj_line = f"**Subproject:** {subproj_label}\n"
|
|
else:
|
|
anchor_attrs = f'id="{formatted_aid}" class="task" data-project="{proj_key}" data-status="{status_key}" data-task-type="{type_key}"'
|
|
subproj_line = ""
|
|
|
|
if epic_key:
|
|
anchor_attrs += f' data-epic="{epic_key}"'
|
|
epic_line = f"**Epic:** {epic_label}\n" if epic_key else ""
|
|
|
|
new_task_chunk = (
|
|
f'<a {anchor_attrs}></a>\n'
|
|
f"### {title.strip()}\n"
|
|
f"**ID:** {formatted_id}\n"
|
|
f"**Project:** {proj_label}\n"
|
|
f"{subproj_line}"
|
|
f"{epic_line}"
|
|
f"**Status:** {status_label}\n"
|
|
f"**Type:** {type_label}\n\n"
|
|
f"**Description:**\n"
|
|
f"{desc_body}\n"
|
|
)
|
|
|
|
# 4. Insert before Notes marker
|
|
notes_marker = "\n---\n\n## Notes\n"
|
|
notes_pos = text.find(notes_marker)
|
|
if notes_pos == -1:
|
|
raise ValueError("Could not find ## Notes marker to insert task before")
|
|
|
|
updated_text = text[:notes_pos].rstrip() + "\n\n" + new_task_chunk + "\n---\n\n## Notes\n" + text[notes_pos + len(notes_marker):]
|
|
|
|
with open(tasks_path, "w", encoding="utf-8") as f:
|
|
f.write(updated_text)
|
|
|
|
# 5. Re-run update_tasks_document to sort, standardize, and refresh tables
|
|
update_mod.update_tasks_document(tasks_path)
|
|
|
|
return formatted_id, title.strip(), proj_label, subproj_label, status_label, type_label, tasks_path
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Add a new task to tasks.md and update tables.")
|
|
parser.add_argument("title", help="Title of the new task")
|
|
parser.add_argument("--epic", default=None, help="Epic value or label from frontmatter (optional)")
|
|
parser.add_argument(
|
|
"-p", "--project",
|
|
default="shared",
|
|
help="Project name or value (default: shared)"
|
|
)
|
|
parser.add_argument(
|
|
"-S", "--subproject",
|
|
default=None,
|
|
help="Subproject name or category (optional, e.g. actions, expressions, schema)"
|
|
)
|
|
parser.add_argument(
|
|
"-t", "--type",
|
|
default="feature",
|
|
help="Task type (default: feature, e.g. chore, bug, feature, lint, docs)"
|
|
)
|
|
parser.add_argument(
|
|
"-s", "--status",
|
|
default="triage",
|
|
help="Initial task status (default: triage, e.g. planning, triage, pending, fixed)"
|
|
)
|
|
parser.add_argument(
|
|
"-d", "--description",
|
|
default=None,
|
|
help="Task description and requirements"
|
|
)
|
|
parser.add_argument(
|
|
"-f", "--file",
|
|
default=None,
|
|
help="Path to tasks.md (optional)"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
new_id, title, proj, subproj, status, ttype, tasks_path = add_task(
|
|
title=args.title,
|
|
project=args.project,
|
|
subproject=args.subproject,
|
|
task_type=args.type,
|
|
status=args.status,
|
|
description=args.description,
|
|
tasks_file=args.file,
|
|
epic=args.epic,
|
|
)
|
|
tag_info = f"{proj} | {subproj} | {status} | {ttype}" if subproj else f"{proj} | {status} | {ttype}"
|
|
print(f"Successfully added {new_id}: \"{title}\" [{tag_info}] to {tasks_path}")
|
|
except Exception as e:
|
|
print(f"Error adding task: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|