From d78cfb6330902f1d93d0147afbbe2a85aa10b469 Mon Sep 17 00:00:00 2001 From: "Rai (Michael Pokorny)" Date: Tue, 24 Jun 2025 17:32:40 -0700 Subject: [PATCH] agentydragon(tasks): normalize task 18 front-matter; add manager_utils package --- .../18-chat-ui-textarea-overlay-border-fix.md | 4 +- agentydragon/tools/manager_utils/__init__.py | 4 + .../tools/manager_utils/agentydragon_task.py | 93 +++++++++++++++++++ agentydragon/tools/manager_utils/tasklib.py | 35 +++++++ .../tools/manager_utils/tests/test_tasklib.py | 34 +++++++ 5 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 agentydragon/tools/manager_utils/__init__.py create mode 100644 agentydragon/tools/manager_utils/agentydragon_task.py create mode 100644 agentydragon/tools/manager_utils/tasklib.py create mode 100644 agentydragon/tools/manager_utils/tests/test_tasklib.py diff --git a/agentydragon/tasks/18-chat-ui-textarea-overlay-border-fix.md b/agentydragon/tasks/18-chat-ui-textarea-overlay-border-fix.md index 7d2f55b1ae..a80953a27f 100644 --- a/agentydragon/tasks/18-chat-ui-textarea-overlay-border-fix.md +++ b/agentydragon/tasks/18-chat-ui-textarea-overlay-border-fix.md @@ -3,7 +3,7 @@ --- id: 18 title: Chat UI Textarea Overlay and Border Styling Fix -status: Not started # one of: Not started, Started, Needs manual review, Done, Cancelled +status: Not started summary: Fix overlay of waiting messages and streamline borders between chat window and input area to improve visibility and reclaim terminal space. goal: | Adjust the TUI chat interface so that waiting/status messages no longer overlay the first line of the input textarea (ensuring user drafts remain visible), and merge/remove borders as follows: @@ -39,4 +39,4 @@ goal: | Run the frontmatter linter to ensure conformance: ```bash python3 agentydragon/tools/check_task_frontmatter.py agentydragon/tasks/18-chat-ui-textarea-overlay-border-fix.md -``` \ No newline at end of file +``` diff --git a/agentydragon/tools/manager_utils/__init__.py b/agentydragon/tools/manager_utils/__init__.py new file mode 100644 index 0000000000..247f167bc4 --- /dev/null +++ b/agentydragon/tools/manager_utils/__init__.py @@ -0,0 +1,4 @@ +""" +agentydragon manager utilities package. +""" +__version__ = '0.1' diff --git a/agentydragon/tools/manager_utils/agentydragon_task.py b/agentydragon/tools/manager_utils/agentydragon_task.py new file mode 100644 index 0000000000..b00dd4057c --- /dev/null +++ b/agentydragon/tools/manager_utils/agentydragon_task.py @@ -0,0 +1,93 @@ +""" +CLI for managing agentydragon tasks: status, set-status, set-deps, dispose, launch. +""" +import sys +import subprocess +from datetime import datetime +from pathlib import Path +import click +from tasklib import load_task, save_task, TaskMeta + +TASK_DIR = Path(__file__).parent.parent / 'tasks' + +@click.group() +def cli(): + """Manage agentydragon tasks.""" + pass + +@cli.command() +def status(): + """Show a table of task id, title, status, dependencies, last_updated""" + rows = [] + for md in sorted(TASK_DIR.glob('[0-9][0-9]-*.md')): + meta, _ = load_task(md) + rows.append((meta.id, meta.title, meta.status, + meta.dependencies.replace('\n', ' '), + meta.last_updated.strftime('%Y-%m-%d %H:%M'))) + # simple table + fmt = '{:>2} {:<50} {:<12} {:<30} {:<16}' + print(fmt.format('ID','Title','Status','Dependencies','Updated')) + for r in rows: + print(fmt.format(*r)) + +@cli.command() +@click.argument('task_id') +@click.argument('status') +def set_status(task_id, status): + """Set status of TASK_ID to STATUS""" + md = TASK_DIR / f"{task_id}-*.md" + files = list(TASK_DIR.glob(f'{task_id}-*.md')) + if not files: + click.echo(f'Task {task_id} not found', err=True) + sys.exit(1) + path = files[0] + meta, body = load_task(path) + meta.status = status + meta.last_updated = datetime.utcnow() + save_task(path, meta, body) + +@cli.command() +@click.argument('task_id') +@click.argument('deps', nargs=-1) +def set_deps(task_id, deps): + """Set dependencies of TASK_ID""" + files = list(TASK_DIR.glob(f'{task_id}-*.md')) + if not files: + click.echo(f'Task {task_id} not found', err=True) + sys.exit(1) + path = files[0] + meta, body = load_task(path) + now = datetime.utcnow().isoformat() + meta.dependencies = f'as of {now}: ' + ', '.join(deps) + meta.last_updated = datetime.utcnow() + save_task(path, meta, body) + +@cli.command() +@click.argument('task_id', nargs=-1) +def dispose(task_id): + """Dispose worktree and delete branch for TASK_ID(s)""" + for tid in task_id: + branch = f'agentydragon-{tid}-*' + # remove worktree + subprocess.run(['git', 'worktree', 'remove', f'tasks/.worktrees/{tid}-*', '--force']) + # delete branch + subprocess.run(['git', 'branch', '-D', f'agentydragon-{tid}-*']) + click.echo(f'Disposed task {tid}') + +@cli.command() +@click.argument('task_id', nargs=-1) +def launch(task_id): + """Copy tmux launch one-liner for TASK_ID(s) to clipboard""" + cmd = ['create-task-worktree.sh', '--agent', '--tmux'] + list(task_id) + line = ' '.join(cmd) + # system clipboard + try: + subprocess.run(['pbcopy'], input=line.encode(), check=True) + click.echo('Copied to clipboard:') + except FileNotFoundError: + click.echo(line) + return + click.echo(line) + +if __name__ == '__main__': + cli() diff --git a/agentydragon/tools/manager_utils/tasklib.py b/agentydragon/tools/manager_utils/tasklib.py new file mode 100644 index 0000000000..891c20cc65 --- /dev/null +++ b/agentydragon/tools/manager_utils/tasklib.py @@ -0,0 +1,35 @@ +""" +Simple library for loading and saving task metadata embedded as TOML front-matter +in task Markdown files. +""" +import re +import toml +from pathlib import Path +from datetime import datetime +from pydantic import BaseModel, Field + +FRONTMATTER_RE = re.compile(r"^\+\+\+\s*(.*?)\s*\+\+\+", re.S | re.M) + +class TaskMeta(BaseModel): + id: str + title: str + status: str + dependencies: str = Field(default="") + last_updated: datetime = Field(default_factory=datetime.utcnow) + +def load_task(path: Path) -> (TaskMeta, str): + text = path.read_text(encoding='utf-8') + m = FRONTMATTER_RE.match(text) + if not m: + raise ValueError(f"No TOML frontmatter in {path}") + meta = toml.loads(m.group(1)) + tm = TaskMeta(**meta) + body = text[m.end():].lstrip('\n') + return tm, body + +def save_task(path: Path, meta: TaskMeta, body: str) -> None: + tm = meta.dict() + tm['last_updated'] = meta.last_updated.isoformat() + fm = toml.dumps(tm).strip() + content = f"+++\n{fm}\n+++\n\n{body.lstrip()}" + path.write_text(content, encoding='utf-8') diff --git a/agentydragon/tools/manager_utils/tests/test_tasklib.py b/agentydragon/tools/manager_utils/tests/test_tasklib.py new file mode 100644 index 0000000000..de663bd5cf --- /dev/null +++ b/agentydragon/tools/manager_utils/tests/test_tasklib.py @@ -0,0 +1,34 @@ +import tempfile +from pathlib import Path +import toml +import pytest + +from agentydragon.tools.manager_utils.tasklib import TaskMeta, load_task, save_task + +SAMPLE = '''''' +++ +id = "99" +title = "Sample Task" +status = "Not started" +dependencies = "" +last_updated = "2023-01-01T12:00:00" +++ + +# Body here +''' + +def test_load_and_save(tmp_path): + md = tmp_path / '99-sample.md' + md.write_text(SAMPLE) + meta, body = load_task(md) + assert meta.id == '99' + assert 'Body here' in body + meta.status = 'Done' + save_task(md, meta, body) + text = md.read_text() + data = toml.loads(text.split('+++')[1]) + assert data['status'] == 'Done' + +def test_meta_model_validation(): + with pytest.raises(ValueError): + TaskMeta(id='a', title='t', status='bogus', dependencies='', last_updated='bad')