@jaguilar87/gaia-ops 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +315 -0
- package/CLAUDE.md +154 -0
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/agents/aws-troubleshooter.md +50 -0
- package/agents/claude-architect.md +821 -0
- package/agents/devops-developer.md +92 -0
- package/agents/gcp-troubleshooter.md +50 -0
- package/agents/gitops-operator.md +360 -0
- package/agents/terraform-architect.md +289 -0
- package/bin/gaia-init.js +620 -0
- package/commands/architect.md +97 -0
- package/commands/restore-session.md +87 -0
- package/commands/save-session.md +88 -0
- package/commands/session-status.md +61 -0
- package/commands/speckit.add-task.md +144 -0
- package/commands/speckit.analyze-task.md +65 -0
- package/commands/speckit.implement.md +96 -0
- package/commands/speckit.init.md +237 -0
- package/commands/speckit.plan.md +88 -0
- package/commands/speckit.specify.md +161 -0
- package/commands/speckit.tasks.md +188 -0
- package/config/AGENTS.md +162 -0
- package/config/agent-catalog.md +604 -0
- package/config/context-contracts.md +682 -0
- package/config/git-standards.md +674 -0
- package/config/git_standards.json +69 -0
- package/config/orchestration-workflow.md +735 -0
- package/hooks/__pycache__/post_tool_use.cpython-312.pyc +0 -0
- package/hooks/__pycache__/pre_kubectl_security.cpython-312.pyc +0 -0
- package/hooks/__pycache__/pre_tool_use.cpython-312.pyc +0 -0
- package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
- package/hooks/__pycache__/subagent_stop.cpython-312.pyc +0 -0
- package/hooks/post_tool_use.py +463 -0
- package/hooks/pre_kubectl_security.py +205 -0
- package/hooks/pre_tool_use.py +530 -0
- package/hooks/session_start.py +315 -0
- package/hooks/subagent_stop.py +549 -0
- package/index.js +92 -0
- package/package.json +59 -0
- package/speckit/README.en.md +648 -0
- package/speckit/README.md +353 -0
- package/speckit/governance.md +169 -0
- package/speckit/scripts/check-prerequisites.sh +194 -0
- package/speckit/scripts/common.sh +126 -0
- package/speckit/scripts/create-new-feature.sh +131 -0
- package/speckit/scripts/init.sh +42 -0
- package/speckit/scripts/setup-plan.sh +95 -0
- package/speckit/scripts/update-agent-context.sh +718 -0
- package/speckit/templates/adr-template.md +118 -0
- package/speckit/templates/agent-file-template.md +23 -0
- package/speckit/templates/plan-template.md +233 -0
- package/speckit/templates/spec-template.md +116 -0
- package/speckit/templates/tasks-template-bkp.md +136 -0
- package/speckit/templates/tasks-template.md +345 -0
- package/templates/CLAUDE.template.md +170 -0
- package/templates/code-examples/approval_gate_workflow.py +141 -0
- package/templates/code-examples/clarification_workflow.py +94 -0
- package/templates/code-examples/commit_validation.py +86 -0
- package/templates/project-context.template.json +126 -0
- package/templates/settings.template.json +307 -0
- package/tools/__pycache__/agent_router.cpython-312.pyc +0 -0
- package/tools/__pycache__/approval_gate.cpython-312.pyc +0 -0
- package/tools/__pycache__/clarify_engine.cpython-312.pyc +0 -0
- package/tools/__pycache__/clarify_patterns.cpython-312.pyc +0 -0
- package/tools/__pycache__/commit_validator.cpython-312.pyc +0 -0
- package/tools/__pycache__/context_section_reader.cpython-312.pyc +0 -0
- package/tools/__pycache__/routing_dashboard.cpython-312.pyc +0 -0
- package/tools/__pycache__/routing_feedback.cpython-312.pyc +0 -0
- package/tools/__pycache__/semantic_matcher.cpython-312.pyc +0 -0
- package/tools/__pycache__/task_manager.cpython-312.pyc +0 -0
- package/tools/agent_capabilities.json +231 -0
- package/tools/agent_invoker_helper.py +239 -0
- package/tools/agent_router.py +730 -0
- package/tools/approval_gate.py +318 -0
- package/tools/clarify_engine.py +511 -0
- package/tools/clarify_patterns.py +356 -0
- package/tools/commit_validator.py +338 -0
- package/tools/context_provider.py +181 -0
- package/tools/context_section_reader.py +301 -0
- package/tools/demo_clarify.py +104 -0
- package/tools/generate_embeddings.py +168 -0
- package/tools/quicktriage_aws_troubleshooter.sh +45 -0
- package/tools/quicktriage_devops_developer.sh +38 -0
- package/tools/quicktriage_gcp_troubleshooter.sh +51 -0
- package/tools/quicktriage_gitops_operator.sh +47 -0
- package/tools/quicktriage_terraform_architect.sh +40 -0
- package/tools/semantic_matcher.py +222 -0
- package/tools/task_manager.py +547 -0
- package/tools/task_manager_README.md +395 -0
- package/tools/task_manager_example.py +215 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task Manager
|
|
3
|
+
|
|
4
|
+
Efficient task file operations for large projects without loading entire files.
|
|
5
|
+
This utility is designed to handle tasks.md files that exceed Claude's Read tool
|
|
6
|
+
token limits (>25,000 tokens) by using targeted Grep and Edit operations.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from task_manager import TaskManager
|
|
10
|
+
|
|
11
|
+
# Initialize with path to tasks.md file
|
|
12
|
+
tm = TaskManager("/path/to/tasks.md")
|
|
13
|
+
|
|
14
|
+
# Mark a task as complete
|
|
15
|
+
tm.mark_task_complete("T045")
|
|
16
|
+
|
|
17
|
+
# Get pending tasks
|
|
18
|
+
pending = tm.get_pending_tasks(limit=10)
|
|
19
|
+
|
|
20
|
+
# Get full details for a specific task
|
|
21
|
+
details = tm.get_task_details("T045")
|
|
22
|
+
|
|
23
|
+
Architecture:
|
|
24
|
+
- Uses Grep tool for searching (avoids loading full file)
|
|
25
|
+
- Uses Edit tool for targeted updates (no full-file rewrites)
|
|
26
|
+
- Parses task metadata from HTML comments
|
|
27
|
+
- Handles large files efficiently
|
|
28
|
+
|
|
29
|
+
Task Format Recognition:
|
|
30
|
+
Tasks follow this pattern:
|
|
31
|
+
```
|
|
32
|
+
- [ ] T045 Deploy query-api HelmRelease
|
|
33
|
+
<!-- 🤖 Agent: gitops-operator | ✅ T3 | ⚡ 0.95 -->
|
|
34
|
+
<!-- 🏷️ Tags: #kubernetes #helm -->
|
|
35
|
+
|
|
36
|
+
**Description:** Create HelmRelease manifest...
|
|
37
|
+
|
|
38
|
+
**Acceptance Criteria:**
|
|
39
|
+
- Criterion 1
|
|
40
|
+
- Criterion 2
|
|
41
|
+
```
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
import os
|
|
45
|
+
import re
|
|
46
|
+
import subprocess
|
|
47
|
+
from typing import Dict, List, Any, Optional, Tuple
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TaskManager:
|
|
51
|
+
"""Efficient task file operations for large projects"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, tasks_file_path: str):
|
|
54
|
+
"""
|
|
55
|
+
Initialize with path to tasks.md file
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
tasks_file_path: Absolute path to tasks.md file
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
FileNotFoundError: If tasks.md file doesn't exist
|
|
62
|
+
"""
|
|
63
|
+
self.tasks_file_path = os.path.abspath(tasks_file_path)
|
|
64
|
+
|
|
65
|
+
if not os.path.exists(self.tasks_file_path):
|
|
66
|
+
raise FileNotFoundError(
|
|
67
|
+
f"Tasks file not found: {self.tasks_file_path}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
self.file_dir = os.path.dirname(self.tasks_file_path)
|
|
71
|
+
|
|
72
|
+
def mark_task_complete(self, task_id: str) -> bool:
|
|
73
|
+
r"""
|
|
74
|
+
Mark a task as complete using Grep to find + Edit to update.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
task_id: Task identifier (e.g., "T045")
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
True if task was marked complete, False if not found or already complete
|
|
81
|
+
|
|
82
|
+
Process:
|
|
83
|
+
1. Use Grep to find line: "^- \[ \] {task_id}"
|
|
84
|
+
2. Verify task is pending (has [ ] checkbox)
|
|
85
|
+
3. Replace "- [ ]" with "- [x]" on that specific line
|
|
86
|
+
4. Use Edit to write back
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
ValueError: If task_id not found in file
|
|
90
|
+
"""
|
|
91
|
+
# Sanitize task_id (remove any special chars for safety)
|
|
92
|
+
task_id = task_id.strip().upper()
|
|
93
|
+
|
|
94
|
+
if not re.match(r'^T\d+$', task_id):
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"Invalid task_id format: {task_id}. Must be 'T' followed by digits (e.g., T045)"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Search for the task line using grep
|
|
100
|
+
# Pattern: "- [ ] T045" (pending task)
|
|
101
|
+
try:
|
|
102
|
+
result = subprocess.run(
|
|
103
|
+
['grep', '-n', f'^- \\[ \\] {task_id} ', self.tasks_file_path],
|
|
104
|
+
capture_output=True,
|
|
105
|
+
text=True,
|
|
106
|
+
check=False
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if result.returncode != 0:
|
|
110
|
+
# Task not found or already completed
|
|
111
|
+
# Check if task exists but is already complete
|
|
112
|
+
completed_result = subprocess.run(
|
|
113
|
+
['grep', '-n', f'^- \\[x\\] {task_id} ', self.tasks_file_path],
|
|
114
|
+
capture_output=True,
|
|
115
|
+
text=True,
|
|
116
|
+
check=False
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if completed_result.returncode == 0:
|
|
120
|
+
# Task already complete
|
|
121
|
+
return False
|
|
122
|
+
else:
|
|
123
|
+
# Task not found at all
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"Task {task_id} not found in {self.tasks_file_path}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Parse the line number and content
|
|
129
|
+
# Format: "123:- [ ] T045 Task description"
|
|
130
|
+
line_output = result.stdout.strip()
|
|
131
|
+
if not line_output:
|
|
132
|
+
raise ValueError(f"Task {task_id} not found")
|
|
133
|
+
|
|
134
|
+
# Get the line with task checkbox
|
|
135
|
+
line_content = line_output.split(':', 1)[1] if ':' in line_output else line_output
|
|
136
|
+
|
|
137
|
+
# Replace [ ] with [x]
|
|
138
|
+
new_line_content = line_content.replace('- [ ]', '- [x]', 1)
|
|
139
|
+
|
|
140
|
+
# Use sed to edit the file in place
|
|
141
|
+
# Find the line and replace it
|
|
142
|
+
subprocess.run(
|
|
143
|
+
['sed', '-i', f's/^- \\[ \\] {task_id} /- [x] {task_id} /', self.tasks_file_path],
|
|
144
|
+
check=True
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
except subprocess.CalledProcessError as e:
|
|
150
|
+
raise RuntimeError(f"Failed to mark task complete: {e}")
|
|
151
|
+
|
|
152
|
+
def get_pending_tasks(self, limit: int = 10) -> List[Dict[str, str]]:
|
|
153
|
+
r"""
|
|
154
|
+
Get list of pending tasks using Grep.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
limit: Maximum number of tasks to return
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
List of dicts with keys: task_id, title, line_number
|
|
161
|
+
|
|
162
|
+
Process:
|
|
163
|
+
1. Use Grep: "^- \[ \] T[0-9]+" to find pending tasks
|
|
164
|
+
2. Parse results to extract task ID and title
|
|
165
|
+
3. Return up to 'limit' results
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
# Search for pending tasks: "- [ ] T001"
|
|
169
|
+
result = subprocess.run(
|
|
170
|
+
['grep', '-n', '^- \\[ \\] T[0-9]\\+', self.tasks_file_path],
|
|
171
|
+
capture_output=True,
|
|
172
|
+
text=True,
|
|
173
|
+
check=False
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if result.returncode != 0:
|
|
177
|
+
# No pending tasks found
|
|
178
|
+
return []
|
|
179
|
+
|
|
180
|
+
# Parse the grep output
|
|
181
|
+
# Format: "123:- [ ] T045 Deploy query-api HelmRelease"
|
|
182
|
+
lines = result.stdout.strip().split('\n')
|
|
183
|
+
tasks = []
|
|
184
|
+
|
|
185
|
+
for line in lines[:limit]:
|
|
186
|
+
if not line:
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
# Extract line number and content
|
|
190
|
+
parts = line.split(':', 1)
|
|
191
|
+
if len(parts) != 2:
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
line_number = parts[0]
|
|
195
|
+
content = parts[1]
|
|
196
|
+
|
|
197
|
+
# Extract task ID and title
|
|
198
|
+
# Pattern: "- [ ] T045 Title of the task"
|
|
199
|
+
match = re.match(r'^- \[ \] (T\d+)\s+(.+)$', content)
|
|
200
|
+
if match:
|
|
201
|
+
task_id = match.group(1)
|
|
202
|
+
title = match.group(2)
|
|
203
|
+
|
|
204
|
+
tasks.append({
|
|
205
|
+
'task_id': task_id,
|
|
206
|
+
'title': title,
|
|
207
|
+
'line_number': int(line_number)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
return tasks
|
|
211
|
+
|
|
212
|
+
except subprocess.CalledProcessError as e:
|
|
213
|
+
raise RuntimeError(f"Failed to get pending tasks: {e}")
|
|
214
|
+
|
|
215
|
+
def get_task_details(self, task_id: str) -> Dict[str, Any]:
|
|
216
|
+
"""
|
|
217
|
+
Load full details for a specific task.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
task_id: Task identifier (e.g., "T045")
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Dict with keys: task_id, title, description, metadata, line_number, status
|
|
224
|
+
|
|
225
|
+
Process:
|
|
226
|
+
1. Use Grep to find task start line (both pending and complete)
|
|
227
|
+
2. Read that line + next 20 lines (captures description + metadata)
|
|
228
|
+
3. Parse task block to extract metadata from HTML comments
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
ValueError: If task_id not found
|
|
232
|
+
"""
|
|
233
|
+
# Sanitize task_id
|
|
234
|
+
task_id = task_id.strip().upper()
|
|
235
|
+
|
|
236
|
+
if not re.match(r'^T\d+$', task_id):
|
|
237
|
+
raise ValueError(
|
|
238
|
+
f"Invalid task_id format: {task_id}. Must be 'T' followed by digits"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
# Search for task (both pending and complete)
|
|
243
|
+
# Pattern: "- [ ] T045" or "- [x] T045"
|
|
244
|
+
result = subprocess.run(
|
|
245
|
+
['grep', '-n', f'^- \\[.\\] {task_id} ', self.tasks_file_path],
|
|
246
|
+
capture_output=True,
|
|
247
|
+
text=True,
|
|
248
|
+
check=False
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if result.returncode != 0:
|
|
252
|
+
raise ValueError(f"Task {task_id} not found in {self.tasks_file_path}")
|
|
253
|
+
|
|
254
|
+
# Parse the line number
|
|
255
|
+
line_output = result.stdout.strip()
|
|
256
|
+
line_number = int(line_output.split(':')[0])
|
|
257
|
+
|
|
258
|
+
# Determine status
|
|
259
|
+
status = 'completed' if '- [x]' in line_output else 'pending'
|
|
260
|
+
|
|
261
|
+
# Extract title
|
|
262
|
+
title_match = re.search(r'^- \[.\] T\d+\s+(.+)$', line_output.split(':', 1)[1])
|
|
263
|
+
title = title_match.group(1) if title_match else "Unknown"
|
|
264
|
+
|
|
265
|
+
# Read the task block (next 25 lines for full context)
|
|
266
|
+
# Use sed to extract lines
|
|
267
|
+
end_line = line_number + 25
|
|
268
|
+
lines_result = subprocess.run(
|
|
269
|
+
['sed', '-n', f'{line_number},{end_line}p', self.tasks_file_path],
|
|
270
|
+
capture_output=True,
|
|
271
|
+
text=True,
|
|
272
|
+
check=True
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
task_block = lines_result.stdout
|
|
276
|
+
|
|
277
|
+
# Parse metadata from HTML comments
|
|
278
|
+
metadata = self._parse_task_metadata(task_block)
|
|
279
|
+
|
|
280
|
+
# Extract description
|
|
281
|
+
description = self._extract_description(task_block)
|
|
282
|
+
|
|
283
|
+
# Extract acceptance criteria
|
|
284
|
+
acceptance_criteria = self._extract_acceptance_criteria(task_block)
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
'task_id': task_id,
|
|
288
|
+
'title': title,
|
|
289
|
+
'status': status,
|
|
290
|
+
'line_number': line_number,
|
|
291
|
+
'metadata': metadata,
|
|
292
|
+
'description': description,
|
|
293
|
+
'acceptance_criteria': acceptance_criteria
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
except subprocess.CalledProcessError as e:
|
|
297
|
+
raise RuntimeError(f"Failed to get task details: {e}")
|
|
298
|
+
|
|
299
|
+
def _parse_task_metadata(self, task_block: str) -> Dict[str, Any]:
|
|
300
|
+
"""
|
|
301
|
+
Parse metadata from HTML comments in task block.
|
|
302
|
+
|
|
303
|
+
Extracts:
|
|
304
|
+
- Agent name
|
|
305
|
+
- Security tier
|
|
306
|
+
- Confidence score
|
|
307
|
+
- Tags
|
|
308
|
+
- Skills
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
task_block: Text block containing the task
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Dict with extracted metadata
|
|
315
|
+
"""
|
|
316
|
+
metadata = {
|
|
317
|
+
'agent': None,
|
|
318
|
+
'security_tier': None,
|
|
319
|
+
'confidence': None,
|
|
320
|
+
'tags': [],
|
|
321
|
+
'skill': None,
|
|
322
|
+
'fallback': None,
|
|
323
|
+
'result': None
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
# Extract agent, tier, and confidence
|
|
327
|
+
# Pattern: <!-- 🤖 Agent: gitops-operator | ✅ T3 | ⚡ 0.95 -->
|
|
328
|
+
agent_match = re.search(
|
|
329
|
+
r'<!-- 🤖 Agent: ([a-z-]+) \| [^|]+ ([T0-3]+) \| [^0-9]+ ([\d.]+) -->',
|
|
330
|
+
task_block
|
|
331
|
+
)
|
|
332
|
+
if agent_match:
|
|
333
|
+
metadata['agent'] = agent_match.group(1)
|
|
334
|
+
metadata['security_tier'] = agent_match.group(2)
|
|
335
|
+
metadata['confidence'] = float(agent_match.group(3))
|
|
336
|
+
|
|
337
|
+
# Extract tags
|
|
338
|
+
# Pattern: <!-- 🏷️ Tags: #kubernetes #helm -->
|
|
339
|
+
tags_match = re.search(r'<!-- 🏷️ Tags: (.+) -->', task_block)
|
|
340
|
+
if tags_match:
|
|
341
|
+
tags_str = tags_match.group(1)
|
|
342
|
+
metadata['tags'] = [tag.strip() for tag in tags_str.split('#') if tag.strip()]
|
|
343
|
+
|
|
344
|
+
# Extract skill
|
|
345
|
+
# Pattern: <!-- 🎯 skill: testing_validation (10.0) -->
|
|
346
|
+
skill_match = re.search(r'<!-- 🎯 skill: ([a-z_]+) \(([\d.]+)\) -->', task_block)
|
|
347
|
+
if skill_match:
|
|
348
|
+
metadata['skill'] = {
|
|
349
|
+
'name': skill_match.group(1),
|
|
350
|
+
'score': float(skill_match.group(2))
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
# Extract fallback agent
|
|
354
|
+
# Pattern: <!-- 🔄 Fallback: terraform-architect -->
|
|
355
|
+
fallback_match = re.search(r'<!-- 🔄 Fallback: ([a-z-]+) -->', task_block)
|
|
356
|
+
if fallback_match:
|
|
357
|
+
metadata['fallback'] = fallback_match.group(1)
|
|
358
|
+
|
|
359
|
+
# Extract result
|
|
360
|
+
# Pattern: <!-- 📍 Result: ... -->
|
|
361
|
+
result_match = re.search(r'<!-- 📍 Result: (.+) -->', task_block)
|
|
362
|
+
if result_match:
|
|
363
|
+
metadata['result'] = result_match.group(1)
|
|
364
|
+
|
|
365
|
+
return metadata
|
|
366
|
+
|
|
367
|
+
def _extract_description(self, task_block: str) -> Optional[str]:
|
|
368
|
+
"""
|
|
369
|
+
Extract description from task block.
|
|
370
|
+
|
|
371
|
+
Pattern: **Description:** followed by text until next section
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
task_block: Text block containing the task
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
Description text or None if not found
|
|
378
|
+
"""
|
|
379
|
+
desc_match = re.search(
|
|
380
|
+
r'\*\*Description:\*\*\s+(.+?)(?=\*\*|\n\n|$)',
|
|
381
|
+
task_block,
|
|
382
|
+
re.DOTALL
|
|
383
|
+
)
|
|
384
|
+
if desc_match:
|
|
385
|
+
return desc_match.group(1).strip()
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
def _extract_acceptance_criteria(self, task_block: str) -> List[str]:
|
|
389
|
+
"""
|
|
390
|
+
Extract acceptance criteria from task block.
|
|
391
|
+
|
|
392
|
+
Pattern: **Acceptance Criteria:** followed by bullet list
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
task_block: Text block containing the task
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
List of acceptance criteria strings
|
|
399
|
+
"""
|
|
400
|
+
criteria = []
|
|
401
|
+
|
|
402
|
+
# Find the acceptance criteria section
|
|
403
|
+
criteria_match = re.search(
|
|
404
|
+
r'\*\*Acceptance Criteria:\*\*(.+?)(?=\*\*|\n\n|$)',
|
|
405
|
+
task_block,
|
|
406
|
+
re.DOTALL
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
if criteria_match:
|
|
410
|
+
criteria_text = criteria_match.group(1)
|
|
411
|
+
# Extract bullet points (lines starting with -)
|
|
412
|
+
lines = criteria_text.split('\n')
|
|
413
|
+
for line in lines:
|
|
414
|
+
line = line.strip()
|
|
415
|
+
if line.startswith('- '):
|
|
416
|
+
criteria.append(line[2:].strip())
|
|
417
|
+
|
|
418
|
+
return criteria
|
|
419
|
+
|
|
420
|
+
def get_task_statistics(self) -> Dict[str, Any]:
|
|
421
|
+
"""
|
|
422
|
+
Get overall statistics for the tasks file.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Dict with statistics:
|
|
426
|
+
- total_tasks: Total number of tasks
|
|
427
|
+
- pending_tasks: Number of pending tasks
|
|
428
|
+
- completed_tasks: Number of completed tasks
|
|
429
|
+
- completion_rate: Percentage complete
|
|
430
|
+
"""
|
|
431
|
+
try:
|
|
432
|
+
# Count total tasks (both pending and complete)
|
|
433
|
+
total_result = subprocess.run(
|
|
434
|
+
['grep', '-c', '^- \\[.\\] T[0-9]\\+', self.tasks_file_path],
|
|
435
|
+
capture_output=True,
|
|
436
|
+
text=True,
|
|
437
|
+
check=False
|
|
438
|
+
)
|
|
439
|
+
total_tasks = int(total_result.stdout.strip()) if total_result.returncode == 0 else 0
|
|
440
|
+
|
|
441
|
+
# Count completed tasks
|
|
442
|
+
completed_result = subprocess.run(
|
|
443
|
+
['grep', '-c', '^- \\[x\\] T[0-9]\\+', self.tasks_file_path],
|
|
444
|
+
capture_output=True,
|
|
445
|
+
text=True,
|
|
446
|
+
check=False
|
|
447
|
+
)
|
|
448
|
+
completed_tasks = int(completed_result.stdout.strip()) if completed_result.returncode == 0 else 0
|
|
449
|
+
|
|
450
|
+
# Calculate pending
|
|
451
|
+
pending_tasks = total_tasks - completed_tasks
|
|
452
|
+
|
|
453
|
+
# Calculate completion rate
|
|
454
|
+
completion_rate = (completed_tasks / total_tasks * 100) if total_tasks > 0 else 0.0
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
'total_tasks': total_tasks,
|
|
458
|
+
'pending_tasks': pending_tasks,
|
|
459
|
+
'completed_tasks': completed_tasks,
|
|
460
|
+
'completion_rate': round(completion_rate, 2)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
except Exception as e:
|
|
464
|
+
raise RuntimeError(f"Failed to get task statistics: {e}")
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
# Test function for demonstration
|
|
468
|
+
def test_task_manager():
|
|
469
|
+
"""
|
|
470
|
+
Test function demonstrating usage of TaskManager.
|
|
471
|
+
|
|
472
|
+
This function requires a sample tasks.md file to be present.
|
|
473
|
+
"""
|
|
474
|
+
import sys
|
|
475
|
+
|
|
476
|
+
print("=" * 60)
|
|
477
|
+
print("TaskManager Test Suite")
|
|
478
|
+
print("=" * 60)
|
|
479
|
+
|
|
480
|
+
# Check if path is provided
|
|
481
|
+
if len(sys.argv) < 2:
|
|
482
|
+
print("\nUsage: python task_manager.py <path_to_tasks.md>")
|
|
483
|
+
print("\nExample:")
|
|
484
|
+
print(" python task_manager.py /path/to/tasks.md")
|
|
485
|
+
return
|
|
486
|
+
|
|
487
|
+
tasks_file = sys.argv[1]
|
|
488
|
+
|
|
489
|
+
try:
|
|
490
|
+
# Initialize TaskManager
|
|
491
|
+
print(f"\n1. Initializing TaskManager with: {tasks_file}")
|
|
492
|
+
tm = TaskManager(tasks_file)
|
|
493
|
+
print(" ✅ TaskManager initialized successfully")
|
|
494
|
+
|
|
495
|
+
# Get statistics
|
|
496
|
+
print("\n2. Getting task statistics...")
|
|
497
|
+
stats = tm.get_task_statistics()
|
|
498
|
+
print(f" Total tasks: {stats['total_tasks']}")
|
|
499
|
+
print(f" Pending: {stats['pending_tasks']}")
|
|
500
|
+
print(f" Completed: {stats['completed_tasks']}")
|
|
501
|
+
print(f" Completion rate: {stats['completion_rate']}%")
|
|
502
|
+
|
|
503
|
+
# Get pending tasks
|
|
504
|
+
print("\n3. Getting pending tasks (limit 5)...")
|
|
505
|
+
pending = tm.get_pending_tasks(limit=5)
|
|
506
|
+
print(f" Found {len(pending)} pending tasks:")
|
|
507
|
+
for task in pending:
|
|
508
|
+
print(f" - {task['task_id']}: {task['title']}")
|
|
509
|
+
|
|
510
|
+
# Get details for first pending task
|
|
511
|
+
if pending:
|
|
512
|
+
task_id = pending[0]['task_id']
|
|
513
|
+
print(f"\n4. Getting details for {task_id}...")
|
|
514
|
+
details = tm.get_task_details(task_id)
|
|
515
|
+
print(f" Task ID: {details['task_id']}")
|
|
516
|
+
print(f" Title: {details['title']}")
|
|
517
|
+
print(f" Status: {details['status']}")
|
|
518
|
+
print(f" Agent: {details['metadata'].get('agent', 'N/A')}")
|
|
519
|
+
print(f" Security Tier: {details['metadata'].get('security_tier', 'N/A')}")
|
|
520
|
+
print(f" Tags: {', '.join(details['metadata'].get('tags', []))}")
|
|
521
|
+
|
|
522
|
+
if details.get('description'):
|
|
523
|
+
print(f" Description: {details['description'][:100]}...")
|
|
524
|
+
|
|
525
|
+
# Test marking task complete (COMMENTED OUT - Destructive operation)
|
|
526
|
+
# print(f"\n5. Testing mark_task_complete (dry run)...")
|
|
527
|
+
# print(f" Would mark {task_id} as complete")
|
|
528
|
+
# Uncomment to actually test:
|
|
529
|
+
# result = tm.mark_task_complete(task_id)
|
|
530
|
+
# print(f" Result: {result}")
|
|
531
|
+
|
|
532
|
+
print("\n" + "=" * 60)
|
|
533
|
+
print("✅ All tests passed successfully")
|
|
534
|
+
print("=" * 60)
|
|
535
|
+
|
|
536
|
+
except FileNotFoundError as e:
|
|
537
|
+
print(f"\n❌ Error: {e}")
|
|
538
|
+
sys.exit(1)
|
|
539
|
+
except Exception as e:
|
|
540
|
+
print(f"\n❌ Unexpected error: {e}")
|
|
541
|
+
import traceback
|
|
542
|
+
traceback.print_exc()
|
|
543
|
+
sys.exit(1)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
if __name__ == '__main__':
|
|
547
|
+
test_task_manager()
|