M5: LSP pool and diagnostics wiring

Adds the DiagnosticsSource seam trait in harness-core::lsp (kept out of harness-tools so tools never link the LSP crate directly), and implements it in harness-lsp as a pool that lazily spawns one language server per file extension (rust-analyzer, typescript-language-server, gopls, pyright-langserver) only when the binary is on PATH. Adds harness-tools::diagnostics and wires diagnostics into edit/write/bash/glob/grep output.
This commit is contained in:
2026-07-10 16:20:22 +02:00
parent e32f61e4f8
commit f71a347061
19 changed files with 762 additions and 2 deletions
+21
View File
@@ -16,6 +16,7 @@ use harness_core::config::{self, Config, ConfigError};
use harness_core::engine::jobs::{JobBoard, JobState, LaunchSpec};
use harness_core::engine::{run_session, RunConfig, StepContext};
use harness_core::event::{AppEvent, EventBus, RunOutcome};
use harness_core::lsp::DiagnosticsSource;
use harness_core::permission::{PermissionReply, PermissionService, Rule, Ruleset};
use harness_core::store::{Store, StoreError};
use harness_core::tool::{
@@ -90,6 +91,9 @@ struct EngineInner {
providers: ProviderRegistry,
catalog: ModelCatalog,
agents: AgentRegistry,
/// LSP diagnostics pool (edit/write surface errors). Shared across sessions; servers spawn
/// lazily on first touch.
diagnostics: Option<Arc<dyn DiagnosticsSource>>,
cwd: PathBuf,
data_dir: PathBuf,
runs: Mutex<HashMap<SessionId, RunHandle>>,
@@ -196,6 +200,20 @@ impl EngineHandle {
// `init` warms the cache in the background for the next launch.
let catalog = ModelCatalog::load_cached_or_baked(&ModelCatalog::default_cache_path());
// LSP pool: built-ins present on PATH, plus any config-declared servers.
let lsp_servers: Vec<harness_lsp::ServerConfig> = config
.lsp
.iter()
.map(|(name, c)| harness_lsp::ServerConfig {
name: name.clone(),
command: c.command.clone(),
args: c.args.clone(),
extensions: c.extensions.clone(),
})
.collect();
let diagnostics: Option<Arc<dyn DiagnosticsSource>> =
Some(Arc::new(harness_lsp::LspPool::new(cwd.clone(), lsp_servers)));
let inner = Arc::new(EngineInner {
config,
store,
@@ -205,6 +223,7 @@ impl EngineHandle {
providers,
catalog,
agents,
diagnostics,
cwd,
data_dir,
runs: Mutex::new(HashMap::new()),
@@ -328,6 +347,7 @@ impl EngineHandle {
spawner: Some(self.inner.clone()),
job_board: Some(board),
context_reporter: None, // root session has no parent board to report to
diagnostics: self.inner.diagnostics.clone(),
};
let run_config = RunConfig {
agent_name: agent.name.clone(),
@@ -633,6 +653,7 @@ impl EngineInner {
spawner: Some(self.arc()),
job_board: Some(board),
context_reporter: reporter,
diagnostics: self.diagnostics.clone(),
}
}
}