From 3e9fe41aa371dc395a4a71336239e75853b413e7 Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sun, 30 Nov 2025 08:41:13 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 21 ++++ README.md | 3 + agents/do-task.md | 12 ++ commands/add-one-task.md | 27 ++++ commands/do-all-tasks.md | 48 ++++++++ commands/do-one-task.md | 7 ++ commands/plan-tasks.md | 36 ++++++ commands/sweep-todos.md | 21 ++++ plugin.lock.json | 85 +++++++++++++ skills/tasks/SKILL.md | 23 ++++ skills/tasks/scripts/task_add.py | 122 +++++++++++++++++++ skills/tasks/scripts/task_archive.py | 44 +++++++ skills/tasks/scripts/task_complete.py | 169 ++++++++++++++++++++++++++ skills/tasks/scripts/task_get.py | 57 +++++++++ 14 files changed, 675 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 agents/do-task.md create mode 100644 commands/add-one-task.md create mode 100644 commands/do-all-tasks.md create mode 100644 commands/do-one-task.md create mode 100644 commands/plan-tasks.md create mode 100644 commands/sweep-todos.md create mode 100644 plugin.lock.json create mode 100644 skills/tasks/SKILL.md create mode 100755 skills/tasks/scripts/task_add.py create mode 100755 skills/tasks/scripts/task_archive.py create mode 100755 skills/tasks/scripts/task_complete.py create mode 100755 skills/tasks/scripts/task_get.py diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..0a5f631 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,21 @@ +{ + "name": "markdown-tasks", + "description": "Task management system using markdown checkboxes in .llm/todo.md with slash commands for planning, implementing, and tracking tasks", + "version": "0.18.0", + "author": { + "name": "Craig Motlin" + }, + "skills": [ + "./skills/tasks" + ], + "agents": [ + "./agents/do-task.md" + ], + "commands": [ + "./commands/plan-tasks.md", + "./commands/do-one-task.md", + "./commands/add-one-task.md", + "./commands/do-all-tasks.md", + "./commands/sweep-todos.md" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec709ea --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# markdown-tasks + +Task management system using markdown checkboxes in .llm/todo.md with slash commands for planning, implementing, and tracking tasks diff --git a/agents/do-task.md b/agents/do-task.md new file mode 100644 index 0000000..037f891 --- /dev/null +++ b/agents/do-task.md @@ -0,0 +1,12 @@ +--- +name: do-task +description: Use this agent to find and implement the next incomplete task from the project's task list in `.llm/todo.md` +model: inherit +color: purple +permissionMode: acceptEdits +skills: markdown-tasks:tasks +--- + +Find and implement the next incomplete task from the project task list. + +@../shared/task-workflow.md diff --git a/commands/add-one-task.md b/commands/add-one-task.md new file mode 100644 index 0000000..15ec301 --- /dev/null +++ b/commands/add-one-task.md @@ -0,0 +1,27 @@ +--- +argument-hint: task description +description: Add a task to the project task list +--- + +Add a task to the project task list. + +If the user provided a description, it will appear here: + + +$ARGUMENTS + + +## Steps + +1. Extract the description from the user's input +2. If no description was provided, ask the user for one +3. Add the task: + +@../shared/scripts/task-add.md + +4. Confirm to the user that the task was added + +## Notes + +- The description should be clear and actionable +- Do not include the checkbox syntax in the description (the script adds it) diff --git a/commands/do-all-tasks.md b/commands/do-all-tasks.md new file mode 100644 index 0000000..bdaa461 --- /dev/null +++ b/commands/do-all-tasks.md @@ -0,0 +1,48 @@ +--- +argument-hint: optional instructions +description: Process all tasks automatically +--- + +Process all tasks automatically. + +Repeatedly work through incomplete tasks from the project task list. + +If the user provided additional instructions, they will appear here: + + +$ARGUMENTS + + +If the user did not provide instructions, work through ALL incomplete tasks until NONE remain. + +## Steps + +1. Track attempt count and previously attempted tasks to prevent infinite loops +2. Use the `@tasks` skill to extract the first incomplete task from `.llm/todo.md` +3. If a task is found: + - Check if we have already attempted this task 1 time + - If yes, mark it as blocked (with `- [!]`) and continue to next task + - If no, launch the `@tasks:do-task` agent to implement it + - **Do NOT add instructions to the agent prompt** - the agent is self-contained and follows its own workflow (including precommit, commit, rebase) + - Do NOT mark the task as complete yourself - the `do-task` agent does this +4. Repeat until no incomplete tasks remain or the user's instructions are met +5. When all tasks are completed, archive the task list: + +@../shared/scripts/task-archive.md + +## Notes + +- Each task is handled completely by the `do-task` agent before moving to the next +- The `do-task` agent marks tasks as complete - do NOT call `task_complete.py` yourself +- Each task gets its own commit for clear history +- After each agent returns, check the task list again to see if more tasks remain + +## User feedback + +Throughout the process, provide clear status updates: + +- "Starting task: [task description]" +- "Task completed successfully: [task description]" +- "Task failed: [task description]" +- "Skipping blocked task: [task description]" +- "All tasks completed - task list archived to .llm/YYYY-MM-DD-todo.md" or "Stopping due to failures" diff --git a/commands/do-one-task.md b/commands/do-one-task.md new file mode 100644 index 0000000..f99013f --- /dev/null +++ b/commands/do-one-task.md @@ -0,0 +1,7 @@ +--- +description: Find and implement the next incomplete task from the project task list +--- + +Find and implement the next incomplete task from the project task list. + +@../shared/task-workflow.md diff --git a/commands/plan-tasks.md b/commands/plan-tasks.md new file mode 100644 index 0000000..9ff241b --- /dev/null +++ b/commands/plan-tasks.md @@ -0,0 +1,36 @@ +--- +name: plan-tasks +description: Capture conversation planning into self-contained tasks at end of discussion +--- + +# Plan Tasks + +Transform conversation planning and requirements into a markdown task list where each task is completely self-contained with all necessary context inline. + +@../shared/task-format.md + +## When to Use + +Use this command at the **end of a planning conversation** when you have discussed requirements, approaches, and implementation details but have not started coding yet. This captures the conversation context into actionable tasks in `.llm/todo.md`. + +## Input + +The input is the current conversation where planning and requirements have been discussed. Transform the plans, ideas, and requirements from the discussion into self-contained tasks in a markdown checklist format, appended to `.llm/todo.md`. + +## Task Writing Guidelines + +Each task should be written so it can be read independently from `- [ ]` to the next `- [ ]` and contain: + +1. **Full absolute paths** - Never use relative paths +2. **Exact class/function names** - Specify exact names of code elements +3. **Analogies to existing code** - Reference similar existing implementations +4. **Specific implementation details** - List concrete methods or operations +5. **Module/package context** - State which module or package the work belongs to +6. **Dependencies and prerequisites** - Note what needs to exist or be imported +7. **Expected outcomes** - Describe what success looks like + +## Example + +```markdown +- [ ] Create a new test class `SynchronizedBagTest` at `/Users/craig/projects/eclipse-collections/unit-tests-thread-safety/src/test/java/org/eclipse/collections/impl/bag/mutable/SynchronizedBagTest.java` to test thread-safety of `org.eclipse.collections.impl.bag.mutable.SynchronizedBag`. Similar to how `SynchronizedMutableListTest` covers `SynchronizedMutableList`, this should extend `SynchronizedTestTrait` and implement test traits like `SynchronizedCollectionTestTrait`, `SynchronizedMutableIterableTestTrait`, and `SynchronizedRichIterableTestTrait`. The test should verify that all public methods of SynchronizedBag properly synchronize on the lock object using the `assertSynchronized()` method. Include tests for bag-specific methods like `addOccurrences()`, `removeOccurrences()`, `occurrencesOf()`, `forEachWithOccurrences()`, and `toMapOfItemToCount()`. +``` diff --git a/commands/sweep-todos.md b/commands/sweep-todos.md new file mode 100644 index 0000000..cd67f44 --- /dev/null +++ b/commands/sweep-todos.md @@ -0,0 +1,21 @@ +--- +description: Find all TODO and TASK comments and add them to the project task list +--- + +Find all TODO and TASK comments and add them to the project task list. + +Search the codebase for all TODO and TASK comments and add them to `.llm/todo.md`. Each TODO or TASK found in the code will be converted to a task in the markdown task list. + +## Steps + +1. Find all occurrences of "TODO" in the codebase using grep/search +2. For each occurrence, gather: + - File path + - Line number + - Full TODO comment text +3. Strip comment markers (`//`, `#`, `/* */`) from the TODO/TASK text +4. Add each TODO or TASK as a new task entry to `.llm/todo.md`: + ```markdown + - [ ] Implement TODO from src/api/client.ts:87: Extract commonality in getRootNodes and getChildNodes + - [ ] Implement TODO from test/utils.test.ts:103: Use deep object equality rather than loose assertions + ``` diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..23fc28b --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,85 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:motlin/claude-code-plugins:plugins/markdown-tasks", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "177f228e136295340caff5d8410440fb2143fe86", + "treeHash": "f4fad57ef59e81c61ed2c1d04b8c0a17dc1d0260dc166a0fa66e5909543066a3", + "generatedAt": "2025-11-28T10:27:09.088941Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "markdown-tasks", + "description": "Task management system using markdown checkboxes in .llm/todo.md with slash commands for planning, implementing, and tracking tasks", + "version": "0.18.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "3975783eb0a8e49f2b00604624893d1c539d26be75f5ea057a25ef680d5c8a40" + }, + { + "path": "agents/do-task.md", + "sha256": "825f50123fe122f5b4a7a2affadab0dbc373b9e74151f12ecadd0fe59683a2d7" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "750cf0aa76846c588e963ae904dc4a4a3da56c09de129044885a2a4e2a0b2ea3" + }, + { + "path": "commands/do-one-task.md", + "sha256": "e5436e7581957bc2c62cd06d3e5a1ecb03cfa27274fe251f6633ef417ca21ecf" + }, + { + "path": "commands/plan-tasks.md", + "sha256": "db7846ca1ffededc0ff4d5d3220ff806e7845f9ede591deed4bc6aa05fb27954" + }, + { + "path": "commands/do-all-tasks.md", + "sha256": "1f3502228d189bf2fd648937fed388bb8c39554a9465205134068c5d5cd379cb" + }, + { + "path": "commands/sweep-todos.md", + "sha256": "c2d0d2165934653badd1fef87b2592598b0740ea2c403ca9afbaf613ab645678" + }, + { + "path": "commands/add-one-task.md", + "sha256": "867549eec6d38b9b70198439b3966d85698718564d58317cff4ac3081a1e4bff" + }, + { + "path": "skills/tasks/SKILL.md", + "sha256": "d5b94a1d56e2dcc6679baa0cf582feecde1f52ae3a9c77f2da751edb92c96af7" + }, + { + "path": "skills/tasks/scripts/task_add.py", + "sha256": "7b9371f634aaba2bbe80ce1d6d262b56bd44dcb5c1a1dac65e159093354e5664" + }, + { + "path": "skills/tasks/scripts/task_get.py", + "sha256": "562522e7d42601cc374a8d09e2fb04ea4aaa3317cb3fbebe6c2e88a3139e2d60" + }, + { + "path": "skills/tasks/scripts/task_complete.py", + "sha256": "b2ea60c049f6ebf3ed1f8cf08689dd3802299a7c8a603b2c43ae29731932c7fb" + }, + { + "path": "skills/tasks/scripts/task_archive.py", + "sha256": "28d0b25bbc36fbd3d854f75cffc354fe358596a6ccc55c3ca20c4e84d074ebb7" + } + ], + "dirSha256": "f4fad57ef59e81c61ed2c1d04b8c0a17dc1d0260dc166a0fa66e5909543066a3" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/tasks/SKILL.md b/skills/tasks/SKILL.md new file mode 100644 index 0000000..78c417a --- /dev/null +++ b/skills/tasks/SKILL.md @@ -0,0 +1,23 @@ +--- +name: markdown-tasks +description: Work with markdown-based task lists in .llm/todo.md files. Use when managing tasks, working with todo lists, extracting incomplete tasks, marking tasks complete, or implementing tasks from a task list. +--- + +# Markdown Task Management + +This skill enables working with markdown task lists stored in `.llm/todo.md` at the repository root. + +See [shared/task-format.md](../../shared/task-format.md) for task format and location. + +## Scripts + +| Script | Purpose | Documentation | +| ------------------ | ------------------------ | --------------------------------------------------------- | +| `task_get.py` | Extract first incomplete | [task-get.md](../../shared/scripts/task-get.md) | +| `task_add.py` | Add new task | [task-add.md](../../shared/scripts/task-add.md) | +| `task_complete.py` | Mark task done | [task-complete.md](../../shared/scripts/task-complete.md) | +| `task_archive.py` | Archive completed list | [task-archive.md](../../shared/scripts/task-archive.md) | + +## Dependencies + +These scripts require Python 3 with standard library only (no external packages needed). diff --git a/skills/tasks/scripts/task_add.py b/skills/tasks/scripts/task_add.py new file mode 100755 index 0000000..c1d230f --- /dev/null +++ b/skills/tasks/scripts/task_add.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 + +import sys +import os +import subprocess +import argparse + + +def find_git_root(start_path): + try: + result = subprocess.run( + ["git", "-C", start_path, "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + except subprocess.CalledProcessError: + return None + + +def is_file_in_git_status(filename): + directory = os.path.dirname(filename) or "." + git_root = find_git_root(directory) + + if not git_root: + return False + + absolute_filename = os.path.realpath(filename) + git_root_real = os.path.realpath(git_root) + relative_filename = os.path.relpath(absolute_filename, git_root_real) + + try: + result = subprocess.run( + ["git", "-C", git_root, "status", "--short", relative_filename], + capture_output=True, + text=True, + check=True, + ) + return bool(result.stdout.strip()) + except subprocess.CalledProcessError: + return False + + +def add_to_git_exclude(filename): + directory = os.path.dirname(filename) + if not directory: + return False + + git_root = find_git_root(directory) + if not git_root: + return False + + exclude_file = os.path.join(git_root, ".git", "info", "exclude") + exclude_dir = os.path.dirname(exclude_file) + + if not os.path.exists(exclude_dir): + return False + + llm_relative = "/.llm" + + if os.path.exists(exclude_file): + with open(exclude_file, "r") as file: + content = file.read() + if llm_relative in content.splitlines(): + return True + + with open(exclude_file, "a") as file: + if os.path.getsize(exclude_file) > 0: + content = open(exclude_file, "r").read() + if not content.endswith("\n"): + file.write("\n") + file.write(f"{llm_relative}\n") + + return True + + +def ensure_gitignored(filename): + if not is_file_in_git_status(filename): + return + + if not add_to_git_exclude(filename): + return + + if is_file_in_git_status(filename): + print( + f"Warning: {filename} is tracked by git and cannot be excluded. Run: git rm --cached {filename}", + file=sys.stderr, + ) + + +def add_task(filename, description): + try: + directory = os.path.dirname(filename) + if directory and not os.path.exists(directory): + os.makedirs(directory) + + file_exists = os.path.exists(filename) + + with open(filename, "a") as file: + if file_exists and os.path.getsize(filename) > 0: + file.write("\n") + + file.write(f"- [ ] {description}\n") + + ensure_gitignored(filename) + + print(f"- [ ] {description}") + + except Exception as exception: + print(f"Error: {exception}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Add a task to the task list") + parser.add_argument("filename", help="File containing tasks") + parser.add_argument("description", help="Task description") + + args = parser.parse_args() + + add_task(args.filename, args.description) diff --git a/skills/tasks/scripts/task_archive.py b/skills/tasks/scripts/task_archive.py new file mode 100755 index 0000000..9f3d84c --- /dev/null +++ b/skills/tasks/scripts/task_archive.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +import sys +import os +from datetime import datetime + + +def archive_task_file(filename): + try: + if not os.path.exists(filename): + print(f"No file to archive (file doesn't exist): {filename}", file=sys.stderr) + sys.exit(1) + + directory = os.path.dirname(filename) + basename = os.path.basename(filename) + name_without_ext, extension = os.path.splitext(basename) + + timestamp = datetime.now().strftime("%Y-%m-%d") + archived_filename = os.path.join(directory, f"{timestamp}-{name_without_ext}{extension}") + + counter = 1 + while os.path.exists(archived_filename): + archived_filename = os.path.join( + directory, f"{timestamp}-{name_without_ext}-{counter}{extension}" + ) + counter += 1 + + os.rename(filename, archived_filename) + print(f"Archived to: {archived_filename}") + + except FileNotFoundError: + print(f"Error: File '{filename}' not found", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: task_archive.py ", file=sys.stderr) + sys.exit(1) + + archive_task_file(sys.argv[1]) diff --git a/skills/tasks/scripts/task_complete.py b/skills/tasks/scripts/task_complete.py new file mode 100755 index 0000000..6b86f93 --- /dev/null +++ b/skills/tasks/scripts/task_complete.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 + +import sys +import os +import subprocess +import re +import argparse + + +def find_git_root(start_path): + try: + result = subprocess.run( + ["git", "-C", start_path, "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + except subprocess.CalledProcessError: + return None + + +def is_file_in_git_status(filename): + directory = os.path.dirname(filename) or "." + git_root = find_git_root(directory) + + if not git_root: + return False + + absolute_filename = os.path.realpath(filename) + git_root_real = os.path.realpath(git_root) + relative_filename = os.path.relpath(absolute_filename, git_root_real) + + try: + result = subprocess.run( + ["git", "-C", git_root, "status", "--short", relative_filename], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + except subprocess.CalledProcessError: + return False + + +def is_file_tracked(filename): + directory = os.path.dirname(filename) or "." + git_root = find_git_root(directory) + + if not git_root: + return False + + absolute_filename = os.path.realpath(filename) + git_root_real = os.path.realpath(git_root) + relative_filename = os.path.relpath(absolute_filename, git_root_real) + + try: + result = subprocess.run( + ["git", "-C", git_root, "ls-files", relative_filename], + capture_output=True, + text=True, + check=True, + ) + return bool(result.stdout.strip()) + except subprocess.CalledProcessError: + return False + + +def verify_gitignored(filename): + status = is_file_in_git_status(filename) + if not status: + return + + if is_file_tracked(filename): + print( + f"Warning: {filename} is tracked by git and cannot be excluded. Run: git rm --cached {filename}", + file=sys.stderr, + ) + else: + print( + f"Warning: {filename} is not gitignored. Add /.llm to .git/info/exclude", + file=sys.stderr, + ) + + +def mark_first_task(filename, mark_type): + try: + if not os.path.exists(filename): + print(f"No tasks found (file doesn't exist)", file=sys.stderr) + sys.exit(1) + + with open(filename, "r") as file: + lines = file.readlines() + + modified = False + task_lines = [] + found_task = False + + for i, line in enumerate(lines): + if re.match(r"^- \[ \]", line): + if mark_type == "progress": + lines[i] = re.sub(r"^- \[ \]", "- [>]", line) + else: + lines[i] = re.sub(r"^- \[ \]", "- [x]", line) + + task_lines.append(lines[i]) + modified = True + found_task = True + + j = i + 1 + while j < len(lines): + next_line = lines[j] + if re.match(r"^[\s\t]+", next_line) and next_line.strip(): + task_lines.append(next_line) + elif re.match(r"^- \[[x>\s]\]", next_line): + break + elif re.match(r"^#", next_line): + break + elif next_line.strip() == "": + task_lines.append(next_line) + else: + break + j += 1 + break + + if modified: + with open(filename, "w") as file: + file.writelines(lines) + + verify_gitignored(filename) + + while task_lines and task_lines[-1].strip() == "": + task_lines.pop() + + print("".join(task_lines), end="") + else: + print("No incomplete tasks found", file=sys.stderr) + sys.exit(1) + + except FileNotFoundError: + print(f"Error: File '{filename}' not found", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Mark first incomplete task as done or in-progress" + ) + parser.add_argument("filename", help="File containing tasks") + parser.add_argument( + "--progress", + action="store_true", + help="Mark as in-progress [>] instead of done [x]", + ) + parser.add_argument( + "--done", action="store_true", help="Mark as done [x] (default)" + ) + + args = parser.parse_args() + + if args.progress and args.done: + print("Error: Cannot specify both --progress and --done", file=sys.stderr) + sys.exit(1) + + mark_type = "progress" if args.progress else "done" + mark_first_task(args.filename, mark_type) diff --git a/skills/tasks/scripts/task_get.py b/skills/tasks/scripts/task_get.py new file mode 100755 index 0000000..bb98de1 --- /dev/null +++ b/skills/tasks/scripts/task_get.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 + +import sys +import os +import re + + +def extract_first_task(filename): + try: + if not os.path.exists(filename): + print(f"No tasks found (file doesn't exist)", file=sys.stderr) + sys.exit(1) + + with open(filename, "r") as file: + lines = file.readlines() + + task_lines = [] + in_task = False + + for i, line in enumerate(lines): + if re.match(r"^- \[ \]", line): + if in_task: + break + task_lines.append(line) + in_task = True + elif in_task: + if re.match(r"^[\s\t]+", line) and line.strip(): + task_lines.append(line) + elif re.match(r"^- \[[x>]\]", line): + break + elif re.match(r"^#", line): + break + elif line.strip() == "": + task_lines.append(line) + else: + break + + while task_lines and task_lines[-1].strip() == "": + task_lines.pop() + + if task_lines: + print("".join(task_lines), end="") + + except FileNotFoundError: + print(f"Error: File '{filename}' not found", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: task-get ", file=sys.stderr) + sys.exit(1) + + extract_first_task(sys.argv[1])