Skip to main content
Glama
completer.py11.4 kB
"""project completion functionality.""" import re import yaml from pathlib import Path from datetime import date from execution_system_mcp.config import ConfigManager from execution_system_mcp.utils import git_move class ProjectCompleter: """Handles completing projects.""" def __init__(self, config: ConfigManager): """ Initialize ProjectCompleter. Args: config: ConfigManager instance """ self._config = config def find_active_project(self, title: str) -> tuple[Path | None, str]: """ Find an active project by title. Searches all area subdirectories in the active folder for a project with the given title in its YAML frontmatter. Also checks if the project exists in other folders (completed, incubator, descoped). Args: title: Project title (exact match) Returns: Tuple of (project_path, area_kebab) if found in active, or (None, error_message) if not found or not active """ repo_path = Path(self._config.get_repo_path()) projects_base = repo_path / "docs" / "execution_system" / "10k-projects" # Search all area subdirectories in active folder active_base = projects_base / "active" if active_base.exists(): for area_dir in active_base.iterdir(): if not area_dir.is_dir(): continue for project_file in area_dir.glob("*.md"): # Parse frontmatter to check title try: with open(project_file, 'r') as f: lines = [f.readline() for _ in range(20)] # Simple YAML frontmatter parsing in_frontmatter = False project_title = None for line in lines: line = line.strip() if line == "---": if in_frontmatter: break in_frontmatter = True continue if in_frontmatter and line.startswith("title:"): project_title = line.split(":", 1)[1].strip() break if project_title == title: return (project_file, area_dir.name) except Exception: continue # Check if project exists in other folders for folder in ["completed", "incubator", "descoped"]: folder_base = projects_base / folder if not folder_base.exists(): continue for area_dir in folder_base.iterdir(): if not area_dir.is_dir(): continue for project_file in area_dir.glob("*.md"): try: with open(project_file, 'r') as f: lines = [f.readline() for _ in range(20)] in_frontmatter = False project_title = None for line in lines: line = line.strip() if line == "---": if in_frontmatter: break in_frontmatter = True continue if in_frontmatter and line.startswith("title:"): project_title = line.split(":", 1)[1].strip() break if project_title == title: if folder == "completed": return (None, f"Project '{title}' is already completed") else: return (None, f"Project '{title}' is not active (found in {folder})") except Exception: continue return (None, f"Project '{title}' not found in active folder") def check_0k_blockers(self, project_kebab: str) -> list[dict]: """ Check for open 0k horizon items tagged with the project. Scans @waiting.md, @incubating.md, @deferred.md, and contexts/*.md for unchecked items (- [ ]) containing +{project_kebab}. Ignores completed.md file and checked items (- [x]). Args: project_kebab: Project identifier in kebab-case Returns: List of dicts with keys "file" and "line" for each blocking item """ repo_path = Path(self._config.get_repo_path()) next_actions_base = repo_path / "docs" / "execution_system" / "00k-next-actions" blockers = [] project_tag = f"+{project_kebab}" # Files to check (excluding completed.md) files_to_check = [] # Add @ files for filename in ["@waiting.md", "@incubating.md", "@deferred.md"]: file_path = next_actions_base / filename if file_path.exists(): files_to_check.append((filename, file_path)) # Add context files contexts_dir = next_actions_base / "contexts" if contexts_dir.exists(): for context_file in contexts_dir.glob("*.md"): relative_name = f"contexts/{context_file.name}" files_to_check.append((relative_name, context_file)) # Scan each file for unchecked items with project tag for filename, file_path in files_to_check: try: with open(file_path, 'r') as f: for line in f: # Only match unchecked items if line.strip().startswith("- [ ]") and project_tag in line: blockers.append({ "file": filename, "line": line.strip() }) except Exception: continue return blockers def parse_frontmatter(self, file_path: Path) -> dict: """ Parse YAML frontmatter from a project file. Args: file_path: Path to project markdown file Returns: Dict with frontmatter fields (preserves insertion order) """ with open(file_path, 'r') as f: content = f.read() # Extract frontmatter between --- markers parts = content.split("---", 2) if len(parts) < 3: raise ValueError("Invalid frontmatter format") yaml_content = parts[1] frontmatter = yaml.safe_load(yaml_content) # Convert date objects to strings for key, value in frontmatter.items(): if isinstance(value, date): frontmatter[key] = str(value) return frontmatter def add_completed_date(self, frontmatter: dict) -> dict: """ Add completed date to frontmatter dict. Maintains field order: area, title, type, created, started, last_reviewed, due (if present), completed. Args: frontmatter: Existing frontmatter dict Returns: New dict with completed date added """ result = {} completed_value = str(date.today()) # Build in correct order for key in ["area", "title", "type", "created", "started", "last_reviewed", "due"]: if key in frontmatter: result[key] = frontmatter[key] # Add completed at the end result["completed"] = completed_value return result def generate_frontmatter_yaml(self, frontmatter: dict) -> str: """ Generate YAML frontmatter string from dict. Args: frontmatter: Frontmatter dict Returns: YAML frontmatter string with --- delimiters """ lines = ["---"] for key, value in frontmatter.items(): lines.append(f"{key}: {value}") lines.append("---") return "\n".join(lines) + "\n" def complete_project(self, title: str) -> str: """ Complete a project. Validates no open 0k items exist, then moves project from active/ to completed/ folder with completed date added to frontmatter. Args: title: Project title Returns: Success or error message """ # Find active project project_path, area_or_error = self.find_active_project(title) if project_path is None: return f"✗ Error: {area_or_error}" area_kebab = area_or_error project_kebab = project_path.stem # Check for 0k blockers blockers = self.check_0k_blockers(project_kebab) if blockers: error_lines = [f"✗ Cannot complete project '{title}' - {len(blockers)} open item(s) at 0k horizon:", ""] # Group by file by_file = {} for blocker in blockers: file = blocker["file"] if file not in by_file: by_file[file] = [] by_file[file].append(blocker["line"]) for file, lines in by_file.items(): file_display = file.replace("@", "").replace(".md", "").replace("contexts/", "").title() error_lines.append(f"{file_display} ({file}):") for line in lines: # Extract just the description part parts = line.split(" ", 3) if len(parts) >= 4: desc = parts[3].split(" +")[0] error_lines.append(f" • {desc}") error_lines.append("") error_lines.append("Complete or remove these items before completing the project.") return "\n".join(error_lines) # Parse existing frontmatter frontmatter = self.parse_frontmatter(project_path) # Add completed date frontmatter = self.add_completed_date(frontmatter) # Read project body (everything after frontmatter) with open(project_path, 'r') as f: content = f.read() parts = content.split("---", 2) body = parts[2] if len(parts) >= 3 else "" # Generate new frontmatter new_frontmatter = self.generate_frontmatter_yaml(frontmatter) # Update the file in-place with completed date with open(project_path, 'w') as f: f.write(new_frontmatter + body) # Create completed directory if needed repo_path = Path(self._config.get_repo_path()) completed_base = repo_path / "docs" / "execution_system" / "10k-projects" / "completed" completed_area_dir = completed_base / area_kebab completed_area_dir.mkdir(parents=True, exist_ok=True) # Move to completed folder using git mv to preserve history completed_path = completed_area_dir / project_path.name git_move(project_path, completed_path) return f"✓ Successfully completed project '{title}'\n Moved from: {project_path}\n Moved to: {completed_path}\n Completed: {date.today()}"

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/elinsky/execution-system-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server