1 line
250 KiB
JSON
1 line
250 KiB
JSON
{"repo": "albertoclemente/swpp", "files": {"swpp_speckit.py": {"9dd4721755fb8a69a2d24d9619ea805ff390d527": {"t": 1785746643.6751392, "n": 50259, "h": "79b305e67fe81ede", "c": "# swpp_speckit.py\n# SWPP Runner with SpecKit (spec-driven planning) + SWPP Agents (execution)\n#\n# SpecKit phases (via `claude -p` slash commands) - ALL MANDATORY:\n# /speckit.constitution → project principles (REQUIRED)\n# /speckit.specify → feature specification (spec.md) (REQUIRED)\n# /speckit.clarify → de-risk ambiguity in spec (REQUIRED)\n# /speckit.plan → implementation plan (plan.md) (REQUIRED)\n# /speckit.checklist → quality validation checklists (REQUIRED)\n# /speckit.tasks → actionable task list (tasks.md) (REQUIRED)\n# /speckit.analyze → cross-artifact consistency check (REQUIRED)\n# /speckit.implement → SKIPPED (SWPP agents handle execution)\n#\n# Execution: SWPP AI agents (Anthropic API or CrewAI)\n#\n# Human Interaction: ONE checkpoint AFTER all phases to review generated artifacts\n#\n# Requires: `specify` CLI (GitHub Spec Kit) + `claude` CLI (Claude Code)\n#\n# Run: python swpp_speckit.py \"Your feature description\"\n\nimport os\nimport sys\nimport json\nimport re\nimport time\nimport shutil\nimport subprocess\nimport argparse\nfrom datetime import datetime\nfrom pathlib import Path\nfrom textwrap import dedent\n\n# =============================================================================\n# CONFIGURATION\n# =============================================================================\n\n# PROJECT_ROOT is set dynamically based on --project flag (see main)\nPROJECT_ROOT = Path.cwd()\n\ndef get_project_paths(project_root: Path) -> dict:\n \"\"\"Return all project paths based on root directory.\"\"\"\n return {\n \"root\": project_root,\n \"swpp_dir\": project_root / \".swpp\",\n \"plans_dir\": project_root / \".swpp\" / \"plans\",\n \"context_file\": project_root / \".swpp\" / \"context.md\",\n \"speckit_dir\": project_root / \".specify\",\n \"claude_commands_dir\": project_root / \".claude\" / \"commands\",\n \"specs_dir\": project_root / \"specs\",\n }\n\n# Default paths (updated when --project is used)\nSWPP_DIR = PROJECT_ROOT / \".swpp\"\nPLANS_DIR = SWPP_DIR / \"plans\"\nCONTEXT_FILE = SWPP_DIR / \"context.md\"\nSPECKIT_DIR = PROJECT_ROOT / \".specify\"\nCLAUDE_COMMANDS_DIR = PROJECT_ROOT / \".claude\" / \"commands\"\nSPECS_DIR = PROJECT_ROOT / \"specs\"\n\nFAST_MODE_TASK_LIMIT = 3\nHIGH_RISK_KEYWORDS = {\n \"auth\", \"authentication\", \"authorization\", \"permission\", \"permissions\",\n \"secret\", \"token\", \"password\", \"credential\", \"oauth\", \"security\",\n \"encryption\", \"decrypt\", \"migration\", \"schema\", \"database\", \"sql\",\n \"prod\", \"production\", \"billing\", \"payment\", \"compliance\",\n}\n\n# Claude Code CLI timeout per phase (seconds)\nSPECKIT_PHASE_TIMEOUT = 300 # 5 min per slash command\n\n# Model Configuration\nEXECUTOR_MODEL = os.getenv(\"SWPP_EXECUTOR_MODEL\", \"deepseek-chat\") # or \"claude-sonnet-4-5-20250929\"\nREVIEWER_MODEL = os.getenv(\"SWPP_REVIEWER_MODEL\", \"claude-sonnet-4-5-20250929\")\n\n# Executor backend: \"crewai\" or \"anthropic\"\n# crewai uses CrewAI agents, anthropic uses raw Anthropic SDK\nDEFAULT_EXECUTOR_BACKEND = \"anthropic\"\n\n\n# =============================================================================\n# PREREQUISITES\n# =============================================================================\n\ndef _check_cli(name: str, check_cmd: list[str]) -> bool:\n \"\"\"Verify a CLI tool is installed and reachable.\"\"\"\n try:\n subprocess.run(check_cmd, capture_output=True, timeout=10)\n return True\n except (FileNotFoundError, subprocess.TimeoutExpired):\n return False\n\n\ndef check_prerequisites() -> list[str]:\n \"\"\"Return list of missing prerequisites.\"\"\"\n missing = []\n if not _check_cli(\"specify\", [\"specify\", \"version\"]):\n missing.append(\"specify (GitHub Spec Kit) — install: https://github.com/specifyit/specify\")\n if not _check_cli(\"claude\", [\"claude\", \"--version\"]):\n missing.append(\"claude (Claude Code CLI) — install: https://docs.anthropic.com/en/docs/claude-code\")\n if not os.getenv(\"ANTHROPIC_API_KEY\"):\n missing.append(\"ANTHROPIC_API_KEY environment variable\")\n return missing\n\n\n# =============================================================================\n# PROJECT SETUP\n# =============================================================================\n\ndef setup_project_directory(project_name: str, base_dir: Path = Path.home() / \"projects\") -> Path:\n \"\"\"Create project directory and initialize basic structure.\n \n Args:\n project_name: Name of the project (e.g., \"HelloWorld\")\n base_dir: Parent directory where project will be created\n \n Returns:\n Path to the created/existing project directory\n \"\"\"\n # Sanitize project name (remove spaces, special chars)\n safe_name = re.sub(r'[^a-zA-Z0-9_-]', '-', project_name)\n project_path = base_dir / safe_name\n \n if project_path.exists():\n print(f\" ℹ️ Project directory already exists: {project_path}\")\n else:\n print(f\" 📁 Creating project directory: {project_path}\")\n project_path.mkdir(parents=True, exist_ok=True)\n \n # Create basic Python project structure\n (project_path / \"src\").mkdir(exist_ok=True)\n (project_path / \"tests\").mkdir(exist_ok=True)\n (project_path / \"tests\" / \"unit\").mkdir(exist_ok=True)\n (project_path / \"tests\" / \"integration\").mkdir(exist_ok=True)\n (project_path / \"tests\" / \"safety\").mkdir(exist_ok=True)\n (project_path / \"tests\" / \"solid\").mkdir(exist_ok=True)\n \n # Create __init__.py files\n (project_path / \"src\" / \"__init__.py\").touch()\n (project_path / \"tests\" / \"__init__.py\").touch()\n (project_path / \"tests\" / \"unit\" / \"__init__.py\").touch()\n (project_path / \"tests\" / \"integration\" / \"__init__.py\").touch()\n (project_path / \"tests\" / \"safety\" / \"__init__.py\").touch()\n (project_path / \"tests\" / \"solid\" / \"__init__.py\").touch()\n \n # Initialize git if not already a repo\n if not (project_path / \".git\").exists():\n print(\" 🔧 Initializing git repository...\")\n subprocess.run([\"git\", \"init\"], cwd=project_path, capture_output=True)\n subprocess.run([\"git\", \"checkout\", \"-b\", \"main\"], cwd=project_path, capture_output=True)\n \n # Create .swpp/context.md with template\n swpp_dir = project_path / \".swpp\"\n swpp_dir.mkdir(exist_ok=True)\n context_file = swpp_dir / \"context.md\"\n \n if not context_file.exists():\n print(\" 📝 Creating context.md template...\")\n context_template = dedent(f\"\"\"\n # {safe_name} - Project Context\n \n ## Project Overview\n \n **Purpose**: [Describe what this project does and why it exists]\n \n **Target Users**: [Who will use this?]\n \n **Key Features**:\n - [Feature 1]\n - [Feature 2]\n - [Feature 3]\n \n ## Technical Stack\n \n **Language**: Python 3.13+\n \n **Framework**: [e.g., FastAPI, Django, Flask, or N/A for libraries]\n \n **Database**: [e.g., PostgreSQL, MySQL, SQLite, or N/A]\n \n **Key Dependencies**:\n - [Library 1]\n - [Library 2]\n \n ## Architecture & Patterns\n \n **Project Structure**:\n ```\n src/ # Application code\n tests/ # Test suites\n ```\n \n **Design Patterns**: [e.g., MVC, Repository, Service Layer]\n \n ## Coding Standards\n \n **Style Guide**: PEP 8 + type hints\n \n **Naming Conventions**:\n - Classes: PascalCase\n - Functions/variables: snake_case\n - Constants: UPPER_SNAKE_CASE\n \n **Testing Requirements**:\n - Unit tests for all business logic\n - Integration tests for API endpoints\n - Safety tests for error handling\n - SOLID principle validation\n \n ## Development Guidelines\n \n **Git Workflow**: Feature branches → main\n \n **Commit Format**: `type: description [TASK-ID]`\n - Types: feat, fix, docs, test, refactor\n \n **Documentation**: Docstrings for all public APIs\n \n ## Notes\n \n [Add any project-specific notes, constraints, or considerations]\n \"\"\").strip()\n context_file.write_text(context_template)\n print(f\" ⚠️ IMPORTANT: Edit .swpp/context.md with your project details!\")\n \n print(f\" ✅ Project structure created\")\n \n return project_path\n\n\n# =============================================================================\n# SPECKIT SCAFFOLD\n# =============================================================================\n\ndef ensure_speckit_scaffold() -> bool:\n \"\"\"Ensure SpecKit project scaffold exists (.specify/ and .claude/commands/).\"\"\"\n if SPECKIT_DIR.exists() and CLAUDE_COMMANDS_DIR.exists():\n print(\" ✅ SpecKit scaffold already exists\")\n return True\n\n print(\" 🔧 Initializing SpecKit scaffold automatically...\")\n print(\" (This may take 30-60s to download template from GitHub)\")\n \n result = subprocess.run(\n [\"specify\", \"init\", \".\", \"--ai\", \"claude\", \"--here\", \"--force\", \"--no-git\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=180,\n )\n \n if result.returncode != 0:\n print(f\" ❌ specify init failed:\\n{result.stderr}\")\n return False\n\n # Verify key artefacts\n if not CLAUDE_COMMANDS_DIR.exists():\n print(\" ❌ .claude/commands/ not created – specify init may have failed silently\")\n return False\n\n print(\" ✅ SpecKit scaffold created successfully\")\n return True\n\n\n# =============================================================================\n# CLAUDE CODE SLASH COMMAND RUNNER\n# =============================================================================\n\ndef run_speckit_command(\n slash_command: str,\n args: str = \"\",\n context: str = \"\",\n timeout: int = SPECKIT_PHASE_TIMEOUT,\n) -> tuple[bool, str]:\n \"\"\"\n Execute a SpecKit slash command via ``claude -p``.\n \n Args:\n slash_command: SpecKit command name (e.g., \"speckit.specify\")\n args: Feature description or command arguments\n context: Project context to inject into prompt\n timeout: Command timeout in seconds\n\n Returns (success, output_text).\n \"\"\"\n # Build prompt with context injection\n if context:\n prompt = f\"/{slash_command} CONTEXT: {args}\"\n else:\n prompt = f\"/{slash_command} {args}\".strip()\n \n cmd = [\n \"claude\",\n \"-p\", prompt,\n \"--output-format\", \"text\",\n \"--allowedTools\", \"Bash Read Write Edit\",\n ]\n \n # Add context as environment or file if needed\n if context:\n # Write context to temp file that Claude can read\n context_temp = PROJECT_ROOT / \".swpp\" / \"_temp_context.md\"\n context_temp.write_text(context)\n \n print(f\" ▸ claude -p \\\"/{slash_command} {args[:60]}{'…' if len(args) > 60 else ''}\\\"\")\n if context:\n print(f\" (with project context: {len(context)} chars)\")\n\n try:\n result = subprocess.run(\n cmd, cwd=PROJECT_ROOT,\n capture_output=True, text=True,\n timeout=timeout,\n )\n output = result.stdout + result.stderr\n success = result.returncode == 0\n if not success:\n print(f\" ⚠️ /{slash_command} exited with code {result.returncode}\")\n return success, output\n except subprocess.TimeoutExpired:\n print(f\" ⚠️ /{slash_command} timed out after {timeout}s\")\n return False, f\"Timed out after {timeout}s\"\n\n\n# =============================================================================\n# SPECKIT PLANNING PHASES\n# =============================================================================\n\ndef phase_constitution(context: str = \"\", args: str = \"\") -> tuple[bool, str]:\n \"\"\"Establish project governing principles via /speckit.constitution.\"\"\"\n print(\"\\n📜 SPECKIT: Establishing project constitution…\")\n return run_speckit_command(\"speckit.constitution\", args, context)\n\n\ndef phase_specify(feature: str, context: str = \"\") -> tuple[bool, str]:\n \"\"\"Generate feature specification via /speckit.specify.\"\"\"\n print(\"\\n📋 SPECKIT: Generating feature specification…\")\n return run_speckit_command(\"speckit.specify\", feature, context)\n\n\ndef phase_clarify(context: str = \"\", args: str = \"\") -> tuple[bool, str]:\n \"\"\"Clarify underspecified areas in spec via /speckit.clarify.\"\"\"\n print(\"\\n🔎 SPECKIT: Clarifying specification…\")\n return run_speckit_command(\"speckit.clarify\", args, context)\n\n\ndef phase_plan(context: str = \"\") -> tuple[bool, str]:\n \"\"\"Generate implementation plan via /speckit.plan.\"\"\"\n print(\"\\n📐 SPECKIT: Generating implementation plan…\")\n return run_speckit_command(\"speckit.plan\", \"\", context)\n\n\ndef phase_checklist(context: str = \"\", args: str = \"\") -> tuple[bool, str]:\n \"\"\"Generate quality checklists via /speckit.checklist.\"\"\"\n print(\"\\n✅ SPECKIT: Generating quality checklist…\")\n return run_speckit_command(\"speckit.checklist\", args, context)\n\n\ndef phase_tasks(context: str = \"\") -> tuple[bool, str]:\n \"\"\"Generate actionable tasks via /speckit.tasks.\"\"\"\n print(\"\\n📝 SPECKIT: Generating actionable task list…\")\n return run_speckit_command(\"speckit.tasks\", \"\", context)\n\n\ndef phase_analyze(context: str = \"\") -> tuple[bool, str]:\n \"\"\"Cross-artifact consistency analysis via /speckit.analyze.\"\"\"\n print(\"\\n🔬 SPECKIT: Analyzing cross-artifact consistency…\")\n return run_speckit_command(\"speckit.analyze\", \"\", context)\n\n\n# =============================================================================\n# TASK PARSING\n# =============================================================================\n\n_TASK_LINE_RE = re.compile(\n r\"^- \\[[ xX]?\\]\\s+\" # checkbox\n r\"(T\\d{3})\" # task ID (T001, T002, …)\n r\"(?:\\s+\\[P\\])?\" # optional parallel marker\n r\"(?:\\s+\\[US\\d+\\])?\" # optional user-story label\n r\"\\s+(.+)$\" # description\n)\n\n\ndef _find_tasks_md() -> Path | None:\n \"\"\"Locate the tasks.md generated by SpecKit inside specs/<branch>/.\"\"\"\n # Try to find from current git branch\n try:\n branch = subprocess.run(\n [\"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=5,\n ).stdout.strip()\n except Exception:\n branch = \"\"\n\n if branch and (SPECS_DIR / branch / \"tasks.md\").exists():\n return SPECS_DIR / branch / \"tasks.md\"\n\n # Fallback: find newest tasks.md under specs/\n if SPECS_DIR.exists():\n candidates = sorted(SPECS_DIR.glob(\"*/tasks.md\"), key=lambda p: p.stat().st_mtime, reverse=True)\n if candidates:\n return candidates[0]\n\n return None\n\n\ndef _find_spec_md() -> Path | None:\n \"\"\"Locate the spec.md generated by SpecKit.\"\"\"\n try:\n branch = subprocess.run(\n [\"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=5,\n ).stdout.strip()\n except Exception:\n branch = \"\"\n\n if branch and (SPECS_DIR / branch / \"spec.md\").exists():\n return SPECS_DIR / branch / \"spec.md\"\n\n if SPECS_DIR.exists():\n candidates = sorted(SPECS_DIR.glob(\"*/spec.md\"), key=lambda p: p.stat().st_mtime, reverse=True)\n if candidates:\n return candidates[0]\n return None\n\n\ndef _find_plan_md() -> Path | None:\n \"\"\"Locate the plan.md generated by SpecKit.\"\"\"\n try:\n branch = subprocess.run(\n [\"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=5,\n ).stdout.strip()\n except Exception:\n branch = \"\"\n\n if branch and (SPECS_DIR / branch / \"plan.md\").exists():\n return SPECS_DIR / branch / \"plan.md\"\n\n if SPECS_DIR.exists():\n candidates = sorted(SPECS_DIR.glob(\"*/plan.md\"), key=lambda p: p.stat().st_mtime, reverse=True)\n if candidates:\n return candidates[0]\n return None\n\n\ndef parse_tasks(tasks_md_path: Path) -> list[dict]:\n \"\"\"Parse tasks.md into a list of task dicts with id, name, parallel flag.\"\"\"\n content = tasks_md_path.read_text()\n tasks = []\n current_phase = \"\"\n\n for line in content.splitlines():\n # Detect phase headings\n if line.startswith(\"## \") or line.startswith(\"### \"):\n current_phase = line.lstrip(\"#\").strip()\n continue\n\n m = _TASK_LINE_RE.match(line.strip())\n if m:\n task_id = m.group(1)\n description = m.group(2).strip()\n is_parallel = \"[P]\" in line\n us_match = re.search(r\"\\[US(\\d+)\\]\", line)\n user_story = f\"US{us_match.group(1)}\" if us_match else None\n\n tasks.append({\n \"id\": task_id,\n \"name\": description,\n \"description\": description,\n \"phase\": current_phase,\n \"parallel\": is_parallel,\n \"user_story\": user_story,\n })\n\n return tasks\n\n\n# =============================================================================\n# RISK DETECTION\n# =============================================================================\n\ndef _detect_task_risk(task_info: dict, plan_content: str = \"\") -> tuple[bool, list[str]]:\n \"\"\"Detect whether a task is high-risk based on metadata and plan text.\"\"\"\n haystack = \" \".join([\n str(task_info.get(\"id\", \"\")),\n str(task_info.get(\"name\", \"\")),\n str(task_info.get(\"description\", \"\")),\n plan_content,\n ]).lower()\n\n matches = sorted(\n {kw for kw in HIGH_RISK_KEYWORDS if re.search(rf\"\\b{re.escape(kw)}\\b\", haystack)}\n )\n return (len(matches) > 0, matches)\n\n\n# =============================================================================\n# EXECUTION — ANTHROPIC SDK (default backend)\n# =============================================================================\n\ndef _execute_task_anthropic(task_info: dict, plan_content: str, context_content: str) -> str:\n \"\"\"Execute a single task using Anthropic API or DeepSeek (OpenAI-compatible).\"\"\"\n model = EXECUTOR_MODEL\n\n prompt = dedent(f\"\"\"\\\n You are a precise developer implementing a task from an approved SpecKit plan.\n\n TASK: {task_info['id']} — {task_info['name']}\n DESCRIPTION: {task_info.get('description', '')}\n\n IMPLEMENTATION PLAN (from SpecKit):\n {plan_content}\n\n PROJECT STANDARDS:\n {context_content}\n\n RULES:\n 1. ONLY create or modify files directly related to this task.\n 2. Follow project standards and conventions exactly.\n 3. If the plan is unclear, make the safest possible implementation choice.\n 4. Output your work using the format below.\n\n OUTPUT FORMAT:\n === FILE: <relative-path> ===\n [complete file contents]\n === END FILE ===\n\n Provide ALL files you create or modify.\n \"\"\")\n\n # Use DeepSeek (OpenAI-compatible) if model starts with \"deepseek\"\n if model.startswith(\"deepseek\"):\n from openai import OpenAI\n client = OpenAI(\n api_key=os.getenv(\"DEEPSEEK_API_KEY\"),\n base_url=\"https://api.deepseek.com\",\n )\n response = client.chat.completions.create(\n model=model,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n max_tokens=8192,\n temperature=0.2,\n )\n return response.choices[0].message.content\n else:\n # Use Anthropic Claude\n from anthropic import Anthropic\n client = Anthropic()\n response = client.messages.create(\n model=model,\n max_tokens=8192,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n )\n return response.content[0].text\n\n\ndef _parse_files_from_response(response_text: str) -> dict[str, str]:\n \"\"\"Extract file paths and contents from === FILE: ... === blocks.\"\"\"\n files = {}\n pattern = re.compile(\n r\"=== FILE:\\s*(.+?)\\s*===\\s*\\n(.*?)\\n=== END FILE ===\",\n re.DOTALL,\n )\n for match in pattern.finditer(response_text):\n filepath = match.group(1).strip()\n content = match.group(2)\n files[filepath] = content\n return files\n\n\ndef _write_executor_files(files: dict[str, str]) -> list[str]:\n \"\"\"Write parsed files to disk, return list of written paths.\"\"\"\n written = []\n for rel_path, content in files.items():\n path = PROJECT_ROOT / rel_path\n path.parent.mkdir(parents=True, exist_ok=True)\n path.write_text(content)\n written.append(rel_path)\n return written\n\n\n# =============================================================================\n# EXECUTION — CREWAI BACKEND (optional)\n# =============================================================================\n\ndef _execute_task_crewai(task_info: dict, plan_content: str, context_content: str) -> str:\n \"\"\"Execute a single task using CrewAI agents (mirrors swpp_crew.py executor).\"\"\"\n from crewai import Agent, Task, Crew, LLM\n from crewai.tools import tool as crewai_tool\n\n model = EXECUTOR_MODEL\n\n # Use appropriate LangChain wrapper based on model\n if model.startswith(\"deepseek\"):\n from langchain_openai import ChatOpenAI\n executor_model = ChatOpenAI(\n model=model,\n api_key=os.getenv(\"DEEPSEEK_API_KEY\"),\n base_url=\"https://api.deepseek.com\",\n temperature=0.2,\n )\n else:\n from langchain_anthropic import ChatAnthropic\n executor_model = ChatAnthropic(\n model=model,\n temperature=0.2,\n )\n\n @crewai_tool\n def read_file(filepath: str) -> str:\n \"\"\"Read contents of a file in the project.\"\"\"\n path = PROJECT_ROOT / filepath\n return path.read_text() if path.exists() else f\"File not found: {filepath}\"\n\n @crewai_tool\n def write_file(filepath: str, content: str) -> str:\n \"\"\"Write content to a file in the project.\"\"\"\n path = PROJECT_ROOT / filepath\n path.parent.mkdir(parents=True, exist_ok=True)\n path.write_text(content)\n return f\"Written: {filepath}\"\n\n @crewai_tool\n def list_files(directory: str = \"src\") -> str:\n \"\"\"List files in a directory.\"\"\"\n path = PROJECT_ROOT / directory\n if not path.exists():\n return f\"Directory not found: {directory}\"\n files = [str(f.relative_to(PROJECT_ROOT)) for f in path.rglob(\"*\") if f.is_file()]\n return \"\\n\".join(files[:50])\n\n @crewai_tool\n def run_command(command: str) -> str:\n \"\"\"Run a shell command.\"\"\"\n try:\n result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=60, cwd=PROJECT_ROOT)\n return result.stdout + result.stderr\n except subprocess.TimeoutExpired:\n return \"Command timed out\"\n\n executor = Agent(\n role=\"Execution Agent\",\n goal=\"Implement code exactly according to approved SpecKit plans\",\n backstory=dedent(\"\"\"\\\n You are a precise developer who implements code following SpecKit plans exactly.\n You only modify files specified in the plan. You follow project\n standards religiously.\n \"\"\"),\n llm=executor_model,\n tools=[read_file, write_file, list_files, run_command],\n verbose=True,\n )\n\n execution_task = Task(\n description=dedent(f\"\"\"\\\n Implement code according to this approved SpecKit plan:\n\n TASK: {task_info['id']} — {task_info['name']}\n\n PLAN:\n {plan_content}\n\n PROJECT STANDARDS:\n {context_content}\n\n RULES:\n 1. ONLY modify files listed in the plan\n 2. Follow project standards exactly\n 3. After implementation, run linting if available\n\n Report what you created/modified.\n \"\"\"),\n expected_output=\"Implementation report with files created/modified\",\n agent=executor,\n )\n\n crew = Crew(agents=[executor], tasks=[execution_task], verbose=True)\n result = crew.kickoff()\n return str(result)\n\n\n# =============================================================================\n# TESTS & GIT\n# =============================================================================\n\ndef _run_tests(task_id: str, fast_fail: bool = False) -> dict:\n \"\"\"Run the project test suite. Returns dict with passed, summary, details.\"\"\"\n venv_python = str(PROJECT_ROOT / \".venv\" / \"bin\" / \"python\")\n if not Path(venv_python).exists():\n venv_python = \"python\"\n\n test_commands = [\n (\"unit\", f\"{venv_python} -m pytest tests/unit/ -v --tb=short\"),\n (\"integration\", f\"{venv_python} -m pytest tests/integration/ -v --tb=short\"),\n (\"safety\", f\"{venv_python} -m pytest tests/safety/ -v --tb=short\"),\n (\"solid\", f\"{venv_python} -m pytest tests/solid/ -v --tb=short\"),\n ]\n\n results = {}\n all_passed = True\n lines = [f\"\\n🧪 Running tests for {task_id}…\\n\"]\n\n for name, cmd in test_commands:\n try:\n res = subprocess.run(cmd, shell=True, cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=120)\n except subprocess.TimeoutExpired:\n res = type(\"R\", (), {\"returncode\": 1, \"stdout\": \"\", \"stderr\": \"Timed out\"})()\n\n passed = res.returncode == 0 or \"passed\" in getattr(res, \"stdout\", \"\").lower()\n results[name] = {\"passed\": passed, \"output\": getattr(res, \"stdout\", \"\") + getattr(res, \"stderr\", \"\")}\n lines.append(f\" {name}: {'✅ PASS' if passed else '❌ FAIL'}\")\n\n if not passed:\n all_passed = False\n if fast_fail:\n lines.append(\" ⚡ Fast mode: stopping at first failure\")\n break\n\n lines.append(f\"\\n{'✅ All tests passed!' if all_passed else '❌ Some tests failed'}\\n\")\n return {\"passed\": all_passed, \"summary\": \"\\n\".join(lines), \"details\": results}\n\n\ndef _git_commit(message: str) -> str:\n \"\"\"Stage all changes and commit.\"\"\"\n subprocess.run([\"git\", \"add\", \"-A\"], cwd=PROJECT_ROOT)\n result = subprocess.run([\"git\", \"commit\", \"-m\", message], cwd=PROJECT_ROOT, capture_output=True, text=True)\n return result.stdout + result.stderr\n\n\ndef _get_committed_tasks() -> set[str]:\n \"\"\"Return set of task IDs already committed (format: feat: ... [TXXX]).\"\"\"\n result = subprocess.run(\n [\"git\", \"log\", \"--oneline\", \"-n\", \"100\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True,\n )\n committed = set()\n for line in result.stdout.splitlines():\n m = re.search(r\"\\[(T\\d{3})\\]\", line)\n if m:\n committed.add(m.group(1))\n return committed\n\n\n# =============================================================================\n# REVIEW\n# =============================================================================\n\ndef _review_task_anthropic(task_info: dict, plan_content: str, context_content: str) -> str:\n \"\"\"Review implementation of a single task using Claude Sonnet.\"\"\"\n from anthropic import Anthropic\n\n client = Anthropic()\n model = REVIEWER_MODEL\n\n prompt = dedent(f\"\"\"\\\n Review the implementation of task {task_info['id']}: {task_info['name']}.\n\n ORIGINAL PLAN:\n {plan_content}\n\n PROJECT STANDARDS:\n {context_content}\n\n REVIEW CHECKLIST:\n 1. Does implementation match the plan?\n 2. Are project standards followed?\n 3. Is error handling adequate?\n 4. Are there any bugs or edge cases missed?\n 5. Is the code clean and maintainable?\n\n OUTPUT: PASS or FAIL with issues found and suggestions.\n \"\"\")\n\n response = client.messages.create(\n model=model,\n max_tokens=4096,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n )\n return response.content[0].text\n\n\ndef _review_task_crewai(task_info: dict, plan_content: str, context_content: str) -> str:\n \"\"\"Review implementation of a single task using CrewAI reviewer agent.\"\"\"\n from crewai import Agent, Task, Crew\n\n model = REVIEWER_MODEL\n\n # Reviewer always uses Claude (from REVIEWER_MODEL)\n from langchain_anthropic import ChatAnthropic\n reviewer_model = ChatAnthropic(\n model=model,\n temperature=0.1, # Low temperature for consistent review\n )\n\n reviewer = Agent(\n role=\"Code Reviewer\",\n goal=\"Ensure code quality, adherence to standards, and identify bugs\",\n backstory=dedent(\"\"\"\\\n You are a meticulous code reviewer with expertise in software quality.\n You verify implementations match approved plans, follow project standards,\n and have proper error handling. You catch edge cases and maintainability issues.\n \"\"\"),\n llm=reviewer_model,\n tools=[], # Reviewer doesn't need file tools - reviews what was already committed\n verbose=True,\n )\n\n review_task = Task(\n description=dedent(f\"\"\"\\\n Review the implementation of task {task_info['id']}: {task_info['name']}.\n\n ORIGINAL PLAN:\n {plan_content}\n\n PROJECT STANDARDS:\n {context_content}\n\n REVIEW CHECKLIST:\n 1. Does implementation match the plan?\n 2. Are project standards followed?\n 3. Is error handling adequate?\n 4. Are there any bugs or edge cases missed?\n 5. Is the code clean and maintainable?\n\n OUTPUT: Start with \"PASS\" or \"FAIL\", then provide detailed findings and suggestions.\n \"\"\"),\n expected_output=\"Review verdict (PASS/FAIL) with detailed findings\",\n agent=reviewer,\n )\n\n crew = Crew(agents=[reviewer], tasks=[review_task], verbose=True)\n result = crew.kickoff()\n return str(result)\n\n\n# =============================================================================\n# TELEMETRY\n# =============================================================================\n\ndef _write_telemetry(feature: str, telemetry: dict) -> Path:\n \"\"\"Persist run telemetry to .swpp/.\"\"\"\n SWPP_DIR.mkdir(exist_ok=True)\n path = SWPP_DIR / f\"metrics_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json\"\n payload = {\"feature\": feature, \"generated_at\": datetime.now().isoformat(), **telemetry}\n path.write_text(json.dumps(payload, indent=2))\n return path\n\n\n# =============================================================================\n# MAIN ORCHESTRATOR\n# =============================================================================\n\ndef run_swpp_speckit(\n feature: str,\n *,\n fast_mode: bool = False,\n auto_approve: bool = False,\n skip_review: bool = False,\n force_auto_approve: bool = False,\n executor_backend: str = DEFAULT_EXECUTOR_BACKEND,\n mode_label: str = \"STANDARD\",\n context_file: Path | None = None,\n):\n run_started = time.perf_counter()\n telemetry: dict = {\n \"runner\": \"swpp_speckit\",\n \"mode\": mode_label,\n \"executor_backend\": executor_backend,\n \"phase_seconds\": {},\n \"tasks\": [],\n \"checkpoint\": {},\n \"summary\": {},\n }\n\n print(\"\\n\" + \"=\" * 60)\n print(\"SWPP-SPECKIT AUTOMATIC EXECUTION\")\n print(f\"Feature: {feature}\")\n print(f\"Mode: {mode_label} | Executor: {executor_backend}\")\n print(\"=\" * 60)\n\n # ------------------------------------------------------------------\n # STEP 0: Prerequisites\n # ------------------------------------------------------------------\n missing = check_prerequisites()\n if missing:\n print(\"\\n❌ Missing prerequisites:\")\n for m in missing:\n print(f\" • {m}\")\n sys.exit(1)\n\n # ------------------------------------------------------------------\n # STEP 1: Ensure SpecKit scaffold\n # ------------------------------------------------------------------\n print(\"\\n🔧 Checking SpecKit scaffold…\")\n if not ensure_speckit_scaffold():\n sys.exit(1)\n\n # ------------------------------------------------------------------\n # STEP 1.5: Load and validate context file\n # ------------------------------------------------------------------\n # Use custom context file if specified, otherwise default\n context_file_path = context_file if context_file else CONTEXT_FILE\n \n if not context_file_path.exists():\n print(\"\\n\" + \"=\" * 60)\n print(\"❌ REQUIRED: Project context file not found\")\n print(\"=\" * 60)\n print(f\"\\nContext file not found: {context_file_path}\")\n print(\"\\nCreate a context file with:\")\n print(\" • Project purpose and goals\")\n print(\" • Tech stack (language, framework, database)\")\n print(\" • Architecture and design patterns\")\n print(\" • Coding standards and naming conventions\")\n print(\" • Testing requirements\")\n print(\"\\nThen run with: --context <your-file.md>\")\n print(\"\\nFor new projects, use --project flag to auto-generate template.\")\n print(\"=\" * 60)\n sys.exit(1)\n \n context_content = context_file_path.read_text()\n try:\n display_path = context_file_path.relative_to(PROJECT_ROOT)\n except ValueError:\n display_path = context_file_path\n print(f\" ✅ Loaded context: {display_path} ({len(context_content)} chars)\")\n\n # ------------------------------------------------------------------\n # STEP 2: SpecKit planning phases (with context injection)\n # ------------------------------------------------------------------\n phase_start = time.perf_counter()\n speckit_phases_run: list[str] = []\n\n # 2a: /speckit.constitution → .specify/memory/constitution.md\n ok, output = phase_constitution(context_content)\n if not ok:\n print(f\"\\n⚠️ Constitution phase failed (non-fatal). Output:\\n{output[:500]}\")\n else:\n speckit_phases_run.append(\"constitution\")\n print(\" ✅ Constitution established\")\n\n # 2b: /speckit.specify → spec.md\n ok, output = phase_specify(feature, context_content)\n if not ok:\n print(f\"\\n❌ Specification phase failed. Output:\\n{output[:1000]}\")\n sys.exit(1)\n\n spec_path = _find_spec_md()\n if spec_path:\n print(f\" ✅ Spec: {spec_path.relative_to(PROJECT_ROOT)}\")\n speckit_phases_run.append(\"specify\")\n else:\n print(\" ⚠️ spec.md not found after /speckit.specify — continuing anyway\")\n\n # 2c: /speckit.clarify → refine spec.md\n ok, output = phase_clarify(context_content)\n if not ok:\n print(f\"\\n⚠️ Clarify phase failed (non-fatal). Output:\\n{output[:500]}\")\n else:\n speckit_phases_run.append(\"clarify\")\n print(\" ✅ Spec clarified\")\n\n # 2d: /speckit.plan → plan.md\n ok, output = phase_plan(context_content)\n if not ok:\n print(f\"\\n❌ Planning phase failed. Output:\\n{output[:1000]}\")\n sys.exit(1)\n\n plan_path = _find_plan_md()\n if plan_path:\n print(f\" ✅ Plan: {plan_path.relative_to(PROJECT_ROOT)}\")\n speckit_phases_run.append(\"plan\")\n else:\n print(\" ⚠️ plan.md not found after /speckit.plan — continuing anyway\")\n\n # 2e: /speckit.checklist → quality validation\n ok, output = phase_checklist(context_content)\n if not ok:\n print(f\"\\n⚠️ Checklist phase failed (non-fatal). Output:\\n{output[:500]}\")\n else:\n speckit_phases_run.append(\"checklist\")\n print(\" ✅ Quality checklist generated\")\n\n # 2f: /speckit.tasks → tasks.md\n ok, output = phase_tasks(context_content)\n if not ok:\n print(f\"\\n❌ Task generation failed. Output:\\n{output[:1000]}\")\n sys.exit(1)\n\n tasks_md = _find_tasks_md()\n if not tasks_md:\n print(\"\\n❌ tasks.md not found after /speckit.tasks. Cannot proceed.\")\n sys.exit(1)\n\n print(f\" ✅ Tasks: {tasks_md.relative_to(PROJECT_ROOT)}\")\n speckit_phases_run.append(\"tasks\")\n\n # 2g: /speckit.analyze → cross-artifact consistency check\n ok, output = phase_analyze(context_content)\n if not ok:\n print(f\"\\n⚠️ Analyze phase failed (non-fatal). Output:\\n{output[:500]}\")\n else:\n speckit_phases_run.append(\"analyze\")\n print(\" ✅ Cross-artifact analysis complete\")\n\n telemetry[\"phase_seconds\"][\"speckit_planning\"] = round(time.perf_counter() - phase_start, 3)\n telemetry[\"speckit_phases_run\"] = speckit_phases_run\n\n # ------------------------------------------------------------------\n # STEP 3: Parse tasks\n # ------------------------------------------------------------------\n tasks = parse_tasks(tasks_md)\n\n if not tasks:\n print(f\"\\n❌ No tasks parsed from {tasks_md}. Check tasks.md format.\")\n sys.exit(1)\n\n if fast_mode and len(tasks) > FAST_MODE_TASK_LIMIT:\n tasks = tasks[:FAST_MODE_TASK_LIMIT]\n print(f\"⚡ Fast mode: limited to first {FAST_MODE_TASK_LIMIT} tasks\")\n\n print(f\"\\n✅ Parsed {len(tasks)} tasks:\")\n for t in tasks:\n marker = \" [P]\" if t[\"parallel\"] else \"\"\n us = f\" [{t['user_story']}]\" if t.get(\"user_story\") else \"\"\n print(f\" {t['id']}{marker}{us}: {t['name']}\")\n\n # Load plan content for executor context (context_content already loaded earlier)\n plan_content = plan_path.read_text() if plan_path and plan_path.exists() else \"\"\n\n # Risk detection\n risk_profiles: dict[str, dict] = {}\n for t in tasks:\n is_risky, signals = _detect_task_risk(t, plan_content)\n risk_profiles[t[\"id\"]] = {\"is_high_risk\": is_risky, \"signals\": signals}\n\n high_risk_tasks = [t for t in tasks if risk_profiles[t[\"id\"]][\"is_high_risk\"]]\n\n # ------------------------------------------------------------------\n # STEP 4: Checkpoint\n # ------------------------------------------------------------------\n print(\"\\n\" + \"=\" * 60)\n print(\"🔍 CHECKPOINT: Review SpecKit artefacts\")\n print(\"=\" * 60)\n\n if spec_path:\n print(f\" 📄 {spec_path.relative_to(PROJECT_ROOT)}\")\n if plan_path:\n print(f\" 📄 {plan_path.relative_to(PROJECT_ROOT)}\")\n print(f\" 📄 {tasks_md.relative_to(PROJECT_ROOT)}\")\n\n if high_risk_tasks:\n print(\"\\n ⚠️ High-risk tasks detected:\")\n for t in high_risk_tasks:\n signals = \", \".join(risk_profiles[t[\"id\"]][\"signals\"][:5])\n print(f\" - {t['id']}: {t['name']} ({signals})\")\n\n checkpoint_start = time.perf_counter()\n if auto_approve and not (high_risk_tasks and not force_auto_approve):\n print(\"\\n⚡ Auto-approved checkpoint\")\n telemetry[\"checkpoint\"] = {\"auto_approved\": True, \"high_risk_task_count\": len(high_risk_tasks)}\n else:\n if auto_approve and high_risk_tasks and not force_auto_approve:\n print(\"\\n⚠️ Auto-approve paused because high-risk tasks were detected.\")\n approval = input(\"\\nApprove and continue execution? [y/n]: \").strip().lower()\n if approval not in (\"y\", \"yes\", \"\"):\n print(\"❌ Execution cancelled.\")\n telemetry[\"checkpoint\"] = {\"auto_approved\": False, \"approved\": False}\n telemetry[\"phase_seconds\"][\"checkpoint\"] = round(time.perf_counter() - checkpoint_start, 3)\n telemetry[\"total_seconds\"] = round(time.perf_counter() - run_started, 3)\n _write_telemetry(feature, telemetry)\n return\n telemetry[\"checkpoint\"] = {\"auto_approved\": False, \"approved\": True, \"high_risk_task_count\": len(high_risk_tasks)}\n\n telemetry[\"phase_seconds\"][\"checkpoint\"] = round(time.perf_counter() - checkpoint_start, 3)\n\n # ------------------------------------------------------------------\n # STEP 5: Execution (SWPP agents — NOT Claude Code)\n # ------------------------------------------------------------------\n print(f\"\\n⚡ PHASE 4: Executing tasks via SWPP {executor_backend} agents…\")\n\n committed_tasks = _get_committed_tasks()\n if committed_tasks:\n print(f\" ⏭️ Already committed: {', '.join(sorted(committed_tasks))}\")\n\n execute_fn = _execute_task_crewai if executor_backend == \"crewai\" else _execute_task_anthropic\n exec_phase_start = time.perf_counter()\n\n for task_info in tasks:\n tid = task_info[\"id\"]\n\n if tid in committed_tasks:\n print(f\"\\n ⏭️ Skipping {tid} (already committed)\")\n telemetry[\"tasks\"].append({\"task_id\": tid, \"skipped\": True, \"reason\": \"already_committed\"})\n continue\n\n print(f\"\\n Executing {tid}: {task_info['name']}…\")\n\n task_metrics: dict = {\n \"task_id\": tid,\n \"name\": task_info[\"name\"],\n \"high_risk\": risk_profiles[tid][\"is_high_risk\"],\n \"risk_signals\": risk_profiles[tid][\"signals\"],\n \"skipped\": False,\n }\n\n exec_start = time.perf_counter()\n try:\n result_text = execute_fn(task_info, plan_content, context_content)\n except Exception as e:\n print(f\" ❌ Executor error for {tid}: {e}\")\n task_metrics[\"execution_seconds\"] = round(time.perf_counter() - exec_start, 3)\n task_metrics[\"committed\"] = False\n task_metrics[\"error\"] = str(e)\n telemetry[\"tasks\"].append(task_metrics)\n telemetry[\"phase_seconds\"][\"execute_and_test\"] = round(time.perf_counter() - exec_phase_start, 3)\n telemetry[\"total_seconds\"] = round(time.perf_counter() - run_started, 3)\n _write_telemetry(feature, telemetry)\n return\n\n task_metrics[\"execution_seconds\"] = round(time.perf_counter() - exec_start, 3)\n\n # For anthropic backend, parse and write files from response\n if executor_backend == \"anthropic\":\n files = _parse_files_from_response(result_text)\n if files:\n written = _write_executor_files(files)\n print(f\" 📁 Wrote {len(written)} file(s): {', '.join(written)}\")\n else:\n print(f\" ⚠️ No === FILE: === blocks found in executor response for {tid}\")\n\n # Also copy/sync plan to .swpp/plans/ for consistency with other runners\n PLANS_DIR.mkdir(parents=True, exist_ok=True)\n (PLANS_DIR / f\"{tid}.md\").write_text(\n f\"# {tid}: {task_info['name']}\\n\\n\"\n f\"*Source: SpecKit plan*\\n\\n\"\n f\"## Task Description\\n{task_info.get('description', '')}\\n\"\n )\n\n # Tests\n print(f\"\\n 🧪 Running tests for {tid}…\")\n fast_fail = fast_mode and not task_metrics[\"high_risk\"]\n task_metrics[\"test_strategy\"] = \"fast-fail\" if fast_fail else \"strict\"\n test_start = time.perf_counter()\n test_result = _run_tests(tid, fast_fail=fast_fail)\n task_metrics[\"tests_seconds\"] = round(time.perf_counter() - test_start, 3)\n task_metrics[\"tests_passed\"] = test_result[\"passed\"]\n print(test_result[\"summary\"])\n\n # Commit only if tests pass\n if test_result[\"passed\"]:\n commit_msg = f\"feat: {task_info['name']} [{tid}]\"\n _git_commit(commit_msg)\n print(f\" ✅ {tid} committed\")\n task_metrics[\"committed\"] = True\n else:\n print(f\" ❌ {tid} tests failed. Stopping execution.\")\n for name, detail in test_result[\"details\"].items():\n if not detail[\"passed\"]:\n print(f\"\\n {name} output:\\n{detail['output'][:500]}\")\n task_metrics[\"committed\"] = False\n telemetry[\"tasks\"].append(task_metrics)\n telemetry[\"phase_seconds\"][\"execute_and_test\"] = round(time.perf_counter() - exec_phase_start, 3)\n telemetry[\"summary\"] = {\"status\": \"failed_tests\", \"failed_task\": tid}\n telemetry[\"total_seconds\"] = round(time.perf_counter() - run_started, 3)\n _write_telemetry(feature, telemetry)\n return\n\n telemetry[\"tasks\"].append(task_metrics)\n\n telemetry[\"phase_seconds\"][\"execute_and_test\"] = round(time.perf_counter() - exec_phase_start, 3)\n\n # ------------------------------------------------------------------\n # STEP 6: Review\n # ------------------------------------------------------------------\n review_results = []\n if skip_review:\n print(\"\\n🔎 PHASE 5: Review skipped\")\n telemetry[\"phase_seconds\"][\"review\"] = 0.0\n else:\n print(\"\\n🔎 PHASE 5: Reviewing implementation…\")\n review_start = time.perf_counter()\n all_committed = _get_committed_tasks()\n newly_committed = all_committed - committed_tasks\n\n review_fn = _review_task_crewai if executor_backend == \"crewai\" else _review_task_anthropic\n\n for task_info in tasks:\n tid = task_info[\"id\"]\n if tid not in newly_committed:\n continue\n try:\n review_text = review_fn(task_info, plan_content, context_content)\n review_results.append((tid, review_text))\n except Exception as e:\n review_results.append((tid, f\"Review error: {e}\"))\n\n telemetry[\"phase_seconds\"][\"review\"] = round(time.perf_counter() - review_start, 3)\n\n # ------------------------------------------------------------------\n # STEP 7: Summary\n # ------------------------------------------------------------------\n print(\"\\n\" + \"=\" * 60)\n print(\"✨ SWPP-SPECKIT EXECUTION COMPLETE\")\n print(\"=\" * 60)\n print(f\"\\nFeature: {feature}\")\n print(f\"Tasks completed: {len([t for t in telemetry['tasks'] if t.get('committed')])}/{len(tasks)}\")\n print(f\"High-risk tasks: {len(high_risk_tasks)}\")\n\n if review_results:\n print(\"\\nReview Summary:\")\n for tid, result in review_results:\n status = \"✅ PASS\" if \"PASS\" in result.upper() else \"⚠️ NEEDS ATTENTION\"\n print(f\" {tid}: {status}\")\n\n telemetry[\"summary\"] = {\n \"status\": \"completed\",\n \"tasks_total\": len(tasks),\n \"tasks_committed\": len([t for t in telemetry[\"tasks\"] if t.get(\"committed\")]),\n \"high_risk_task_count\": len(high_risk_tasks),\n \"reviews_completed\": len(review_results),\n }\n telemetry[\"total_seconds\"] = round(time.perf_counter() - run_started, 3)\n metrics_path = _write_telemetry(feature, telemetry)\n\n print(\"\\n📁 Artefacts:\")\n if spec_path:\n print(f\" Spec: {spec_path.relative_to(PROJECT_ROOT)}\")\n if plan_path:\n print(f\" Plan: {plan_path.relative_to(PROJECT_ROOT)}\")\n print(f\" Tasks: {tasks_md.relative_to(PROJECT_ROOT)}\")\n print(f\" SWPP plans: .swpp/plans/\")\n print(f\" Commits: git log --oneline -n {len(tasks)}\")\n print(f\" Telemetry: {metrics_path}\")\n\n\n# =============================================================================\n# CLI\n# =============================================================================\n\nif __name__ == \"__main__\":\n parser = argparse.ArgumentParser(\n description=\"SWPP with SpecKit planning + SWPP agent execution\",\n epilog=\"Planning uses SpecKit (via claude -p). Execution uses SWPP AI agents.\",\n )\n parser.add_argument(\"feature\", nargs=\"+\", help=\"Feature description\")\n parser.add_argument(\n \"--project\", type=str,\n help=\"Project name (creates new directory in ~/projects/<name> if doesn't exist)\"\n )\n parser.add_argument(\n \"--project-dir\", type=str,\n help=\"Custom base directory for projects (default: ~/projects)\"\n )\n parser.add_argument(\n \"--context\", type=str,\n help=\"Path to project context file (default: .swpp/context.md)\"\n )\n\n mode_group = parser.add_mutually_exclusive_group()\n mode_group.add_argument(\n \"--fast\", action=\"store_true\",\n help=\"Auto-approve, skip review, limit to first 3 tasks (ALL SpecKit phases still run)\",\n )\n mode_group.add_argument(\n \"--balanced\", action=\"store_true\",\n help=\"Auto-approve, keep review, limit to first 3 tasks (ALL SpecKit phases still run)\",\n )\n\n parser.add_argument(\"--auto-approve\", action=\"store_true\", help=\"Skip checkpoint prompt\")\n parser.add_argument(\"--force-auto-approve\", action=\"store_true\", help=\"Auto-approve even for high-risk tasks\")\n parser.add_argument(\"--skip-review\", action=\"store_true\", help=\"Skip review phase\")\n parser.add_argument(\n \"--executor\", choices=[\"anthropic\", \"crewai\"], default=DEFAULT_EXECUTOR_BACKEND,\n help=f\"Executor backend (default: {DEFAULT_EXECUTOR_BACKEND})\",\n )\n\n # All SpecKit phases are now mandatory for quality assurance\n\n args = parser.parse_args()\n feature = \" \".join(args.feature)\n\n # Setup project directory if --project flag is used\n if args.project:\n base_dir = Path(args.project_dir) if args.project_dir else Path.home() / \"projects\"\n project_path = setup_project_directory(args.project, base_dir)\n \n # Change working directory to project\n import os\n os.chdir(project_path)\n \n print(f\"\\n📂 Working in project: {project_path}\\n\")\n\n fast_mode = args.fast or args.balanced\n mode_label = \"FAST\" if args.fast else \"BALANCED\" if args.balanced else \"STANDARD\"\n auto_approve = args.auto_approve or fast_mode\n skip_review = args.skip_review or args.fast\n \n # Determine context file path\n context_file_path = None\n if args.context:\n context_file_path = Path(args.context)\n if not context_file_path.is_absolute():\n context_file_path = PROJECT_ROOT / context_file_path\n\n run_swpp_speckit(\n feature,\n fast_mode=fast_mode,\n auto_approve=auto_approve,\n skip_review=skip_review,\n force_auto_approve=args.force_auto_approve,\n executor_backend=args.executor,\n mode_label=mode_label,\n context_file=context_file_path,\n )\n"}, "280c3c5d094c7a4b399cedeb09ffef8ca4a5fbcf": {"t": 1785766529.4180365, "n": 51425, "h": "189d3f25ce6469c8", "c": "# swpp_speckit.py\n# SWPP Runner with SpecKit (spec-driven planning) + SWPP Agents (execution)\n#\n# SpecKit phases (via `claude -p` slash commands):\n# /speckit.constitution → project principles\n# /speckit.specify → feature specification (spec.md)\n# /speckit.clarify → de-risk ambiguity in spec\n# /speckit.plan → implementation plan (plan.md)\n# /speckit.checklist → quality validation checklists\n# /speckit.tasks → actionable task list (tasks.md)\n# /speckit.analyze → cross-artifact consistency check\n# /speckit.implement → SKIPPED (SWPP agents handle execution)\n#\n# Execution: SWPP AI agents (Anthropic API or CrewAI)\n#\n# Requires: `specify` CLI (GitHub Spec Kit) + `claude` CLI (Claude Code)\n#\n# Run: python swpp_speckit.py \"Your feature description\"\n\nimport os\nimport sys\nimport json\nimport re\nimport time\nimport shutil\nimport subprocess\nimport argparse\nfrom datetime import datetime\nfrom pathlib import Path\nfrom textwrap import dedent\n\n# =============================================================================\n# CONFIGURATION\n# =============================================================================\n\n# PROJECT_ROOT is set dynamically based on --project flag (see main)\nPROJECT_ROOT = Path.cwd()\n\ndef get_project_paths(project_root: Path) -> dict:\n \"\"\"Return all project paths based on root directory.\"\"\"\n return {\n \"root\": project_root,\n \"swpp_dir\": project_root / \".swpp\",\n \"plans_dir\": project_root / \".swpp\" / \"plans\",\n \"context_file\": project_root / \".swpp\" / \"context.md\",\n \"speckit_dir\": project_root / \".specify\",\n \"claude_commands_dir\": project_root / \".claude\" / \"commands\",\n \"specs_dir\": project_root / \"specs\",\n }\n\n# Default paths (updated when --project is used)\nSWPP_DIR = PROJECT_ROOT / \".swpp\"\nPLANS_DIR = SWPP_DIR / \"plans\"\nCONTEXT_FILE = SWPP_DIR / \"context.md\"\nSPECKIT_DIR = PROJECT_ROOT / \".specify\"\nCLAUDE_COMMANDS_DIR = PROJECT_ROOT / \".claude\" / \"commands\"\nSPECS_DIR = PROJECT_ROOT / \"specs\"\n\nFAST_MODE_TASK_LIMIT = 3\nHIGH_RISK_KEYWORDS = {\n \"auth\", \"authentication\", \"authorization\", \"permission\", \"permissions\",\n \"secret\", \"token\", \"password\", \"credential\", \"oauth\", \"security\",\n \"encryption\", \"decrypt\", \"migration\", \"schema\", \"database\", \"sql\",\n \"prod\", \"production\", \"billing\", \"payment\", \"compliance\",\n}\n\n# Claude Code CLI timeout per phase (seconds)\nSPECKIT_PHASE_TIMEOUT = 300 # 5 min per slash command\n\n# Model Configuration\nEXECUTOR_MODEL = os.getenv(\"SWPP_EXECUTOR_MODEL\", \"deepseek-chat\") # or \"claude-sonnet-4-5-20250929\"\nREVIEWER_MODEL = os.getenv(\"SWPP_REVIEWER_MODEL\", \"claude-sonnet-4-5-20250929\")\n\n# Executor backend: \"crewai\" or \"anthropic\"\n# crewai uses CrewAI agents, anthropic uses raw Anthropic SDK\nDEFAULT_EXECUTOR_BACKEND = \"anthropic\"\n\n\n# =============================================================================\n# PREREQUISITES\n# =============================================================================\n\ndef _check_cli(name: str, check_cmd: list[str]) -> bool:\n \"\"\"Verify a CLI tool is installed and reachable.\"\"\"\n try:\n subprocess.run(check_cmd, capture_output=True, timeout=10)\n return True\n except (FileNotFoundError, subprocess.TimeoutExpired):\n return False\n\n\ndef check_prerequisites() -> list[str]:\n \"\"\"Return list of missing prerequisites.\"\"\"\n missing = []\n if not _check_cli(\"specify\", [\"specify\", \"version\"]):\n missing.append(\"specify (GitHub Spec Kit) — install: https://github.com/specifyit/specify\")\n if not _check_cli(\"claude\", [\"claude\", \"--version\"]):\n missing.append(\"claude (Claude Code CLI) — install: https://docs.anthropic.com/en/docs/claude-code\")\n if not os.getenv(\"ANTHROPIC_API_KEY\"):\n missing.append(\"ANTHROPIC_API_KEY environment variable\")\n return missing\n\n\n# =============================================================================\n# PROJECT SETUP\n# =============================================================================\n\ndef setup_project_directory(project_name: str, base_dir: Path = Path.home() / \"projects\") -> Path:\n \"\"\"Create project directory and initialize basic structure.\n \n Args:\n project_name: Name of the project (e.g., \"HelloWorld\")\n base_dir: Parent directory where project will be created\n \n Returns:\n Path to the created/existing project directory\n \"\"\"\n # Sanitize project name (remove spaces, special chars)\n safe_name = re.sub(r'[^a-zA-Z0-9_-]', '-', project_name)\n project_path = base_dir / safe_name\n \n if project_path.exists():\n print(f\" ℹ️ Project directory already exists: {project_path}\")\n else:\n print(f\" 📁 Creating project directory: {project_path}\")\n project_path.mkdir(parents=True, exist_ok=True)\n \n # Create basic Python project structure\n (project_path / \"src\").mkdir(exist_ok=True)\n (project_path / \"tests\").mkdir(exist_ok=True)\n (project_path / \"tests\" / \"unit\").mkdir(exist_ok=True)\n (project_path / \"tests\" / \"integration\").mkdir(exist_ok=True)\n (project_path / \"tests\" / \"safety\").mkdir(exist_ok=True)\n (project_path / \"tests\" / \"solid\").mkdir(exist_ok=True)\n \n # Create __init__.py files\n (project_path / \"src\" / \"__init__.py\").touch()\n (project_path / \"tests\" / \"__init__.py\").touch()\n (project_path / \"tests\" / \"unit\" / \"__init__.py\").touch()\n (project_path / \"tests\" / \"integration\" / \"__init__.py\").touch()\n (project_path / \"tests\" / \"safety\" / \"__init__.py\").touch()\n (project_path / \"tests\" / \"solid\" / \"__init__.py\").touch()\n \n # Initialize git if not already a repo\n if not (project_path / \".git\").exists():\n print(\" 🔧 Initializing git repository...\")\n subprocess.run([\"git\", \"init\"], cwd=project_path, capture_output=True)\n subprocess.run([\"git\", \"checkout\", \"-b\", \"main\"], cwd=project_path, capture_output=True)\n \n # Create .swpp/context.md with template\n swpp_dir = project_path / \".swpp\"\n swpp_dir.mkdir(exist_ok=True)\n context_file = swpp_dir / \"context.md\"\n \n if not context_file.exists():\n print(\" 📝 Creating context.md template...\")\n context_template = dedent(f\"\"\"\n # {safe_name} - Project Context\n \n ## Project Overview\n \n **Purpose**: [Describe what this project does and why it exists]\n \n **Target Users**: [Who will use this?]\n \n **Key Features**:\n - [Feature 1]\n - [Feature 2]\n - [Feature 3]\n \n ## Technical Stack\n \n **Language**: Python 3.13+\n \n **Framework**: [e.g., FastAPI, Django, Flask, or N/A for libraries]\n \n **Database**: [e.g., PostgreSQL, MySQL, SQLite, or N/A]\n \n **Key Dependencies**:\n - [Library 1]\n - [Library 2]\n \n ## Architecture & Patterns\n \n **Project Structure**:\n ```\n src/ # Application code\n tests/ # Test suites\n ```\n \n **Design Patterns**: [e.g., MVC, Repository, Service Layer]\n \n ## Coding Standards\n \n **Style Guide**: PEP 8 + type hints\n \n **Naming Conventions**:\n - Classes: PascalCase\n - Functions/variables: snake_case\n - Constants: UPPER_SNAKE_CASE\n \n **Testing Requirements**:\n - Unit tests for all business logic\n - Integration tests for API endpoints\n - Safety tests for error handling\n - SOLID principle validation\n \n ## Development Guidelines\n \n **Git Workflow**: Feature branches → main\n \n **Commit Format**: `type: description [TASK-ID]`\n - Types: feat, fix, docs, test, refactor\n \n **Documentation**: Docstrings for all public APIs\n \n ## Notes\n \n [Add any project-specific notes, constraints, or considerations]\n \"\"\").strip()\n context_file.write_text(context_template)\n print(f\" ⚠️ IMPORTANT: Edit .swpp/context.md with your project details!\")\n \n print(f\" ✅ Project structure created\")\n \n return project_path\n\n\n# =============================================================================\n# SPECKIT SCAFFOLD\n# =============================================================================\n\ndef ensure_speckit_scaffold() -> bool:\n \"\"\"Ensure SpecKit project scaffold exists (.specify/ and .claude/commands/).\"\"\"\n if SPECKIT_DIR.exists() and CLAUDE_COMMANDS_DIR.exists():\n print(\" ✅ SpecKit scaffold already exists\")\n return True\n\n print(\" 🔧 Initializing SpecKit scaffold automatically...\")\n print(\" (This may take 30-60s to download template from GitHub)\")\n \n result = subprocess.run(\n [\"specify\", \"init\", \".\", \"--ai\", \"claude\", \"--here\", \"--force\", \"--no-git\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=180,\n )\n \n if result.returncode != 0:\n print(f\" ❌ specify init failed:\\n{result.stderr}\")\n return False\n\n # Verify key artefacts\n if not CLAUDE_COMMANDS_DIR.exists():\n print(\" ❌ .claude/commands/ not created – specify init may have failed silently\")\n return False\n\n print(\" ✅ SpecKit scaffold created successfully\")\n return True\n\n\n# =============================================================================\n# CLAUDE CODE SLASH COMMAND RUNNER\n# =============================================================================\n\ndef run_speckit_command(\n slash_command: str,\n args: str = \"\",\n context: str = \"\",\n timeout: int = SPECKIT_PHASE_TIMEOUT,\n) -> tuple[bool, str]:\n \"\"\"\n Execute a SpecKit slash command via ``claude -p``.\n \n Args:\n slash_command: SpecKit command name (e.g., \"speckit.specify\")\n args: Feature description or command arguments\n context: Project context to inject into prompt\n timeout: Command timeout in seconds\n\n Returns (success, output_text).\n \"\"\"\n # Build prompt with context injection\n if context:\n prompt = f\"/{slash_command} CONTEXT: {args}\"\n else:\n prompt = f\"/{slash_command} {args}\".strip()\n \n cmd = [\n \"claude\",\n \"-p\", prompt,\n \"--output-format\", \"text\",\n \"--allowedTools\", \"Bash Read Write Edit\",\n ]\n \n # Add context as environment or file if needed\n if context:\n # Write context to temp file that Claude can read\n context_temp = PROJECT_ROOT / \".swpp\" / \"_temp_context.md\"\n context_temp.write_text(context)\n \n print(f\" ▸ claude -p \\\"/{slash_command} {args[:60]}{'…' if len(args) > 60 else ''}\\\"\")\n if context:\n print(f\" (with project context: {len(context)} chars)\")\n\n try:\n result = subprocess.run(\n cmd, cwd=PROJECT_ROOT,\n capture_output=True, text=True,\n timeout=timeout,\n )\n output = result.stdout + result.stderr\n success = result.returncode == 0\n if not success:\n print(f\" ⚠️ /{slash_command} exited with code {result.returncode}\")\n return success, output\n except subprocess.TimeoutExpired:\n print(f\" ⚠️ /{slash_command} timed out after {timeout}s\")\n return False, f\"Timed out after {timeout}s\"\n\n\n# =============================================================================\n# SPECKIT PLANNING PHASES\n# =============================================================================\n\ndef phase_constitution(context: str = \"\", args: str = \"\") -> tuple[bool, str]:\n \"\"\"Establish project governing principles via /speckit.constitution.\"\"\"\n print(\"\\n📜 SPECKIT: Establishing project constitution…\")\n return run_speckit_command(\"speckit.constitution\", args, context)\n\n\ndef phase_specify(feature: str, context: str = \"\") -> tuple[bool, str]:\n \"\"\"Generate feature specification via /speckit.specify.\"\"\"\n print(\"\\n📋 SPECKIT: Generating feature specification…\")\n return run_speckit_command(\"speckit.specify\", feature, context)\n\n\ndef phase_clarify(context: str = \"\", args: str = \"\") -> tuple[bool, str]:\n \"\"\"Clarify underspecified areas in spec via /speckit.clarify.\"\"\"\n print(\"\\n🔎 SPECKIT: Clarifying specification…\")\n return run_speckit_command(\"speckit.clarify\", args, context)\n\n\ndef phase_plan(context: str = \"\") -> tuple[bool, str]:\n \"\"\"Generate implementation plan via /speckit.plan.\"\"\"\n print(\"\\n📐 SPECKIT: Generating implementation plan…\")\n return run_speckit_command(\"speckit.plan\", \"\", context)\n\n\ndef phase_checklist(context: str = \"\", args: str = \"\") -> tuple[bool, str]:\n \"\"\"Generate quality checklists via /speckit.checklist.\"\"\"\n print(\"\\n✅ SPECKIT: Generating quality checklist…\")\n return run_speckit_command(\"speckit.checklist\", args, context)\n\n\ndef phase_tasks(context: str = \"\") -> tuple[bool, str]:\n \"\"\"Generate actionable tasks via /speckit.tasks.\"\"\"\n print(\"\\n📝 SPECKIT: Generating actionable task list…\")\n return run_speckit_command(\"speckit.tasks\", \"\", context)\n\n\ndef phase_analyze(context: str = \"\") -> tuple[bool, str]:\n \"\"\"Cross-artifact consistency analysis via /speckit.analyze.\"\"\"\n print(\"\\n🔬 SPECKIT: Analyzing cross-artifact consistency…\")\n return run_speckit_command(\"speckit.analyze\", \"\", context)\n\n\n# =============================================================================\n# TASK PARSING\n# =============================================================================\n\n_TASK_LINE_RE = re.compile(\n r\"^- \\[[ xX]?\\]\\s+\" # checkbox\n r\"(T\\d{3})\" # task ID (T001, T002, …)\n r\"(?:\\s+\\[P\\])?\" # optional parallel marker\n r\"(?:\\s+\\[US\\d+\\])?\" # optional user-story label\n r\"\\s+(.+)$\" # description\n)\n\n\ndef _find_tasks_md() -> Path | None:\n \"\"\"Locate the tasks.md generated by SpecKit inside specs/<branch>/.\"\"\"\n # Try to find from current git branch\n try:\n branch = subprocess.run(\n [\"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=5,\n ).stdout.strip()\n except Exception:\n branch = \"\"\n\n if branch and (SPECS_DIR / branch / \"tasks.md\").exists():\n return SPECS_DIR / branch / \"tasks.md\"\n\n # Fallback: find newest tasks.md under specs/\n if SPECS_DIR.exists():\n candidates = sorted(SPECS_DIR.glob(\"*/tasks.md\"), key=lambda p: p.stat().st_mtime, reverse=True)\n if candidates:\n return candidates[0]\n\n return None\n\n\ndef _find_spec_md() -> Path | None:\n \"\"\"Locate the spec.md generated by SpecKit.\"\"\"\n try:\n branch = subprocess.run(\n [\"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=5,\n ).stdout.strip()\n except Exception:\n branch = \"\"\n\n if branch and (SPECS_DIR / branch / \"spec.md\").exists():\n return SPECS_DIR / branch / \"spec.md\"\n\n if SPECS_DIR.exists():\n candidates = sorted(SPECS_DIR.glob(\"*/spec.md\"), key=lambda p: p.stat().st_mtime, reverse=True)\n if candidates:\n return candidates[0]\n return None\n\n\ndef _find_plan_md() -> Path | None:\n \"\"\"Locate the plan.md generated by SpecKit.\"\"\"\n try:\n branch = subprocess.run(\n [\"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=5,\n ).stdout.strip()\n except Exception:\n branch = \"\"\n\n if branch and (SPECS_DIR / branch / \"plan.md\").exists():\n return SPECS_DIR / branch / \"plan.md\"\n\n if SPECS_DIR.exists():\n candidates = sorted(SPECS_DIR.glob(\"*/plan.md\"), key=lambda p: p.stat().st_mtime, reverse=True)\n if candidates:\n return candidates[0]\n return None\n\n\ndef parse_tasks(tasks_md_path: Path) -> list[dict]:\n \"\"\"Parse tasks.md into a list of task dicts with id, name, parallel flag.\"\"\"\n content = tasks_md_path.read_text()\n tasks = []\n current_phase = \"\"\n\n for line in content.splitlines():\n # Detect phase headings\n if line.startswith(\"## \") or line.startswith(\"### \"):\n current_phase = line.lstrip(\"#\").strip()\n continue\n\n m = _TASK_LINE_RE.match(line.strip())\n if m:\n task_id = m.group(1)\n description = m.group(2).strip()\n is_parallel = \"[P]\" in line\n us_match = re.search(r\"\\[US(\\d+)\\]\", line)\n user_story = f\"US{us_match.group(1)}\" if us_match else None\n\n tasks.append({\n \"id\": task_id,\n \"name\": description,\n \"description\": description,\n \"phase\": current_phase,\n \"parallel\": is_parallel,\n \"user_story\": user_story,\n })\n\n return tasks\n\n\n# =============================================================================\n# RISK DETECTION\n# =============================================================================\n\ndef _detect_task_risk(task_info: dict, plan_content: str = \"\") -> tuple[bool, list[str]]:\n \"\"\"Detect whether a task is high-risk based on metadata and plan text.\"\"\"\n haystack = \" \".join([\n str(task_info.get(\"id\", \"\")),\n str(task_info.get(\"name\", \"\")),\n str(task_info.get(\"description\", \"\")),\n plan_content,\n ]).lower()\n\n matches = sorted(\n {kw for kw in HIGH_RISK_KEYWORDS if re.search(rf\"\\b{re.escape(kw)}\\b\", haystack)}\n )\n return (len(matches) > 0, matches)\n\n\n# =============================================================================\n# EXECUTION — ANTHROPIC SDK (default backend)\n# =============================================================================\n\ndef _execute_task_anthropic(task_info: dict, plan_content: str, context_content: str) -> str:\n \"\"\"Execute a single task using Anthropic API or DeepSeek (OpenAI-compatible).\"\"\"\n model = EXECUTOR_MODEL\n\n prompt = dedent(f\"\"\"\\\n You are a precise developer implementing a task from an approved SpecKit plan.\n\n TASK: {task_info['id']} — {task_info['name']}\n DESCRIPTION: {task_info.get('description', '')}\n\n IMPLEMENTATION PLAN (from SpecKit):\n {plan_content}\n\n PROJECT STANDARDS:\n {context_content}\n\n RULES:\n 1. ONLY create or modify files directly related to this task.\n 2. Follow project standards and conventions exactly.\n 3. If the plan is unclear, make the safest possible implementation choice.\n 4. Output your work using the format below.\n\n OUTPUT FORMAT:\n === FILE: <relative-path> ===\n [complete file contents]\n === END FILE ===\n\n Provide ALL files you create or modify.\n \"\"\")\n\n # Use DeepSeek (OpenAI-compatible) if model starts with \"deepseek\"\n if model.startswith(\"deepseek\"):\n from openai import OpenAI\n client = OpenAI(\n api_key=os.getenv(\"DEEPSEEK_API_KEY\"),\n base_url=\"https://api.deepseek.com\",\n )\n response = client.chat.completions.create(\n model=model,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n max_tokens=8192,\n temperature=0.2,\n )\n return response.choices[0].message.content\n else:\n # Use Anthropic Claude\n from anthropic import Anthropic\n client = Anthropic()\n response = client.messages.create(\n model=model,\n max_tokens=8192,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n )\n return response.content[0].text\n\n\ndef _parse_files_from_response(response_text: str) -> dict[str, str]:\n \"\"\"Extract file paths and contents from === FILE: ... === blocks.\"\"\"\n files = {}\n pattern = re.compile(\n r\"=== FILE:\\s*(.+?)\\s*===\\s*\\n(.*?)\\n=== END FILE ===\",\n re.DOTALL,\n )\n for match in pattern.finditer(response_text):\n filepath = match.group(1).strip()\n content = match.group(2)\n files[filepath] = content\n return files\n\n\ndef _write_executor_files(files: dict[str, str]) -> list[str]:\n \"\"\"Write parsed files to disk, return list of written paths.\"\"\"\n written = []\n for rel_path, content in files.items():\n path = PROJECT_ROOT / rel_path\n path.parent.mkdir(parents=True, exist_ok=True)\n path.write_text(content)\n written.append(rel_path)\n return written\n\n\n# =============================================================================\n# EXECUTION — CREWAI BACKEND (optional)\n# =============================================================================\n\ndef _execute_task_crewai(task_info: dict, plan_content: str, context_content: str) -> str:\n \"\"\"Execute a single task using CrewAI agents (mirrors swpp_crew.py executor).\"\"\"\n from crewai import Agent, Task, Crew, LLM\n from crewai.tools import tool as crewai_tool\n\n model = EXECUTOR_MODEL\n\n # Use appropriate LangChain wrapper based on model\n if model.startswith(\"deepseek\"):\n from langchain_openai import ChatOpenAI\n executor_model = ChatOpenAI(\n model=model,\n api_key=os.getenv(\"DEEPSEEK_API_KEY\"),\n base_url=\"https://api.deepseek.com\",\n temperature=0.2,\n )\n else:\n from langchain_anthropic import ChatAnthropic\n executor_model = ChatAnthropic(\n model=model,\n temperature=0.2,\n )\n\n @crewai_tool\n def read_file(filepath: str) -> str:\n \"\"\"Read contents of a file in the project.\"\"\"\n path = PROJECT_ROOT / filepath\n return path.read_text() if path.exists() else f\"File not found: {filepath}\"\n\n @crewai_tool\n def write_file(filepath: str, content: str) -> str:\n \"\"\"Write content to a file in the project.\"\"\"\n path = PROJECT_ROOT / filepath\n path.parent.mkdir(parents=True, exist_ok=True)\n path.write_text(content)\n return f\"Written: {filepath}\"\n\n @crewai_tool\n def list_files(directory: str = \"src\") -> str:\n \"\"\"List files in a directory.\"\"\"\n path = PROJECT_ROOT / directory\n if not path.exists():\n return f\"Directory not found: {directory}\"\n files = [str(f.relative_to(PROJECT_ROOT)) for f in path.rglob(\"*\") if f.is_file()]\n return \"\\n\".join(files[:50])\n\n @crewai_tool\n def run_command(command: str) -> str:\n \"\"\"Run a shell command.\"\"\"\n try:\n result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=60, cwd=PROJECT_ROOT)\n return result.stdout + result.stderr\n except subprocess.TimeoutExpired:\n return \"Command timed out\"\n\n executor = Agent(\n role=\"Execution Agent\",\n goal=\"Implement code exactly according to approved SpecKit plans\",\n backstory=dedent(\"\"\"\\\n You are a precise developer who implements code following SpecKit plans exactly.\n You only modify files specified in the plan. You follow project\n standards religiously.\n \"\"\"),\n llm=executor_model,\n tools=[read_file, write_file, list_files, run_command],\n verbose=True,\n )\n\n execution_task = Task(\n description=dedent(f\"\"\"\\\n Implement code according to this approved SpecKit plan:\n\n TASK: {task_info['id']} — {task_info['name']}\n\n PLAN:\n {plan_content}\n\n PROJECT STANDARDS:\n {context_content}\n\n RULES:\n 1. ONLY modify files listed in the plan\n 2. Follow project standards exactly\n 3. After implementation, run linting if available\n\n Report what you created/modified.\n \"\"\"),\n expected_output=\"Implementation report with files created/modified\",\n agent=executor,\n )\n\n crew = Crew(agents=[executor], tasks=[execution_task], verbose=True)\n result = crew.kickoff()\n return str(result)\n\n\n# =============================================================================\n# TESTS & GIT\n# =============================================================================\n\ndef _run_tests(task_id: str, fast_fail: bool = False) -> dict:\n \"\"\"Run the project test suite. Returns dict with passed, summary, details.\"\"\"\n venv_python = str(PROJECT_ROOT / \".venv\" / \"bin\" / \"python\")\n if not Path(venv_python).exists():\n venv_python = \"python\"\n\n test_commands = [\n (\"unit\", f\"{venv_python} -m pytest tests/unit/ -v --tb=short\"),\n (\"integration\", f\"{venv_python} -m pytest tests/integration/ -v --tb=short\"),\n (\"safety\", f\"{venv_python} -m pytest tests/safety/ -v --tb=short\"),\n (\"solid\", f\"{venv_python} -m pytest tests/solid/ -v --tb=short\"),\n ]\n\n results = {}\n all_passed = True\n lines = [f\"\\n🧪 Running tests for {task_id}…\\n\"]\n\n for name, cmd in test_commands:\n try:\n res = subprocess.run(cmd, shell=True, cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=120)\n except subprocess.TimeoutExpired:\n res = type(\"R\", (), {\"returncode\": 1, \"stdout\": \"\", \"stderr\": \"Timed out\"})()\n\n passed = res.returncode == 0 or \"passed\" in getattr(res, \"stdout\", \"\").lower()\n results[name] = {\"passed\": passed, \"output\": getattr(res, \"stdout\", \"\") + getattr(res, \"stderr\", \"\")}\n lines.append(f\" {name}: {'✅ PASS' if passed else '❌ FAIL'}\")\n\n if not passed:\n all_passed = False\n if fast_fail:\n lines.append(\" ⚡ Fast mode: stopping at first failure\")\n break\n\n lines.append(f\"\\n{'✅ All tests passed!' if all_passed else '❌ Some tests failed'}\\n\")\n return {\"passed\": all_passed, \"summary\": \"\\n\".join(lines), \"details\": results}\n\n\ndef _git_commit(message: str) -> str:\n \"\"\"Stage all changes and commit.\"\"\"\n subprocess.run([\"git\", \"add\", \"-A\"], cwd=PROJECT_ROOT)\n result = subprocess.run([\"git\", \"commit\", \"-m\", message], cwd=PROJECT_ROOT, capture_output=True, text=True)\n return result.stdout + result.stderr\n\n\ndef _get_committed_tasks() -> set[str]:\n \"\"\"Return set of task IDs already committed (format: feat: ... [TXXX]).\"\"\"\n result = subprocess.run(\n [\"git\", \"log\", \"--oneline\", \"-n\", \"100\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True,\n )\n committed = set()\n for line in result.stdout.splitlines():\n m = re.search(r\"\\[(T\\d{3})\\]\", line)\n if m:\n committed.add(m.group(1))\n return committed\n\n\n# =============================================================================\n# REVIEW\n# =============================================================================\n\ndef _review_task_anthropic(task_info: dict, plan_content: str, context_content: str) -> str:\n \"\"\"Review implementation of a single task using Claude Sonnet.\"\"\"\n from anthropic import Anthropic\n\n client = Anthropic()\n model = REVIEWER_MODEL\n\n prompt = dedent(f\"\"\"\\\n Review the implementation of task {task_info['id']}: {task_info['name']}.\n\n ORIGINAL PLAN:\n {plan_content}\n\n PROJECT STANDARDS:\n {context_content}\n\n REVIEW CHECKLIST:\n 1. Does implementation match the plan?\n 2. Are project standards followed?\n 3. Is error handling adequate?\n 4. Are there any bugs or edge cases missed?\n 5. Is the code clean and maintainable?\n\n OUTPUT: PASS or FAIL with issues found and suggestions.\n \"\"\")\n\n response = client.messages.create(\n model=model,\n max_tokens=4096,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n )\n return response.content[0].text\n\n\ndef _review_task_crewai(task_info: dict, plan_content: str, context_content: str) -> str:\n \"\"\"Review implementation of a single task using CrewAI reviewer agent.\"\"\"\n from crewai import Agent, Task, Crew\n\n model = REVIEWER_MODEL\n\n # Reviewer always uses Claude (from REVIEWER_MODEL)\n from langchain_anthropic import ChatAnthropic\n reviewer_model = ChatAnthropic(\n model=model,\n temperature=0.1, # Low temperature for consistent review\n )\n\n reviewer = Agent(\n role=\"Code Reviewer\",\n goal=\"Ensure code quality, adherence to standards, and identify bugs\",\n backstory=dedent(\"\"\"\\\n You are a meticulous code reviewer with expertise in software quality.\n You verify implementations match approved plans, follow project standards,\n and have proper error handling. You catch edge cases and maintainability issues.\n \"\"\"),\n llm=reviewer_model,\n tools=[], # Reviewer doesn't need file tools - reviews what was already committed\n verbose=True,\n )\n\n review_task = Task(\n description=dedent(f\"\"\"\\\n Review the implementation of task {task_info['id']}: {task_info['name']}.\n\n ORIGINAL PLAN:\n {plan_content}\n\n PROJECT STANDARDS:\n {context_content}\n\n REVIEW CHECKLIST:\n 1. Does implementation match the plan?\n 2. Are project standards followed?\n 3. Is error handling adequate?\n 4. Are there any bugs or edge cases missed?\n 5. Is the code clean and maintainable?\n\n OUTPUT: Start with \"PASS\" or \"FAIL\", then provide detailed findings and suggestions.\n \"\"\"),\n expected_output=\"Review verdict (PASS/FAIL) with detailed findings\",\n agent=reviewer,\n )\n\n crew = Crew(agents=[reviewer], tasks=[review_task], verbose=True)\n result = crew.kickoff()\n return str(result)\n\n\n# =============================================================================\n# TELEMETRY\n# =============================================================================\n\ndef _write_telemetry(feature: str, telemetry: dict) -> Path:\n \"\"\"Persist run telemetry to .swpp/.\"\"\"\n SWPP_DIR.mkdir(exist_ok=True)\n path = SWPP_DIR / f\"metrics_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json\"\n payload = {\"feature\": feature, \"generated_at\": datetime.now().isoformat(), **telemetry}\n path.write_text(json.dumps(payload, indent=2))\n return path\n\n\n# =============================================================================\n# MAIN ORCHESTRATOR\n# =============================================================================\n\ndef run_swpp_speckit(\n feature: str,\n *,\n fast_mode: bool = False,\n auto_approve: bool = False,\n skip_review: bool = False,\n force_auto_approve: bool = False,\n executor_backend: str = DEFAULT_EXECUTOR_BACKEND,\n mode_label: str = \"STANDARD\",\n skip_constitution: bool = False,\n skip_clarify: bool = False,\n skip_checklist: bool = False,\n skip_analyze: bool = False,\n context_file: Path | None = None,\n):\n run_started = time.perf_counter()\n telemetry: dict = {\n \"runner\": \"swpp_speckit\",\n \"mode\": mode_label,\n \"executor_backend\": executor_backend,\n \"phase_seconds\": {},\n \"tasks\": [],\n \"checkpoint\": {},\n \"summary\": {},\n }\n\n print(\"\\n\" + \"=\" * 60)\n print(\"SWPP-SPECKIT AUTOMATIC EXECUTION\")\n print(f\"Feature: {feature}\")\n print(f\"Mode: {mode_label} | Executor: {executor_backend}\")\n print(\"=\" * 60)\n\n # ------------------------------------------------------------------\n # STEP 0: Prerequisites\n # ------------------------------------------------------------------\n missing = check_prerequisites()\n if missing:\n print(\"\\n❌ Missing prerequisites:\")\n for m in missing:\n print(f\" • {m}\")\n sys.exit(1)\n\n # ------------------------------------------------------------------\n # STEP 1: Ensure SpecKit scaffold\n # ------------------------------------------------------------------\n print(\"\\n🔧 Checking SpecKit scaffold…\")\n if not ensure_speckit_scaffold():\n sys.exit(1)\n\n # ------------------------------------------------------------------\n # STEP 1.5: Load and validate context file\n # ------------------------------------------------------------------\n # Use custom context file if specified, otherwise default\n context_file_path = context_file if context_file else CONTEXT_FILE\n \n if not context_file_path.exists():\n print(\"\\n\" + \"=\" * 60)\n print(\"❌ REQUIRED: Project context file not found\")\n print(\"=\" * 60)\n print(f\"\\nContext file not found: {context_file_path}\")\n print(\"\\nCreate a context file with:\")\n print(\" • Project purpose and goals\")\n print(\" • Tech stack (language, framework, database)\")\n print(\" • Architecture and design patterns\")\n print(\" • Coding standards and naming conventions\")\n print(\" • Testing requirements\")\n print(\"\\nThen run with: --context <your-file.md>\")\n print(\"\\nFor new projects, use --project flag to auto-generate template.\")\n print(\"=\" * 60)\n sys.exit(1)\n \n context_content = context_file_path.read_text()\n try:\n display_path = context_file_path.relative_to(PROJECT_ROOT)\n except ValueError:\n display_path = context_file_path\n print(f\" ✅ Loaded context: {display_path} ({len(context_content)} chars)\")\n\n # ------------------------------------------------------------------\n # STEP 2: SpecKit planning phases (with context injection)\n # ------------------------------------------------------------------\n phase_start = time.perf_counter()\n speckit_phases_run: list[str] = []\n\n # 2a: /speckit.constitution → .specify/memory/constitution.md\n if not skip_constitution:\n ok, output = phase_constitution(context_content)\n if not ok:\n print(f\"\\n⚠️ Constitution phase failed (non-fatal). Output:\\n{output[:500]}\")\n else:\n speckit_phases_run.append(\"constitution\")\n print(\" ✅ Constitution established\")\n else:\n print(\"\\n📜 SPECKIT: Skipping /speckit.constitution\")\n\n # 2b: /speckit.specify → spec.md\n ok, output = phase_specify(feature, context_content)\n if not ok:\n print(f\"\\n❌ Specification phase failed. Output:\\n{output[:1000]}\")\n sys.exit(1)\n\n spec_path = _find_spec_md()\n if spec_path:\n print(f\" ✅ Spec: {spec_path.relative_to(PROJECT_ROOT)}\")\n speckit_phases_run.append(\"specify\")\n else:\n print(\" ⚠️ spec.md not found after /speckit.specify — continuing anyway\")\n\n # 2c: /speckit.clarify → refine spec.md\n if not skip_clarify:\n ok, output = phase_clarify(context_content)\n if not ok:\n print(f\"\\n⚠️ Clarify phase failed (non-fatal). Output:\\n{output[:500]}\")\n else:\n speckit_phases_run.append(\"clarify\")\n print(\" ✅ Spec clarified\")\n else:\n print(\"\\n🔎 SPECKIT: Skipping /speckit.clarify\")\n\n # 2d: /speckit.plan → plan.md\n ok, output = phase_plan(context_content)\n if not ok:\n print(f\"\\n❌ Planning phase failed. Output:\\n{output[:1000]}\")\n sys.exit(1)\n\n plan_path = _find_plan_md()\n if plan_path:\n print(f\" ✅ Plan: {plan_path.relative_to(PROJECT_ROOT)}\")\n speckit_phases_run.append(\"plan\")\n else:\n print(\" ⚠️ plan.md not found after /speckit.plan — continuing anyway\")\n\n # 2e: /speckit.checklist → quality validation\n if not skip_checklist:\n ok, output = phase_checklist(context_content)\n if not ok:\n print(f\"\\n⚠️ Checklist phase failed (non-fatal). Output:\\n{output[:500]}\")\n else:\n speckit_phases_run.append(\"checklist\")\n print(\" ✅ Quality checklist generated\")\n else:\n print(\"\\n✅ SPECKIT: Skipping /speckit.checklist\")\n\n # 2f: /speckit.tasks → tasks.md\n ok, output = phase_tasks(context_content)\n if not ok:\n print(f\"\\n❌ Task generation failed. Output:\\n{output[:1000]}\")\n sys.exit(1)\n\n tasks_md = _find_tasks_md()\n if not tasks_md:\n print(\"\\n❌ tasks.md not found after /speckit.tasks. Cannot proceed.\")\n sys.exit(1)\n\n print(f\" ✅ Tasks: {tasks_md.relative_to(PROJECT_ROOT)}\")\n speckit_phases_run.append(\"tasks\")\n\n # 2g: /speckit.analyze → cross-artifact consistency check\n if not skip_analyze:\n ok, output = phase_analyze(context_content)\n if not ok:\n print(f\"\\n⚠️ Analyze phase failed (non-fatal). Output:\\n{output[:500]}\")\n else:\n speckit_phases_run.append(\"analyze\")\n print(\" ✅ Cross-artifact analysis complete\")\n else:\n print(\"\\n🔬 SPECKIT: Skipping /speckit.analyze\")\n\n telemetry[\"phase_seconds\"][\"speckit_planning\"] = round(time.perf_counter() - phase_start, 3)\n telemetry[\"speckit_phases_run\"] = speckit_phases_run\n\n # ------------------------------------------------------------------\n # STEP 3: Parse tasks\n # ------------------------------------------------------------------\n tasks = parse_tasks(tasks_md)\n\n if not tasks:\n print(f\"\\n❌ No tasks parsed from {tasks_md}. Check tasks.md format.\")\n sys.exit(1)\n\n if fast_mode and len(tasks) > FAST_MODE_TASK_LIMIT:\n tasks = tasks[:FAST_MODE_TASK_LIMIT]\n print(f\"⚡ Fast mode: limited to first {FAST_MODE_TASK_LIMIT} tasks\")\n\n print(f\"\\n✅ Parsed {len(tasks)} tasks:\")\n for t in tasks:\n marker = \" [P]\" if t[\"parallel\"] else \"\"\n us = f\" [{t['user_story']}]\" if t.get(\"user_story\") else \"\"\n print(f\" {t['id']}{marker}{us}: {t['name']}\")\n\n # Load plan content for executor context (context_content already loaded earlier)\n plan_content = plan_path.read_text() if plan_path and plan_path.exists() else \"\"\n\n # Risk detection\n risk_profiles: dict[str, dict] = {}\n for t in tasks:\n is_risky, signals = _detect_task_risk(t, plan_content)\n risk_profiles[t[\"id\"]] = {\"is_high_risk\": is_risky, \"signals\": signals}\n\n high_risk_tasks = [t for t in tasks if risk_profiles[t[\"id\"]][\"is_high_risk\"]]\n\n # ------------------------------------------------------------------\n # STEP 4: Checkpoint\n # ------------------------------------------------------------------\n print(\"\\n\" + \"=\" * 60)\n print(\"🔍 CHECKPOINT: Review SpecKit artefacts\")\n print(\"=\" * 60)\n\n if spec_path:\n print(f\" 📄 {spec_path.relative_to(PROJECT_ROOT)}\")\n if plan_path:\n print(f\" 📄 {plan_path.relative_to(PROJECT_ROOT)}\")\n print(f\" 📄 {tasks_md.relative_to(PROJECT_ROOT)}\")\n\n if high_risk_tasks:\n print(\"\\n ⚠️ High-risk tasks detected:\")\n for t in high_risk_tasks:\n signals = \", \".join(risk_profiles[t[\"id\"]][\"signals\"][:5])\n print(f\" - {t['id']}: {t['name']} ({signals})\")\n\n checkpoint_start = time.perf_counter()\n if auto_approve and not (high_risk_tasks and not force_auto_approve):\n print(\"\\n⚡ Auto-approved checkpoint\")\n telemetry[\"checkpoint\"] = {\"auto_approved\": True, \"high_risk_task_count\": len(high_risk_tasks)}\n else:\n if auto_approve and high_risk_tasks and not force_auto_approve:\n print(\"\\n⚠️ Auto-approve paused because high-risk tasks were detected.\")\n approval = input(\"\\nApprove and continue execution? [y/n]: \").strip().lower()\n if approval not in (\"y\", \"yes\", \"\"):\n print(\"❌ Execution cancelled.\")\n telemetry[\"checkpoint\"] = {\"auto_approved\": False, \"approved\": False}\n telemetry[\"phase_seconds\"][\"checkpoint\"] = round(time.perf_counter() - checkpoint_start, 3)\n telemetry[\"total_seconds\"] = round(time.perf_counter() - run_started, 3)\n _write_telemetry(feature, telemetry)\n return\n telemetry[\"checkpoint\"] = {\"auto_approved\": False, \"approved\": True, \"high_risk_task_count\": len(high_risk_tasks)}\n\n telemetry[\"phase_seconds\"][\"checkpoint\"] = round(time.perf_counter() - checkpoint_start, 3)\n\n # ------------------------------------------------------------------\n # STEP 5: Execution (SWPP agents — NOT Claude Code)\n # ------------------------------------------------------------------\n print(f\"\\n⚡ PHASE 4: Executing tasks via SWPP {executor_backend} agents…\")\n\n committed_tasks = _get_committed_tasks()\n if committed_tasks:\n print(f\" ⏭️ Already committed: {', '.join(sorted(committed_tasks))}\")\n\n execute_fn = _execute_task_crewai if executor_backend == \"crewai\" else _execute_task_anthropic\n exec_phase_start = time.perf_counter()\n\n for task_info in tasks:\n tid = task_info[\"id\"]\n\n if tid in committed_tasks:\n print(f\"\\n ⏭️ Skipping {tid} (already committed)\")\n telemetry[\"tasks\"].append({\"task_id\": tid, \"skipped\": True, \"reason\": \"already_committed\"})\n continue\n\n print(f\"\\n Executing {tid}: {task_info['name']}…\")\n\n task_metrics: dict = {\n \"task_id\": tid,\n \"name\": task_info[\"name\"],\n \"high_risk\": risk_profiles[tid][\"is_high_risk\"],\n \"risk_signals\": risk_profiles[tid][\"signals\"],\n \"skipped\": False,\n }\n\n exec_start = time.perf_counter()\n try:\n result_text = execute_fn(task_info, plan_content, context_content)\n except Exception as e:\n print(f\" ❌ Executor error for {tid}: {e}\")\n task_metrics[\"execution_seconds\"] = round(time.perf_counter() - exec_start, 3)\n task_metrics[\"committed\"] = False\n task_metrics[\"error\"] = str(e)\n telemetry[\"tasks\"].append(task_metrics)\n telemetry[\"phase_seconds\"][\"execute_and_test\"] = round(time.perf_counter() - exec_phase_start, 3)\n telemetry[\"total_seconds\"] = round(time.perf_counter() - run_started, 3)\n _write_telemetry(feature, telemetry)\n return\n\n task_metrics[\"execution_seconds\"] = round(time.perf_counter() - exec_start, 3)\n\n # For anthropic backend, parse and write files from response\n if executor_backend == \"anthropic\":\n files = _parse_files_from_response(result_text)\n if files:\n written = _write_executor_files(files)\n print(f\" 📁 Wrote {len(written)} file(s): {', '.join(written)}\")\n else:\n print(f\" ⚠️ No === FILE: === blocks found in executor response for {tid}\")\n\n # Also copy/sync plan to .swpp/plans/ for consistency with other runners\n PLANS_DIR.mkdir(parents=True, exist_ok=True)\n (PLANS_DIR / f\"{tid}.md\").write_text(\n f\"# {tid}: {task_info['name']}\\n\\n\"\n f\"*Source: SpecKit plan*\\n\\n\"\n f\"## Task Description\\n{task_info.get('description', '')}\\n\"\n )\n\n # Tests\n print(f\"\\n 🧪 Running tests for {tid}…\")\n fast_fail = fast_mode and not task_metrics[\"high_risk\"]\n task_metrics[\"test_strategy\"] = \"fast-fail\" if fast_fail else \"strict\"\n test_start = time.perf_counter()\n test_result = _run_tests(tid, fast_fail=fast_fail)\n task_metrics[\"tests_seconds\"] = round(time.perf_counter() - test_start, 3)\n task_metrics[\"tests_passed\"] = test_result[\"passed\"]\n print(test_result[\"summary\"])\n\n # Commit only if tests pass\n if test_result[\"passed\"]:\n commit_msg = f\"feat: {task_info['name']} [{tid}]\"\n _git_commit(commit_msg)\n print(f\" ✅ {tid} committed\")\n task_metrics[\"committed\"] = True\n else:\n print(f\" ❌ {tid} tests failed. Stopping execution.\")\n for name, detail in test_result[\"details\"].items():\n if not detail[\"passed\"]:\n print(f\"\\n {name} output:\\n{detail['output'][:500]}\")\n task_metrics[\"committed\"] = False\n telemetry[\"tasks\"].append(task_metrics)\n telemetry[\"phase_seconds\"][\"execute_and_test\"] = round(time.perf_counter() - exec_phase_start, 3)\n telemetry[\"summary\"] = {\"status\": \"failed_tests\", \"failed_task\": tid}\n telemetry[\"total_seconds\"] = round(time.perf_counter() - run_started, 3)\n _write_telemetry(feature, telemetry)\n return\n\n telemetry[\"tasks\"].append(task_metrics)\n\n telemetry[\"phase_seconds\"][\"execute_and_test\"] = round(time.perf_counter() - exec_phase_start, 3)\n\n # ------------------------------------------------------------------\n # STEP 6: Review\n # ------------------------------------------------------------------\n review_results = []\n if skip_review:\n print(\"\\n🔎 PHASE 5: Review skipped\")\n telemetry[\"phase_seconds\"][\"review\"] = 0.0\n else:\n print(\"\\n🔎 PHASE 5: Reviewing implementation…\")\n review_start = time.perf_counter()\n all_committed = _get_committed_tasks()\n newly_committed = all_committed - committed_tasks\n\n review_fn = _review_task_crewai if executor_backend == \"crewai\" else _review_task_anthropic\n\n for task_info in tasks:\n tid = task_info[\"id\"]\n if tid not in newly_committed:\n continue\n try:\n review_text = review_fn(task_info, plan_content, context_content)\n review_results.append((tid, review_text))\n except Exception as e:\n review_results.append((tid, f\"Review error: {e}\"))\n\n telemetry[\"phase_seconds\"][\"review\"] = round(time.perf_counter() - review_start, 3)\n\n # ------------------------------------------------------------------\n # STEP 7: Summary\n # ------------------------------------------------------------------\n print(\"\\n\" + \"=\" * 60)\n print(\"✨ SWPP-SPECKIT EXECUTION COMPLETE\")\n print(\"=\" * 60)\n print(f\"\\nFeature: {feature}\")\n print(f\"Tasks completed: {len([t for t in telemetry['tasks'] if t.get('committed')])}/{len(tasks)}\")\n print(f\"High-risk tasks: {len(high_risk_tasks)}\")\n\n if review_results:\n print(\"\\nReview Summary:\")\n for tid, result in review_results:\n status = \"✅ PASS\" if \"PASS\" in result.upper() else \"⚠️ NEEDS ATTENTION\"\n print(f\" {tid}: {status}\")\n\n telemetry[\"summary\"] = {\n \"status\": \"completed\",\n \"tasks_total\": len(tasks),\n \"tasks_committed\": len([t for t in telemetry[\"tasks\"] if t.get(\"committed\")]),\n \"high_risk_task_count\": len(high_risk_tasks),\n \"reviews_completed\": len(review_results),\n }\n telemetry[\"total_seconds\"] = round(time.perf_counter() - run_started, 3)\n metrics_path = _write_telemetry(feature, telemetry)\n\n print(\"\\n📁 Artefacts:\")\n if spec_path:\n print(f\" Spec: {spec_path.relative_to(PROJECT_ROOT)}\")\n if plan_path:\n print(f\" Plan: {plan_path.relative_to(PROJECT_ROOT)}\")\n print(f\" Tasks: {tasks_md.relative_to(PROJECT_ROOT)}\")\n print(f\" SWPP plans: .swpp/plans/\")\n print(f\" Commits: git log --oneline -n {len(tasks)}\")\n print(f\" Telemetry: {metrics_path}\")\n\n\n# =============================================================================\n# CLI\n# =============================================================================\n\nif __name__ == \"__main__\":\n parser = argparse.ArgumentParser(\n description=\"SWPP with SpecKit planning + SWPP agent execution\",\n epilog=\"Planning uses SpecKit (via claude -p). Execution uses SWPP AI agents.\",\n )\n parser.add_argument(\"feature\", nargs=\"+\", help=\"Feature description\")\n parser.add_argument(\n \"--project\", type=str,\n help=\"Project name (creates new directory in ~/projects/<name> if doesn't exist)\"\n )\n parser.add_argument(\n \"--project-dir\", type=str,\n help=\"Custom base directory for projects (default: ~/projects)\"\n )\n parser.add_argument(\n \"--context\", type=str,\n help=\"Path to project context file (default: .swpp/context.md)\"\n )\n\n mode_group = parser.add_mutually_exclusive_group()\n mode_group.add_argument(\n \"--fast\", action=\"store_true\",\n help=\"Lean planning, auto-approve, skip review\",\n )\n mode_group.add_argument(\n \"--balanced\", action=\"store_true\",\n help=\"Lean planning, auto-approve, keep review\",\n )\n\n parser.add_argument(\"--auto-approve\", action=\"store_true\", help=\"Skip checkpoint prompt\")\n parser.add_argument(\"--force-auto-approve\", action=\"store_true\", help=\"Auto-approve even for high-risk tasks\")\n parser.add_argument(\"--skip-review\", action=\"store_true\", help=\"Skip review phase\")\n parser.add_argument(\n \"--executor\", choices=[\"anthropic\", \"crewai\"], default=DEFAULT_EXECUTOR_BACKEND,\n help=f\"Executor backend (default: {DEFAULT_EXECUTOR_BACKEND})\",\n )\n\n # SpecKit optional phase toggles\n parser.add_argument(\"--skip-constitution\", action=\"store_true\", help=\"Skip /speckit.constitution phase\")\n parser.add_argument(\"--skip-clarify\", action=\"store_true\", help=\"Skip /speckit.clarify phase\")\n parser.add_argument(\"--skip-checklist\", action=\"store_true\", help=\"Skip /speckit.checklist phase\")\n parser.add_argument(\"--skip-analyze\", action=\"store_true\", help=\"Skip /speckit.analyze phase\")\n\n args = parser.parse_args()\n feature = \" \".join(args.feature)\n\n # Setup project directory if --project flag is used\n if args.project:\n base_dir = Path(args.project_dir) if args.project_dir else Path.home() / \"projects\"\n project_path = setup_project_directory(args.project, base_dir)\n \n # Change working directory to project\n import os\n os.chdir(project_path)\n \n print(f\"\\n📂 Working in project: {project_path}\\n\")\n\n fast_mode = args.fast or args.balanced\n mode_label = \"FAST\" if args.fast else \"BALANCED\" if args.balanced else \"STANDARD\"\n auto_approve = args.auto_approve or fast_mode\n skip_review = args.skip_review or args.fast\n\n # In fast mode, skip optional enhancement phases by default\n skip_constitution = args.skip_constitution or args.fast\n skip_clarify = args.skip_clarify or args.fast\n skip_checklist = args.skip_checklist or args.fast\n skip_analyze = args.skip_analyze or args.fast\n \n # Determine context file path\n context_file_path = None\n if args.context:\n context_file_path = Path(args.context)\n if not context_file_path.is_absolute():\n context_file_path = PROJECT_ROOT / context_file_path\n\n run_swpp_speckit(\n feature,\n fast_mode=fast_mode,\n auto_approve=auto_approve,\n skip_review=skip_review,\n force_auto_approve=args.force_auto_approve,\n executor_backend=args.executor,\n mode_label=mode_label,\n skip_constitution=skip_constitution,\n skip_clarify=skip_clarify,\n skip_checklist=skip_checklist,\n skip_analyze=skip_analyze,\n context_file=context_file_path,\n )\n"}, "a73d5597cb85e2416e085195055d49925b6f99d5": {"t": 1785766531.156833, "n": 50689, "h": "9154455611f706da", "c": "# swpp_speckit.py\n# SWPP Runner with SpecKit (spec-driven planning) + SWPP Agents (execution)\n#\n# SpecKit phases (via `claude -p` slash commands):\n# /speckit.constitution → project principles\n# /speckit.specify → feature specification (spec.md)\n# /speckit.clarify → de-risk ambiguity in spec\n# /speckit.plan → implementation plan (plan.md)\n# /speckit.checklist → quality validation checklists\n# /speckit.tasks → actionable task list (tasks.md)\n# /speckit.analyze → cross-artifact consistency check\n# /speckit.implement → SKIPPED (SWPP agents handle execution)\n#\n# Execution: SWPP AI agents (Anthropic API or CrewAI)\n#\n# Requires: `specify` CLI (GitHub Spec Kit) + `claude` CLI (Claude Code)\n#\n# Run: python swpp_speckit.py \"Your feature description\"\n\nimport os\nimport sys\nimport json\nimport re\nimport time\nimport shutil\nimport subprocess\nimport argparse\nfrom datetime import datetime\nfrom pathlib import Path\nfrom textwrap import dedent\n\n# =============================================================================\n# CONFIGURATION\n# =============================================================================\n\n# PROJECT_ROOT is set dynamically based on --project flag (see main)\nPROJECT_ROOT = Path.cwd()\n\ndef get_project_paths(project_root: Path) -> dict:\n \"\"\"Return all project paths based on root directory.\"\"\"\n return {\n \"root\": project_root,\n \"swpp_dir\": project_root / \".swpp\",\n \"plans_dir\": project_root / \".swpp\" / \"plans\",\n \"context_file\": project_root / \".swpp\" / \"context.md\",\n \"speckit_dir\": project_root / \".specify\",\n \"claude_commands_dir\": project_root / \".claude\" / \"commands\",\n \"specs_dir\": project_root / \"specs\",\n }\n\n# Default paths (updated when --project is used)\nSWPP_DIR = PROJECT_ROOT / \".swpp\"\nPLANS_DIR = SWPP_DIR / \"plans\"\nCONTEXT_FILE = SWPP_DIR / \"context.md\"\nSPECKIT_DIR = PROJECT_ROOT / \".specify\"\nCLAUDE_COMMANDS_DIR = PROJECT_ROOT / \".claude\" / \"commands\"\nSPECS_DIR = PROJECT_ROOT / \"specs\"\n\nFAST_MODE_TASK_LIMIT = 3\nHIGH_RISK_KEYWORDS = {\n \"auth\", \"authentication\", \"authorization\", \"permission\", \"permissions\",\n \"secret\", \"token\", \"password\", \"credential\", \"oauth\", \"security\",\n \"encryption\", \"decrypt\", \"migration\", \"schema\", \"database\", \"sql\",\n \"prod\", \"production\", \"billing\", \"payment\", \"compliance\",\n}\n\n# Claude Code CLI timeout per phase (seconds)\nSPECKIT_PHASE_TIMEOUT = 300 # 5 min per slash command\n\n# Model Configuration\nEXECUTOR_MODEL = os.getenv(\"SWPP_EXECUTOR_MODEL\", \"deepseek-chat\") # or \"claude-sonnet-4-5-20250929\"\nREVIEWER_MODEL = os.getenv(\"SWPP_REVIEWER_MODEL\", \"claude-sonnet-4-5-20250929\")\n\n# Executor backend: \"crewai\" or \"anthropic\"\n# crewai uses CrewAI agents, anthropic uses raw Anthropic SDK\nDEFAULT_EXECUTOR_BACKEND = \"anthropic\"\n\n\n# =============================================================================\n# PREREQUISITES\n# =============================================================================\n\ndef _check_cli(name: str, check_cmd: list[str]) -> bool:\n \"\"\"Verify a CLI tool is installed and reachable.\"\"\"\n try:\n subprocess.run(check_cmd, capture_output=True, timeout=10)\n return True\n except (FileNotFoundError, subprocess.TimeoutExpired):\n return False\n\n\ndef check_prerequisites() -> list[str]:\n \"\"\"Return list of missing prerequisites.\"\"\"\n missing = []\n if not _check_cli(\"specify\", [\"specify\", \"version\"]):\n missing.append(\"specify (GitHub Spec Kit) — install: https://github.com/specifyit/specify\")\n if not _check_cli(\"claude\", [\"claude\", \"--version\"]):\n missing.append(\"claude (Claude Code CLI) — install: https://docs.anthropic.com/en/docs/claude-code\")\n if not os.getenv(\"ANTHROPIC_API_KEY\"):\n missing.append(\"ANTHROPIC_API_KEY environment variable\")\n return missing\n\n\n# =============================================================================\n# PROJECT SETUP\n# =============================================================================\n\ndef setup_project_directory(project_name: str, base_dir: Path = Path.home() / \"projects\") -> Path:\n \"\"\"Create project directory and initialize basic structure.\n \n Args:\n project_name: Name of the project (e.g., \"HelloWorld\")\n base_dir: Parent directory where project will be created\n \n Returns:\n Path to the created/existing project directory\n \"\"\"\n # Sanitize project name (remove spaces, special chars)\n safe_name = re.sub(r'[^a-zA-Z0-9_-]', '-', project_name)\n project_path = base_dir / safe_name\n \n if project_path.exists():\n print(f\" ℹ️ Project directory already exists: {project_path}\")\n else:\n print(f\" 📁 Creating project directory: {project_path}\")\n project_path.mkdir(parents=True, exist_ok=True)\n \n # Create basic Python project structure\n (project_path / \"src\").mkdir(exist_ok=True)\n (project_path / \"tests\").mkdir(exist_ok=True)\n (project_path / \"tests\" / \"unit\").mkdir(exist_ok=True)\n (project_path / \"tests\" / \"integration\").mkdir(exist_ok=True)\n (project_path / \"tests\" / \"safety\").mkdir(exist_ok=True)\n (project_path / \"tests\" / \"solid\").mkdir(exist_ok=True)\n \n # Create __init__.py files\n (project_path / \"src\" / \"__init__.py\").touch()\n (project_path / \"tests\" / \"__init__.py\").touch()\n (project_path / \"tests\" / \"unit\" / \"__init__.py\").touch()\n (project_path / \"tests\" / \"integration\" / \"__init__.py\").touch()\n (project_path / \"tests\" / \"safety\" / \"__init__.py\").touch()\n (project_path / \"tests\" / \"solid\" / \"__init__.py\").touch()\n \n # Initialize git if not already a repo\n if not (project_path / \".git\").exists():\n print(\" 🔧 Initializing git repository...\")\n subprocess.run([\"git\", \"init\"], cwd=project_path, capture_output=True)\n subprocess.run([\"git\", \"checkout\", \"-b\", \"main\"], cwd=project_path, capture_output=True)\n \n # Create .swpp/context.md with template\n swpp_dir = project_path / \".swpp\"\n swpp_dir.mkdir(exist_ok=True)\n context_file = swpp_dir / \"context.md\"\n \n if not context_file.exists():\n print(\" 📝 Creating context.md template...\")\n context_template = dedent(f\"\"\"\n # {safe_name} - Project Context\n \n ## Project Overview\n \n **Purpose**: [Describe what this project does and why it exists]\n \n **Target Users**: [Who will use this?]\n \n **Key Features**:\n - [Feature 1]\n - [Feature 2]\n - [Feature 3]\n \n ## Technical Stack\n \n **Language**: Python 3.13+\n \n **Framework**: [e.g., FastAPI, Django, Flask, or N/A for libraries]\n \n **Database**: [e.g., PostgreSQL, MySQL, SQLite, or N/A]\n \n **Key Dependencies**:\n - [Library 1]\n - [Library 2]\n \n ## Architecture & Patterns\n \n **Project Structure**:\n ```\n src/ # Application code\n tests/ # Test suites\n ```\n \n **Design Patterns**: [e.g., MVC, Repository, Service Layer]\n \n ## Coding Standards\n \n **Style Guide**: PEP 8 + type hints\n \n **Naming Conventions**:\n - Classes: PascalCase\n - Functions/variables: snake_case\n - Constants: UPPER_SNAKE_CASE\n \n **Testing Requirements**:\n - Unit tests for all business logic\n - Integration tests for API endpoints\n - Safety tests for error handling\n - SOLID principle validation\n \n ## Development Guidelines\n \n **Git Workflow**: Feature branches → main\n \n **Commit Format**: `type: description [TASK-ID]`\n - Types: feat, fix, docs, test, refactor\n \n **Documentation**: Docstrings for all public APIs\n \n ## Notes\n \n [Add any project-specific notes, constraints, or considerations]\n \"\"\").strip()\n context_file.write_text(context_template)\n print(f\" ⚠️ IMPORTANT: Edit .swpp/context.md with your project details!\")\n \n print(f\" ✅ Project structure created\")\n \n return project_path\n\n\n# =============================================================================\n# SPECKIT SCAFFOLD\n# =============================================================================\n\ndef ensure_speckit_scaffold() -> bool:\n \"\"\"Ensure SpecKit project scaffold exists (.specify/ and .claude/commands/).\"\"\"\n if SPECKIT_DIR.exists() and CLAUDE_COMMANDS_DIR.exists():\n print(\" ✅ SpecKit scaffold already exists\")\n return True\n\n print(\" 🔧 Initializing SpecKit scaffold automatically...\")\n print(\" (This may take 30-60s to download template from GitHub)\")\n \n result = subprocess.run(\n [\"specify\", \"init\", \".\", \"--ai\", \"claude\", \"--here\", \"--force\", \"--no-git\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=180,\n )\n \n if result.returncode != 0:\n print(f\" ❌ specify init failed:\\n{result.stderr}\")\n return False\n\n # Verify key artefacts\n if not CLAUDE_COMMANDS_DIR.exists():\n print(\" ❌ .claude/commands/ not created – specify init may have failed silently\")\n return False\n\n print(\" ✅ SpecKit scaffold created successfully\")\n return True\n\n\n# =============================================================================\n# CLAUDE CODE SLASH COMMAND RUNNER\n# =============================================================================\n\ndef run_speckit_command(\n slash_command: str,\n args: str = \"\",\n context: str = \"\",\n timeout: int = SPECKIT_PHASE_TIMEOUT,\n) -> tuple[bool, str]:\n \"\"\"\n Execute a SpecKit slash command via ``claude -p``.\n \n Args:\n slash_command: SpecKit command name (e.g., \"speckit.specify\")\n args: Feature description or command arguments\n context: Project context to inject into prompt\n timeout: Command timeout in seconds\n\n Returns (success, output_text).\n \"\"\"\n # Build prompt with context injection\n if context:\n prompt = f\"/{slash_command} CONTEXT: {args}\"\n else:\n prompt = f\"/{slash_command} {args}\".strip()\n \n cmd = [\n \"claude\",\n \"-p\", prompt,\n \"--output-format\", \"text\",\n \"--allowedTools\", \"Bash Read Write Edit\",\n ]\n \n # Add context as environment or file if needed\n if context:\n # Write context to temp file that Claude can read\n context_temp = PROJECT_ROOT / \".swpp\" / \"_temp_context.md\"\n context_temp.write_text(context)\n \n print(f\" ▸ claude -p \\\"/{slash_command} {args[:60]}{'…' if len(args) > 60 else ''}\\\"\")\n if context:\n print(f\" (with project context: {len(context)} chars)\")\n\n try:\n result = subprocess.run(\n cmd, cwd=PROJECT_ROOT,\n capture_output=True, text=True,\n timeout=timeout,\n )\n output = result.stdout + result.stderr\n success = result.returncode == 0\n if not success:\n print(f\" ⚠️ /{slash_command} exited with code {result.returncode}\")\n return success, output\n except subprocess.TimeoutExpired:\n print(f\" ⚠️ /{slash_command} timed out after {timeout}s\")\n return False, f\"Timed out after {timeout}s\"\n\n\n# =============================================================================\n# SPECKIT PLANNING PHASES\n# =============================================================================\n\ndef phase_constitution(context: str = \"\", args: str = \"\") -> tuple[bool, str]:\n \"\"\"Establish project governing principles via /speckit.constitution.\"\"\"\n print(\"\\n📜 SPECKIT: Establishing project constitution…\")\n return run_speckit_command(\"speckit.constitution\", args, context)\n\n\ndef phase_specify(feature: str, context: str = \"\") -> tuple[bool, str]:\n \"\"\"Generate feature specification via /speckit.specify.\"\"\"\n print(\"\\n📋 SPECKIT: Generating feature specification…\")\n return run_speckit_command(\"speckit.specify\", feature, context)\n\n\ndef phase_clarify(context: str = \"\", args: str = \"\") -> tuple[bool, str]:\n \"\"\"Clarify underspecified areas in spec via /speckit.clarify.\"\"\"\n print(\"\\n🔎 SPECKIT: Clarifying specification…\")\n return run_speckit_command(\"speckit.clarify\", args, context)\n\n\ndef phase_plan(context: str = \"\") -> tuple[bool, str]:\n \"\"\"Generate implementation plan via /speckit.plan.\"\"\"\n print(\"\\n📐 SPECKIT: Generating implementation plan…\")\n return run_speckit_command(\"speckit.plan\", \"\", context)\n\n\ndef phase_checklist(context: str = \"\", args: str = \"\") -> tuple[bool, str]:\n \"\"\"Generate quality checklists via /speckit.checklist.\"\"\"\n print(\"\\n✅ SPECKIT: Generating quality checklist…\")\n return run_speckit_command(\"speckit.checklist\", args, context)\n\n\ndef phase_tasks(context: str = \"\") -> tuple[bool, str]:\n \"\"\"Generate actionable tasks via /speckit.tasks.\"\"\"\n print(\"\\n📝 SPECKIT: Generating actionable task list…\")\n return run_speckit_command(\"speckit.tasks\", \"\", context)\n\n\ndef phase_analyze(context: str = \"\") -> tuple[bool, str]:\n \"\"\"Cross-artifact consistency analysis via /speckit.analyze.\"\"\"\n print(\"\\n🔬 SPECKIT: Analyzing cross-artifact consistency…\")\n return run_speckit_command(\"speckit.analyze\", \"\", context)\n\n\n# =============================================================================\n# TASK PARSING\n# =============================================================================\n\n_TASK_LINE_RE = re.compile(\n r\"^- \\[[ xX]?\\]\\s+\" # checkbox\n r\"(T\\d{3})\" # task ID (T001, T002, …)\n r\"(?:\\s+\\[P\\])?\" # optional parallel marker\n r\"(?:\\s+\\[US\\d+\\])?\" # optional user-story label\n r\"\\s+(.+)$\" # description\n)\n\n\ndef _find_tasks_md() -> Path | None:\n \"\"\"Locate the tasks.md generated by SpecKit inside specs/<branch>/.\"\"\"\n # Try to find from current git branch\n try:\n branch = subprocess.run(\n [\"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=5,\n ).stdout.strip()\n except Exception:\n branch = \"\"\n\n if branch and (SPECS_DIR / branch / \"tasks.md\").exists():\n return SPECS_DIR / branch / \"tasks.md\"\n\n # Fallback: find newest tasks.md under specs/\n if SPECS_DIR.exists():\n candidates = sorted(SPECS_DIR.glob(\"*/tasks.md\"), key=lambda p: p.stat().st_mtime, reverse=True)\n if candidates:\n return candidates[0]\n\n return None\n\n\ndef _find_spec_md() -> Path | None:\n \"\"\"Locate the spec.md generated by SpecKit.\"\"\"\n try:\n branch = subprocess.run(\n [\"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=5,\n ).stdout.strip()\n except Exception:\n branch = \"\"\n\n if branch and (SPECS_DIR / branch / \"spec.md\").exists():\n return SPECS_DIR / branch / \"spec.md\"\n\n if SPECS_DIR.exists():\n candidates = sorted(SPECS_DIR.glob(\"*/spec.md\"), key=lambda p: p.stat().st_mtime, reverse=True)\n if candidates:\n return candidates[0]\n return None\n\n\ndef _find_plan_md() -> Path | None:\n \"\"\"Locate the plan.md generated by SpecKit.\"\"\"\n try:\n branch = subprocess.run(\n [\"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=5,\n ).stdout.strip()\n except Exception:\n branch = \"\"\n\n if branch and (SPECS_DIR / branch / \"plan.md\").exists():\n return SPECS_DIR / branch / \"plan.md\"\n\n if SPECS_DIR.exists():\n candidates = sorted(SPECS_DIR.glob(\"*/plan.md\"), key=lambda p: p.stat().st_mtime, reverse=True)\n if candidates:\n return candidates[0]\n return None\n\n\ndef parse_tasks(tasks_md_path: Path) -> list[dict]:\n \"\"\"Parse tasks.md into a list of task dicts with id, name, parallel flag.\"\"\"\n content = tasks_md_path.read_text()\n tasks = []\n current_phase = \"\"\n\n for line in content.splitlines():\n # Detect phase headings\n if line.startswith(\"## \") or line.startswith(\"### \"):\n current_phase = line.lstrip(\"#\").strip()\n continue\n\n m = _TASK_LINE_RE.match(line.strip())\n if m:\n task_id = m.group(1)\n description = m.group(2).strip()\n is_parallel = \"[P]\" in line\n us_match = re.search(r\"\\[US(\\d+)\\]\", line)\n user_story = f\"US{us_match.group(1)}\" if us_match else None\n\n tasks.append({\n \"id\": task_id,\n \"name\": description,\n \"description\": description,\n \"phase\": current_phase,\n \"parallel\": is_parallel,\n \"user_story\": user_story,\n })\n\n return tasks\n\n\n# =============================================================================\n# RISK DETECTION\n# =============================================================================\n\ndef _detect_task_risk(task_info: dict, plan_content: str = \"\") -> tuple[bool, list[str]]:\n \"\"\"Detect whether a task is high-risk based on metadata and plan text.\"\"\"\n haystack = \" \".join([\n str(task_info.get(\"id\", \"\")),\n str(task_info.get(\"name\", \"\")),\n str(task_info.get(\"description\", \"\")),\n plan_content,\n ]).lower()\n\n matches = sorted(\n {kw for kw in HIGH_RISK_KEYWORDS if re.search(rf\"\\b{re.escape(kw)}\\b\", haystack)}\n )\n return (len(matches) > 0, matches)\n\n\n# =============================================================================\n# EXECUTION — ANTHROPIC SDK (default backend)\n# =============================================================================\n\ndef _execute_task_anthropic(task_info: dict, plan_content: str, context_content: str) -> str:\n \"\"\"Execute a single task using Anthropic API or DeepSeek (OpenAI-compatible).\"\"\"\n model = EXECUTOR_MODEL\n\n prompt = dedent(f\"\"\"\\\n You are a precise developer implementing a task from an approved SpecKit plan.\n\n TASK: {task_info['id']} — {task_info['name']}\n DESCRIPTION: {task_info.get('description', '')}\n\n IMPLEMENTATION PLAN (from SpecKit):\n {plan_content}\n\n PROJECT STANDARDS:\n {context_content}\n\n RULES:\n 1. ONLY create or modify files directly related to this task.\n 2. Follow project standards and conventions exactly.\n 3. If the plan is unclear, make the safest possible implementation choice.\n 4. Output your work using the format below.\n\n OUTPUT FORMAT:\n === FILE: <relative-path> ===\n [complete file contents]\n === END FILE ===\n\n Provide ALL files you create or modify.\n \"\"\")\n\n # Use DeepSeek (OpenAI-compatible) if model starts with \"deepseek\"\n if model.startswith(\"deepseek\"):\n from openai import OpenAI\n client = OpenAI(\n api_key=os.getenv(\"DEEPSEEK_API_KEY\"),\n base_url=\"https://api.deepseek.com\",\n )\n response = client.chat.completions.create(\n model=model,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n max_tokens=8192,\n temperature=0.2,\n )\n return response.choices[0].message.content\n else:\n # Use Anthropic Claude\n from anthropic import Anthropic\n client = Anthropic()\n response = client.messages.create(\n model=model,\n max_tokens=8192,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n )\n return response.content[0].text\n\n\ndef _parse_files_from_response(response_text: str) -> dict[str, str]:\n \"\"\"Extract file paths and contents from === FILE: ... === blocks.\"\"\"\n files = {}\n pattern = re.compile(\n r\"=== FILE:\\s*(.+?)\\s*===\\s*\\n(.*?)\\n=== END FILE ===\",\n re.DOTALL,\n )\n for match in pattern.finditer(response_text):\n filepath = match.group(1).strip()\n content = match.group(2)\n files[filepath] = content\n return files\n\n\ndef _write_executor_files(files: dict[str, str]) -> list[str]:\n \"\"\"Write parsed files to disk, return list of written paths.\"\"\"\n written = []\n for rel_path, content in files.items():\n path = PROJECT_ROOT / rel_path\n path.parent.mkdir(parents=True, exist_ok=True)\n path.write_text(content)\n written.append(rel_path)\n return written\n\n\n# =============================================================================\n# EXECUTION — CREWAI BACKEND (optional)\n# =============================================================================\n\ndef _execute_task_crewai(task_info: dict, plan_content: str, context_content: str) -> str:\n \"\"\"Execute a single task using CrewAI agents (mirrors swpp_crew.py executor).\"\"\"\n from crewai import Agent, Task, Crew, LLM\n from crewai.tools import tool as crewai_tool\n\n model = EXECUTOR_MODEL\n\n # Use appropriate LangChain wrapper based on model\n if model.startswith(\"deepseek\"):\n from langchain_openai import ChatOpenAI\n executor_model = ChatOpenAI(\n model=model,\n api_key=os.getenv(\"DEEPSEEK_API_KEY\"),\n base_url=\"https://api.deepseek.com\",\n temperature=0.2,\n )\n else:\n from langchain_anthropic import ChatAnthropic\n executor_model = ChatAnthropic(\n model=model,\n temperature=0.2,\n )\n\n @crewai_tool\n def read_file(filepath: str) -> str:\n \"\"\"Read contents of a file in the project.\"\"\"\n path = PROJECT_ROOT / filepath\n return path.read_text() if path.exists() else f\"File not found: {filepath}\"\n\n @crewai_tool\n def write_file(filepath: str, content: str) -> str:\n \"\"\"Write content to a file in the project.\"\"\"\n path = PROJECT_ROOT / filepath\n path.parent.mkdir(parents=True, exist_ok=True)\n path.write_text(content)\n return f\"Written: {filepath}\"\n\n @crewai_tool\n def list_files(directory: str = \"src\") -> str:\n \"\"\"List files in a directory.\"\"\"\n path = PROJECT_ROOT / directory\n if not path.exists():\n return f\"Directory not found: {directory}\"\n files = [str(f.relative_to(PROJECT_ROOT)) for f in path.rglob(\"*\") if f.is_file()]\n return \"\\n\".join(files[:50])\n\n @crewai_tool\n def run_command(command: str) -> str:\n \"\"\"Run a shell command.\"\"\"\n try:\n result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=60, cwd=PROJECT_ROOT)\n return result.stdout + result.stderr\n except subprocess.TimeoutExpired:\n return \"Command timed out\"\n\n executor = Agent(\n role=\"Execution Agent\",\n goal=\"Implement code exactly according to approved SpecKit plans\",\n backstory=dedent(\"\"\"\\\n You are a precise developer who implements code following SpecKit plans exactly.\n You only modify files specified in the plan. You follow project\n standards religiously.\n \"\"\"),\n llm=executor_model,\n tools=[read_file, write_file, list_files, run_command],\n verbose=True,\n )\n\n execution_task = Task(\n description=dedent(f\"\"\"\\\n Implement code according to this approved SpecKit plan:\n\n TASK: {task_info['id']} — {task_info['name']}\n\n PLAN:\n {plan_content}\n\n PROJECT STANDARDS:\n {context_content}\n\n RULES:\n 1. ONLY modify files listed in the plan\n 2. Follow project standards exactly\n 3. After implementation, run linting if available\n\n Report what you created/modified.\n \"\"\"),\n expected_output=\"Implementation report with files created/modified\",\n agent=executor,\n )\n\n crew = Crew(agents=[executor], tasks=[execution_task], verbose=True)\n result = crew.kickoff()\n return str(result)\n\n\n# =============================================================================\n# TESTS & GIT\n# =============================================================================\n\ndef _run_tests(task_id: str, fast_fail: bool = False) -> dict:\n \"\"\"Run the project test suite. Returns dict with passed, summary, details.\"\"\"\n venv_python = str(PROJECT_ROOT / \".venv\" / \"bin\" / \"python\")\n if not Path(venv_python).exists():\n venv_python = \"python\"\n\n test_commands = [\n (\"unit\", f\"{venv_python} -m pytest tests/unit/ -v --tb=short\"),\n (\"integration\", f\"{venv_python} -m pytest tests/integration/ -v --tb=short\"),\n (\"safety\", f\"{venv_python} -m pytest tests/safety/ -v --tb=short\"),\n (\"solid\", f\"{venv_python} -m pytest tests/solid/ -v --tb=short\"),\n ]\n\n results = {}\n all_passed = True\n lines = [f\"\\n🧪 Running tests for {task_id}…\\n\"]\n\n for name, cmd in test_commands:\n try:\n res = subprocess.run(cmd, shell=True, cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=120)\n except subprocess.TimeoutExpired:\n res = type(\"R\", (), {\"returncode\": 1, \"stdout\": \"\", \"stderr\": \"Timed out\"})()\n\n passed = res.returncode == 0 or \"passed\" in getattr(res, \"stdout\", \"\").lower()\n results[name] = {\"passed\": passed, \"output\": getattr(res, \"stdout\", \"\") + getattr(res, \"stderr\", \"\")}\n lines.append(f\" {name}: {'✅ PASS' if passed else '❌ FAIL'}\")\n\n if not passed:\n all_passed = False\n if fast_fail:\n lines.append(\" ⚡ Fast mode: stopping at first failure\")\n break\n\n lines.append(f\"\\n{'✅ All tests passed!' if all_passed else '❌ Some tests failed'}\\n\")\n return {\"passed\": all_passed, \"summary\": \"\\n\".join(lines), \"details\": results}\n\n\ndef _git_commit(message: str) -> str:\n \"\"\"Stage all changes and commit.\"\"\"\n subprocess.run([\"git\", \"add\", \"-A\"], cwd=PROJECT_ROOT)\n result = subprocess.run([\"git\", \"commit\", \"-m\", message], cwd=PROJECT_ROOT, capture_output=True, text=True)\n return result.stdout + result.stderr\n\n\ndef _get_committed_tasks() -> set[str]:\n \"\"\"Return set of task IDs already committed (format: feat: ... [TXXX]).\"\"\"\n result = subprocess.run(\n [\"git\", \"log\", \"--oneline\", \"-n\", \"100\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True,\n )\n committed = set()\n for line in result.stdout.splitlines():\n m = re.search(r\"\\[(T\\d{3})\\]\", line)\n if m:\n committed.add(m.group(1))\n return committed\n\n\n# =============================================================================\n# REVIEW\n# =============================================================================\n\ndef _review_task_anthropic(task_info: dict, plan_content: str, context_content: str) -> str:\n \"\"\"Review implementation of a single task using Claude Sonnet.\"\"\"\n from anthropic import Anthropic\n\n client = Anthropic()\n model = REVIEWER_MODEL\n\n prompt = dedent(f\"\"\"\\\n Review the implementation of task {task_info['id']}: {task_info['name']}.\n\n ORIGINAL PLAN:\n {plan_content}\n\n PROJECT STANDARDS:\n {context_content}\n\n REVIEW CHECKLIST:\n 1. Does implementation match the plan?\n 2. Are project standards followed?\n 3. Is error handling adequate?\n 4. Are there any bugs or edge cases missed?\n 5. Is the code clean and maintainable?\n\n OUTPUT: PASS or FAIL with issues found and suggestions.\n \"\"\")\n\n response = client.messages.create(\n model=model,\n max_tokens=4096,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n )\n return response.content[0].text\n\n\ndef _review_task_crewai(task_info: dict, plan_content: str, context_content: str) -> str:\n \"\"\"Review implementation of a single task using CrewAI reviewer agent.\"\"\"\n from crewai import Agent, Task, Crew\n\n model = REVIEWER_MODEL\n\n # Reviewer always uses Claude (from REVIEWER_MODEL)\n from langchain_anthropic import ChatAnthropic\n reviewer_model = ChatAnthropic(\n model=model,\n temperature=0.1, # Low temperature for consistent review\n )\n\n reviewer = Agent(\n role=\"Code Reviewer\",\n goal=\"Ensure code quality, adherence to standards, and identify bugs\",\n backstory=dedent(\"\"\"\\\n You are a meticulous code reviewer with expertise in software quality.\n You verify implementations match approved plans, follow project standards,\n and have proper error handling. You catch edge cases and maintainability issues.\n \"\"\"),\n llm=reviewer_model,\n tools=[], # Reviewer doesn't need file tools - reviews what was already committed\n verbose=True,\n )\n\n review_task = Task(\n description=dedent(f\"\"\"\\\n Review the implementation of task {task_info['id']}: {task_info['name']}.\n\n ORIGINAL PLAN:\n {plan_content}\n\n PROJECT STANDARDS:\n {context_content}\n\n REVIEW CHECKLIST:\n 1. Does implementation match the plan?\n 2. Are project standards followed?\n 3. Is error handling adequate?\n 4. Are there any bugs or edge cases missed?\n 5. Is the code clean and maintainable?\n\n OUTPUT: Start with \"PASS\" or \"FAIL\", then provide detailed findings and suggestions.\n \"\"\"),\n expected_output=\"Review verdict (PASS/FAIL) with detailed findings\",\n agent=reviewer,\n )\n\n crew = Crew(agents=[reviewer], tasks=[review_task], verbose=True)\n result = crew.kickoff()\n return str(result)\n\n\n# =============================================================================\n# TELEMETRY\n# =============================================================================\n\ndef _write_telemetry(feature: str, telemetry: dict) -> Path:\n \"\"\"Persist run telemetry to .swpp/.\"\"\"\n SWPP_DIR.mkdir(exist_ok=True)\n path = SWPP_DIR / f\"metrics_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json\"\n payload = {\"feature\": feature, \"generated_at\": datetime.now().isoformat(), **telemetry}\n path.write_text(json.dumps(payload, indent=2))\n return path\n\n\n# =============================================================================\n# MAIN ORCHESTRATOR\n# =============================================================================\n\ndef run_swpp_speckit(\n feature: str,\n *,\n fast_mode: bool = False,\n auto_approve: bool = False,\n skip_review: bool = False,\n force_auto_approve: bool = False,\n executor_backend: str = DEFAULT_EXECUTOR_BACKEND,\n mode_label: str = \"STANDARD\",\n skip_constitution: bool = False,\n skip_clarify: bool = False,\n skip_checklist: bool = False,\n skip_analyze: bool = False,\n):\n run_started = time.perf_counter()\n telemetry: dict = {\n \"runner\": \"swpp_speckit\",\n \"mode\": mode_label,\n \"executor_backend\": executor_backend,\n \"phase_seconds\": {},\n \"tasks\": [],\n \"checkpoint\": {},\n \"summary\": {},\n }\n\n print(\"\\n\" + \"=\" * 60)\n print(\"SWPP-SPECKIT AUTOMATIC EXECUTION\")\n print(f\"Feature: {feature}\")\n print(f\"Mode: {mode_label} | Executor: {executor_backend}\")\n print(\"=\" * 60)\n\n # ------------------------------------------------------------------\n # STEP 0: Prerequisites\n # ------------------------------------------------------------------\n missing = check_prerequisites()\n if missing:\n print(\"\\n❌ Missing prerequisites:\")\n for m in missing:\n print(f\" • {m}\")\n sys.exit(1)\n\n # ------------------------------------------------------------------\n # STEP 1: Ensure SpecKit scaffold\n # ------------------------------------------------------------------\n print(\"\\n🔧 Checking SpecKit scaffold…\")\n if not ensure_speckit_scaffold():\n sys.exit(1)\n\n # ------------------------------------------------------------------\n # STEP 1.5: Load and validate context file\n # ------------------------------------------------------------------\n if not CONTEXT_FILE.exists():\n print(\"\\n\" + \"=\" * 60)\n print(\"❌ REQUIRED: Project context file not found\")\n print(\"=\" * 60)\n print(f\"\\nCreate {CONTEXT_FILE.relative_to(PROJECT_ROOT)} with:\")\n print(\" • Project purpose and goals\")\n print(\" • Tech stack (language, framework, database)\")\n print(\" • Architecture and design patterns\")\n print(\" • Coding standards and naming conventions\")\n print(\" • Testing requirements\")\n print(\"\\nThis context is passed to Claude Code for better planning.\")\n print(\"\\nFor new projects, run with --project flag to auto-generate template.\")\n print(\"=\" * 60)\n sys.exit(1)\n \n context_content = CONTEXT_FILE.read_text()\n print(f\" ✅ Loaded context: {CONTEXT_FILE.relative_to(PROJECT_ROOT)} ({len(context_content)} chars)\")\n\n # ------------------------------------------------------------------\n # STEP 2: SpecKit planning phases (with context injection)\n # ------------------------------------------------------------------\n phase_start = time.perf_counter()\n speckit_phases_run: list[str] = []\n\n # 2a: /speckit.constitution → .specify/memory/constitution.md\n if not skip_constitution:\n ok, output = phase_constitution(context_content)\n if not ok:\n print(f\"\\n⚠️ Constitution phase failed (non-fatal). Output:\\n{output[:500]}\")\n else:\n speckit_phases_run.append(\"constitution\")\n print(\" ✅ Constitution established\")\n else:\n print(\"\\n📜 SPECKIT: Skipping /speckit.constitution\")\n\n # 2b: /speckit.specify → spec.md\n ok, output = phase_specify(feature, context_content)\n if not ok:\n print(f\"\\n❌ Specification phase failed. Output:\\n{output[:1000]}\")\n sys.exit(1)\n\n spec_path = _find_spec_md()\n if spec_path:\n print(f\" ✅ Spec: {spec_path.relative_to(PROJECT_ROOT)}\")\n speckit_phases_run.append(\"specify\")\n else:\n print(\" ⚠️ spec.md not found after /speckit.specify — continuing anyway\")\n\n # 2c: /speckit.clarify → refine spec.md\n if not skip_clarify:\n ok, output = phase_clarify(context_content)\n if not ok:\n print(f\"\\n⚠️ Clarify phase failed (non-fatal). Output:\\n{output[:500]}\")\n else:\n speckit_phases_run.append(\"clarify\")\n print(\" ✅ Spec clarified\")\n else:\n print(\"\\n🔎 SPECKIT: Skipping /speckit.clarify\")\n\n # 2d: /speckit.plan → plan.md\n ok, output = phase_plan(context_content)\n if not ok:\n print(f\"\\n❌ Planning phase failed. Output:\\n{output[:1000]}\")\n sys.exit(1)\n\n plan_path = _find_plan_md()\n if plan_path:\n print(f\" ✅ Plan: {plan_path.relative_to(PROJECT_ROOT)}\")\n speckit_phases_run.append(\"plan\")\n else:\n print(\" ⚠️ plan.md not found after /speckit.plan — continuing anyway\")\n\n # 2e: /speckit.checklist → quality validation\n if not skip_checklist:\n ok, output = phase_checklist(context_content)\n if not ok:\n print(f\"\\n⚠️ Checklist phase failed (non-fatal). Output:\\n{output[:500]}\")\n else:\n speckit_phases_run.append(\"checklist\")\n print(\" ✅ Quality checklist generated\")\n else:\n print(\"\\n✅ SPECKIT: Skipping /speckit.checklist\")\n\n # 2f: /speckit.tasks → tasks.md\n ok, output = phase_tasks(context_content)\n if not ok:\n print(f\"\\n❌ Task generation failed. Output:\\n{output[:1000]}\")\n sys.exit(1)\n\n tasks_md = _find_tasks_md()\n if not tasks_md:\n print(\"\\n❌ tasks.md not found after /speckit.tasks. Cannot proceed.\")\n sys.exit(1)\n\n print(f\" ✅ Tasks: {tasks_md.relative_to(PROJECT_ROOT)}\")\n speckit_phases_run.append(\"tasks\")\n\n # 2g: /speckit.analyze → cross-artifact consistency check\n if not skip_analyze:\n ok, output = phase_analyze(context_content)\n if not ok:\n print(f\"\\n⚠️ Analyze phase failed (non-fatal). Output:\\n{output[:500]}\")\n else:\n speckit_phases_run.append(\"analyze\")\n print(\" ✅ Cross-artifact analysis complete\")\n else:\n print(\"\\n🔬 SPECKIT: Skipping /speckit.analyze\")\n\n telemetry[\"phase_seconds\"][\"speckit_planning\"] = round(time.perf_counter() - phase_start, 3)\n telemetry[\"speckit_phases_run\"] = speckit_phases_run\n\n # ------------------------------------------------------------------\n # STEP 3: Parse tasks\n # ------------------------------------------------------------------\n tasks = parse_tasks(tasks_md)\n\n if not tasks:\n print(f\"\\n❌ No tasks parsed from {tasks_md}. Check tasks.md format.\")\n sys.exit(1)\n\n if fast_mode and len(tasks) > FAST_MODE_TASK_LIMIT:\n tasks = tasks[:FAST_MODE_TASK_LIMIT]\n print(f\"⚡ Fast mode: limited to first {FAST_MODE_TASK_LIMIT} tasks\")\n\n print(f\"\\n✅ Parsed {len(tasks)} tasks:\")\n for t in tasks:\n marker = \" [P]\" if t[\"parallel\"] else \"\"\n us = f\" [{t['user_story']}]\" if t.get(\"user_story\") else \"\"\n print(f\" {t['id']}{marker}{us}: {t['name']}\")\n\n # Load plan content for executor context (context_content already loaded earlier)\n plan_content = plan_path.read_text() if plan_path and plan_path.exists() else \"\"\n\n # Risk detection\n risk_profiles: dict[str, dict] = {}\n for t in tasks:\n is_risky, signals = _detect_task_risk(t, plan_content)\n risk_profiles[t[\"id\"]] = {\"is_high_risk\": is_risky, \"signals\": signals}\n\n high_risk_tasks = [t for t in tasks if risk_profiles[t[\"id\"]][\"is_high_risk\"]]\n\n # ------------------------------------------------------------------\n # STEP 4: Checkpoint\n # ------------------------------------------------------------------\n print(\"\\n\" + \"=\" * 60)\n print(\"🔍 CHECKPOINT: Review SpecKit artefacts\")\n print(\"=\" * 60)\n\n if spec_path:\n print(f\" 📄 {spec_path.relative_to(PROJECT_ROOT)}\")\n if plan_path:\n print(f\" 📄 {plan_path.relative_to(PROJECT_ROOT)}\")\n print(f\" 📄 {tasks_md.relative_to(PROJECT_ROOT)}\")\n\n if high_risk_tasks:\n print(\"\\n ⚠️ High-risk tasks detected:\")\n for t in high_risk_tasks:\n signals = \", \".join(risk_profiles[t[\"id\"]][\"signals\"][:5])\n print(f\" - {t['id']}: {t['name']} ({signals})\")\n\n checkpoint_start = time.perf_counter()\n if auto_approve and not (high_risk_tasks and not force_auto_approve):\n print(\"\\n⚡ Auto-approved checkpoint\")\n telemetry[\"checkpoint\"] = {\"auto_approved\": True, \"high_risk_task_count\": len(high_risk_tasks)}\n else:\n if auto_approve and high_risk_tasks and not force_auto_approve:\n print(\"\\n⚠️ Auto-approve paused because high-risk tasks were detected.\")\n approval = input(\"\\nApprove and continue execution? [y/n]: \").strip().lower()\n if approval not in (\"y\", \"yes\", \"\"):\n print(\"❌ Execution cancelled.\")\n telemetry[\"checkpoint\"] = {\"auto_approved\": False, \"approved\": False}\n telemetry[\"phase_seconds\"][\"checkpoint\"] = round(time.perf_counter() - checkpoint_start, 3)\n telemetry[\"total_seconds\"] = round(time.perf_counter() - run_started, 3)\n _write_telemetry(feature, telemetry)\n return\n telemetry[\"checkpoint\"] = {\"auto_approved\": False, \"approved\": True, \"high_risk_task_count\": len(high_risk_tasks)}\n\n telemetry[\"phase_seconds\"][\"checkpoint\"] = round(time.perf_counter() - checkpoint_start, 3)\n\n # ------------------------------------------------------------------\n # STEP 5: Execution (SWPP agents — NOT Claude Code)\n # ------------------------------------------------------------------\n print(f\"\\n⚡ PHASE 4: Executing tasks via SWPP {executor_backend} agents…\")\n\n committed_tasks = _get_committed_tasks()\n if committed_tasks:\n print(f\" ⏭️ Already committed: {', '.join(sorted(committed_tasks))}\")\n\n execute_fn = _execute_task_crewai if executor_backend == \"crewai\" else _execute_task_anthropic\n exec_phase_start = time.perf_counter()\n\n for task_info in tasks:\n tid = task_info[\"id\"]\n\n if tid in committed_tasks:\n print(f\"\\n ⏭️ Skipping {tid} (already committed)\")\n telemetry[\"tasks\"].append({\"task_id\": tid, \"skipped\": True, \"reason\": \"already_committed\"})\n continue\n\n print(f\"\\n Executing {tid}: {task_info['name']}…\")\n\n task_metrics: dict = {\n \"task_id\": tid,\n \"name\": task_info[\"name\"],\n \"high_risk\": risk_profiles[tid][\"is_high_risk\"],\n \"risk_signals\": risk_profiles[tid][\"signals\"],\n \"skipped\": False,\n }\n\n exec_start = time.perf_counter()\n try:\n result_text = execute_fn(task_info, plan_content, context_content)\n except Exception as e:\n print(f\" ❌ Executor error for {tid}: {e}\")\n task_metrics[\"execution_seconds\"] = round(time.perf_counter() - exec_start, 3)\n task_metrics[\"committed\"] = False\n task_metrics[\"error\"] = str(e)\n telemetry[\"tasks\"].append(task_metrics)\n telemetry[\"phase_seconds\"][\"execute_and_test\"] = round(time.perf_counter() - exec_phase_start, 3)\n telemetry[\"total_seconds\"] = round(time.perf_counter() - run_started, 3)\n _write_telemetry(feature, telemetry)\n return\n\n task_metrics[\"execution_seconds\"] = round(time.perf_counter() - exec_start, 3)\n\n # For anthropic backend, parse and write files from response\n if executor_backend == \"anthropic\":\n files = _parse_files_from_response(result_text)\n if files:\n written = _write_executor_files(files)\n print(f\" 📁 Wrote {len(written)} file(s): {', '.join(written)}\")\n else:\n print(f\" ⚠️ No === FILE: === blocks found in executor response for {tid}\")\n\n # Also copy/sync plan to .swpp/plans/ for consistency with other runners\n PLANS_DIR.mkdir(parents=True, exist_ok=True)\n (PLANS_DIR / f\"{tid}.md\").write_text(\n f\"# {tid}: {task_info['name']}\\n\\n\"\n f\"*Source: SpecKit plan*\\n\\n\"\n f\"## Task Description\\n{task_info.get('description', '')}\\n\"\n )\n\n # Tests\n print(f\"\\n 🧪 Running tests for {tid}…\")\n fast_fail = fast_mode and not task_metrics[\"high_risk\"]\n task_metrics[\"test_strategy\"] = \"fast-fail\" if fast_fail else \"strict\"\n test_start = time.perf_counter()\n test_result = _run_tests(tid, fast_fail=fast_fail)\n task_metrics[\"tests_seconds\"] = round(time.perf_counter() - test_start, 3)\n task_metrics[\"tests_passed\"] = test_result[\"passed\"]\n print(test_result[\"summary\"])\n\n # Commit only if tests pass\n if test_result[\"passed\"]:\n commit_msg = f\"feat: {task_info['name']} [{tid}]\"\n _git_commit(commit_msg)\n print(f\" ✅ {tid} committed\")\n task_metrics[\"committed\"] = True\n else:\n print(f\" ❌ {tid} tests failed. Stopping execution.\")\n for name, detail in test_result[\"details\"].items():\n if not detail[\"passed\"]:\n print(f\"\\n {name} output:\\n{detail['output'][:500]}\")\n task_metrics[\"committed\"] = False\n telemetry[\"tasks\"].append(task_metrics)\n telemetry[\"phase_seconds\"][\"execute_and_test\"] = round(time.perf_counter() - exec_phase_start, 3)\n telemetry[\"summary\"] = {\"status\": \"failed_tests\", \"failed_task\": tid}\n telemetry[\"total_seconds\"] = round(time.perf_counter() - run_started, 3)\n _write_telemetry(feature, telemetry)\n return\n\n telemetry[\"tasks\"].append(task_metrics)\n\n telemetry[\"phase_seconds\"][\"execute_and_test\"] = round(time.perf_counter() - exec_phase_start, 3)\n\n # ------------------------------------------------------------------\n # STEP 6: Review\n # ------------------------------------------------------------------\n review_results = []\n if skip_review:\n print(\"\\n🔎 PHASE 5: Review skipped\")\n telemetry[\"phase_seconds\"][\"review\"] = 0.0\n else:\n print(\"\\n🔎 PHASE 5: Reviewing implementation…\")\n review_start = time.perf_counter()\n all_committed = _get_committed_tasks()\n newly_committed = all_committed - committed_tasks\n\n review_fn = _review_task_crewai if executor_backend == \"crewai\" else _review_task_anthropic\n\n for task_info in tasks:\n tid = task_info[\"id\"]\n if tid not in newly_committed:\n continue\n try:\n review_text = review_fn(task_info, plan_content, context_content)\n review_results.append((tid, review_text))\n except Exception as e:\n review_results.append((tid, f\"Review error: {e}\"))\n\n telemetry[\"phase_seconds\"][\"review\"] = round(time.perf_counter() - review_start, 3)\n\n # ------------------------------------------------------------------\n # STEP 7: Summary\n # ------------------------------------------------------------------\n print(\"\\n\" + \"=\" * 60)\n print(\"✨ SWPP-SPECKIT EXECUTION COMPLETE\")\n print(\"=\" * 60)\n print(f\"\\nFeature: {feature}\")\n print(f\"Tasks completed: {len([t for t in telemetry['tasks'] if t.get('committed')])}/{len(tasks)}\")\n print(f\"High-risk tasks: {len(high_risk_tasks)}\")\n\n if review_results:\n print(\"\\nReview Summary:\")\n for tid, result in review_results:\n status = \"✅ PASS\" if \"PASS\" in result.upper() else \"⚠️ NEEDS ATTENTION\"\n print(f\" {tid}: {status}\")\n\n telemetry[\"summary\"] = {\n \"status\": \"completed\",\n \"tasks_total\": len(tasks),\n \"tasks_committed\": len([t for t in telemetry[\"tasks\"] if t.get(\"committed\")]),\n \"high_risk_task_count\": len(high_risk_tasks),\n \"reviews_completed\": len(review_results),\n }\n telemetry[\"total_seconds\"] = round(time.perf_counter() - run_started, 3)\n metrics_path = _write_telemetry(feature, telemetry)\n\n print(\"\\n📁 Artefacts:\")\n if spec_path:\n print(f\" Spec: {spec_path.relative_to(PROJECT_ROOT)}\")\n if plan_path:\n print(f\" Plan: {plan_path.relative_to(PROJECT_ROOT)}\")\n print(f\" Tasks: {tasks_md.relative_to(PROJECT_ROOT)}\")\n print(f\" SWPP plans: .swpp/plans/\")\n print(f\" Commits: git log --oneline -n {len(tasks)}\")\n print(f\" Telemetry: {metrics_path}\")\n\n\n# =============================================================================\n# CLI\n# =============================================================================\n\nif __name__ == \"__main__\":\n parser = argparse.ArgumentParser(\n description=\"SWPP with SpecKit planning + SWPP agent execution\",\n epilog=\"Planning uses SpecKit (via claude -p). Execution uses SWPP AI agents.\",\n )\n parser.add_argument(\"feature\", nargs=\"+\", help=\"Feature description\")\n parser.add_argument(\n \"--project\", type=str,\n help=\"Project name (creates new directory in ~/projects/<name> if doesn't exist)\"\n )\n parser.add_argument(\n \"--project-dir\", type=str,\n help=\"Custom base directory for projects (default: ~/projects)\"\n )\n\n mode_group = parser.add_mutually_exclusive_group()\n mode_group.add_argument(\n \"--fast\", action=\"store_true\",\n help=\"Lean planning, auto-approve, skip review\",\n )\n mode_group.add_argument(\n \"--balanced\", action=\"store_true\",\n help=\"Lean planning, auto-approve, keep review\",\n )\n\n parser.add_argument(\"--auto-approve\", action=\"store_true\", help=\"Skip checkpoint prompt\")\n parser.add_argument(\"--force-auto-approve\", action=\"store_true\", help=\"Auto-approve even for high-risk tasks\")\n parser.add_argument(\"--skip-review\", action=\"store_true\", help=\"Skip review phase\")\n parser.add_argument(\n \"--executor\", choices=[\"anthropic\", \"crewai\"], default=DEFAULT_EXECUTOR_BACKEND,\n help=f\"Executor backend (default: {DEFAULT_EXECUTOR_BACKEND})\",\n )\n\n # SpecKit optional phase toggles\n parser.add_argument(\"--skip-constitution\", action=\"store_true\", help=\"Skip /speckit.constitution phase\")\n parser.add_argument(\"--skip-clarify\", action=\"store_true\", help=\"Skip /speckit.clarify phase\")\n parser.add_argument(\"--skip-checklist\", action=\"store_true\", help=\"Skip /speckit.checklist phase\")\n parser.add_argument(\"--skip-analyze\", action=\"store_true\", help=\"Skip /speckit.analyze phase\")\n\n args = parser.parse_args()\n feature = \" \".join(args.feature)\n\n # Setup project directory if --project flag is used\n if args.project:\n base_dir = Path(args.project_dir) if args.project_dir else Path.home() / \"projects\"\n project_path = setup_project_directory(args.project, base_dir)\n \n # Change working directory to project\n import os\n os.chdir(project_path)\n \n print(f\"\\n📂 Working in project: {project_path}\\n\")\n\n fast_mode = args.fast or args.balanced\n mode_label = \"FAST\" if args.fast else \"BALANCED\" if args.balanced else \"STANDARD\"\n auto_approve = args.auto_approve or fast_mode\n skip_review = args.skip_review or args.fast\n\n # In fast mode, skip optional enhancement phases by default\n skip_constitution = args.skip_constitution or args.fast\n skip_clarify = args.skip_clarify or args.fast\n skip_checklist = args.skip_checklist or args.fast\n skip_analyze = args.skip_analyze or args.fast\n\n run_swpp_speckit(\n feature,\n fast_mode=fast_mode,\n auto_approve=auto_approve,\n skip_review=skip_review,\n force_auto_approve=args.force_auto_approve,\n executor_backend=args.executor,\n mode_label=mode_label,\n skip_constitution=skip_constitution,\n skip_clarify=skip_clarify,\n skip_checklist=skip_checklist,\n skip_analyze=skip_analyze,\n )\n"}, "eba3856a00bca0163c6d04454cb9872b94666019": {"t": 1785766533.1293905, "n": 45878, "h": "5dd82f76ef781b84", "c": "# swpp_speckit.py\n# SWPP Runner with SpecKit (spec-driven planning) + SWPP Agents (execution)\n#\n# SpecKit phases (via `claude -p` slash commands):\n# /speckit.constitution → project principles\n# /speckit.specify → feature specification (spec.md)\n# /speckit.clarify → de-risk ambiguity in spec\n# /speckit.plan → implementation plan (plan.md)\n# /speckit.checklist → quality validation checklists\n# /speckit.tasks → actionable task list (tasks.md)\n# /speckit.analyze → cross-artifact consistency check\n# /speckit.implement → SKIPPED (SWPP agents handle execution)\n#\n# Execution: SWPP AI agents (Anthropic API or CrewAI)\n#\n# Requires: `specify` CLI (GitHub Spec Kit) + `claude` CLI (Claude Code)\n#\n# Run: python swpp_speckit.py \"Your feature description\"\n\nimport os\nimport sys\nimport json\nimport re\nimport time\nimport shutil\nimport subprocess\nimport argparse\nfrom datetime import datetime\nfrom pathlib import Path\nfrom textwrap import dedent\n\n# =============================================================================\n# CONFIGURATION\n# =============================================================================\n\n# PROJECT_ROOT is set dynamically based on --project flag (see main)\nPROJECT_ROOT = Path.cwd()\n\ndef get_project_paths(project_root: Path) -> dict:\n \"\"\"Return all project paths based on root directory.\"\"\"\n return {\n \"root\": project_root,\n \"swpp_dir\": project_root / \".swpp\",\n \"plans_dir\": project_root / \".swpp\" / \"plans\",\n \"context_file\": project_root / \".swpp\" / \"context.md\",\n \"speckit_dir\": project_root / \".specify\",\n \"claude_commands_dir\": project_root / \".claude\" / \"commands\",\n \"specs_dir\": project_root / \"specs\",\n }\n\n# Default paths (updated when --project is used)\nSWPP_DIR = PROJECT_ROOT / \".swpp\"\nPLANS_DIR = SWPP_DIR / \"plans\"\nCONTEXT_FILE = SWPP_DIR / \"context.md\"\nSPECKIT_DIR = PROJECT_ROOT / \".specify\"\nCLAUDE_COMMANDS_DIR = PROJECT_ROOT / \".claude\" / \"commands\"\nSPECS_DIR = PROJECT_ROOT / \"specs\"\n\nFAST_MODE_TASK_LIMIT = 3\nHIGH_RISK_KEYWORDS = {\n \"auth\", \"authentication\", \"authorization\", \"permission\", \"permissions\",\n \"secret\", \"token\", \"password\", \"credential\", \"oauth\", \"security\",\n \"encryption\", \"decrypt\", \"migration\", \"schema\", \"database\", \"sql\",\n \"prod\", \"production\", \"billing\", \"payment\", \"compliance\",\n}\n\n# Claude Code CLI timeout per phase (seconds)\nSPECKIT_PHASE_TIMEOUT = 300 # 5 min per slash command\n\n# Model Configuration\nEXECUTOR_MODEL = os.getenv(\"SWPP_EXECUTOR_MODEL\", \"deepseek-chat\") # or \"claude-sonnet-4-5-20250929\"\nREVIEWER_MODEL = os.getenv(\"SWPP_REVIEWER_MODEL\", \"claude-sonnet-4-5-20250929\")\n\n# Executor backend: \"crewai\" or \"anthropic\"\n# crewai uses CrewAI agents, anthropic uses raw Anthropic SDK\nDEFAULT_EXECUTOR_BACKEND = \"anthropic\"\n\n\n# =============================================================================\n# PREREQUISITES\n# =============================================================================\n\ndef _check_cli(name: str, check_cmd: list[str]) -> bool:\n \"\"\"Verify a CLI tool is installed and reachable.\"\"\"\n try:\n subprocess.run(check_cmd, capture_output=True, timeout=10)\n return True\n except (FileNotFoundError, subprocess.TimeoutExpired):\n return False\n\n\ndef check_prerequisites() -> list[str]:\n \"\"\"Return list of missing prerequisites.\"\"\"\n missing = []\n if not _check_cli(\"specify\", [\"specify\", \"version\"]):\n missing.append(\"specify (GitHub Spec Kit) — install: https://github.com/specifyit/specify\")\n if not _check_cli(\"claude\", [\"claude\", \"--version\"]):\n missing.append(\"claude (Claude Code CLI) — install: https://docs.anthropic.com/en/docs/claude-code\")\n if not os.getenv(\"ANTHROPIC_API_KEY\"):\n missing.append(\"ANTHROPIC_API_KEY environment variable\")\n return missing\n\n\n# =============================================================================\n# PROJECT SETUP\n# =============================================================================\n\ndef setup_project_directory(project_name: str, base_dir: Path = Path.home() / \"projects\") -> Path:\n \"\"\"Create project directory and initialize basic structure.\n \n Args:\n project_name: Name of the project (e.g., \"HelloWorld\")\n base_dir: Parent directory where project will be created\n \n Returns:\n Path to the created/existing project directory\n \"\"\"\n # Sanitize project name (remove spaces, special chars)\n safe_name = re.sub(r'[^a-zA-Z0-9_-]', '-', project_name)\n project_path = base_dir / safe_name\n \n if project_path.exists():\n print(f\" ℹ️ Project directory already exists: {project_path}\")\n else:\n print(f\" 📁 Creating project directory: {project_path}\")\n project_path.mkdir(parents=True, exist_ok=True)\n \n # Create basic Python project structure\n (project_path / \"src\").mkdir(exist_ok=True)\n (project_path / \"tests\").mkdir(exist_ok=True)\n (project_path / \"tests\" / \"unit\").mkdir(exist_ok=True)\n (project_path / \"tests\" / \"integration\").mkdir(exist_ok=True)\n (project_path / \"tests\" / \"safety\").mkdir(exist_ok=True)\n (project_path / \"tests\" / \"solid\").mkdir(exist_ok=True)\n \n # Create __init__.py files\n (project_path / \"src\" / \"__init__.py\").touch()\n (project_path / \"tests\" / \"__init__.py\").touch()\n (project_path / \"tests\" / \"unit\" / \"__init__.py\").touch()\n (project_path / \"tests\" / \"integration\" / \"__init__.py\").touch()\n (project_path / \"tests\" / \"safety\" / \"__init__.py\").touch()\n (project_path / \"tests\" / \"solid\" / \"__init__.py\").touch()\n \n # Initialize git if not already a repo\n if not (project_path / \".git\").exists():\n print(\" 🔧 Initializing git repository...\")\n subprocess.run([\"git\", \"init\"], cwd=project_path, capture_output=True)\n subprocess.run([\"git\", \"checkout\", \"-b\", \"main\"], cwd=project_path, capture_output=True)\n \n print(f\" ✅ Project structure created\")\n \n return project_path\n\n\n# =============================================================================\n# SPECKIT SCAFFOLD\n# =============================================================================\n\ndef ensure_speckit_scaffold() -> bool:\n \"\"\"Ensure SpecKit project scaffold exists (.specify/ and .claude/commands/).\"\"\"\n if SPECKIT_DIR.exists() and CLAUDE_COMMANDS_DIR.exists():\n print(\" ✅ SpecKit scaffold already exists\")\n return True\n\n print(\" 🔧 Initializing SpecKit scaffold automatically...\")\n print(\" (This may take 30-60s to download template from GitHub)\")\n \n result = subprocess.run(\n [\"specify\", \"init\", \".\", \"--ai\", \"claude\", \"--here\", \"--force\", \"--no-git\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=180,\n )\n \n if result.returncode != 0:\n print(f\" ❌ specify init failed:\\n{result.stderr}\")\n return False\n\n # Verify key artefacts\n if not CLAUDE_COMMANDS_DIR.exists():\n print(\" ❌ .claude/commands/ not created – specify init may have failed silently\")\n return False\n\n print(\" ✅ SpecKit scaffold created successfully\")\n return True\n\n\n# =============================================================================\n# CLAUDE CODE SLASH COMMAND RUNNER\n# =============================================================================\n\ndef run_speckit_command(\n slash_command: str,\n args: str = \"\",\n timeout: int = SPECKIT_PHASE_TIMEOUT,\n) -> tuple[bool, str]:\n \"\"\"\n Execute a SpecKit slash command via ``claude -p``.\n\n Returns (success, output_text).\n \"\"\"\n prompt = f\"/{slash_command} {args}\".strip()\n cmd = [\n \"claude\",\n \"-p\", prompt,\n \"--output-format\", \"text\",\n \"--allowedTools\", \"Bash Read Write Edit\",\n ]\n print(f\" ▸ claude -p \\\"/{slash_command} {args[:60]}{'…' if len(args) > 60 else ''}\\\"\")\n\n try:\n result = subprocess.run(\n cmd, cwd=PROJECT_ROOT,\n capture_output=True, text=True,\n timeout=timeout,\n )\n output = result.stdout + result.stderr\n success = result.returncode == 0\n if not success:\n print(f\" ⚠️ /{slash_command} exited with code {result.returncode}\")\n return success, output\n except subprocess.TimeoutExpired:\n print(f\" ⚠️ /{slash_command} timed out after {timeout}s\")\n return False, f\"Timed out after {timeout}s\"\n\n\n# =============================================================================\n# SPECKIT PLANNING PHASES\n# =============================================================================\n\ndef phase_constitution(args: str = \"\") -> tuple[bool, str]:\n \"\"\"Establish project governing principles via /speckit.constitution.\"\"\"\n print(\"\\n📜 SPECKIT: Establishing project constitution…\")\n return run_speckit_command(\"speckit.constitution\", args)\n\n\ndef phase_specify(feature: str) -> tuple[bool, str]:\n \"\"\"Generate feature specification via /speckit.specify.\"\"\"\n print(\"\\n📋 SPECKIT: Generating feature specification…\")\n return run_speckit_command(\"speckit.specify\", feature)\n\n\ndef phase_clarify(args: str = \"\") -> tuple[bool, str]:\n \"\"\"Clarify underspecified areas in spec via /speckit.clarify.\"\"\"\n print(\"\\n🔎 SPECKIT: Clarifying specification…\")\n return run_speckit_command(\"speckit.clarify\", args)\n\n\ndef phase_plan() -> tuple[bool, str]:\n \"\"\"Generate implementation plan via /speckit.plan.\"\"\"\n print(\"\\n📐 SPECKIT: Generating implementation plan…\")\n return run_speckit_command(\"speckit.plan\")\n\n\ndef phase_checklist(args: str = \"\") -> tuple[bool, str]:\n \"\"\"Generate quality checklists via /speckit.checklist.\"\"\"\n print(\"\\n✅ SPECKIT: Generating quality checklist…\")\n return run_speckit_command(\"speckit.checklist\", args)\n\n\ndef phase_tasks() -> tuple[bool, str]:\n \"\"\"Generate actionable tasks via /speckit.tasks.\"\"\"\n print(\"\\n📝 SPECKIT: Generating actionable task list…\")\n return run_speckit_command(\"speckit.tasks\")\n\n\ndef phase_analyze() -> tuple[bool, str]:\n \"\"\"Cross-artifact consistency analysis via /speckit.analyze.\"\"\"\n print(\"\\n🔬 SPECKIT: Analyzing cross-artifact consistency…\")\n return run_speckit_command(\"speckit.analyze\")\n\n\n# =============================================================================\n# TASK PARSING\n# =============================================================================\n\n_TASK_LINE_RE = re.compile(\n r\"^- \\[[ xX]?\\]\\s+\" # checkbox\n r\"(T\\d{3})\" # task ID (T001, T002, …)\n r\"(?:\\s+\\[P\\])?\" # optional parallel marker\n r\"(?:\\s+\\[US\\d+\\])?\" # optional user-story label\n r\"\\s+(.+)$\" # description\n)\n\n\ndef _find_tasks_md() -> Path | None:\n \"\"\"Locate the tasks.md generated by SpecKit inside specs/<branch>/.\"\"\"\n # Try to find from current git branch\n try:\n branch = subprocess.run(\n [\"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=5,\n ).stdout.strip()\n except Exception:\n branch = \"\"\n\n if branch and (SPECS_DIR / branch / \"tasks.md\").exists():\n return SPECS_DIR / branch / \"tasks.md\"\n\n # Fallback: find newest tasks.md under specs/\n if SPECS_DIR.exists():\n candidates = sorted(SPECS_DIR.glob(\"*/tasks.md\"), key=lambda p: p.stat().st_mtime, reverse=True)\n if candidates:\n return candidates[0]\n\n return None\n\n\ndef _find_spec_md() -> Path | None:\n \"\"\"Locate the spec.md generated by SpecKit.\"\"\"\n try:\n branch = subprocess.run(\n [\"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=5,\n ).stdout.strip()\n except Exception:\n branch = \"\"\n\n if branch and (SPECS_DIR / branch / \"spec.md\").exists():\n return SPECS_DIR / branch / \"spec.md\"\n\n if SPECS_DIR.exists():\n candidates = sorted(SPECS_DIR.glob(\"*/spec.md\"), key=lambda p: p.stat().st_mtime, reverse=True)\n if candidates:\n return candidates[0]\n return None\n\n\ndef _find_plan_md() -> Path | None:\n \"\"\"Locate the plan.md generated by SpecKit.\"\"\"\n try:\n branch = subprocess.run(\n [\"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=5,\n ).stdout.strip()\n except Exception:\n branch = \"\"\n\n if branch and (SPECS_DIR / branch / \"plan.md\").exists():\n return SPECS_DIR / branch / \"plan.md\"\n\n if SPECS_DIR.exists():\n candidates = sorted(SPECS_DIR.glob(\"*/plan.md\"), key=lambda p: p.stat().st_mtime, reverse=True)\n if candidates:\n return candidates[0]\n return None\n\n\ndef parse_tasks(tasks_md_path: Path) -> list[dict]:\n \"\"\"Parse tasks.md into a list of task dicts with id, name, parallel flag.\"\"\"\n content = tasks_md_path.read_text()\n tasks = []\n current_phase = \"\"\n\n for line in content.splitlines():\n # Detect phase headings\n if line.startswith(\"## \") or line.startswith(\"### \"):\n current_phase = line.lstrip(\"#\").strip()\n continue\n\n m = _TASK_LINE_RE.match(line.strip())\n if m:\n task_id = m.group(1)\n description = m.group(2).strip()\n is_parallel = \"[P]\" in line\n us_match = re.search(r\"\\[US(\\d+)\\]\", line)\n user_story = f\"US{us_match.group(1)}\" if us_match else None\n\n tasks.append({\n \"id\": task_id,\n \"name\": description,\n \"description\": description,\n \"phase\": current_phase,\n \"parallel\": is_parallel,\n \"user_story\": user_story,\n })\n\n return tasks\n\n\n# =============================================================================\n# RISK DETECTION\n# =============================================================================\n\ndef _detect_task_risk(task_info: dict, plan_content: str = \"\") -> tuple[bool, list[str]]:\n \"\"\"Detect whether a task is high-risk based on metadata and plan text.\"\"\"\n haystack = \" \".join([\n str(task_info.get(\"id\", \"\")),\n str(task_info.get(\"name\", \"\")),\n str(task_info.get(\"description\", \"\")),\n plan_content,\n ]).lower()\n\n matches = sorted(\n {kw for kw in HIGH_RISK_KEYWORDS if re.search(rf\"\\b{re.escape(kw)}\\b\", haystack)}\n )\n return (len(matches) > 0, matches)\n\n\n# =============================================================================\n# EXECUTION — ANTHROPIC SDK (default backend)\n# =============================================================================\n\ndef _execute_task_anthropic(task_info: dict, plan_content: str, context_content: str) -> str:\n \"\"\"Execute a single task using Anthropic API or DeepSeek (OpenAI-compatible).\"\"\"\n model = EXECUTOR_MODEL\n\n prompt = dedent(f\"\"\"\\\n You are a precise developer implementing a task from an approved SpecKit plan.\n\n TASK: {task_info['id']} — {task_info['name']}\n DESCRIPTION: {task_info.get('description', '')}\n\n IMPLEMENTATION PLAN (from SpecKit):\n {plan_content}\n\n PROJECT STANDARDS:\n {context_content}\n\n RULES:\n 1. ONLY create or modify files directly related to this task.\n 2. Follow project standards and conventions exactly.\n 3. If the plan is unclear, make the safest possible implementation choice.\n 4. Output your work using the format below.\n\n OUTPUT FORMAT:\n === FILE: <relative-path> ===\n [complete file contents]\n === END FILE ===\n\n Provide ALL files you create or modify.\n \"\"\")\n\n # Use DeepSeek (OpenAI-compatible) if model starts with \"deepseek\"\n if model.startswith(\"deepseek\"):\n from openai import OpenAI\n client = OpenAI(\n api_key=os.getenv(\"DEEPSEEK_API_KEY\"),\n base_url=\"https://api.deepseek.com\",\n )\n response = client.chat.completions.create(\n model=model,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n max_tokens=8192,\n temperature=0.2,\n )\n return response.choices[0].message.content\n else:\n # Use Anthropic Claude\n from anthropic import Anthropic\n client = Anthropic()\n response = client.messages.create(\n model=model,\n max_tokens=8192,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n )\n return response.content[0].text\n\n\ndef _parse_files_from_response(response_text: str) -> dict[str, str]:\n \"\"\"Extract file paths and contents from === FILE: ... === blocks.\"\"\"\n files = {}\n pattern = re.compile(\n r\"=== FILE:\\s*(.+?)\\s*===\\s*\\n(.*?)\\n=== END FILE ===\",\n re.DOTALL,\n )\n for match in pattern.finditer(response_text):\n filepath = match.group(1).strip()\n content = match.group(2)\n files[filepath] = content\n return files\n\n\ndef _write_executor_files(files: dict[str, str]) -> list[str]:\n \"\"\"Write parsed files to disk, return list of written paths.\"\"\"\n written = []\n for rel_path, content in files.items():\n path = PROJECT_ROOT / rel_path\n path.parent.mkdir(parents=True, exist_ok=True)\n path.write_text(content)\n written.append(rel_path)\n return written\n\n\n# =============================================================================\n# EXECUTION — CREWAI BACKEND (optional)\n# =============================================================================\n\ndef _execute_task_crewai(task_info: dict, plan_content: str, context_content: str) -> str:\n \"\"\"Execute a single task using CrewAI agents (mirrors swpp_crew.py executor).\"\"\"\n from crewai import Agent, Task, Crew, LLM\n from crewai.tools import tool as crewai_tool\n\n model = EXECUTOR_MODEL\n\n # Use appropriate LangChain wrapper based on model\n if model.startswith(\"deepseek\"):\n from langchain_openai import ChatOpenAI\n executor_model = ChatOpenAI(\n model=model,\n api_key=os.getenv(\"DEEPSEEK_API_KEY\"),\n base_url=\"https://api.deepseek.com\",\n temperature=0.2,\n )\n else:\n from langchain_anthropic import ChatAnthropic\n executor_model = ChatAnthropic(\n model=model,\n temperature=0.2,\n )\n\n @crewai_tool\n def read_file(filepath: str) -> str:\n \"\"\"Read contents of a file in the project.\"\"\"\n path = PROJECT_ROOT / filepath\n return path.read_text() if path.exists() else f\"File not found: {filepath}\"\n\n @crewai_tool\n def write_file(filepath: str, content: str) -> str:\n \"\"\"Write content to a file in the project.\"\"\"\n path = PROJECT_ROOT / filepath\n path.parent.mkdir(parents=True, exist_ok=True)\n path.write_text(content)\n return f\"Written: {filepath}\"\n\n @crewai_tool\n def list_files(directory: str = \"src\") -> str:\n \"\"\"List files in a directory.\"\"\"\n path = PROJECT_ROOT / directory\n if not path.exists():\n return f\"Directory not found: {directory}\"\n files = [str(f.relative_to(PROJECT_ROOT)) for f in path.rglob(\"*\") if f.is_file()]\n return \"\\n\".join(files[:50])\n\n @crewai_tool\n def run_command(command: str) -> str:\n \"\"\"Run a shell command.\"\"\"\n try:\n result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=60, cwd=PROJECT_ROOT)\n return result.stdout + result.stderr\n except subprocess.TimeoutExpired:\n return \"Command timed out\"\n\n executor = Agent(\n role=\"Execution Agent\",\n goal=\"Implement code exactly according to approved SpecKit plans\",\n backstory=dedent(\"\"\"\\\n You are a precise developer who implements code following SpecKit plans exactly.\n You only modify files specified in the plan. You follow project\n standards religiously.\n \"\"\"),\n llm=executor_model,\n tools=[read_file, write_file, list_files, run_command],\n verbose=True,\n )\n\n execution_task = Task(\n description=dedent(f\"\"\"\\\n Implement code according to this approved SpecKit plan:\n\n TASK: {task_info['id']} — {task_info['name']}\n\n PLAN:\n {plan_content}\n\n PROJECT STANDARDS:\n {context_content}\n\n RULES:\n 1. ONLY modify files listed in the plan\n 2. Follow project standards exactly\n 3. After implementation, run linting if available\n\n Report what you created/modified.\n \"\"\"),\n expected_output=\"Implementation report with files created/modified\",\n agent=executor,\n )\n\n crew = Crew(agents=[executor], tasks=[execution_task], verbose=True)\n result = crew.kickoff()\n return str(result)\n\n\n# =============================================================================\n# TESTS & GIT\n# =============================================================================\n\ndef _run_tests(task_id: str, fast_fail: bool = False) -> dict:\n \"\"\"Run the project test suite. Returns dict with passed, summary, details.\"\"\"\n venv_python = str(PROJECT_ROOT / \".venv\" / \"bin\" / \"python\")\n if not Path(venv_python).exists():\n venv_python = \"python\"\n\n test_commands = [\n (\"unit\", f\"{venv_python} -m pytest tests/unit/ -v --tb=short\"),\n (\"integration\", f\"{venv_python} -m pytest tests/integration/ -v --tb=short\"),\n (\"safety\", f\"{venv_python} -m pytest tests/safety/ -v --tb=short\"),\n (\"solid\", f\"{venv_python} -m pytest tests/solid/ -v --tb=short\"),\n ]\n\n results = {}\n all_passed = True\n lines = [f\"\\n🧪 Running tests for {task_id}…\\n\"]\n\n for name, cmd in test_commands:\n try:\n res = subprocess.run(cmd, shell=True, cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=120)\n except subprocess.TimeoutExpired:\n res = type(\"R\", (), {\"returncode\": 1, \"stdout\": \"\", \"stderr\": \"Timed out\"})()\n\n passed = res.returncode == 0 or \"passed\" in getattr(res, \"stdout\", \"\").lower()\n results[name] = {\"passed\": passed, \"output\": getattr(res, \"stdout\", \"\") + getattr(res, \"stderr\", \"\")}\n lines.append(f\" {name}: {'✅ PASS' if passed else '❌ FAIL'}\")\n\n if not passed:\n all_passed = False\n if fast_fail:\n lines.append(\" ⚡ Fast mode: stopping at first failure\")\n break\n\n lines.append(f\"\\n{'✅ All tests passed!' if all_passed else '❌ Some tests failed'}\\n\")\n return {\"passed\": all_passed, \"summary\": \"\\n\".join(lines), \"details\": results}\n\n\ndef _git_commit(message: str) -> str:\n \"\"\"Stage all changes and commit.\"\"\"\n subprocess.run([\"git\", \"add\", \"-A\"], cwd=PROJECT_ROOT)\n result = subprocess.run([\"git\", \"commit\", \"-m\", message], cwd=PROJECT_ROOT, capture_output=True, text=True)\n return result.stdout + result.stderr\n\n\ndef _get_committed_tasks() -> set[str]:\n \"\"\"Return set of task IDs already committed (format: feat: ... [TXXX]).\"\"\"\n result = subprocess.run(\n [\"git\", \"log\", \"--oneline\", \"-n\", \"100\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True,\n )\n committed = set()\n for line in result.stdout.splitlines():\n m = re.search(r\"\\[(T\\d{3})\\]\", line)\n if m:\n committed.add(m.group(1))\n return committed\n\n\n# =============================================================================\n# REVIEW\n# =============================================================================\n\ndef _review_task_anthropic(task_info: dict, plan_content: str, context_content: str) -> str:\n \"\"\"Review implementation of a single task using Claude Sonnet.\"\"\"\n from anthropic import Anthropic\n\n client = Anthropic()\n model = REVIEWER_MODEL\n\n prompt = dedent(f\"\"\"\\\n Review the implementation of task {task_info['id']}: {task_info['name']}.\n\n ORIGINAL PLAN:\n {plan_content}\n\n PROJECT STANDARDS:\n {context_content}\n\n REVIEW CHECKLIST:\n 1. Does implementation match the plan?\n 2. Are project standards followed?\n 3. Is error handling adequate?\n 4. Are there any bugs or edge cases missed?\n 5. Is the code clean and maintainable?\n\n OUTPUT: PASS or FAIL with issues found and suggestions.\n \"\"\")\n\n response = client.messages.create(\n model=model,\n max_tokens=4096,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n )\n return response.content[0].text\n\n\ndef _review_task_crewai(task_info: dict, plan_content: str, context_content: str) -> str:\n \"\"\"Review implementation of a single task using CrewAI reviewer agent.\"\"\"\n from crewai import Agent, Task, Crew\n\n model = REVIEWER_MODEL\n\n # Reviewer always uses Claude (from REVIEWER_MODEL)\n from langchain_anthropic import ChatAnthropic\n reviewer_model = ChatAnthropic(\n model=model,\n temperature=0.1, # Low temperature for consistent review\n )\n\n reviewer = Agent(\n role=\"Code Reviewer\",\n goal=\"Ensure code quality, adherence to standards, and identify bugs\",\n backstory=dedent(\"\"\"\\\n You are a meticulous code reviewer with expertise in software quality.\n You verify implementations match approved plans, follow project standards,\n and have proper error handling. You catch edge cases and maintainability issues.\n \"\"\"),\n llm=reviewer_model,\n tools=[], # Reviewer doesn't need file tools - reviews what was already committed\n verbose=True,\n )\n\n review_task = Task(\n description=dedent(f\"\"\"\\\n Review the implementation of task {task_info['id']}: {task_info['name']}.\n\n ORIGINAL PLAN:\n {plan_content}\n\n PROJECT STANDARDS:\n {context_content}\n\n REVIEW CHECKLIST:\n 1. Does implementation match the plan?\n 2. Are project standards followed?\n 3. Is error handling adequate?\n 4. Are there any bugs or edge cases missed?\n 5. Is the code clean and maintainable?\n\n OUTPUT: Start with \"PASS\" or \"FAIL\", then provide detailed findings and suggestions.\n \"\"\"),\n expected_output=\"Review verdict (PASS/FAIL) with detailed findings\",\n agent=reviewer,\n )\n\n crew = Crew(agents=[reviewer], tasks=[review_task], verbose=True)\n result = crew.kickoff()\n return str(result)\n\n\n# =============================================================================\n# TELEMETRY\n# =============================================================================\n\ndef _write_telemetry(feature: str, telemetry: dict) -> Path:\n \"\"\"Persist run telemetry to .swpp/.\"\"\"\n SWPP_DIR.mkdir(exist_ok=True)\n path = SWPP_DIR / f\"metrics_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json\"\n payload = {\"feature\": feature, \"generated_at\": datetime.now().isoformat(), **telemetry}\n path.write_text(json.dumps(payload, indent=2))\n return path\n\n\n# =============================================================================\n# MAIN ORCHESTRATOR\n# =============================================================================\n\ndef run_swpp_speckit(\n feature: str,\n *,\n fast_mode: bool = False,\n auto_approve: bool = False,\n skip_review: bool = False,\n force_auto_approve: bool = False,\n executor_backend: str = DEFAULT_EXECUTOR_BACKEND,\n mode_label: str = \"STANDARD\",\n skip_constitution: bool = False,\n skip_clarify: bool = False,\n skip_checklist: bool = False,\n skip_analyze: bool = False,\n):\n run_started = time.perf_counter()\n telemetry: dict = {\n \"runner\": \"swpp_speckit\",\n \"mode\": mode_label,\n \"executor_backend\": executor_backend,\n \"phase_seconds\": {},\n \"tasks\": [],\n \"checkpoint\": {},\n \"summary\": {},\n }\n\n print(\"\\n\" + \"=\" * 60)\n print(\"SWPP-SPECKIT AUTOMATIC EXECUTION\")\n print(f\"Feature: {feature}\")\n print(f\"Mode: {mode_label} | Executor: {executor_backend}\")\n print(\"=\" * 60)\n\n # ------------------------------------------------------------------\n # STEP 0: Prerequisites\n # ------------------------------------------------------------------\n missing = check_prerequisites()\n if missing:\n print(\"\\n❌ Missing prerequisites:\")\n for m in missing:\n print(f\" • {m}\")\n sys.exit(1)\n\n # ------------------------------------------------------------------\n # STEP 1: Ensure SpecKit scaffold\n # ------------------------------------------------------------------\n print(\"\\n🔧 Checking SpecKit scaffold…\")\n if not ensure_speckit_scaffold():\n sys.exit(1)\n\n # ------------------------------------------------------------------\n # STEP 2: SpecKit planning phases\n # ------------------------------------------------------------------\n phase_start = time.perf_counter()\n speckit_phases_run: list[str] = []\n\n # 2a: /speckit.constitution → .specify/memory/constitution.md\n if not skip_constitution:\n ok, output = phase_constitution()\n if not ok:\n print(f\"\\n⚠️ Constitution phase failed (non-fatal). Output:\\n{output[:500]}\")\n else:\n speckit_phases_run.append(\"constitution\")\n print(\" ✅ Constitution established\")\n else:\n print(\"\\n📜 SPECKIT: Skipping /speckit.constitution\")\n\n # 2b: /speckit.specify → spec.md\n ok, output = phase_specify(feature)\n if not ok:\n print(f\"\\n❌ Specification phase failed. Output:\\n{output[:1000]}\")\n sys.exit(1)\n\n spec_path = _find_spec_md()\n if spec_path:\n print(f\" ✅ Spec: {spec_path.relative_to(PROJECT_ROOT)}\")\n speckit_phases_run.append(\"specify\")\n else:\n print(\" ⚠️ spec.md not found after /speckit.specify — continuing anyway\")\n\n # 2c: /speckit.clarify → refine spec.md\n if not skip_clarify:\n ok, output = phase_clarify()\n if not ok:\n print(f\"\\n⚠️ Clarify phase failed (non-fatal). Output:\\n{output[:500]}\")\n else:\n speckit_phases_run.append(\"clarify\")\n print(\" ✅ Spec clarified\")\n else:\n print(\"\\n🔎 SPECKIT: Skipping /speckit.clarify\")\n\n # 2d: /speckit.plan → plan.md\n ok, output = phase_plan()\n if not ok:\n print(f\"\\n❌ Planning phase failed. Output:\\n{output[:1000]}\")\n sys.exit(1)\n\n plan_path = _find_plan_md()\n if plan_path:\n print(f\" ✅ Plan: {plan_path.relative_to(PROJECT_ROOT)}\")\n speckit_phases_run.append(\"plan\")\n else:\n print(\" ⚠️ plan.md not found after /speckit.plan — continuing anyway\")\n\n # 2e: /speckit.checklist → quality validation\n if not skip_checklist:\n ok, output = phase_checklist()\n if not ok:\n print(f\"\\n⚠️ Checklist phase failed (non-fatal). Output:\\n{output[:500]}\")\n else:\n speckit_phases_run.append(\"checklist\")\n print(\" ✅ Quality checklist generated\")\n else:\n print(\"\\n✅ SPECKIT: Skipping /speckit.checklist\")\n\n # 2f: /speckit.tasks → tasks.md\n ok, output = phase_tasks()\n if not ok:\n print(f\"\\n❌ Task generation failed. Output:\\n{output[:1000]}\")\n sys.exit(1)\n\n tasks_md = _find_tasks_md()\n if not tasks_md:\n print(\"\\n❌ tasks.md not found after /speckit.tasks. Cannot proceed.\")\n sys.exit(1)\n\n print(f\" ✅ Tasks: {tasks_md.relative_to(PROJECT_ROOT)}\")\n speckit_phases_run.append(\"tasks\")\n\n # 2g: /speckit.analyze → cross-artifact consistency check\n if not skip_analyze:\n ok, output = phase_analyze()\n if not ok:\n print(f\"\\n⚠️ Analyze phase failed (non-fatal). Output:\\n{output[:500]}\")\n else:\n speckit_phases_run.append(\"analyze\")\n print(\" ✅ Cross-artifact analysis complete\")\n else:\n print(\"\\n🔬 SPECKIT: Skipping /speckit.analyze\")\n\n telemetry[\"phase_seconds\"][\"speckit_planning\"] = round(time.perf_counter() - phase_start, 3)\n telemetry[\"speckit_phases_run\"] = speckit_phases_run\n\n # ------------------------------------------------------------------\n # STEP 3: Parse tasks\n # ------------------------------------------------------------------\n tasks = parse_tasks(tasks_md)\n\n if not tasks:\n print(f\"\\n❌ No tasks parsed from {tasks_md}. Check tasks.md format.\")\n sys.exit(1)\n\n if fast_mode and len(tasks) > FAST_MODE_TASK_LIMIT:\n tasks = tasks[:FAST_MODE_TASK_LIMIT]\n print(f\"⚡ Fast mode: limited to first {FAST_MODE_TASK_LIMIT} tasks\")\n\n print(f\"\\n✅ Parsed {len(tasks)} tasks:\")\n for t in tasks:\n marker = \" [P]\" if t[\"parallel\"] else \"\"\n us = f\" [{t['user_story']}]\" if t.get(\"user_story\") else \"\"\n print(f\" {t['id']}{marker}{us}: {t['name']}\")\n\n # Load plan content for executor context\n plan_content = plan_path.read_text() if plan_path and plan_path.exists() else \"\"\n context_content = CONTEXT_FILE.read_text() if CONTEXT_FILE.exists() else \"\"\n\n # Risk detection\n risk_profiles: dict[str, dict] = {}\n for t in tasks:\n is_risky, signals = _detect_task_risk(t, plan_content)\n risk_profiles[t[\"id\"]] = {\"is_high_risk\": is_risky, \"signals\": signals}\n\n high_risk_tasks = [t for t in tasks if risk_profiles[t[\"id\"]][\"is_high_risk\"]]\n\n # ------------------------------------------------------------------\n # STEP 4: Checkpoint\n # ------------------------------------------------------------------\n print(\"\\n\" + \"=\" * 60)\n print(\"🔍 CHECKPOINT: Review SpecKit artefacts\")\n print(\"=\" * 60)\n\n if spec_path:\n print(f\" 📄 {spec_path.relative_to(PROJECT_ROOT)}\")\n if plan_path:\n print(f\" 📄 {plan_path.relative_to(PROJECT_ROOT)}\")\n print(f\" 📄 {tasks_md.relative_to(PROJECT_ROOT)}\")\n\n if high_risk_tasks:\n print(\"\\n ⚠️ High-risk tasks detected:\")\n for t in high_risk_tasks:\n signals = \", \".join(risk_profiles[t[\"id\"]][\"signals\"][:5])\n print(f\" - {t['id']}: {t['name']} ({signals})\")\n\n checkpoint_start = time.perf_counter()\n if auto_approve and not (high_risk_tasks and not force_auto_approve):\n print(\"\\n⚡ Auto-approved checkpoint\")\n telemetry[\"checkpoint\"] = {\"auto_approved\": True, \"high_risk_task_count\": len(high_risk_tasks)}\n else:\n if auto_approve and high_risk_tasks and not force_auto_approve:\n print(\"\\n⚠️ Auto-approve paused because high-risk tasks were detected.\")\n approval = input(\"\\nApprove and continue execution? [y/n]: \").strip().lower()\n if approval not in (\"y\", \"yes\", \"\"):\n print(\"❌ Execution cancelled.\")\n telemetry[\"checkpoint\"] = {\"auto_approved\": False, \"approved\": False}\n telemetry[\"phase_seconds\"][\"checkpoint\"] = round(time.perf_counter() - checkpoint_start, 3)\n telemetry[\"total_seconds\"] = round(time.perf_counter() - run_started, 3)\n _write_telemetry(feature, telemetry)\n return\n telemetry[\"checkpoint\"] = {\"auto_approved\": False, \"approved\": True, \"high_risk_task_count\": len(high_risk_tasks)}\n\n telemetry[\"phase_seconds\"][\"checkpoint\"] = round(time.perf_counter() - checkpoint_start, 3)\n\n # ------------------------------------------------------------------\n # STEP 5: Execution (SWPP agents — NOT Claude Code)\n # ------------------------------------------------------------------\n print(f\"\\n⚡ PHASE 4: Executing tasks via SWPP {executor_backend} agents…\")\n\n committed_tasks = _get_committed_tasks()\n if committed_tasks:\n print(f\" ⏭️ Already committed: {', '.join(sorted(committed_tasks))}\")\n\n execute_fn = _execute_task_crewai if executor_backend == \"crewai\" else _execute_task_anthropic\n exec_phase_start = time.perf_counter()\n\n for task_info in tasks:\n tid = task_info[\"id\"]\n\n if tid in committed_tasks:\n print(f\"\\n ⏭️ Skipping {tid} (already committed)\")\n telemetry[\"tasks\"].append({\"task_id\": tid, \"skipped\": True, \"reason\": \"already_committed\"})\n continue\n\n print(f\"\\n Executing {tid}: {task_info['name']}…\")\n\n task_metrics: dict = {\n \"task_id\": tid,\n \"name\": task_info[\"name\"],\n \"high_risk\": risk_profiles[tid][\"is_high_risk\"],\n \"risk_signals\": risk_profiles[tid][\"signals\"],\n \"skipped\": False,\n }\n\n exec_start = time.perf_counter()\n try:\n result_text = execute_fn(task_info, plan_content, context_content)\n except Exception as e:\n print(f\" ❌ Executor error for {tid}: {e}\")\n task_metrics[\"execution_seconds\"] = round(time.perf_counter() - exec_start, 3)\n task_metrics[\"committed\"] = False\n task_metrics[\"error\"] = str(e)\n telemetry[\"tasks\"].append(task_metrics)\n telemetry[\"phase_seconds\"][\"execute_and_test\"] = round(time.perf_counter() - exec_phase_start, 3)\n telemetry[\"total_seconds\"] = round(time.perf_counter() - run_started, 3)\n _write_telemetry(feature, telemetry)\n return\n\n task_metrics[\"execution_seconds\"] = round(time.perf_counter() - exec_start, 3)\n\n # For anthropic backend, parse and write files from response\n if executor_backend == \"anthropic\":\n files = _parse_files_from_response(result_text)\n if files:\n written = _write_executor_files(files)\n print(f\" 📁 Wrote {len(written)} file(s): {', '.join(written)}\")\n else:\n print(f\" ⚠️ No === FILE: === blocks found in executor response for {tid}\")\n\n # Also copy/sync plan to .swpp/plans/ for consistency with other runners\n PLANS_DIR.mkdir(parents=True, exist_ok=True)\n (PLANS_DIR / f\"{tid}.md\").write_text(\n f\"# {tid}: {task_info['name']}\\n\\n\"\n f\"*Source: SpecKit plan*\\n\\n\"\n f\"## Task Description\\n{task_info.get('description', '')}\\n\"\n )\n\n # Tests\n print(f\"\\n 🧪 Running tests for {tid}…\")\n fast_fail = fast_mode and not task_metrics[\"high_risk\"]\n task_metrics[\"test_strategy\"] = \"fast-fail\" if fast_fail else \"strict\"\n test_start = time.perf_counter()\n test_result = _run_tests(tid, fast_fail=fast_fail)\n task_metrics[\"tests_seconds\"] = round(time.perf_counter() - test_start, 3)\n task_metrics[\"tests_passed\"] = test_result[\"passed\"]\n print(test_result[\"summary\"])\n\n # Commit only if tests pass\n if test_result[\"passed\"]:\n commit_msg = f\"feat: {task_info['name']} [{tid}]\"\n _git_commit(commit_msg)\n print(f\" ✅ {tid} committed\")\n task_metrics[\"committed\"] = True\n else:\n print(f\" ❌ {tid} tests failed. Stopping execution.\")\n for name, detail in test_result[\"details\"].items():\n if not detail[\"passed\"]:\n print(f\"\\n {name} output:\\n{detail['output'][:500]}\")\n task_metrics[\"committed\"] = False\n telemetry[\"tasks\"].append(task_metrics)\n telemetry[\"phase_seconds\"][\"execute_and_test\"] = round(time.perf_counter() - exec_phase_start, 3)\n telemetry[\"summary\"] = {\"status\": \"failed_tests\", \"failed_task\": tid}\n telemetry[\"total_seconds\"] = round(time.perf_counter() - run_started, 3)\n _write_telemetry(feature, telemetry)\n return\n\n telemetry[\"tasks\"].append(task_metrics)\n\n telemetry[\"phase_seconds\"][\"execute_and_test\"] = round(time.perf_counter() - exec_phase_start, 3)\n\n # ------------------------------------------------------------------\n # STEP 6: Review\n # ------------------------------------------------------------------\n review_results = []\n if skip_review:\n print(\"\\n🔎 PHASE 5: Review skipped\")\n telemetry[\"phase_seconds\"][\"review\"] = 0.0\n else:\n print(\"\\n🔎 PHASE 5: Reviewing implementation…\")\n review_start = time.perf_counter()\n all_committed = _get_committed_tasks()\n newly_committed = all_committed - committed_tasks\n\n review_fn = _review_task_crewai if executor_backend == \"crewai\" else _review_task_anthropic\n\n for task_info in tasks:\n tid = task_info[\"id\"]\n if tid not in newly_committed:\n continue\n try:\n review_text = review_fn(task_info, plan_content, context_content)\n review_results.append((tid, review_text))\n except Exception as e:\n review_results.append((tid, f\"Review error: {e}\"))\n\n telemetry[\"phase_seconds\"][\"review\"] = round(time.perf_counter() - review_start, 3)\n\n # ------------------------------------------------------------------\n # STEP 7: Summary\n # ------------------------------------------------------------------\n print(\"\\n\" + \"=\" * 60)\n print(\"✨ SWPP-SPECKIT EXECUTION COMPLETE\")\n print(\"=\" * 60)\n print(f\"\\nFeature: {feature}\")\n print(f\"Tasks completed: {len([t for t in telemetry['tasks'] if t.get('committed')])}/{len(tasks)}\")\n print(f\"High-risk tasks: {len(high_risk_tasks)}\")\n\n if review_results:\n print(\"\\nReview Summary:\")\n for tid, result in review_results:\n status = \"✅ PASS\" if \"PASS\" in result.upper() else \"⚠️ NEEDS ATTENTION\"\n print(f\" {tid}: {status}\")\n\n telemetry[\"summary\"] = {\n \"status\": \"completed\",\n \"tasks_total\": len(tasks),\n \"tasks_committed\": len([t for t in telemetry[\"tasks\"] if t.get(\"committed\")]),\n \"high_risk_task_count\": len(high_risk_tasks),\n \"reviews_completed\": len(review_results),\n }\n telemetry[\"total_seconds\"] = round(time.perf_counter() - run_started, 3)\n metrics_path = _write_telemetry(feature, telemetry)\n\n print(\"\\n📁 Artefacts:\")\n if spec_path:\n print(f\" Spec: {spec_path.relative_to(PROJECT_ROOT)}\")\n if plan_path:\n print(f\" Plan: {plan_path.relative_to(PROJECT_ROOT)}\")\n print(f\" Tasks: {tasks_md.relative_to(PROJECT_ROOT)}\")\n print(f\" SWPP plans: .swpp/plans/\")\n print(f\" Commits: git log --oneline -n {len(tasks)}\")\n print(f\" Telemetry: {metrics_path}\")\n\n\n# =============================================================================\n# CLI\n# =============================================================================\n\nif __name__ == \"__main__\":\n parser = argparse.ArgumentParser(\n description=\"SWPP with SpecKit planning + SWPP agent execution\",\n epilog=\"Planning uses SpecKit (via claude -p). Execution uses SWPP AI agents.\",\n )\n parser.add_argument(\"feature\", nargs=\"+\", help=\"Feature description\")\n parser.add_argument(\n \"--project\", type=str,\n help=\"Project name (creates new directory in ~/projects/<name> if doesn't exist)\"\n )\n parser.add_argument(\n \"--project-dir\", type=str,\n help=\"Custom base directory for projects (default: ~/projects)\"\n )\n\n mode_group = parser.add_mutually_exclusive_group()\n mode_group.add_argument(\n \"--fast\", action=\"store_true\",\n help=\"Lean planning, auto-approve, skip review\",\n )\n mode_group.add_argument(\n \"--balanced\", action=\"store_true\",\n help=\"Lean planning, auto-approve, keep review\",\n )\n\n parser.add_argument(\"--auto-approve\", action=\"store_true\", help=\"Skip checkpoint prompt\")\n parser.add_argument(\"--force-auto-approve\", action=\"store_true\", help=\"Auto-approve even for high-risk tasks\")\n parser.add_argument(\"--skip-review\", action=\"store_true\", help=\"Skip review phase\")\n parser.add_argument(\n \"--executor\", choices=[\"anthropic\", \"crewai\"], default=DEFAULT_EXECUTOR_BACKEND,\n help=f\"Executor backend (default: {DEFAULT_EXECUTOR_BACKEND})\",\n )\n\n # SpecKit optional phase toggles\n parser.add_argument(\"--skip-constitution\", action=\"store_true\", help=\"Skip /speckit.constitution phase\")\n parser.add_argument(\"--skip-clarify\", action=\"store_true\", help=\"Skip /speckit.clarify phase\")\n parser.add_argument(\"--skip-checklist\", action=\"store_true\", help=\"Skip /speckit.checklist phase\")\n parser.add_argument(\"--skip-analyze\", action=\"store_true\", help=\"Skip /speckit.analyze phase\")\n\n args = parser.parse_args()\n feature = \" \".join(args.feature)\n\n # Setup project directory if --project flag is used\n if args.project:\n base_dir = Path(args.project_dir) if args.project_dir else Path.home() / \"projects\"\n project_path = setup_project_directory(args.project, base_dir)\n \n # Change working directory to project\n import os\n os.chdir(project_path)\n \n print(f\"\\n📂 Working in project: {project_path}\\n\")\n\n fast_mode = args.fast or args.balanced\n mode_label = \"FAST\" if args.fast else \"BALANCED\" if args.balanced else \"STANDARD\"\n auto_approve = args.auto_approve or fast_mode\n skip_review = args.skip_review or args.fast\n\n # In fast mode, skip optional enhancement phases by default\n skip_constitution = args.skip_constitution or args.fast\n skip_clarify = args.skip_clarify or args.fast\n skip_checklist = args.skip_checklist or args.fast\n skip_analyze = args.skip_analyze or args.fast\n\n run_swpp_speckit(\n feature,\n fast_mode=fast_mode,\n auto_approve=auto_approve,\n skip_review=skip_review,\n force_auto_approve=args.force_auto_approve,\n executor_backend=args.executor,\n mode_label=mode_label,\n skip_constitution=skip_constitution,\n skip_clarify=skip_clarify,\n skip_checklist=skip_checklist,\n skip_analyze=skip_analyze,\n )\n"}, "c898d45f2ce19c7441a9f949281b169c8f11ec2d": {"t": 1785766535.161042, "n": 42136, "h": "feabc6d288288d6a", "c": "# swpp_speckit.py\n# SWPP Runner with SpecKit (spec-driven planning) + SWPP Agents (execution)\n#\n# SpecKit phases (via `claude -p` slash commands):\n# /speckit.constitution → project principles\n# /speckit.specify → feature specification (spec.md)\n# /speckit.clarify → de-risk ambiguity in spec\n# /speckit.plan → implementation plan (plan.md)\n# /speckit.checklist → quality validation checklists\n# /speckit.tasks → actionable task list (tasks.md)\n# /speckit.analyze → cross-artifact consistency check\n# /speckit.implement → SKIPPED (SWPP agents handle execution)\n#\n# Execution: SWPP AI agents (Anthropic API or CrewAI)\n#\n# Requires: `specify` CLI (GitHub Spec Kit) + `claude` CLI (Claude Code)\n#\n# Run: python swpp_speckit.py \"Your feature description\"\n\nimport os\nimport sys\nimport json\nimport re\nimport time\nimport shutil\nimport subprocess\nimport argparse\nfrom datetime import datetime\nfrom pathlib import Path\nfrom textwrap import dedent\n\n# =============================================================================\n# CONFIGURATION\n# =============================================================================\n\nPROJECT_ROOT = Path.cwd()\nSWPP_DIR = PROJECT_ROOT / \".swpp\"\nPLANS_DIR = SWPP_DIR / \"plans\"\nCONTEXT_FILE = SWPP_DIR / \"context.md\"\nSPECKIT_DIR = PROJECT_ROOT / \".specify\"\nCLAUDE_COMMANDS_DIR = PROJECT_ROOT / \".claude\" / \"commands\"\nSPECS_DIR = PROJECT_ROOT / \"specs\"\n\nFAST_MODE_TASK_LIMIT = 3\nHIGH_RISK_KEYWORDS = {\n \"auth\", \"authentication\", \"authorization\", \"permission\", \"permissions\",\n \"secret\", \"token\", \"password\", \"credential\", \"oauth\", \"security\",\n \"encryption\", \"decrypt\", \"migration\", \"schema\", \"database\", \"sql\",\n \"prod\", \"production\", \"billing\", \"payment\", \"compliance\",\n}\n\n# Claude Code CLI timeout per phase (seconds)\nSPECKIT_PHASE_TIMEOUT = 300 # 5 min per slash command\n\n# Model Configuration\nEXECUTOR_MODEL = os.getenv(\"SWPP_EXECUTOR_MODEL\", \"deepseek-chat\") # or \"claude-sonnet-4-5-20250929\"\nREVIEWER_MODEL = os.getenv(\"SWPP_REVIEWER_MODEL\", \"claude-sonnet-4-5-20250929\")\n\n# Executor backend: \"crewai\" or \"anthropic\"\n# crewai uses CrewAI agents, anthropic uses raw Anthropic SDK\nDEFAULT_EXECUTOR_BACKEND = \"anthropic\"\n\n\n# =============================================================================\n# PREREQUISITES\n# =============================================================================\n\ndef _check_cli(name: str, check_cmd: list[str]) -> bool:\n \"\"\"Verify a CLI tool is installed and reachable.\"\"\"\n try:\n subprocess.run(check_cmd, capture_output=True, timeout=10)\n return True\n except (FileNotFoundError, subprocess.TimeoutExpired):\n return False\n\n\ndef check_prerequisites() -> list[str]:\n \"\"\"Return list of missing prerequisites.\"\"\"\n missing = []\n if not _check_cli(\"specify\", [\"specify\", \"version\"]):\n missing.append(\"specify (GitHub Spec Kit) — install: https://github.com/specifyit/specify\")\n if not _check_cli(\"claude\", [\"claude\", \"--version\"]):\n missing.append(\"claude (Claude Code CLI) — install: https://docs.anthropic.com/en/docs/claude-code\")\n if not os.getenv(\"ANTHROPIC_API_KEY\"):\n missing.append(\"ANTHROPIC_API_KEY environment variable\")\n return missing\n\n\n# =============================================================================\n# SPECKIT SCAFFOLD\n# =============================================================================\n\ndef ensure_speckit_scaffold() -> bool:\n \"\"\"Ensure SpecKit project scaffold exists (.specify/ and .claude/commands/).\"\"\"\n if SPECKIT_DIR.exists() and CLAUDE_COMMANDS_DIR.exists():\n return True\n\n print(\" Initialising SpecKit scaffold…\")\n result = subprocess.run(\n [\"specify\", \"init\", \".\", \"--ai\", \"claude\", \"--here\", \"--force\", \"--no-git\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=60,\n )\n if result.returncode != 0:\n print(f\" ❌ specify init failed:\\n{result.stderr}\")\n return False\n\n # Verify key artefacts\n if not CLAUDE_COMMANDS_DIR.exists():\n print(\" ❌ .claude/commands/ not created – specify init may have failed silently\")\n return False\n\n print(\" ✅ SpecKit scaffold ready\")\n return True\n\n\n# =============================================================================\n# CLAUDE CODE SLASH COMMAND RUNNER\n# =============================================================================\n\ndef run_speckit_command(\n slash_command: str,\n args: str = \"\",\n timeout: int = SPECKIT_PHASE_TIMEOUT,\n) -> tuple[bool, str]:\n \"\"\"\n Execute a SpecKit slash command via ``claude -p``.\n\n Returns (success, output_text).\n \"\"\"\n prompt = f\"/{slash_command} {args}\".strip()\n cmd = [\n \"claude\",\n \"-p\", prompt,\n \"--output-format\", \"text\",\n \"--allowedTools\", \"Bash Read Write Edit\",\n ]\n print(f\" ▸ claude -p \\\"/{slash_command} {args[:60]}{'…' if len(args) > 60 else ''}\\\"\")\n\n try:\n result = subprocess.run(\n cmd, cwd=PROJECT_ROOT,\n capture_output=True, text=True,\n timeout=timeout,\n )\n output = result.stdout + result.stderr\n success = result.returncode == 0\n if not success:\n print(f\" ⚠️ /{slash_command} exited with code {result.returncode}\")\n return success, output\n except subprocess.TimeoutExpired:\n print(f\" ⚠️ /{slash_command} timed out after {timeout}s\")\n return False, f\"Timed out after {timeout}s\"\n\n\n# =============================================================================\n# SPECKIT PLANNING PHASES\n# =============================================================================\n\ndef phase_constitution(args: str = \"\") -> tuple[bool, str]:\n \"\"\"Establish project governing principles via /speckit.constitution.\"\"\"\n print(\"\\n📜 SPECKIT: Establishing project constitution…\")\n return run_speckit_command(\"speckit.constitution\", args)\n\n\ndef phase_specify(feature: str) -> tuple[bool, str]:\n \"\"\"Generate feature specification via /speckit.specify.\"\"\"\n print(\"\\n📋 SPECKIT: Generating feature specification…\")\n return run_speckit_command(\"speckit.specify\", feature)\n\n\ndef phase_clarify(args: str = \"\") -> tuple[bool, str]:\n \"\"\"Clarify underspecified areas in spec via /speckit.clarify.\"\"\"\n print(\"\\n🔎 SPECKIT: Clarifying specification…\")\n return run_speckit_command(\"speckit.clarify\", args)\n\n\ndef phase_plan() -> tuple[bool, str]:\n \"\"\"Generate implementation plan via /speckit.plan.\"\"\"\n print(\"\\n📐 SPECKIT: Generating implementation plan…\")\n return run_speckit_command(\"speckit.plan\")\n\n\ndef phase_checklist(args: str = \"\") -> tuple[bool, str]:\n \"\"\"Generate quality checklists via /speckit.checklist.\"\"\"\n print(\"\\n✅ SPECKIT: Generating quality checklist…\")\n return run_speckit_command(\"speckit.checklist\", args)\n\n\ndef phase_tasks() -> tuple[bool, str]:\n \"\"\"Generate actionable tasks via /speckit.tasks.\"\"\"\n print(\"\\n📝 SPECKIT: Generating actionable task list…\")\n return run_speckit_command(\"speckit.tasks\")\n\n\ndef phase_analyze() -> tuple[bool, str]:\n \"\"\"Cross-artifact consistency analysis via /speckit.analyze.\"\"\"\n print(\"\\n🔬 SPECKIT: Analyzing cross-artifact consistency…\")\n return run_speckit_command(\"speckit.analyze\")\n\n\n# =============================================================================\n# TASK PARSING\n# =============================================================================\n\n_TASK_LINE_RE = re.compile(\n r\"^- \\[[ xX]?\\]\\s+\" # checkbox\n r\"(T\\d{3})\" # task ID (T001, T002, …)\n r\"(?:\\s+\\[P\\])?\" # optional parallel marker\n r\"(?:\\s+\\[US\\d+\\])?\" # optional user-story label\n r\"\\s+(.+)$\" # description\n)\n\n\ndef _find_tasks_md() -> Path | None:\n \"\"\"Locate the tasks.md generated by SpecKit inside specs/<branch>/.\"\"\"\n # Try to find from current git branch\n try:\n branch = subprocess.run(\n [\"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=5,\n ).stdout.strip()\n except Exception:\n branch = \"\"\n\n if branch and (SPECS_DIR / branch / \"tasks.md\").exists():\n return SPECS_DIR / branch / \"tasks.md\"\n\n # Fallback: find newest tasks.md under specs/\n if SPECS_DIR.exists():\n candidates = sorted(SPECS_DIR.glob(\"*/tasks.md\"), key=lambda p: p.stat().st_mtime, reverse=True)\n if candidates:\n return candidates[0]\n\n return None\n\n\ndef _find_spec_md() -> Path | None:\n \"\"\"Locate the spec.md generated by SpecKit.\"\"\"\n try:\n branch = subprocess.run(\n [\"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=5,\n ).stdout.strip()\n except Exception:\n branch = \"\"\n\n if branch and (SPECS_DIR / branch / \"spec.md\").exists():\n return SPECS_DIR / branch / \"spec.md\"\n\n if SPECS_DIR.exists():\n candidates = sorted(SPECS_DIR.glob(\"*/spec.md\"), key=lambda p: p.stat().st_mtime, reverse=True)\n if candidates:\n return candidates[0]\n return None\n\n\ndef _find_plan_md() -> Path | None:\n \"\"\"Locate the plan.md generated by SpecKit.\"\"\"\n try:\n branch = subprocess.run(\n [\"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=5,\n ).stdout.strip()\n except Exception:\n branch = \"\"\n\n if branch and (SPECS_DIR / branch / \"plan.md\").exists():\n return SPECS_DIR / branch / \"plan.md\"\n\n if SPECS_DIR.exists():\n candidates = sorted(SPECS_DIR.glob(\"*/plan.md\"), key=lambda p: p.stat().st_mtime, reverse=True)\n if candidates:\n return candidates[0]\n return None\n\n\ndef parse_tasks(tasks_md_path: Path) -> list[dict]:\n \"\"\"Parse tasks.md into a list of task dicts with id, name, parallel flag.\"\"\"\n content = tasks_md_path.read_text()\n tasks = []\n current_phase = \"\"\n\n for line in content.splitlines():\n # Detect phase headings\n if line.startswith(\"## \") or line.startswith(\"### \"):\n current_phase = line.lstrip(\"#\").strip()\n continue\n\n m = _TASK_LINE_RE.match(line.strip())\n if m:\n task_id = m.group(1)\n description = m.group(2).strip()\n is_parallel = \"[P]\" in line\n us_match = re.search(r\"\\[US(\\d+)\\]\", line)\n user_story = f\"US{us_match.group(1)}\" if us_match else None\n\n tasks.append({\n \"id\": task_id,\n \"name\": description,\n \"description\": description,\n \"phase\": current_phase,\n \"parallel\": is_parallel,\n \"user_story\": user_story,\n })\n\n return tasks\n\n\n# =============================================================================\n# RISK DETECTION\n# =============================================================================\n\ndef _detect_task_risk(task_info: dict, plan_content: str = \"\") -> tuple[bool, list[str]]:\n \"\"\"Detect whether a task is high-risk based on metadata and plan text.\"\"\"\n haystack = \" \".join([\n str(task_info.get(\"id\", \"\")),\n str(task_info.get(\"name\", \"\")),\n str(task_info.get(\"description\", \"\")),\n plan_content,\n ]).lower()\n\n matches = sorted(\n {kw for kw in HIGH_RISK_KEYWORDS if re.search(rf\"\\b{re.escape(kw)}\\b\", haystack)}\n )\n return (len(matches) > 0, matches)\n\n\n# =============================================================================\n# EXECUTION — ANTHROPIC SDK (default backend)\n# =============================================================================\n\ndef _execute_task_anthropic(task_info: dict, plan_content: str, context_content: str) -> str:\n \"\"\"Execute a single task using Anthropic API or DeepSeek (OpenAI-compatible).\"\"\"\n model = EXECUTOR_MODEL\n\n prompt = dedent(f\"\"\"\\\n You are a precise developer implementing a task from an approved SpecKit plan.\n\n TASK: {task_info['id']} — {task_info['name']}\n DESCRIPTION: {task_info.get('description', '')}\n\n IMPLEMENTATION PLAN (from SpecKit):\n {plan_content}\n\n PROJECT STANDARDS:\n {context_content}\n\n RULES:\n 1. ONLY create or modify files directly related to this task.\n 2. Follow project standards and conventions exactly.\n 3. If the plan is unclear, make the safest possible implementation choice.\n 4. Output your work using the format below.\n\n OUTPUT FORMAT:\n === FILE: <relative-path> ===\n [complete file contents]\n === END FILE ===\n\n Provide ALL files you create or modify.\n \"\"\")\n\n # Use DeepSeek (OpenAI-compatible) if model starts with \"deepseek\"\n if model.startswith(\"deepseek\"):\n from openai import OpenAI\n client = OpenAI(\n api_key=os.getenv(\"DEEPSEEK_API_KEY\"),\n base_url=\"https://api.deepseek.com\",\n )\n response = client.chat.completions.create(\n model=model,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n max_tokens=8192,\n temperature=0.2,\n )\n return response.choices[0].message.content\n else:\n # Use Anthropic Claude\n from anthropic import Anthropic\n client = Anthropic()\n response = client.messages.create(\n model=model,\n max_tokens=8192,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n )\n return response.content[0].text\n\n\ndef _parse_files_from_response(response_text: str) -> dict[str, str]:\n \"\"\"Extract file paths and contents from === FILE: ... === blocks.\"\"\"\n files = {}\n pattern = re.compile(\n r\"=== FILE:\\s*(.+?)\\s*===\\s*\\n(.*?)\\n=== END FILE ===\",\n re.DOTALL,\n )\n for match in pattern.finditer(response_text):\n filepath = match.group(1).strip()\n content = match.group(2)\n files[filepath] = content\n return files\n\n\ndef _write_executor_files(files: dict[str, str]) -> list[str]:\n \"\"\"Write parsed files to disk, return list of written paths.\"\"\"\n written = []\n for rel_path, content in files.items():\n path = PROJECT_ROOT / rel_path\n path.parent.mkdir(parents=True, exist_ok=True)\n path.write_text(content)\n written.append(rel_path)\n return written\n\n\n# =============================================================================\n# EXECUTION — CREWAI BACKEND (optional)\n# =============================================================================\n\ndef _execute_task_crewai(task_info: dict, plan_content: str, context_content: str) -> str:\n \"\"\"Execute a single task using CrewAI agents (mirrors swpp_crew.py executor).\"\"\"\n from crewai import Agent, Task, Crew, LLM\n from crewai.tools import tool as crewai_tool\n\n model = EXECUTOR_MODEL\n\n # Use appropriate LangChain wrapper based on model\n if model.startswith(\"deepseek\"):\n from langchain_openai import ChatOpenAI\n executor_model = ChatOpenAI(\n model=model,\n api_key=os.getenv(\"DEEPSEEK_API_KEY\"),\n base_url=\"https://api.deepseek.com\",\n temperature=0.2,\n )\n else:\n from langchain_anthropic import ChatAnthropic\n executor_model = ChatAnthropic(\n model=model,\n temperature=0.2,\n )\n\n @crewai_tool\n def read_file(filepath: str) -> str:\n \"\"\"Read contents of a file in the project.\"\"\"\n path = PROJECT_ROOT / filepath\n return path.read_text() if path.exists() else f\"File not found: {filepath}\"\n\n @crewai_tool\n def write_file(filepath: str, content: str) -> str:\n \"\"\"Write content to a file in the project.\"\"\"\n path = PROJECT_ROOT / filepath\n path.parent.mkdir(parents=True, exist_ok=True)\n path.write_text(content)\n return f\"Written: {filepath}\"\n\n @crewai_tool\n def list_files(directory: str = \"src\") -> str:\n \"\"\"List files in a directory.\"\"\"\n path = PROJECT_ROOT / directory\n if not path.exists():\n return f\"Directory not found: {directory}\"\n files = [str(f.relative_to(PROJECT_ROOT)) for f in path.rglob(\"*\") if f.is_file()]\n return \"\\n\".join(files[:50])\n\n @crewai_tool\n def run_command(command: str) -> str:\n \"\"\"Run a shell command.\"\"\"\n try:\n result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=60, cwd=PROJECT_ROOT)\n return result.stdout + result.stderr\n except subprocess.TimeoutExpired:\n return \"Command timed out\"\n\n executor = Agent(\n role=\"Execution Agent\",\n goal=\"Implement code exactly according to approved SpecKit plans\",\n backstory=dedent(\"\"\"\\\n You are a precise developer who implements code following SpecKit plans exactly.\n You only modify files specified in the plan. You follow project\n standards religiously.\n \"\"\"),\n llm=executor_model,\n tools=[read_file, write_file, list_files, run_command],\n verbose=True,\n )\n\n execution_task = Task(\n description=dedent(f\"\"\"\\\n Implement code according to this approved SpecKit plan:\n\n TASK: {task_info['id']} — {task_info['name']}\n\n PLAN:\n {plan_content}\n\n PROJECT STANDARDS:\n {context_content}\n\n RULES:\n 1. ONLY modify files listed in the plan\n 2. Follow project standards exactly\n 3. After implementation, run linting if available\n\n Report what you created/modified.\n \"\"\"),\n expected_output=\"Implementation report with files created/modified\",\n agent=executor,\n )\n\n crew = Crew(agents=[executor], tasks=[execution_task], verbose=True)\n result = crew.kickoff()\n return str(result)\n\n\n# =============================================================================\n# TESTS & GIT\n# =============================================================================\n\ndef _run_tests(task_id: str, fast_fail: bool = False) -> dict:\n \"\"\"Run the project test suite. Returns dict with passed, summary, details.\"\"\"\n venv_python = str(PROJECT_ROOT / \".venv\" / \"bin\" / \"python\")\n if not Path(venv_python).exists():\n venv_python = \"python\"\n\n test_commands = [\n (\"unit\", f\"{venv_python} -m pytest tests/unit/ -v --tb=short\"),\n (\"integration\", f\"{venv_python} -m pytest tests/integration/ -v --tb=short\"),\n (\"safety\", f\"{venv_python} -m pytest tests/safety/ -v --tb=short\"),\n (\"solid\", f\"{venv_python} -m pytest tests/solid/ -v --tb=short\"),\n ]\n\n results = {}\n all_passed = True\n lines = [f\"\\n🧪 Running tests for {task_id}…\\n\"]\n\n for name, cmd in test_commands:\n try:\n res = subprocess.run(cmd, shell=True, cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=120)\n except subprocess.TimeoutExpired:\n res = type(\"R\", (), {\"returncode\": 1, \"stdout\": \"\", \"stderr\": \"Timed out\"})()\n\n passed = res.returncode == 0 or \"passed\" in getattr(res, \"stdout\", \"\").lower()\n results[name] = {\"passed\": passed, \"output\": getattr(res, \"stdout\", \"\") + getattr(res, \"stderr\", \"\")}\n lines.append(f\" {name}: {'✅ PASS' if passed else '❌ FAIL'}\")\n\n if not passed:\n all_passed = False\n if fast_fail:\n lines.append(\" ⚡ Fast mode: stopping at first failure\")\n break\n\n lines.append(f\"\\n{'✅ All tests passed!' if all_passed else '❌ Some tests failed'}\\n\")\n return {\"passed\": all_passed, \"summary\": \"\\n\".join(lines), \"details\": results}\n\n\ndef _git_commit(message: str) -> str:\n \"\"\"Stage all changes and commit.\"\"\"\n subprocess.run([\"git\", \"add\", \"-A\"], cwd=PROJECT_ROOT)\n result = subprocess.run([\"git\", \"commit\", \"-m\", message], cwd=PROJECT_ROOT, capture_output=True, text=True)\n return result.stdout + result.stderr\n\n\ndef _get_committed_tasks() -> set[str]:\n \"\"\"Return set of task IDs already committed (format: feat: ... [TXXX]).\"\"\"\n result = subprocess.run(\n [\"git\", \"log\", \"--oneline\", \"-n\", \"100\"],\n cwd=PROJECT_ROOT, capture_output=True, text=True,\n )\n committed = set()\n for line in result.stdout.splitlines():\n m = re.search(r\"\\[(T\\d{3})\\]\", line)\n if m:\n committed.add(m.group(1))\n return committed\n\n\n# =============================================================================\n# REVIEW\n# =============================================================================\n\ndef _review_task_anthropic(task_info: dict, plan_content: str, context_content: str) -> str:\n \"\"\"Review implementation of a single task using Claude Sonnet.\"\"\"\n from anthropic import Anthropic\n\n client = Anthropic()\n model = REVIEWER_MODEL\n\n prompt = dedent(f\"\"\"\\\n Review the implementation of task {task_info['id']}: {task_info['name']}.\n\n ORIGINAL PLAN:\n {plan_content}\n\n PROJECT STANDARDS:\n {context_content}\n\n REVIEW CHECKLIST:\n 1. Does implementation match the plan?\n 2. Are project standards followed?\n 3. Is error handling adequate?\n 4. Are there any bugs or edge cases missed?\n 5. Is the code clean and maintainable?\n\n OUTPUT: PASS or FAIL with issues found and suggestions.\n \"\"\")\n\n response = client.messages.create(\n model=model,\n max_tokens=4096,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n )\n return response.content[0].text\n\n\ndef _review_task_crewai(task_info: dict, plan_content: str, context_content: str) -> str:\n \"\"\"Review implementation of a single task using CrewAI reviewer agent.\"\"\"\n from crewai import Agent, Task, Crew\n\n model = REVIEWER_MODEL\n\n # Reviewer always uses Claude (from REVIEWER_MODEL)\n from langchain_anthropic import ChatAnthropic\n reviewer_model = ChatAnthropic(\n model=model,\n temperature=0.1, # Low temperature for consistent review\n )\n\n reviewer = Agent(\n role=\"Code Reviewer\",\n goal=\"Ensure code quality, adherence to standards, and identify bugs\",\n backstory=dedent(\"\"\"\\\n You are a meticulous code reviewer with expertise in software quality.\n You verify implementations match approved plans, follow project standards,\n and have proper error handling. You catch edge cases and maintainability issues.\n \"\"\"),\n llm=reviewer_model,\n tools=[], # Reviewer doesn't need file tools - reviews what was already committed\n verbose=True,\n )\n\n review_task = Task(\n description=dedent(f\"\"\"\\\n Review the implementation of task {task_info['id']}: {task_info['name']}.\n\n ORIGINAL PLAN:\n {plan_content}\n\n PROJECT STANDARDS:\n {context_content}\n\n REVIEW CHECKLIST:\n 1. Does implementation match the plan?\n 2. Are project standards followed?\n 3. Is error handling adequate?\n 4. Are there any bugs or edge cases missed?\n 5. Is the code clean and maintainable?\n\n OUTPUT: Start with \"PASS\" or \"FAIL\", then provide detailed findings and suggestions.\n \"\"\"),\n expected_output=\"Review verdict (PASS/FAIL) with detailed findings\",\n agent=reviewer,\n )\n\n crew = Crew(agents=[reviewer], tasks=[review_task], verbose=True)\n result = crew.kickoff()\n return str(result)\n\n\n# =============================================================================\n# TELEMETRY\n# =============================================================================\n\ndef _write_telemetry(feature: str, telemetry: dict) -> Path:\n \"\"\"Persist run telemetry to .swpp/.\"\"\"\n SWPP_DIR.mkdir(exist_ok=True)\n path = SWPP_DIR / f\"metrics_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json\"\n payload = {\"feature\": feature, \"generated_at\": datetime.now().isoformat(), **telemetry}\n path.write_text(json.dumps(payload, indent=2))\n return path\n\n\n# =============================================================================\n# MAIN ORCHESTRATOR\n# =============================================================================\n\ndef run_swpp_speckit(\n feature: str,\n *,\n fast_mode: bool = False,\n auto_approve: bool = False,\n skip_review: bool = False,\n force_auto_approve: bool = False,\n executor_backend: str = DEFAULT_EXECUTOR_BACKEND,\n mode_label: str = \"STANDARD\",\n skip_constitution: bool = False,\n skip_clarify: bool = False,\n skip_checklist: bool = False,\n skip_analyze: bool = False,\n):\n run_started = time.perf_counter()\n telemetry: dict = {\n \"runner\": \"swpp_speckit\",\n \"mode\": mode_label,\n \"executor_backend\": executor_backend,\n \"phase_seconds\": {},\n \"tasks\": [],\n \"checkpoint\": {},\n \"summary\": {},\n }\n\n print(\"\\n\" + \"=\" * 60)\n print(\"SWPP-SPECKIT AUTOMATIC EXECUTION\")\n print(f\"Feature: {feature}\")\n print(f\"Mode: {mode_label} | Executor: {executor_backend}\")\n print(\"=\" * 60)\n\n # ------------------------------------------------------------------\n # STEP 0: Prerequisites\n # ------------------------------------------------------------------\n missing = check_prerequisites()\n if missing:\n print(\"\\n❌ Missing prerequisites:\")\n for m in missing:\n print(f\" • {m}\")\n sys.exit(1)\n\n # ------------------------------------------------------------------\n # STEP 1: Ensure SpecKit scaffold\n # ------------------------------------------------------------------\n print(\"\\n🔧 Checking SpecKit scaffold…\")\n if not ensure_speckit_scaffold():\n sys.exit(1)\n\n # ------------------------------------------------------------------\n # STEP 2: SpecKit planning phases\n # ------------------------------------------------------------------\n phase_start = time.perf_counter()\n speckit_phases_run: list[str] = []\n\n # 2a: /speckit.constitution → .specify/memory/constitution.md\n if not skip_constitution:\n ok, output = phase_constitution()\n if not ok:\n print(f\"\\n⚠️ Constitution phase failed (non-fatal). Output:\\n{output[:500]}\")\n else:\n speckit_phases_run.append(\"constitution\")\n print(\" ✅ Constitution established\")\n else:\n print(\"\\n📜 SPECKIT: Skipping /speckit.constitution\")\n\n # 2b: /speckit.specify → spec.md\n ok, output = phase_specify(feature)\n if not ok:\n print(f\"\\n❌ Specification phase failed. Output:\\n{output[:1000]}\")\n sys.exit(1)\n\n spec_path = _find_spec_md()\n if spec_path:\n print(f\" ✅ Spec: {spec_path.relative_to(PROJECT_ROOT)}\")\n speckit_phases_run.append(\"specify\")\n else:\n print(\" ⚠️ spec.md not found after /speckit.specify — continuing anyway\")\n\n # 2c: /speckit.clarify → refine spec.md\n if not skip_clarify:\n ok, output = phase_clarify()\n if not ok:\n print(f\"\\n⚠️ Clarify phase failed (non-fatal). Output:\\n{output[:500]}\")\n else:\n speckit_phases_run.append(\"clarify\")\n print(\" ✅ Spec clarified\")\n else:\n print(\"\\n🔎 SPECKIT: Skipping /speckit.clarify\")\n\n # 2d: /speckit.plan → plan.md\n ok, output = phase_plan()\n if not ok:\n print(f\"\\n❌ Planning phase failed. Output:\\n{output[:1000]}\")\n sys.exit(1)\n\n plan_path = _find_plan_md()\n if plan_path:\n print(f\" ✅ Plan: {plan_path.relative_to(PROJECT_ROOT)}\")\n speckit_phases_run.append(\"plan\")\n else:\n print(\" ⚠️ plan.md not found after /speckit.plan — continuing anyway\")\n\n # 2e: /speckit.checklist → quality validation\n if not skip_checklist:\n ok, output = phase_checklist()\n if not ok:\n print(f\"\\n⚠️ Checklist phase failed (non-fatal). Output:\\n{output[:500]}\")\n else:\n speckit_phases_run.append(\"checklist\")\n print(\" ✅ Quality checklist generated\")\n else:\n print(\"\\n✅ SPECKIT: Skipping /speckit.checklist\")\n\n # 2f: /speckit.tasks → tasks.md\n ok, output = phase_tasks()\n if not ok:\n print(f\"\\n❌ Task generation failed. Output:\\n{output[:1000]}\")\n sys.exit(1)\n\n tasks_md = _find_tasks_md()\n if not tasks_md:\n print(\"\\n❌ tasks.md not found after /speckit.tasks. Cannot proceed.\")\n sys.exit(1)\n\n print(f\" ✅ Tasks: {tasks_md.relative_to(PROJECT_ROOT)}\")\n speckit_phases_run.append(\"tasks\")\n\n # 2g: /speckit.analyze → cross-artifact consistency check\n if not skip_analyze:\n ok, output = phase_analyze()\n if not ok:\n print(f\"\\n⚠️ Analyze phase failed (non-fatal). Output:\\n{output[:500]}\")\n else:\n speckit_phases_run.append(\"analyze\")\n print(\" ✅ Cross-artifact analysis complete\")\n else:\n print(\"\\n🔬 SPECKIT: Skipping /speckit.analyze\")\n\n telemetry[\"phase_seconds\"][\"speckit_planning\"] = round(time.perf_counter() - phase_start, 3)\n telemetry[\"speckit_phases_run\"] = speckit_phases_run\n\n # ------------------------------------------------------------------\n # STEP 3: Parse tasks\n # ------------------------------------------------------------------\n tasks = parse_tasks(tasks_md)\n\n if not tasks:\n print(f\"\\n❌ No tasks parsed from {tasks_md}. Check tasks.md format.\")\n sys.exit(1)\n\n if fast_mode and len(tasks) > FAST_MODE_TASK_LIMIT:\n tasks = tasks[:FAST_MODE_TASK_LIMIT]\n print(f\"⚡ Fast mode: limited to first {FAST_MODE_TASK_LIMIT} tasks\")\n\n print(f\"\\n✅ Parsed {len(tasks)} tasks:\")\n for t in tasks:\n marker = \" [P]\" if t[\"parallel\"] else \"\"\n us = f\" [{t['user_story']}]\" if t.get(\"user_story\") else \"\"\n print(f\" {t['id']}{marker}{us}: {t['name']}\")\n\n # Load plan content for executor context\n plan_content = plan_path.read_text() if plan_path and plan_path.exists() else \"\"\n context_content = CONTEXT_FILE.read_text() if CONTEXT_FILE.exists() else \"\"\n\n # Risk detection\n risk_profiles: dict[str, dict] = {}\n for t in tasks:\n is_risky, signals = _detect_task_risk(t, plan_content)\n risk_profiles[t[\"id\"]] = {\"is_high_risk\": is_risky, \"signals\": signals}\n\n high_risk_tasks = [t for t in tasks if risk_profiles[t[\"id\"]][\"is_high_risk\"]]\n\n # ------------------------------------------------------------------\n # STEP 4: Checkpoint\n # ------------------------------------------------------------------\n print(\"\\n\" + \"=\" * 60)\n print(\"🔍 CHECKPOINT: Review SpecKit artefacts\")\n print(\"=\" * 60)\n\n if spec_path:\n print(f\" 📄 {spec_path.relative_to(PROJECT_ROOT)}\")\n if plan_path:\n print(f\" 📄 {plan_path.relative_to(PROJECT_ROOT)}\")\n print(f\" 📄 {tasks_md.relative_to(PROJECT_ROOT)}\")\n\n if high_risk_tasks:\n print(\"\\n ⚠️ High-risk tasks detected:\")\n for t in high_risk_tasks:\n signals = \", \".join(risk_profiles[t[\"id\"]][\"signals\"][:5])\n print(f\" - {t['id']}: {t['name']} ({signals})\")\n\n checkpoint_start = time.perf_counter()\n if auto_approve and not (high_risk_tasks and not force_auto_approve):\n print(\"\\n⚡ Auto-approved checkpoint\")\n telemetry[\"checkpoint\"] = {\"auto_approved\": True, \"high_risk_task_count\": len(high_risk_tasks)}\n else:\n if auto_approve and high_risk_tasks and not force_auto_approve:\n print(\"\\n⚠️ Auto-approve paused because high-risk tasks were detected.\")\n approval = input(\"\\nApprove and continue execution? [y/n]: \").strip().lower()\n if approval not in (\"y\", \"yes\", \"\"):\n print(\"❌ Execution cancelled.\")\n telemetry[\"checkpoint\"] = {\"auto_approved\": False, \"approved\": False}\n telemetry[\"phase_seconds\"][\"checkpoint\"] = round(time.perf_counter() - checkpoint_start, 3)\n telemetry[\"total_seconds\"] = round(time.perf_counter() - run_started, 3)\n _write_telemetry(feature, telemetry)\n return\n telemetry[\"checkpoint\"] = {\"auto_approved\": False, \"approved\": True, \"high_risk_task_count\": len(high_risk_tasks)}\n\n telemetry[\"phase_seconds\"][\"checkpoint\"] = round(time.perf_counter() - checkpoint_start, 3)\n\n # ------------------------------------------------------------------\n # STEP 5: Execution (SWPP agents — NOT Claude Code)\n # ------------------------------------------------------------------\n print(f\"\\n⚡ PHASE 4: Executing tasks via SWPP {executor_backend} agents…\")\n\n committed_tasks = _get_committed_tasks()\n if committed_tasks:\n print(f\" ⏭️ Already committed: {', '.join(sorted(committed_tasks))}\")\n\n execute_fn = _execute_task_crewai if executor_backend == \"crewai\" else _execute_task_anthropic\n exec_phase_start = time.perf_counter()\n\n for task_info in tasks:\n tid = task_info[\"id\"]\n\n if tid in committed_tasks:\n print(f\"\\n ⏭️ Skipping {tid} (already committed)\")\n telemetry[\"tasks\"].append({\"task_id\": tid, \"skipped\": True, \"reason\": \"already_committed\"})\n continue\n\n print(f\"\\n Executing {tid}: {task_info['name']}…\")\n\n task_metrics: dict = {\n \"task_id\": tid,\n \"name\": task_info[\"name\"],\n \"high_risk\": risk_profiles[tid][\"is_high_risk\"],\n \"risk_signals\": risk_profiles[tid][\"signals\"],\n \"skipped\": False,\n }\n\n exec_start = time.perf_counter()\n try:\n result_text = execute_fn(task_info, plan_content, context_content)\n except Exception as e:\n print(f\" ❌ Executor error for {tid}: {e}\")\n task_metrics[\"execution_seconds\"] = round(time.perf_counter() - exec_start, 3)\n task_metrics[\"committed\"] = False\n task_metrics[\"error\"] = str(e)\n telemetry[\"tasks\"].append(task_metrics)\n telemetry[\"phase_seconds\"][\"execute_and_test\"] = round(time.perf_counter() - exec_phase_start, 3)\n telemetry[\"total_seconds\"] = round(time.perf_counter() - run_started, 3)\n _write_telemetry(feature, telemetry)\n return\n\n task_metrics[\"execution_seconds\"] = round(time.perf_counter() - exec_start, 3)\n\n # For anthropic backend, parse and write files from response\n if executor_backend == \"anthropic\":\n files = _parse_files_from_response(result_text)\n if files:\n written = _write_executor_files(files)\n print(f\" 📁 Wrote {len(written)} file(s): {', '.join(written)}\")\n else:\n print(f\" ⚠️ No === FILE: === blocks found in executor response for {tid}\")\n\n # Also copy/sync plan to .swpp/plans/ for consistency with other runners\n PLANS_DIR.mkdir(parents=True, exist_ok=True)\n (PLANS_DIR / f\"{tid}.md\").write_text(\n f\"# {tid}: {task_info['name']}\\n\\n\"\n f\"*Source: SpecKit plan*\\n\\n\"\n f\"## Task Description\\n{task_info.get('description', '')}\\n\"\n )\n\n # Tests\n print(f\"\\n 🧪 Running tests for {tid}…\")\n fast_fail = fast_mode and not task_metrics[\"high_risk\"]\n task_metrics[\"test_strategy\"] = \"fast-fail\" if fast_fail else \"strict\"\n test_start = time.perf_counter()\n test_result = _run_tests(tid, fast_fail=fast_fail)\n task_metrics[\"tests_seconds\"] = round(time.perf_counter() - test_start, 3)\n task_metrics[\"tests_passed\"] = test_result[\"passed\"]\n print(test_result[\"summary\"])\n\n # Commit only if tests pass\n if test_result[\"passed\"]:\n commit_msg = f\"feat: {task_info['name']} [{tid}]\"\n _git_commit(commit_msg)\n print(f\" ✅ {tid} committed\")\n task_metrics[\"committed\"] = True\n else:\n print(f\" ❌ {tid} tests failed. Stopping execution.\")\n for name, detail in test_result[\"details\"].items():\n if not detail[\"passed\"]:\n print(f\"\\n {name} output:\\n{detail['output'][:500]}\")\n task_metrics[\"committed\"] = False\n telemetry[\"tasks\"].append(task_metrics)\n telemetry[\"phase_seconds\"][\"execute_and_test\"] = round(time.perf_counter() - exec_phase_start, 3)\n telemetry[\"summary\"] = {\"status\": \"failed_tests\", \"failed_task\": tid}\n telemetry[\"total_seconds\"] = round(time.perf_counter() - run_started, 3)\n _write_telemetry(feature, telemetry)\n return\n\n telemetry[\"tasks\"].append(task_metrics)\n\n telemetry[\"phase_seconds\"][\"execute_and_test\"] = round(time.perf_counter() - exec_phase_start, 3)\n\n # ------------------------------------------------------------------\n # STEP 6: Review\n # ------------------------------------------------------------------\n review_results = []\n if skip_review:\n print(\"\\n🔎 PHASE 5: Review skipped\")\n telemetry[\"phase_seconds\"][\"review\"] = 0.0\n else:\n print(\"\\n🔎 PHASE 5: Reviewing implementation…\")\n review_start = time.perf_counter()\n all_committed = _get_committed_tasks()\n newly_committed = all_committed - committed_tasks\n\n review_fn = _review_task_crewai if executor_backend == \"crewai\" else _review_task_anthropic\n\n for task_info in tasks:\n tid = task_info[\"id\"]\n if tid not in newly_committed:\n continue\n try:\n review_text = review_fn(task_info, plan_content, context_content)\n review_results.append((tid, review_text))\n except Exception as e:\n review_results.append((tid, f\"Review error: {e}\"))\n\n telemetry[\"phase_seconds\"][\"review\"] = round(time.perf_counter() - review_start, 3)\n\n # ------------------------------------------------------------------\n # STEP 7: Summary\n # ------------------------------------------------------------------\n print(\"\\n\" + \"=\" * 60)\n print(\"✨ SWPP-SPECKIT EXECUTION COMPLETE\")\n print(\"=\" * 60)\n print(f\"\\nFeature: {feature}\")\n print(f\"Tasks completed: {len([t for t in telemetry['tasks'] if t.get('committed')])}/{len(tasks)}\")\n print(f\"High-risk tasks: {len(high_risk_tasks)}\")\n\n if review_results:\n print(\"\\nReview Summary:\")\n for tid, result in review_results:\n status = \"✅ PASS\" if \"PASS\" in result.upper() else \"⚠️ NEEDS ATTENTION\"\n print(f\" {tid}: {status}\")\n\n telemetry[\"summary\"] = {\n \"status\": \"completed\",\n \"tasks_total\": len(tasks),\n \"tasks_committed\": len([t for t in telemetry[\"tasks\"] if t.get(\"committed\")]),\n \"high_risk_task_count\": len(high_risk_tasks),\n \"reviews_completed\": len(review_results),\n }\n telemetry[\"total_seconds\"] = round(time.perf_counter() - run_started, 3)\n metrics_path = _write_telemetry(feature, telemetry)\n\n print(\"\\n📁 Artefacts:\")\n if spec_path:\n print(f\" Spec: {spec_path.relative_to(PROJECT_ROOT)}\")\n if plan_path:\n print(f\" Plan: {plan_path.relative_to(PROJECT_ROOT)}\")\n print(f\" Tasks: {tasks_md.relative_to(PROJECT_ROOT)}\")\n print(f\" SWPP plans: .swpp/plans/\")\n print(f\" Commits: git log --oneline -n {len(tasks)}\")\n print(f\" Telemetry: {metrics_path}\")\n\n\n# =============================================================================\n# CLI\n# =============================================================================\n\nif __name__ == \"__main__\":\n parser = argparse.ArgumentParser(\n description=\"SWPP with SpecKit planning + SWPP agent execution\",\n epilog=\"Planning uses SpecKit (via claude -p). Execution uses SWPP AI agents.\",\n )\n parser.add_argument(\"feature\", nargs=\"+\", help=\"Feature description\")\n\n mode_group = parser.add_mutually_exclusive_group()\n mode_group.add_argument(\n \"--fast\", action=\"store_true\",\n help=\"Lean planning, auto-approve, skip review\",\n )\n mode_group.add_argument(\n \"--balanced\", action=\"store_true\",\n help=\"Lean planning, auto-approve, keep review\",\n )\n\n parser.add_argument(\"--auto-approve\", action=\"store_true\", help=\"Skip checkpoint prompt\")\n parser.add_argument(\"--force-auto-approve\", action=\"store_true\", help=\"Auto-approve even for high-risk tasks\")\n parser.add_argument(\"--skip-review\", action=\"store_true\", help=\"Skip review phase\")\n parser.add_argument(\n \"--executor\", choices=[\"anthropic\", \"crewai\"], default=DEFAULT_EXECUTOR_BACKEND,\n help=f\"Executor backend (default: {DEFAULT_EXECUTOR_BACKEND})\",\n )\n\n # SpecKit optional phase toggles\n parser.add_argument(\"--skip-constitution\", action=\"store_true\", help=\"Skip /speckit.constitution phase\")\n parser.add_argument(\"--skip-clarify\", action=\"store_true\", help=\"Skip /speckit.clarify phase\")\n parser.add_argument(\"--skip-checklist\", action=\"store_true\", help=\"Skip /speckit.checklist phase\")\n parser.add_argument(\"--skip-analyze\", action=\"store_true\", help=\"Skip /speckit.analyze phase\")\n\n args = parser.parse_args()\n feature = \" \".join(args.feature)\n\n fast_mode = args.fast or args.balanced\n mode_label = \"FAST\" if args.fast else \"BALANCED\" if args.balanced else \"STANDARD\"\n auto_approve = args.auto_approve or fast_mode\n skip_review = args.skip_review or args.fast\n\n # In fast mode, skip optional enhancement phases by default\n skip_constitution = args.skip_constitution or args.fast\n skip_clarify = args.skip_clarify or args.fast\n skip_checklist = args.skip_checklist or args.fast\n skip_analyze = args.skip_analyze or args.fast\n\n run_swpp_speckit(\n feature,\n fast_mode=fast_mode,\n auto_approve=auto_approve,\n skip_review=skip_review,\n force_auto_approve=args.force_auto_approve,\n executor_backend=args.executor,\n mode_label=mode_label,\n skip_constitution=skip_constitution,\n skip_clarify=skip_clarify,\n skip_checklist=skip_checklist,\n skip_analyze=skip_analyze,\n )\n"}}}} |