Initial commit
This commit is contained in:
23
skills/tasks/SKILL.md
Normal file
23
skills/tasks/SKILL.md
Normal file
@@ -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).
|
||||
122
skills/tasks/scripts/task_add.py
Executable file
122
skills/tasks/scripts/task_add.py
Executable file
@@ -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)
|
||||
44
skills/tasks/scripts/task_archive.py
Executable file
44
skills/tasks/scripts/task_archive.py
Executable file
@@ -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 <filename>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
archive_task_file(sys.argv[1])
|
||||
169
skills/tasks/scripts/task_complete.py
Executable file
169
skills/tasks/scripts/task_complete.py
Executable file
@@ -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)
|
||||
57
skills/tasks/scripts/task_get.py
Executable file
57
skills/tasks/scripts/task_get.py
Executable file
@@ -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 <filename>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
extract_first_task(sys.argv[1])
|
||||
Reference in New Issue
Block a user