devcouncil 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +201 -6
- package/package.json +9 -2
- package/pyproject.toml +34 -2
- package/src/devcouncil/app/config.py +348 -12
- package/src/devcouncil/app/orchestrator.py +10 -6
- package/src/devcouncil/app/state_machine.py +4 -0
- package/src/devcouncil/artifacts/graph.py +32 -5
- package/src/devcouncil/assets/__init__.py +1 -0
- package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
- package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
- package/src/devcouncil/cli/commands/agents.py +292 -0
- package/src/devcouncil/cli/commands/artifacts.py +6 -3
- package/src/devcouncil/cli/commands/check.py +220 -0
- package/src/devcouncil/cli/commands/config.py +43 -4
- package/src/devcouncil/cli/commands/cost.py +57 -0
- package/src/devcouncil/cli/commands/dashboard.py +6 -1
- package/src/devcouncil/cli/commands/design.py +186 -0
- package/src/devcouncil/cli/commands/doctor.py +379 -22
- package/src/devcouncil/cli/commands/evidence.py +48 -0
- package/src/devcouncil/cli/commands/go.py +532 -33
- package/src/devcouncil/cli/commands/handoff.py +69 -0
- package/src/devcouncil/cli/commands/hook.py +296 -15
- package/src/devcouncil/cli/commands/init.py +161 -20
- package/src/devcouncil/cli/commands/integrate.py +1371 -124
- package/src/devcouncil/cli/commands/logs.py +106 -0
- package/src/devcouncil/cli/commands/map.py +80 -10
- package/src/devcouncil/cli/commands/okf.py +245 -0
- package/src/devcouncil/cli/commands/plan.py +256 -55
- package/src/devcouncil/cli/commands/prompt.py +18 -7
- package/src/devcouncil/cli/commands/repair.py +50 -24
- package/src/devcouncil/cli/commands/report.py +8 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +4 -2
- package/src/devcouncil/cli/commands/rollback.py +27 -28
- package/src/devcouncil/cli/commands/run.py +195 -54
- package/src/devcouncil/cli/commands/runs.py +223 -0
- package/src/devcouncil/cli/commands/scaffold.py +32 -0
- package/src/devcouncil/cli/commands/semantic.py +47 -0
- package/src/devcouncil/cli/commands/setup.py +145 -6
- package/src/devcouncil/cli/commands/shell.py +73 -0
- package/src/devcouncil/cli/commands/skills.py +267 -0
- package/src/devcouncil/cli/commands/status.py +30 -15
- package/src/devcouncil/cli/commands/trace.py +47 -3
- package/src/devcouncil/cli/commands/verify.py +144 -3
- package/src/devcouncil/cli/commands/watch.py +32 -12
- package/src/devcouncil/cli/commands/watch_fs.py +40 -0
- package/src/devcouncil/cli/main.py +91 -7
- package/src/devcouncil/domain/evidence.py +29 -2
- package/src/devcouncil/domain/gap.py +27 -1
- package/src/devcouncil/domain/task.py +31 -2
- package/src/devcouncil/execution/checkpoints.py +256 -0
- package/src/devcouncil/execution/context_builder.py +1 -1
- package/src/devcouncil/execution/fs_watcher.py +205 -0
- package/src/devcouncil/execution/handoff.py +102 -0
- package/src/devcouncil/execution/hook_policy.py +162 -74
- package/src/devcouncil/execution/patch.py +65 -10
- package/src/devcouncil/execution/permissions.py +24 -24
- package/src/devcouncil/execution/policy_engine.py +350 -0
- package/src/devcouncil/execution/prompt_builder.py +751 -23
- package/src/devcouncil/execution/shell_session.py +231 -0
- package/src/devcouncil/execution/task_runner.py +24 -9
- package/src/devcouncil/executors/agent_registry.py +596 -0
- package/src/devcouncil/executors/coding_cli.py +791 -39
- package/src/devcouncil/executors/mini_swe.py +6 -0
- package/src/devcouncil/executors/native/agent.py +135 -19
- package/src/devcouncil/executors/openhands.py +6 -0
- package/src/devcouncil/gating/checks/clean_git.py +3 -1
- package/src/devcouncil/gating/checks/secret_scan_check.py +47 -21
- package/src/devcouncil/gating/policy.py +190 -11
- package/src/devcouncil/hardware.py +184 -0
- package/src/devcouncil/indexing/ast_matcher.py +17 -7
- package/src/devcouncil/indexing/lsp.py +45 -4
- package/src/devcouncil/indexing/repo_mapper.py +1284 -15
- package/src/devcouncil/indexing/semantic_index.py +221 -0
- package/src/devcouncil/integrations/actions.py +166 -0
- package/src/devcouncil/integrations/check.py +426 -0
- package/src/devcouncil/integrations/claude_assets.py +444 -0
- package/src/devcouncil/integrations/code_review_graph.py +13 -2
- package/src/devcouncil/integrations/github_intent.py +149 -0
- package/src/devcouncil/integrations/gitnexus.py +45 -2
- package/src/devcouncil/integrations/mcp/server.py +1944 -32
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
- package/src/devcouncil/integrations/pr_comments.py +9 -0
- package/src/devcouncil/knowledge/__init__.py +23 -0
- package/src/devcouncil/knowledge/design.py +374 -0
- package/src/devcouncil/knowledge/design_conformance.py +317 -0
- package/src/devcouncil/knowledge/fetch.py +223 -0
- package/src/devcouncil/knowledge/frontmatter.py +51 -0
- package/src/devcouncil/knowledge/okf.py +202 -0
- package/src/devcouncil/knowledge/skill_bridge.py +96 -0
- package/src/devcouncil/knowledge/sources.py +239 -0
- package/src/devcouncil/live/cards.py +181 -25
- package/src/devcouncil/live/repair_prompt.py +29 -6
- package/src/devcouncil/live/reviewer.py +72 -13
- package/src/devcouncil/live/signals.py +2 -2
- package/src/devcouncil/live/summary.py +18 -8
- package/src/devcouncil/live/transcripts.py +47 -11
- package/src/devcouncil/llm/cache.py +20 -8
- package/src/devcouncil/llm/model_defaults.yaml +44 -0
- package/src/devcouncil/llm/provider.py +617 -49
- package/src/devcouncil/llm/router.py +337 -53
- package/src/devcouncil/optimization/__init__.py +1 -0
- package/src/devcouncil/optimization/gepa_agent.py +318 -0
- package/src/devcouncil/optimization/skillopt.py +673 -0
- package/src/devcouncil/planning/arbiter_service.py +10 -2
- package/src/devcouncil/planning/correction_manifest.py +346 -0
- package/src/devcouncil/planning/critique_service.py +16 -4
- package/src/devcouncil/planning/plan_service.py +86 -6
- package/src/devcouncil/planning/prompt_enhancer_service.py +206 -1
- package/src/devcouncil/planning/repair_service.py +8 -2
- package/src/devcouncil/planning/spec_service.py +37 -3
- package/src/devcouncil/repo/ci_scaffold.py +165 -0
- package/src/devcouncil/repo/gitignore.py +123 -0
- package/src/devcouncil/repo/sca.py +384 -0
- package/src/devcouncil/reporting/json_report.py +22 -1
- package/src/devcouncil/reporting/markdown_report.py +29 -1
- package/src/devcouncil/reporting/okf_bundle_writer.py +364 -0
- package/src/devcouncil/reporting/okf_html.py +323 -0
- package/src/devcouncil/reporting/report_builder.py +18 -1
- package/src/devcouncil/skills/__init__.py +19 -0
- package/src/devcouncil/skills/library/README.md +46 -0
- package/src/devcouncil/skills/library/ai-training.md +50 -0
- package/src/devcouncil/skills/library/android.md +50 -0
- package/src/devcouncil/skills/library/backend.md +52 -0
- package/src/devcouncil/skills/library/core-engineering.md +95 -0
- package/src/devcouncil/skills/library/data-engineering.md +47 -0
- package/src/devcouncil/skills/library/desktop.md +46 -0
- package/src/devcouncil/skills/library/devops.md +48 -0
- package/src/devcouncil/skills/library/game-dev.md +46 -0
- package/src/devcouncil/skills/library/ios.md +48 -0
- package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
- package/src/devcouncil/skills/library/security.md +48 -0
- package/src/devcouncil/skills/library/systems.md +48 -0
- package/src/devcouncil/skills/library/web.md +47 -0
- package/src/devcouncil/skills/library/windows.md +47 -0
- package/src/devcouncil/skills/registry.py +408 -0
- package/src/devcouncil/storage/db.py +140 -3
- package/src/devcouncil/storage/models.py +125 -0
- package/src/devcouncil/storage/native.py +559 -0
- package/src/devcouncil/storage/repositories.py +157 -78
- package/src/devcouncil/telemetry/cost.py +123 -17
- package/src/devcouncil/telemetry/logging_setup.py +244 -0
- package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
- package/src/devcouncil/telemetry/pricing.py +28 -0
- package/src/devcouncil/telemetry/stages.py +141 -0
- package/src/devcouncil/telemetry/traces.py +62 -7
- package/src/devcouncil/telemetry/tracker.py +24 -10
- package/src/devcouncil/ui/dashboard.py +393 -28
- package/src/devcouncil/utils/redaction.py +9 -3
- package/src/devcouncil/utils/subprocess_env.py +69 -0
- package/src/devcouncil/verification/acceptance_compiler.py +253 -0
- package/src/devcouncil/verification/ad_hoc_check.py +135 -0
- package/src/devcouncil/verification/diff_coverage.py +353 -0
- package/src/devcouncil/verification/implementation_reviewer.py +11 -2
- package/src/devcouncil/verification/next_actions.py +189 -0
- package/src/devcouncil/verification/sandbox.py +181 -0
- package/src/devcouncil/verification/test_resolver.py +91 -0
- package/src/devcouncil/verification/verifier.py +1549 -143
- package/uv.lock +205 -64
- package/src/devcouncil/indexing/symbol_index.py +0 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Host hardware detection used to size local (Ollama) models.
|
|
2
|
+
|
|
3
|
+
DevCouncil runs the council roles against whatever LLM provider is configured.
|
|
4
|
+
For the local ``ollama`` provider the model has to fit in host memory. On Apple
|
|
5
|
+
Silicon Macs that memory is *unified* (shared by CPU/GPU/OS), so total RAM is the
|
|
6
|
+
ceiling. On a host with a discrete GPU (NVIDIA), Ollama offloads to VRAM, so the
|
|
7
|
+
*VRAM* is the practical ceiling instead — a 64 GB box with an 8 GB GPU should not be
|
|
8
|
+
told to run a model that only fits in system RAM. This module exposes small, pure
|
|
9
|
+
helpers so ``dev doctor`` and ``dev setup`` can recommend a model that will actually
|
|
10
|
+
run instead of a one-size-fits-all default, on macOS, Linux and Windows.
|
|
11
|
+
|
|
12
|
+
Everything here is best-effort and stdlib-only: detection failures return
|
|
13
|
+
``None`` rather than raising, so callers degrade to the static default.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import platform
|
|
20
|
+
import shutil
|
|
21
|
+
import subprocess
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
|
|
24
|
+
# Recommended Ollama context window for DevCouncil's large planning prompts.
|
|
25
|
+
# Kept in sync with the value surfaced by `dev doctor`.
|
|
26
|
+
RECOMMENDED_NUM_CTX = 16384
|
|
27
|
+
|
|
28
|
+
# Fallback model when the host is unknown or has little memory. Matches the
|
|
29
|
+
# static defaults in ``llm/model_defaults.yaml``.
|
|
30
|
+
DEFAULT_OLLAMA_MODEL = "qwen2.5-coder:7b"
|
|
31
|
+
|
|
32
|
+
# RAM (GiB) -> recommended qwen2.5-coder size. Highest matching tier wins.
|
|
33
|
+
# Sizes are chosen so the quantized weights plus a 16k context comfortably fit
|
|
34
|
+
# alongside the OS in Apple Silicon unified memory.
|
|
35
|
+
_OLLAMA_MODEL_TIERS: tuple[tuple[float, str], ...] = (
|
|
36
|
+
(48.0, "qwen2.5-coder:32b"),
|
|
37
|
+
(24.0, "qwen2.5-coder:14b"),
|
|
38
|
+
(0.0, "qwen2.5-coder:7b"),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def is_macos() -> bool:
|
|
43
|
+
return platform.system() == "Darwin"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_apple_silicon() -> bool:
|
|
47
|
+
"""True on Apple-Silicon (arm64) Macs."""
|
|
48
|
+
return is_macos() and platform.machine() == "arm64"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def mac_chip_brand() -> str | None:
|
|
52
|
+
"""Marketing CPU string on macOS (e.g. ``Apple M3 Pro``), else None."""
|
|
53
|
+
if not is_macos():
|
|
54
|
+
return None
|
|
55
|
+
try:
|
|
56
|
+
out = subprocess.check_output(
|
|
57
|
+
["sysctl", "-n", "machdep.cpu.brand_string"],
|
|
58
|
+
text=True,
|
|
59
|
+
timeout=5,
|
|
60
|
+
).strip()
|
|
61
|
+
return out or None
|
|
62
|
+
except Exception:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def total_ram_gb() -> float | None:
|
|
67
|
+
"""Total physical RAM in GiB, or None if it cannot be determined."""
|
|
68
|
+
try:
|
|
69
|
+
if is_macos():
|
|
70
|
+
out = subprocess.check_output(
|
|
71
|
+
["sysctl", "-n", "hw.memsize"], text=True, timeout=5
|
|
72
|
+
).strip()
|
|
73
|
+
return int(out) / (1024**3)
|
|
74
|
+
# POSIX (Linux): pages * page size.
|
|
75
|
+
pages = os.sysconf("SC_PHYS_PAGES")
|
|
76
|
+
page_size = os.sysconf("SC_PAGE_SIZE")
|
|
77
|
+
return (pages * page_size) / (1024**3)
|
|
78
|
+
except (OSError, ValueError, AttributeError, subprocess.SubprocessError):
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def nvidia_vram_gb() -> float | None:
|
|
83
|
+
"""Total VRAM (GiB) of the largest NVIDIA GPU via ``nvidia-smi``, else ``None``.
|
|
84
|
+
|
|
85
|
+
On hosts with a discrete NVIDIA GPU this is the real ceiling for Ollama, since the
|
|
86
|
+
model is offloaded to VRAM. Returns ``None`` when ``nvidia-smi`` is absent (no GPU,
|
|
87
|
+
Apple Silicon, or an unsupported vendor), so callers fall back to total RAM."""
|
|
88
|
+
if not shutil.which("nvidia-smi"):
|
|
89
|
+
return None
|
|
90
|
+
try:
|
|
91
|
+
out = subprocess.check_output(
|
|
92
|
+
["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"],
|
|
93
|
+
text=True,
|
|
94
|
+
timeout=5,
|
|
95
|
+
stderr=subprocess.DEVNULL,
|
|
96
|
+
).strip()
|
|
97
|
+
except (OSError, subprocess.SubprocessError):
|
|
98
|
+
return None
|
|
99
|
+
sizes_mib: list[float] = []
|
|
100
|
+
for line in out.splitlines():
|
|
101
|
+
line = line.strip()
|
|
102
|
+
if not line:
|
|
103
|
+
continue
|
|
104
|
+
try:
|
|
105
|
+
sizes_mib.append(float(line))
|
|
106
|
+
except ValueError:
|
|
107
|
+
continue
|
|
108
|
+
if not sizes_mib:
|
|
109
|
+
return None
|
|
110
|
+
return max(sizes_mib) / 1024.0 # MiB -> GiB
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def recommend_ollama_model(ram_gb: float | None = None, vram_gb: float | None = None) -> str:
|
|
114
|
+
"""Largest qwen2.5-coder size expected to run on the host.
|
|
115
|
+
|
|
116
|
+
When a discrete GPU's ``vram_gb`` is known it is the ceiling (Ollama offloads to
|
|
117
|
+
VRAM); otherwise total system RAM is used (correct for unified-memory Macs and
|
|
118
|
+
CPU-only hosts)."""
|
|
119
|
+
if ram_gb is None:
|
|
120
|
+
ram_gb = total_ram_gb()
|
|
121
|
+
effective = vram_gb if vram_gb is not None else ram_gb
|
|
122
|
+
if effective is None:
|
|
123
|
+
return DEFAULT_OLLAMA_MODEL
|
|
124
|
+
for floor, model in _OLLAMA_MODEL_TIERS:
|
|
125
|
+
if effective >= floor:
|
|
126
|
+
return model
|
|
127
|
+
return DEFAULT_OLLAMA_MODEL
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True)
|
|
131
|
+
class HostSummary:
|
|
132
|
+
"""A snapshot of the host relevant to local-model sizing."""
|
|
133
|
+
|
|
134
|
+
is_macos: bool
|
|
135
|
+
is_apple_silicon: bool
|
|
136
|
+
chip: str | None
|
|
137
|
+
ram_gb: float | None
|
|
138
|
+
recommended_ollama_model: str
|
|
139
|
+
vram_gb: float | None = None
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def ram_label(self) -> str:
|
|
143
|
+
return f"{self.ram_gb:.0f} GB" if self.ram_gb is not None else "unknown RAM"
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def vram_label(self) -> str | None:
|
|
147
|
+
return f"{self.vram_gb:.0f} GB VRAM" if self.vram_gb is not None else None
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def chip_label(self) -> str:
|
|
151
|
+
if self.chip:
|
|
152
|
+
return self.chip
|
|
153
|
+
if self.is_apple_silicon:
|
|
154
|
+
return "Apple Silicon"
|
|
155
|
+
if self.vram_gb is not None:
|
|
156
|
+
return "discrete GPU host"
|
|
157
|
+
return "this host"
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def platform_label(self) -> str:
|
|
161
|
+
"""Human label for the sizing row across OSes."""
|
|
162
|
+
if self.is_macos:
|
|
163
|
+
return "Apple Silicon" if self.is_apple_silicon else "Mac (Intel)"
|
|
164
|
+
system = platform.system()
|
|
165
|
+
return system or "Host"
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def memory_label(self) -> str:
|
|
169
|
+
"""Memory ceiling used for the recommendation (VRAM if discrete GPU, else RAM)."""
|
|
170
|
+
vram = self.vram_label
|
|
171
|
+
return f"{vram} (GPU)" if vram else self.ram_label
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def describe_host() -> HostSummary:
|
|
175
|
+
ram = total_ram_gb()
|
|
176
|
+
vram = nvidia_vram_gb()
|
|
177
|
+
return HostSummary(
|
|
178
|
+
is_macos=is_macos(),
|
|
179
|
+
is_apple_silicon=is_apple_silicon(),
|
|
180
|
+
chip=mac_chip_brand(),
|
|
181
|
+
ram_gb=ram,
|
|
182
|
+
vram_gb=vram,
|
|
183
|
+
recommended_ollama_model=recommend_ollama_model(ram, vram),
|
|
184
|
+
)
|
|
@@ -59,7 +59,7 @@ class AstMatcher:
|
|
|
59
59
|
|
|
60
60
|
def _has_tree_sitter(self) -> bool:
|
|
61
61
|
try:
|
|
62
|
-
import tree_sitter # noqa: F401
|
|
62
|
+
import tree_sitter # type: ignore[import-not-found] # noqa: F401
|
|
63
63
|
except Exception:
|
|
64
64
|
return False
|
|
65
65
|
return True
|
|
@@ -71,12 +71,13 @@ class AstMatcher:
|
|
|
71
71
|
language: str | None = None,
|
|
72
72
|
kind: str | None = None,
|
|
73
73
|
limit: int = 100,
|
|
74
|
+
files: list[Path] | None = None,
|
|
74
75
|
) -> list[AstMatch]:
|
|
75
76
|
language = language.lower() if language else None
|
|
76
77
|
kind = kind.lower() if kind else None
|
|
77
78
|
limit = max(1, limit)
|
|
78
79
|
matches: list[AstMatch] = []
|
|
79
|
-
for path in self._candidate_files(language):
|
|
80
|
+
for path in self._candidate_files(language, files):
|
|
80
81
|
try:
|
|
81
82
|
text = path.read_text(encoding="utf-8")
|
|
82
83
|
except (OSError, UnicodeDecodeError):
|
|
@@ -88,22 +89,31 @@ class AstMatcher:
|
|
|
88
89
|
return matches[:limit]
|
|
89
90
|
return matches[:limit]
|
|
90
91
|
|
|
91
|
-
def _candidate_files(self, language: str | None) -> list[Path]:
|
|
92
|
+
def _candidate_files(self, language: str | None, files: list[Path] | None = None) -> list[Path]:
|
|
92
93
|
allowed_exts = {
|
|
93
94
|
ext for ext, ext_language in self._EXT_LANGUAGE.items()
|
|
94
95
|
if language is None or ext_language == language
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
+
# When the caller already walked the tree (e.g. SemanticIndex.create_snapshot
|
|
98
|
+
# shares one traversal across all collectors), filter that list in memory
|
|
99
|
+
# instead of re-globbing. The filter is identical to the rglob path below.
|
|
100
|
+
if files is not None:
|
|
101
|
+
return sorted(
|
|
102
|
+
path for path in files
|
|
103
|
+
if path.is_file() and path.suffix.lower() in allowed_exts
|
|
104
|
+
and not any(part in self._IGNORED_DIRS for part in path.parts)
|
|
105
|
+
)
|
|
106
|
+
candidates: list[Path] = []
|
|
97
107
|
try:
|
|
98
108
|
for path in self.project_root.rglob("*"):
|
|
99
109
|
if not path.is_file() or path.suffix.lower() not in allowed_exts:
|
|
100
110
|
continue
|
|
101
111
|
if any(part in self._IGNORED_DIRS for part in path.parts):
|
|
102
112
|
continue
|
|
103
|
-
|
|
113
|
+
candidates.append(path)
|
|
104
114
|
except OSError:
|
|
105
|
-
return
|
|
106
|
-
return sorted(
|
|
115
|
+
return candidates
|
|
116
|
+
return sorted(candidates)
|
|
107
117
|
|
|
108
118
|
def _match_file(self, rel: str, language: str, text: str, *, query: str, kind: str | None) -> list[AstMatch]:
|
|
109
119
|
if language == "python":
|
|
@@ -16,7 +16,16 @@ class LspServerCandidate:
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class LspInspector:
|
|
19
|
-
"""
|
|
19
|
+
"""Language-server *detection* only — DevCouncil does not run an LSP client.
|
|
20
|
+
|
|
21
|
+
This inspector exists to answer one honest question: which language servers
|
|
22
|
+
for the repo's languages are installed on PATH? It does NOT spawn a server,
|
|
23
|
+
does NOT speak the LSP wire protocol, and does NOT send the ``initialize``
|
|
24
|
+
handshake. ``starter_initialize_payload`` builds the JSON-RPC ``initialize``
|
|
25
|
+
request a *future* client would send, but it is never transmitted — it is
|
|
26
|
+
surfaced purely as a reference/starter payload, clearly labelled as such, so
|
|
27
|
+
no consumer mistakes detection for a live LSP capability.
|
|
28
|
+
"""
|
|
20
29
|
|
|
21
30
|
_LANGUAGE_SERVERS: dict[str, list[list[str]]] = {
|
|
22
31
|
"python": [["pyright-langserver", "--stdio"], ["pylsp"]],
|
|
@@ -76,7 +85,14 @@ class LspInspector:
|
|
|
76
85
|
)
|
|
77
86
|
return candidates
|
|
78
87
|
|
|
79
|
-
def
|
|
88
|
+
def starter_initialize_payload(self, language: str) -> dict[str, Any]:
|
|
89
|
+
"""Build the JSON-RPC ``initialize`` request a future LSP client *would* send.
|
|
90
|
+
|
|
91
|
+
This payload is NEVER sent — DevCouncil has no LSP client. It is provided
|
|
92
|
+
only as a reference/starter for anyone wiring up real LSP support. See the
|
|
93
|
+
``initialize_requests`` block in :meth:`summary`, which carries the same
|
|
94
|
+
non-sent payloads under an explicit ``"_note"`` disclaimer.
|
|
95
|
+
"""
|
|
80
96
|
return {
|
|
81
97
|
"jsonrpc": "2.0",
|
|
82
98
|
"id": 1,
|
|
@@ -97,10 +113,32 @@ class LspInspector:
|
|
|
97
113
|
},
|
|
98
114
|
}
|
|
99
115
|
|
|
116
|
+
# Made explicit in every summary so no consumer reads this as a live LSP feature.
|
|
117
|
+
_DETECTION_ONLY_NOTE = (
|
|
118
|
+
"Detection only: DevCouncil checks which language servers are installed on "
|
|
119
|
+
"PATH; it does not run an LSP client or send any requests."
|
|
120
|
+
)
|
|
121
|
+
_STARTER_PAYLOAD_NOTE = (
|
|
122
|
+
"Starter payloads only — these initialize requests are NEVER sent. They are "
|
|
123
|
+
"a reference for wiring up a real LSP client later."
|
|
124
|
+
)
|
|
125
|
+
|
|
100
126
|
def summary(self, files: list[str] | None = None) -> dict[str, Any]:
|
|
101
127
|
candidates = self.server_candidates(files)
|
|
102
128
|
return {
|
|
129
|
+
"mode": "detection-only",
|
|
130
|
+
"note": self._DETECTION_ONLY_NOTE,
|
|
103
131
|
"languages": self.detect_languages(files),
|
|
132
|
+
"detected_servers": [
|
|
133
|
+
{
|
|
134
|
+
"language": candidate.language,
|
|
135
|
+
"command": candidate.command,
|
|
136
|
+
"available": candidate.available,
|
|
137
|
+
"reason": candidate.reason,
|
|
138
|
+
}
|
|
139
|
+
for candidate in candidates
|
|
140
|
+
],
|
|
141
|
+
# Back-compat alias for "detected_servers" (older consumers/tests).
|
|
104
142
|
"servers": [
|
|
105
143
|
{
|
|
106
144
|
"language": candidate.language,
|
|
@@ -111,8 +149,11 @@ class LspInspector:
|
|
|
111
149
|
for candidate in candidates
|
|
112
150
|
],
|
|
113
151
|
"initialize_requests": {
|
|
114
|
-
|
|
115
|
-
|
|
152
|
+
"_note": self._STARTER_PAYLOAD_NOTE,
|
|
153
|
+
**{
|
|
154
|
+
language: self.starter_initialize_payload(language)
|
|
155
|
+
for language in sorted({candidate.language for candidate in candidates})
|
|
156
|
+
},
|
|
116
157
|
},
|
|
117
158
|
}
|
|
118
159
|
|