temp
This commit is contained in:
243
scripts/add-task
Executable file
243
scripts/add-task
Executable file
@@ -0,0 +1,243 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user