From d5bd93248ae05c31ad2ad012983c0f87956dc417 Mon Sep 17 00:00:00 2001 From: KhawarHabibKhan Date: Fri, 13 Mar 2026 17:42:46 +0500 Subject: [PATCH 1/9] Add specify doctor command for project health diagnostics --- src/specify_cli/__init__.py | 303 ++++++++++++++++++++++++++++++++++++ 1 file changed, 303 insertions(+) diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index dac7eaa54..c8c8103c4 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -1695,6 +1695,309 @@ def check(): if not any(agent_results.values()): console.print("[dim]Tip: Install an AI assistant for the best experience[/dim]") + +@app.command() +def doctor(): + """Diagnose a Specify project and report health issues.""" + show_banner() + console.print("[bold]Running project diagnostics...[/bold]\n") + + project_root = Path.cwd() + issues = [] # (severity, message) tuples: "error", "warning", "info" + + # ── 1. Project structure ────────────────────────────────────────── + tracker = StepTracker("Project Structure") + + specify_dir = project_root / ".specify" + tracker.add("specify_dir", ".specify/ directory") + if specify_dir.is_dir(): + tracker.complete("specify_dir", "found") + else: + tracker.error("specify_dir", "missing") + issues.append(("error", "No .specify/ directory — run 'specify init --here' to initialize")) + + specs_dir = project_root / "specs" + tracker.add("specs_dir", "specs/ directory") + if specs_dir.is_dir(): + tracker.complete("specs_dir", "found") + else: + tracker.skip("specs_dir", "not created yet") + issues.append(("info", "No specs/ directory — created when you run /speckit.specify")) + + scripts_dir = project_root / "scripts" + tracker.add("scripts_dir", "scripts/ directory") + if scripts_dir.is_dir(): + tracker.complete("scripts_dir", "found") + else: + tracker.error("scripts_dir", "missing") + issues.append(("error", "No scripts/ directory — project may not be initialized")) + + templates_dir = project_root / "templates" + tracker.add("templates_dir", "templates/ directory") + if templates_dir.is_dir(): + tracker.complete("templates_dir", "found") + else: + tracker.error("templates_dir", "missing") + issues.append(("error", "No templates/ directory — project may not be initialized")) + + memory_dir = project_root / "memory" + tracker.add("memory_dir", "memory/ directory") + if memory_dir.is_dir(): + tracker.complete("memory_dir", "found") + constitution = memory_dir / "constitution.md" + tracker.add("constitution", "memory/constitution.md") + if constitution.is_file(): + tracker.complete("constitution", "found") + else: + tracker.error("constitution", "missing") + issues.append(("warning", "No constitution.md in memory/ — project governance rules are missing")) + else: + tracker.error("memory_dir", "missing") + issues.append(("error", "No memory/ directory — project may not be initialized")) + + console.print(tracker.render()) + console.print() + + # ── 2. AI agent detection ───────────────────────────────────────── + agent_tracker = StepTracker("AI Agent Configuration") + detected_agents = [] + + for agent_key, agent_config in AGENT_CONFIG.items(): + if agent_key == "generic": + continue + agent_folder = agent_config["folder"] + if agent_folder and (project_root / agent_folder).is_dir(): + detected_agents.append(agent_key) + agent_tracker.add(agent_key, agent_config["name"]) + commands_dir = project_root / agent_folder / agent_config["commands_subdir"] + if commands_dir.is_dir() and any(commands_dir.iterdir()): + agent_tracker.complete(agent_key, f"commands in {agent_folder}{agent_config['commands_subdir']}/") + else: + agent_tracker.error(agent_key, f"folder exists but no commands in {agent_config['commands_subdir']}/") + issues.append(("warning", f"Agent '{agent_config['name']}' folder exists but commands directory is empty")) + + if not detected_agents: + agent_tracker.add("none", "No AI agent configured") + agent_tracker.skip("none", "run 'specify init --here --ai ' to set up") + issues.append(("info", "No AI agent folder detected — this is fine if you use IDE-based agents")) + + console.print(agent_tracker.render()) + console.print() + + # ── 3. Feature specs ────────────────────────────────────────────── + feature_tracker = StepTracker("Feature Specifications") + + if specs_dir.is_dir(): + feature_dirs = sorted( + [d for d in specs_dir.iterdir() if d.is_dir()], + key=lambda d: d.name, + ) + if not feature_dirs: + feature_tracker.add("empty", "No feature directories") + feature_tracker.skip("empty", "run /speckit.specify to create one") + else: + for fdir in feature_dirs: + key = fdir.name + feature_tracker.add(key, key) + + spec_file = fdir / "spec.md" + plan_file = fdir / "plan.md" + tasks_file = fdir / "tasks.md" + + artifacts = [] + missing = [] + for name, path in [("spec", spec_file), ("plan", plan_file), ("tasks", tasks_file)]: + if path.is_file(): + artifacts.append(name) + else: + missing.append(name) + + if missing: + detail = f"{', '.join(artifacts)} present; missing {', '.join(missing)}" + if "spec" in missing: + feature_tracker.error(key, detail) + issues.append(("error", f"Feature '{key}' is missing spec.md")) + else: + feature_tracker.complete(key, detail) + for m in missing: + issues.append(("info", f"Feature '{key}' has no {m}.md — run /speckit.{m} to generate")) + else: + feature_tracker.complete(key, "spec, plan, tasks all present") + else: + feature_tracker.add("none", "No specs/ directory") + feature_tracker.skip("none", "not applicable") + + console.print(feature_tracker.render()) + console.print() + + # ── 4. Scripts health ───────────────────────────────────────────── + script_tracker = StepTracker("Scripts") + + bash_dir = project_root / "scripts" / "bash" + ps_dir = project_root / "scripts" / "powershell" + + expected_scripts = ["common", "check-prerequisites", "create-new-feature", "setup-plan", "update-agent-context"] + + if bash_dir.is_dir(): + for name in expected_scripts: + key = f"sh_{name}" + script_path = bash_dir / f"{name}.sh" + script_tracker.add(key, f"bash/{name}.sh") + if script_path.is_file(): + if os.name != "nt" and not os.access(script_path, os.X_OK): + script_tracker.error(key, "not executable") + issues.append(("warning", f"scripts/bash/{name}.sh is not executable — run chmod +x")) + else: + script_tracker.complete(key, "ok") + else: + script_tracker.error(key, "missing") + issues.append(("error", f"scripts/bash/{name}.sh is missing")) + else: + script_tracker.add("no_bash", "scripts/bash/") + script_tracker.skip("no_bash", "not found") + + if ps_dir.is_dir(): + for name in expected_scripts: + key = f"ps_{name}" + script_path = ps_dir / f"{name}.ps1" + script_tracker.add(key, f"powershell/{name}.ps1") + if script_path.is_file(): + script_tracker.complete(key, "ok") + else: + script_tracker.error(key, "missing") + issues.append(("error", f"scripts/powershell/{name}.ps1 is missing")) + else: + script_tracker.add("no_ps", "scripts/powershell/") + script_tracker.skip("no_ps", "not found") + + console.print(script_tracker.render()) + console.print() + + # ── 5. Extensions health ────────────────────────────────────────── + ext_tracker = StepTracker("Extensions") + + extensions_yml = specify_dir / "extensions.yml" if specify_dir.is_dir() else None + registry_json = specify_dir / "extensions" / "registry.json" if specify_dir.is_dir() else None + + if extensions_yml and extensions_yml.is_file(): + ext_tracker.add("config", "extensions.yml") + try: + with open(extensions_yml) as f: + ext_config = yaml.safe_load(f) + if ext_config and isinstance(ext_config, dict): + ext_tracker.complete("config", "valid YAML") + hooks = ext_config.get("hooks", {}) + if hooks: + hook_count = sum(len(v) if isinstance(v, list) else 0 for v in hooks.values()) + ext_tracker.add("hooks", "Hook registrations") + ext_tracker.complete("hooks", f"{hook_count} hook(s) registered") + else: + ext_tracker.complete("config", "empty or no hooks") + except Exception as e: + ext_tracker.error("config", f"invalid YAML: {e}") + issues.append(("warning", f"extensions.yml has invalid YAML: {e}")) + else: + ext_tracker.add("config", "extensions.yml") + ext_tracker.skip("config", "no extensions configured") + + if registry_json and registry_json.is_file(): + ext_tracker.add("registry", "Extension registry") + try: + with open(registry_json) as f: + registry = json.load(f) + installed = [k for k, v in registry.items() if isinstance(v, dict)] + enabled = [k for k, v in registry.items() if isinstance(v, dict) and v.get("enabled", True)] + ext_tracker.complete("registry", f"{len(installed)} installed, {len(enabled)} enabled") + except Exception as e: + ext_tracker.error("registry", f"corrupt: {e}") + issues.append(("error", f"Extension registry is corrupt: {e}")) + else: + ext_tracker.add("registry", "Extension registry") + ext_tracker.skip("registry", "no extensions installed") + + console.print(ext_tracker.render()) + console.print() + + # ── 6. Git status ───────────────────────────────────────────────── + git_tracker = StepTracker("Git Repository") + git_tracker.add("git", "Git repository") + + git_ok = shutil.which("git") is not None + in_git_repo = False + if git_ok: + try: + result = subprocess.run( + ["git", "rev-parse", "--is-inside-work-tree"], + capture_output=True, text=True, cwd=str(project_root) + ) + in_git_repo = result.returncode == 0 + except Exception: + pass + + if in_git_repo: + git_tracker.complete("git", "inside git repository") + try: + branch = subprocess.run( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + capture_output=True, text=True, cwd=str(project_root) + ).stdout.strip() + git_tracker.add("branch", "Current branch") + git_tracker.complete("branch", branch) + except Exception: + pass + elif git_ok: + git_tracker.skip("git", "not a git repository") + issues.append(("info", "Not inside a git repository — git features like branching won't work")) + else: + git_tracker.skip("git", "git not installed") + issues.append(("info", "Git is not installed — branching and version control unavailable")) + + console.print(git_tracker.render()) + console.print() + + # ── Summary ─────────────────────────────────────────────────────── + errors = [msg for sev, msg in issues if sev == "error"] + warnings = [msg for sev, msg in issues if sev == "warning"] + infos = [msg for sev, msg in issues if sev == "info"] + + if not issues: + console.print(Panel( + "[bold green]All checks passed — project looks healthy![/bold green]", + border_style="green", + padding=(1, 2), + )) + else: + summary_lines = [] + + if errors: + summary_lines.append(f"[bold red]{len(errors)} error(s)[/bold red]") + for msg in errors: + summary_lines.append(f" [red]●[/red] {msg}") + summary_lines.append("") + + if warnings: + summary_lines.append(f"[bold yellow]{len(warnings)} warning(s)[/bold yellow]") + for msg in warnings: + summary_lines.append(f" [yellow]●[/yellow] {msg}") + summary_lines.append("") + + if infos: + summary_lines.append(f"[bold blue]{len(infos)} note(s)[/bold blue]") + for msg in infos: + summary_lines.append(f" [blue]○[/blue] {msg}") + + border = "red" if errors else "yellow" if warnings else "blue" + console.print(Panel( + "\n".join(summary_lines), + title="Diagnostic Summary", + border_style=border, + padding=(1, 2), + )) + + if errors: + raise typer.Exit(1) + + @app.command() def version(): """Display version and system information.""" From 65e12fb62b7f3611a6598ec41a59c8bf681fe607 Mon Sep 17 00:00:00 2001 From: KhawarHabibKhan Date: Fri, 13 Mar 2026 17:43:52 +0500 Subject: [PATCH 2/9] Add tests for specify doctor command --- tests/test_doctor.py | 259 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 tests/test_doctor.py diff --git a/tests/test_doctor.py b/tests/test_doctor.py new file mode 100644 index 000000000..cc151e2a9 --- /dev/null +++ b/tests/test_doctor.py @@ -0,0 +1,259 @@ +""" +Unit tests for the specify doctor command. + +Tests cover: +- Healthy project detection (all checks pass) +- Missing project directories (.specify/, specs/, scripts/, templates/, memory/) +- Missing constitution.md +- AI agent folder detection and empty commands directory +- Feature specification completeness (spec.md, plan.md, tasks.md) +- Script existence validation (bash and powershell) +- Extension config validation (extensions.yml, registry.json) +- Git repository detection +- Exit code 1 on errors, 0 on clean +""" + +import json +import os +import tempfile +import shutil +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from specify_cli import app, AGENT_CONFIG + + +runner = CliRunner() + + +# ===== Fixtures ===== + +@pytest.fixture +def temp_project(): + """Create a temporary directory simulating a spec-kit project.""" + tmpdir = tempfile.mkdtemp() + project = Path(tmpdir) + yield project + shutil.rmtree(tmpdir, ignore_errors=True) + + +@pytest.fixture +def healthy_project(temp_project): + """Create a fully healthy spec-kit project structure.""" + # Core directories + (temp_project / ".specify").mkdir() + (temp_project / "specs").mkdir() + (temp_project / "scripts" / "bash").mkdir(parents=True) + (temp_project / "scripts" / "powershell").mkdir(parents=True) + (temp_project / "templates").mkdir() + (temp_project / "memory").mkdir() + + # Constitution + (temp_project / "memory" / "constitution.md").write_text("# Constitution\n") + + # Scripts + expected_scripts = ["common", "check-prerequisites", "create-new-feature", "setup-plan", "update-agent-context"] + for name in expected_scripts: + (temp_project / "scripts" / "bash" / f"{name}.sh").write_text("#!/bin/bash\n") + (temp_project / "scripts" / "powershell" / f"{name}.ps1").write_text("# PowerShell\n") + + return temp_project + + +# ===== Project Structure Tests ===== + +class TestDoctorProjectStructure: + """Tests for project directory checks.""" + + def test_healthy_project_no_errors(self, healthy_project): + """A fully set up project should report no errors.""" + os.chdir(healthy_project) + result = runner.invoke(app, ["doctor"]) + assert result.exit_code == 0 + assert "error" not in result.output.lower() or "0 error" in result.output.lower() + + def test_missing_specify_dir(self, temp_project): + """Missing .specify/ should be reported as an error.""" + os.chdir(temp_project) + result = runner.invoke(app, ["doctor"]) + assert result.exit_code == 1 + assert "specify" in result.output.lower() + + def test_missing_scripts_dir(self, healthy_project): + """Missing scripts/ should be reported as an error.""" + shutil.rmtree(healthy_project / "scripts") + os.chdir(healthy_project) + result = runner.invoke(app, ["doctor"]) + assert "scripts" in result.output.lower() + + def test_missing_templates_dir(self, healthy_project): + """Missing templates/ should be reported as an error.""" + shutil.rmtree(healthy_project / "templates") + os.chdir(healthy_project) + result = runner.invoke(app, ["doctor"]) + assert "templates" in result.output.lower() + + def test_missing_memory_dir(self, healthy_project): + """Missing memory/ should be reported as an error.""" + shutil.rmtree(healthy_project / "memory") + os.chdir(healthy_project) + result = runner.invoke(app, ["doctor"]) + assert result.exit_code == 1 + + def test_missing_constitution(self, healthy_project): + """Missing constitution.md should be reported as a warning.""" + (healthy_project / "memory" / "constitution.md").unlink() + os.chdir(healthy_project) + result = runner.invoke(app, ["doctor"]) + assert "constitution" in result.output.lower() + + +# ===== AI Agent Tests ===== + +class TestDoctorAgentDetection: + """Tests for AI agent folder detection.""" + + def test_no_agent_detected(self, healthy_project): + """No agent folder should produce an info note.""" + os.chdir(healthy_project) + result = runner.invoke(app, ["doctor"]) + assert "No AI agent" in result.output or "no ai agent" in result.output.lower() + + def test_agent_with_commands(self, healthy_project): + """Agent folder with commands should report as healthy.""" + commands_dir = healthy_project / ".claude" / "commands" + commands_dir.mkdir(parents=True) + (commands_dir / "test.md").write_text("# Test command\n") + os.chdir(healthy_project) + result = runner.invoke(app, ["doctor"]) + assert "Claude Code" in result.output + + def test_agent_folder_empty_commands(self, healthy_project): + """Agent folder without commands should report a warning.""" + (healthy_project / ".claude" / "commands").mkdir(parents=True) + os.chdir(healthy_project) + result = runner.invoke(app, ["doctor"]) + assert "warning" in result.output.lower() or "empty" in result.output.lower() + + +# ===== Feature Specs Tests ===== + +class TestDoctorFeatureSpecs: + """Tests for feature specification checks.""" + + def test_no_specs_dir(self, healthy_project): + """No specs/ directory should skip feature checks gracefully.""" + shutil.rmtree(healthy_project / "specs") + os.chdir(healthy_project) + result = runner.invoke(app, ["doctor"]) + assert "not created yet" in result.output.lower() or "specs" in result.output.lower() + + def test_feature_with_all_artifacts(self, healthy_project): + """Feature with spec, plan, and tasks should be fully green.""" + feature_dir = healthy_project / "specs" / "001-login" + feature_dir.mkdir(parents=True) + (feature_dir / "spec.md").write_text("# Spec\n") + (feature_dir / "plan.md").write_text("# Plan\n") + (feature_dir / "tasks.md").write_text("# Tasks\n") + os.chdir(healthy_project) + result = runner.invoke(app, ["doctor"]) + assert "001-login" in result.output + assert "spec, plan, tasks all present" in result.output + + def test_feature_missing_tasks(self, healthy_project): + """Feature missing tasks.md should report an info note.""" + feature_dir = healthy_project / "specs" / "002-signup" + feature_dir.mkdir(parents=True) + (feature_dir / "spec.md").write_text("# Spec\n") + (feature_dir / "plan.md").write_text("# Plan\n") + os.chdir(healthy_project) + result = runner.invoke(app, ["doctor"]) + assert "002-signup" in result.output + assert "tasks" in result.output.lower() + + def test_feature_missing_spec(self, healthy_project): + """Feature missing spec.md should report an error.""" + feature_dir = healthy_project / "specs" / "003-broken" + feature_dir.mkdir(parents=True) + (feature_dir / "plan.md").write_text("# Plan\n") + os.chdir(healthy_project) + result = runner.invoke(app, ["doctor"]) + assert result.exit_code == 1 + + +# ===== Scripts Tests ===== + +class TestDoctorScripts: + """Tests for script health checks.""" + + def test_all_scripts_present(self, healthy_project): + """All scripts present should report ok.""" + os.chdir(healthy_project) + result = runner.invoke(app, ["doctor"]) + assert result.exit_code == 0 + + def test_missing_bash_script(self, healthy_project): + """Missing a bash script should report an error.""" + (healthy_project / "scripts" / "bash" / "common.sh").unlink() + os.chdir(healthy_project) + result = runner.invoke(app, ["doctor"]) + assert result.exit_code == 1 + assert "common.sh" in result.output + + def test_missing_powershell_script(self, healthy_project): + """Missing a PowerShell script should report an error.""" + (healthy_project / "scripts" / "powershell" / "setup-plan.ps1").unlink() + os.chdir(healthy_project) + result = runner.invoke(app, ["doctor"]) + assert result.exit_code == 1 + assert "setup-plan.ps1" in result.output + + +# ===== Extensions Tests ===== + +class TestDoctorExtensions: + """Tests for extension health checks.""" + + def test_no_extensions(self, healthy_project): + """No extensions configured should skip gracefully.""" + os.chdir(healthy_project) + result = runner.invoke(app, ["doctor"]) + assert "no extensions" in result.output.lower() + + def test_valid_extensions_yml(self, healthy_project): + """Valid extensions.yml should report as healthy.""" + ext_yml = healthy_project / ".specify" / "extensions.yml" + ext_yml.write_text("hooks:\n before_implement:\n - extension: test\n enabled: true\n") + os.chdir(healthy_project) + result = runner.invoke(app, ["doctor"]) + assert "valid YAML" in result.output or "hook" in result.output.lower() + + def test_invalid_extensions_yml(self, healthy_project): + """Invalid YAML in extensions.yml should report a warning.""" + ext_yml = healthy_project / ".specify" / "extensions.yml" + ext_yml.write_text(": : : invalid yaml [[[") + os.chdir(healthy_project) + result = runner.invoke(app, ["doctor"]) + assert "invalid" in result.output.lower() or "warning" in result.output.lower() + + def test_valid_registry(self, healthy_project): + """Valid registry.json should report installed/enabled counts.""" + reg_dir = healthy_project / ".specify" / "extensions" + reg_dir.mkdir(parents=True) + registry = {"test-ext": {"enabled": True}, "other-ext": {"enabled": False}} + (reg_dir / "registry.json").write_text(json.dumps(registry)) + os.chdir(healthy_project) + result = runner.invoke(app, ["doctor"]) + assert "2 installed" in result.output + assert "1 enabled" in result.output + + def test_corrupt_registry(self, healthy_project): + """Corrupt registry.json should report an error.""" + reg_dir = healthy_project / ".specify" / "extensions" + reg_dir.mkdir(parents=True) + (reg_dir / "registry.json").write_text("not json at all {{{") + os.chdir(healthy_project) + result = runner.invoke(app, ["doctor"]) + assert result.exit_code == 1 From c1cfd061293ac5c82acb11d8dcbd07d993ce6b48 Mon Sep 17 00:00:00 2001 From: KhawarHabibKhan Date: Fri, 13 Mar 2026 17:47:31 +0500 Subject: [PATCH 3/9] Document specify doctor command in README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 7bda2b9ce..7458940dd 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,9 @@ specify init --here --ai claude # Check installed tools specify check + +# Diagnose project health +specify doctor ``` To upgrade Specify, see the [Upgrade Guide](./docs/upgrade.md) for detailed instructions. Quick upgrade: @@ -283,6 +286,9 @@ specify init --here --ai gemini --ai-skills # Check system requirements specify check + +# Diagnose project health +specify doctor ``` ### Available Slash Commands From 3f5affd56417614100d4bbe199242c79b74a7060 Mon Sep 17 00:00:00 2001 From: KhawarHabibKhan Date: Fri, 13 Mar 2026 19:10:41 +0500 Subject: [PATCH 4/9] Revert "Document specify doctor command in README" This reverts commit c1cfd061293ac5c82acb11d8dcbd07d993ce6b48. --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 7458940dd..7bda2b9ce 100644 --- a/README.md +++ b/README.md @@ -67,9 +67,6 @@ specify init --here --ai claude # Check installed tools specify check - -# Diagnose project health -specify doctor ``` To upgrade Specify, see the [Upgrade Guide](./docs/upgrade.md) for detailed instructions. Quick upgrade: @@ -286,9 +283,6 @@ specify init --here --ai gemini --ai-skills # Check system requirements specify check - -# Diagnose project health -specify doctor ``` ### Available Slash Commands From 206e0ba39457fdf5d25227b06d0082f70470b664 Mon Sep 17 00:00:00 2001 From: KhawarHabibKhan Date: Fri, 13 Mar 2026 19:11:26 +0500 Subject: [PATCH 5/9] Revert "Add tests for specify doctor command" This reverts commit 65e12fb62b7f3611a6598ec41a59c8bf681fe607. --- tests/test_doctor.py | 259 ------------------------------------------- 1 file changed, 259 deletions(-) delete mode 100644 tests/test_doctor.py diff --git a/tests/test_doctor.py b/tests/test_doctor.py deleted file mode 100644 index cc151e2a9..000000000 --- a/tests/test_doctor.py +++ /dev/null @@ -1,259 +0,0 @@ -""" -Unit tests for the specify doctor command. - -Tests cover: -- Healthy project detection (all checks pass) -- Missing project directories (.specify/, specs/, scripts/, templates/, memory/) -- Missing constitution.md -- AI agent folder detection and empty commands directory -- Feature specification completeness (spec.md, plan.md, tasks.md) -- Script existence validation (bash and powershell) -- Extension config validation (extensions.yml, registry.json) -- Git repository detection -- Exit code 1 on errors, 0 on clean -""" - -import json -import os -import tempfile -import shutil -from pathlib import Path - -import pytest -from typer.testing import CliRunner - -from specify_cli import app, AGENT_CONFIG - - -runner = CliRunner() - - -# ===== Fixtures ===== - -@pytest.fixture -def temp_project(): - """Create a temporary directory simulating a spec-kit project.""" - tmpdir = tempfile.mkdtemp() - project = Path(tmpdir) - yield project - shutil.rmtree(tmpdir, ignore_errors=True) - - -@pytest.fixture -def healthy_project(temp_project): - """Create a fully healthy spec-kit project structure.""" - # Core directories - (temp_project / ".specify").mkdir() - (temp_project / "specs").mkdir() - (temp_project / "scripts" / "bash").mkdir(parents=True) - (temp_project / "scripts" / "powershell").mkdir(parents=True) - (temp_project / "templates").mkdir() - (temp_project / "memory").mkdir() - - # Constitution - (temp_project / "memory" / "constitution.md").write_text("# Constitution\n") - - # Scripts - expected_scripts = ["common", "check-prerequisites", "create-new-feature", "setup-plan", "update-agent-context"] - for name in expected_scripts: - (temp_project / "scripts" / "bash" / f"{name}.sh").write_text("#!/bin/bash\n") - (temp_project / "scripts" / "powershell" / f"{name}.ps1").write_text("# PowerShell\n") - - return temp_project - - -# ===== Project Structure Tests ===== - -class TestDoctorProjectStructure: - """Tests for project directory checks.""" - - def test_healthy_project_no_errors(self, healthy_project): - """A fully set up project should report no errors.""" - os.chdir(healthy_project) - result = runner.invoke(app, ["doctor"]) - assert result.exit_code == 0 - assert "error" not in result.output.lower() or "0 error" in result.output.lower() - - def test_missing_specify_dir(self, temp_project): - """Missing .specify/ should be reported as an error.""" - os.chdir(temp_project) - result = runner.invoke(app, ["doctor"]) - assert result.exit_code == 1 - assert "specify" in result.output.lower() - - def test_missing_scripts_dir(self, healthy_project): - """Missing scripts/ should be reported as an error.""" - shutil.rmtree(healthy_project / "scripts") - os.chdir(healthy_project) - result = runner.invoke(app, ["doctor"]) - assert "scripts" in result.output.lower() - - def test_missing_templates_dir(self, healthy_project): - """Missing templates/ should be reported as an error.""" - shutil.rmtree(healthy_project / "templates") - os.chdir(healthy_project) - result = runner.invoke(app, ["doctor"]) - assert "templates" in result.output.lower() - - def test_missing_memory_dir(self, healthy_project): - """Missing memory/ should be reported as an error.""" - shutil.rmtree(healthy_project / "memory") - os.chdir(healthy_project) - result = runner.invoke(app, ["doctor"]) - assert result.exit_code == 1 - - def test_missing_constitution(self, healthy_project): - """Missing constitution.md should be reported as a warning.""" - (healthy_project / "memory" / "constitution.md").unlink() - os.chdir(healthy_project) - result = runner.invoke(app, ["doctor"]) - assert "constitution" in result.output.lower() - - -# ===== AI Agent Tests ===== - -class TestDoctorAgentDetection: - """Tests for AI agent folder detection.""" - - def test_no_agent_detected(self, healthy_project): - """No agent folder should produce an info note.""" - os.chdir(healthy_project) - result = runner.invoke(app, ["doctor"]) - assert "No AI agent" in result.output or "no ai agent" in result.output.lower() - - def test_agent_with_commands(self, healthy_project): - """Agent folder with commands should report as healthy.""" - commands_dir = healthy_project / ".claude" / "commands" - commands_dir.mkdir(parents=True) - (commands_dir / "test.md").write_text("# Test command\n") - os.chdir(healthy_project) - result = runner.invoke(app, ["doctor"]) - assert "Claude Code" in result.output - - def test_agent_folder_empty_commands(self, healthy_project): - """Agent folder without commands should report a warning.""" - (healthy_project / ".claude" / "commands").mkdir(parents=True) - os.chdir(healthy_project) - result = runner.invoke(app, ["doctor"]) - assert "warning" in result.output.lower() or "empty" in result.output.lower() - - -# ===== Feature Specs Tests ===== - -class TestDoctorFeatureSpecs: - """Tests for feature specification checks.""" - - def test_no_specs_dir(self, healthy_project): - """No specs/ directory should skip feature checks gracefully.""" - shutil.rmtree(healthy_project / "specs") - os.chdir(healthy_project) - result = runner.invoke(app, ["doctor"]) - assert "not created yet" in result.output.lower() or "specs" in result.output.lower() - - def test_feature_with_all_artifacts(self, healthy_project): - """Feature with spec, plan, and tasks should be fully green.""" - feature_dir = healthy_project / "specs" / "001-login" - feature_dir.mkdir(parents=True) - (feature_dir / "spec.md").write_text("# Spec\n") - (feature_dir / "plan.md").write_text("# Plan\n") - (feature_dir / "tasks.md").write_text("# Tasks\n") - os.chdir(healthy_project) - result = runner.invoke(app, ["doctor"]) - assert "001-login" in result.output - assert "spec, plan, tasks all present" in result.output - - def test_feature_missing_tasks(self, healthy_project): - """Feature missing tasks.md should report an info note.""" - feature_dir = healthy_project / "specs" / "002-signup" - feature_dir.mkdir(parents=True) - (feature_dir / "spec.md").write_text("# Spec\n") - (feature_dir / "plan.md").write_text("# Plan\n") - os.chdir(healthy_project) - result = runner.invoke(app, ["doctor"]) - assert "002-signup" in result.output - assert "tasks" in result.output.lower() - - def test_feature_missing_spec(self, healthy_project): - """Feature missing spec.md should report an error.""" - feature_dir = healthy_project / "specs" / "003-broken" - feature_dir.mkdir(parents=True) - (feature_dir / "plan.md").write_text("# Plan\n") - os.chdir(healthy_project) - result = runner.invoke(app, ["doctor"]) - assert result.exit_code == 1 - - -# ===== Scripts Tests ===== - -class TestDoctorScripts: - """Tests for script health checks.""" - - def test_all_scripts_present(self, healthy_project): - """All scripts present should report ok.""" - os.chdir(healthy_project) - result = runner.invoke(app, ["doctor"]) - assert result.exit_code == 0 - - def test_missing_bash_script(self, healthy_project): - """Missing a bash script should report an error.""" - (healthy_project / "scripts" / "bash" / "common.sh").unlink() - os.chdir(healthy_project) - result = runner.invoke(app, ["doctor"]) - assert result.exit_code == 1 - assert "common.sh" in result.output - - def test_missing_powershell_script(self, healthy_project): - """Missing a PowerShell script should report an error.""" - (healthy_project / "scripts" / "powershell" / "setup-plan.ps1").unlink() - os.chdir(healthy_project) - result = runner.invoke(app, ["doctor"]) - assert result.exit_code == 1 - assert "setup-plan.ps1" in result.output - - -# ===== Extensions Tests ===== - -class TestDoctorExtensions: - """Tests for extension health checks.""" - - def test_no_extensions(self, healthy_project): - """No extensions configured should skip gracefully.""" - os.chdir(healthy_project) - result = runner.invoke(app, ["doctor"]) - assert "no extensions" in result.output.lower() - - def test_valid_extensions_yml(self, healthy_project): - """Valid extensions.yml should report as healthy.""" - ext_yml = healthy_project / ".specify" / "extensions.yml" - ext_yml.write_text("hooks:\n before_implement:\n - extension: test\n enabled: true\n") - os.chdir(healthy_project) - result = runner.invoke(app, ["doctor"]) - assert "valid YAML" in result.output or "hook" in result.output.lower() - - def test_invalid_extensions_yml(self, healthy_project): - """Invalid YAML in extensions.yml should report a warning.""" - ext_yml = healthy_project / ".specify" / "extensions.yml" - ext_yml.write_text(": : : invalid yaml [[[") - os.chdir(healthy_project) - result = runner.invoke(app, ["doctor"]) - assert "invalid" in result.output.lower() or "warning" in result.output.lower() - - def test_valid_registry(self, healthy_project): - """Valid registry.json should report installed/enabled counts.""" - reg_dir = healthy_project / ".specify" / "extensions" - reg_dir.mkdir(parents=True) - registry = {"test-ext": {"enabled": True}, "other-ext": {"enabled": False}} - (reg_dir / "registry.json").write_text(json.dumps(registry)) - os.chdir(healthy_project) - result = runner.invoke(app, ["doctor"]) - assert "2 installed" in result.output - assert "1 enabled" in result.output - - def test_corrupt_registry(self, healthy_project): - """Corrupt registry.json should report an error.""" - reg_dir = healthy_project / ".specify" / "extensions" - reg_dir.mkdir(parents=True) - (reg_dir / "registry.json").write_text("not json at all {{{") - os.chdir(healthy_project) - result = runner.invoke(app, ["doctor"]) - assert result.exit_code == 1 From a27395724572f190882ec1bc691958fa88d0fcca Mon Sep 17 00:00:00 2001 From: KhawarHabibKhan Date: Fri, 13 Mar 2026 19:11:38 +0500 Subject: [PATCH 6/9] Revert "Add specify doctor command for project health diagnostics" This reverts commit d5bd93248ae05c31ad2ad012983c0f87956dc417. --- src/specify_cli/__init__.py | 303 ------------------------------------ 1 file changed, 303 deletions(-) diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index c8c8103c4..dac7eaa54 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -1695,309 +1695,6 @@ def check(): if not any(agent_results.values()): console.print("[dim]Tip: Install an AI assistant for the best experience[/dim]") - -@app.command() -def doctor(): - """Diagnose a Specify project and report health issues.""" - show_banner() - console.print("[bold]Running project diagnostics...[/bold]\n") - - project_root = Path.cwd() - issues = [] # (severity, message) tuples: "error", "warning", "info" - - # ── 1. Project structure ────────────────────────────────────────── - tracker = StepTracker("Project Structure") - - specify_dir = project_root / ".specify" - tracker.add("specify_dir", ".specify/ directory") - if specify_dir.is_dir(): - tracker.complete("specify_dir", "found") - else: - tracker.error("specify_dir", "missing") - issues.append(("error", "No .specify/ directory — run 'specify init --here' to initialize")) - - specs_dir = project_root / "specs" - tracker.add("specs_dir", "specs/ directory") - if specs_dir.is_dir(): - tracker.complete("specs_dir", "found") - else: - tracker.skip("specs_dir", "not created yet") - issues.append(("info", "No specs/ directory — created when you run /speckit.specify")) - - scripts_dir = project_root / "scripts" - tracker.add("scripts_dir", "scripts/ directory") - if scripts_dir.is_dir(): - tracker.complete("scripts_dir", "found") - else: - tracker.error("scripts_dir", "missing") - issues.append(("error", "No scripts/ directory — project may not be initialized")) - - templates_dir = project_root / "templates" - tracker.add("templates_dir", "templates/ directory") - if templates_dir.is_dir(): - tracker.complete("templates_dir", "found") - else: - tracker.error("templates_dir", "missing") - issues.append(("error", "No templates/ directory — project may not be initialized")) - - memory_dir = project_root / "memory" - tracker.add("memory_dir", "memory/ directory") - if memory_dir.is_dir(): - tracker.complete("memory_dir", "found") - constitution = memory_dir / "constitution.md" - tracker.add("constitution", "memory/constitution.md") - if constitution.is_file(): - tracker.complete("constitution", "found") - else: - tracker.error("constitution", "missing") - issues.append(("warning", "No constitution.md in memory/ — project governance rules are missing")) - else: - tracker.error("memory_dir", "missing") - issues.append(("error", "No memory/ directory — project may not be initialized")) - - console.print(tracker.render()) - console.print() - - # ── 2. AI agent detection ───────────────────────────────────────── - agent_tracker = StepTracker("AI Agent Configuration") - detected_agents = [] - - for agent_key, agent_config in AGENT_CONFIG.items(): - if agent_key == "generic": - continue - agent_folder = agent_config["folder"] - if agent_folder and (project_root / agent_folder).is_dir(): - detected_agents.append(agent_key) - agent_tracker.add(agent_key, agent_config["name"]) - commands_dir = project_root / agent_folder / agent_config["commands_subdir"] - if commands_dir.is_dir() and any(commands_dir.iterdir()): - agent_tracker.complete(agent_key, f"commands in {agent_folder}{agent_config['commands_subdir']}/") - else: - agent_tracker.error(agent_key, f"folder exists but no commands in {agent_config['commands_subdir']}/") - issues.append(("warning", f"Agent '{agent_config['name']}' folder exists but commands directory is empty")) - - if not detected_agents: - agent_tracker.add("none", "No AI agent configured") - agent_tracker.skip("none", "run 'specify init --here --ai ' to set up") - issues.append(("info", "No AI agent folder detected — this is fine if you use IDE-based agents")) - - console.print(agent_tracker.render()) - console.print() - - # ── 3. Feature specs ────────────────────────────────────────────── - feature_tracker = StepTracker("Feature Specifications") - - if specs_dir.is_dir(): - feature_dirs = sorted( - [d for d in specs_dir.iterdir() if d.is_dir()], - key=lambda d: d.name, - ) - if not feature_dirs: - feature_tracker.add("empty", "No feature directories") - feature_tracker.skip("empty", "run /speckit.specify to create one") - else: - for fdir in feature_dirs: - key = fdir.name - feature_tracker.add(key, key) - - spec_file = fdir / "spec.md" - plan_file = fdir / "plan.md" - tasks_file = fdir / "tasks.md" - - artifacts = [] - missing = [] - for name, path in [("spec", spec_file), ("plan", plan_file), ("tasks", tasks_file)]: - if path.is_file(): - artifacts.append(name) - else: - missing.append(name) - - if missing: - detail = f"{', '.join(artifacts)} present; missing {', '.join(missing)}" - if "spec" in missing: - feature_tracker.error(key, detail) - issues.append(("error", f"Feature '{key}' is missing spec.md")) - else: - feature_tracker.complete(key, detail) - for m in missing: - issues.append(("info", f"Feature '{key}' has no {m}.md — run /speckit.{m} to generate")) - else: - feature_tracker.complete(key, "spec, plan, tasks all present") - else: - feature_tracker.add("none", "No specs/ directory") - feature_tracker.skip("none", "not applicable") - - console.print(feature_tracker.render()) - console.print() - - # ── 4. Scripts health ───────────────────────────────────────────── - script_tracker = StepTracker("Scripts") - - bash_dir = project_root / "scripts" / "bash" - ps_dir = project_root / "scripts" / "powershell" - - expected_scripts = ["common", "check-prerequisites", "create-new-feature", "setup-plan", "update-agent-context"] - - if bash_dir.is_dir(): - for name in expected_scripts: - key = f"sh_{name}" - script_path = bash_dir / f"{name}.sh" - script_tracker.add(key, f"bash/{name}.sh") - if script_path.is_file(): - if os.name != "nt" and not os.access(script_path, os.X_OK): - script_tracker.error(key, "not executable") - issues.append(("warning", f"scripts/bash/{name}.sh is not executable — run chmod +x")) - else: - script_tracker.complete(key, "ok") - else: - script_tracker.error(key, "missing") - issues.append(("error", f"scripts/bash/{name}.sh is missing")) - else: - script_tracker.add("no_bash", "scripts/bash/") - script_tracker.skip("no_bash", "not found") - - if ps_dir.is_dir(): - for name in expected_scripts: - key = f"ps_{name}" - script_path = ps_dir / f"{name}.ps1" - script_tracker.add(key, f"powershell/{name}.ps1") - if script_path.is_file(): - script_tracker.complete(key, "ok") - else: - script_tracker.error(key, "missing") - issues.append(("error", f"scripts/powershell/{name}.ps1 is missing")) - else: - script_tracker.add("no_ps", "scripts/powershell/") - script_tracker.skip("no_ps", "not found") - - console.print(script_tracker.render()) - console.print() - - # ── 5. Extensions health ────────────────────────────────────────── - ext_tracker = StepTracker("Extensions") - - extensions_yml = specify_dir / "extensions.yml" if specify_dir.is_dir() else None - registry_json = specify_dir / "extensions" / "registry.json" if specify_dir.is_dir() else None - - if extensions_yml and extensions_yml.is_file(): - ext_tracker.add("config", "extensions.yml") - try: - with open(extensions_yml) as f: - ext_config = yaml.safe_load(f) - if ext_config and isinstance(ext_config, dict): - ext_tracker.complete("config", "valid YAML") - hooks = ext_config.get("hooks", {}) - if hooks: - hook_count = sum(len(v) if isinstance(v, list) else 0 for v in hooks.values()) - ext_tracker.add("hooks", "Hook registrations") - ext_tracker.complete("hooks", f"{hook_count} hook(s) registered") - else: - ext_tracker.complete("config", "empty or no hooks") - except Exception as e: - ext_tracker.error("config", f"invalid YAML: {e}") - issues.append(("warning", f"extensions.yml has invalid YAML: {e}")) - else: - ext_tracker.add("config", "extensions.yml") - ext_tracker.skip("config", "no extensions configured") - - if registry_json and registry_json.is_file(): - ext_tracker.add("registry", "Extension registry") - try: - with open(registry_json) as f: - registry = json.load(f) - installed = [k for k, v in registry.items() if isinstance(v, dict)] - enabled = [k for k, v in registry.items() if isinstance(v, dict) and v.get("enabled", True)] - ext_tracker.complete("registry", f"{len(installed)} installed, {len(enabled)} enabled") - except Exception as e: - ext_tracker.error("registry", f"corrupt: {e}") - issues.append(("error", f"Extension registry is corrupt: {e}")) - else: - ext_tracker.add("registry", "Extension registry") - ext_tracker.skip("registry", "no extensions installed") - - console.print(ext_tracker.render()) - console.print() - - # ── 6. Git status ───────────────────────────────────────────────── - git_tracker = StepTracker("Git Repository") - git_tracker.add("git", "Git repository") - - git_ok = shutil.which("git") is not None - in_git_repo = False - if git_ok: - try: - result = subprocess.run( - ["git", "rev-parse", "--is-inside-work-tree"], - capture_output=True, text=True, cwd=str(project_root) - ) - in_git_repo = result.returncode == 0 - except Exception: - pass - - if in_git_repo: - git_tracker.complete("git", "inside git repository") - try: - branch = subprocess.run( - ["git", "rev-parse", "--abbrev-ref", "HEAD"], - capture_output=True, text=True, cwd=str(project_root) - ).stdout.strip() - git_tracker.add("branch", "Current branch") - git_tracker.complete("branch", branch) - except Exception: - pass - elif git_ok: - git_tracker.skip("git", "not a git repository") - issues.append(("info", "Not inside a git repository — git features like branching won't work")) - else: - git_tracker.skip("git", "git not installed") - issues.append(("info", "Git is not installed — branching and version control unavailable")) - - console.print(git_tracker.render()) - console.print() - - # ── Summary ─────────────────────────────────────────────────────── - errors = [msg for sev, msg in issues if sev == "error"] - warnings = [msg for sev, msg in issues if sev == "warning"] - infos = [msg for sev, msg in issues if sev == "info"] - - if not issues: - console.print(Panel( - "[bold green]All checks passed — project looks healthy![/bold green]", - border_style="green", - padding=(1, 2), - )) - else: - summary_lines = [] - - if errors: - summary_lines.append(f"[bold red]{len(errors)} error(s)[/bold red]") - for msg in errors: - summary_lines.append(f" [red]●[/red] {msg}") - summary_lines.append("") - - if warnings: - summary_lines.append(f"[bold yellow]{len(warnings)} warning(s)[/bold yellow]") - for msg in warnings: - summary_lines.append(f" [yellow]●[/yellow] {msg}") - summary_lines.append("") - - if infos: - summary_lines.append(f"[bold blue]{len(infos)} note(s)[/bold blue]") - for msg in infos: - summary_lines.append(f" [blue]○[/blue] {msg}") - - border = "red" if errors else "yellow" if warnings else "blue" - console.print(Panel( - "\n".join(summary_lines), - title="Diagnostic Summary", - border_style=border, - padding=(1, 2), - )) - - if errors: - raise typer.Exit(1) - - @app.command() def version(): """Display version and system information.""" From d86d2f8973c30a3aca2476491a119d57bd1c269a Mon Sep 17 00:00:00 2001 From: KhawarHabibKhan Date: Fri, 13 Mar 2026 19:33:35 +0500 Subject: [PATCH 7/9] Add doctor extension to community catalog --- extensions/README.md | 1 + extensions/catalog.community.json | 33 ++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/extensions/README.md b/extensions/README.md index e8f1617e9..7ffaef761 100644 --- a/extensions/README.md +++ b/extensions/README.md @@ -74,6 +74,7 @@ The following community-contributed extensions are available in [`catalog.commun |-----------|---------|-----| | Azure DevOps Integration | Sync user stories and tasks to Azure DevOps work items using OAuth authentication | [spec-kit-azure-devops](https://github.com/pragya247/spec-kit-azure-devops) | | Cleanup Extension | Post-implementation quality gate that reviews changes, fixes small issues (scout rule), creates tasks for medium issues, and generates analysis for large issues | [spec-kit-cleanup](https://github.com/dsrednicki/spec-kit-cleanup) | +| Project Health Check | Diagnose a Spec Kit project and report health issues across structure, agents, features, scripts, extensions, and git | [spec-kit-doctor](https://github.com/KhawarHabibKhan/spec-kit-doctor) | | Fleet Orchestrator | Orchestrate a full feature lifecycle with human-in-the-loop gates across all SpecKit phases | [spec-kit-fleet](https://github.com/sharathsatish/spec-kit-fleet) | | Jira Integration | Create Jira Epics, Stories, and Issues from spec-kit specifications and task breakdowns with configurable hierarchy and custom field support | [spec-kit-jira](https://github.com/mbachorik/spec-kit-jira) | | Ralph Loop | Autonomous implementation loop using AI agent CLI | [spec-kit-ralph](https://github.com/Rubiss/spec-kit-ralph) | diff --git a/extensions/catalog.community.json b/extensions/catalog.community.json index 759bd10d8..745cc0a48 100644 --- a/extensions/catalog.community.json +++ b/extensions/catalog.community.json @@ -1,6 +1,6 @@ { "schema_version": "1.0", - "updated_at": "2026-03-09T00:00:00Z", + "updated_at": "2026-03-13T00:00:00Z", "catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json", "extensions": { "azure-devops": { @@ -74,6 +74,37 @@ "created_at": "2026-02-22T00:00:00Z", "updated_at": "2026-02-22T00:00:00Z" }, + "doctor": { + "name": "Project Health Check", + "id": "doctor", + "description": "Diagnose a Spec Kit project and report health issues across structure, agents, features, scripts, extensions, and git.", + "author": "KhawarHabibKhan", + "version": "1.0.0", + "download_url": "https://github.com/KhawarHabibKhan/spec-kit-doctor/archive/refs/tags/v1.0.0.zip", + "repository": "https://github.com/KhawarHabibKhan/spec-kit-doctor", + "homepage": "https://github.com/KhawarHabibKhan/spec-kit-doctor", + "documentation": "https://github.com/KhawarHabibKhan/spec-kit-doctor/blob/main/README.md", + "changelog": "https://github.com/KhawarHabibKhan/spec-kit-doctor/blob/main/CHANGELOG.md", + "license": "MIT", + "requires": { + "speckit_version": ">=0.1.0" + }, + "provides": { + "commands": 1, + "hooks": 0 + }, + "tags": [ + "diagnostics", + "health-check", + "validation", + "project-structure" + ], + "verified": false, + "downloads": 0, + "stars": 0, + "created_at": "2026-03-13T00:00:00Z", + "updated_at": "2026-03-13T00:00:00Z" + }, "fleet": { "name": "Fleet Orchestrator", "id": "fleet", From 407a311367b69b00d1ed800c631ed4bfe1b70f2f Mon Sep 17 00:00:00 2001 From: KhawarHabibKhan <132604863+KhawarHabibKhan@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:23:54 +0500 Subject: [PATCH 8/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- extensions/catalog.community.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/catalog.community.json b/extensions/catalog.community.json index 745cc0a48..f1e0a0927 100644 --- a/extensions/catalog.community.json +++ b/extensions/catalog.community.json @@ -1,6 +1,6 @@ { "schema_version": "1.0", - "updated_at": "2026-03-13T00:00:00Z", + "updated_at": "2026-03-13T12:00:00Z", "catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json", "extensions": { "azure-devops": { From 22049d4489a642c602b223cbd3cbd1fe078d3485 Mon Sep 17 00:00:00 2001 From: KhawarHabibKhan <132604863+KhawarHabibKhan@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:36:51 +0500 Subject: [PATCH 9/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- extensions/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/README.md b/extensions/README.md index 7ffaef761..4c3f9d801 100644 --- a/extensions/README.md +++ b/extensions/README.md @@ -74,9 +74,9 @@ The following community-contributed extensions are available in [`catalog.commun |-----------|---------|-----| | Azure DevOps Integration | Sync user stories and tasks to Azure DevOps work items using OAuth authentication | [spec-kit-azure-devops](https://github.com/pragya247/spec-kit-azure-devops) | | Cleanup Extension | Post-implementation quality gate that reviews changes, fixes small issues (scout rule), creates tasks for medium issues, and generates analysis for large issues | [spec-kit-cleanup](https://github.com/dsrednicki/spec-kit-cleanup) | -| Project Health Check | Diagnose a Spec Kit project and report health issues across structure, agents, features, scripts, extensions, and git | [spec-kit-doctor](https://github.com/KhawarHabibKhan/spec-kit-doctor) | | Fleet Orchestrator | Orchestrate a full feature lifecycle with human-in-the-loop gates across all SpecKit phases | [spec-kit-fleet](https://github.com/sharathsatish/spec-kit-fleet) | | Jira Integration | Create Jira Epics, Stories, and Issues from spec-kit specifications and task breakdowns with configurable hierarchy and custom field support | [spec-kit-jira](https://github.com/mbachorik/spec-kit-jira) | +| Project Health Check | Diagnose a Spec Kit project and report health issues across structure, agents, features, scripts, extensions, and git | [spec-kit-doctor](https://github.com/KhawarHabibKhan/spec-kit-doctor) | | Ralph Loop | Autonomous implementation loop using AI agent CLI | [spec-kit-ralph](https://github.com/Rubiss/spec-kit-ralph) | | Retrospective Extension | Post-implementation retrospective with spec adherence scoring, drift analysis, and human-gated spec updates | [spec-kit-retrospective](https://github.com/emi-dm/spec-kit-retrospective) | | Review Extension | Post-implementation comprehensive code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification | [spec-kit-review](https://github.com/ismaelJimenez/spec-kit-review) |