Initial commit
This commit is contained in:
21
.claude-plugin/plugin.json
Normal file
21
.claude-plugin/plugin.json
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -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
|
||||||
12
agents/do-task.md
Normal file
12
agents/do-task.md
Normal file
@@ -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
|
||||||
27
commands/add-one-task.md
Normal file
27
commands/add-one-task.md
Normal file
@@ -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:
|
||||||
|
|
||||||
|
<description>
|
||||||
|
$ARGUMENTS
|
||||||
|
</description>
|
||||||
|
|
||||||
|
## 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)
|
||||||
48
commands/do-all-tasks.md
Normal file
48
commands/do-all-tasks.md
Normal file
@@ -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:
|
||||||
|
|
||||||
|
<instructions>
|
||||||
|
$ARGUMENTS
|
||||||
|
</instructions>
|
||||||
|
|
||||||
|
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"
|
||||||
7
commands/do-one-task.md
Normal file
7
commands/do-one-task.md
Normal file
@@ -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
|
||||||
36
commands/plan-tasks.md
Normal file
36
commands/plan-tasks.md
Normal file
@@ -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()`.
|
||||||
|
```
|
||||||
21
commands/sweep-todos.md
Normal file
21
commands/sweep-todos.md
Normal file
@@ -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
|
||||||
|
```
|
||||||
85
plugin.lock.json
Normal file
85
plugin.lock.json
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
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