Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions crates/neuromesh-api/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,13 @@ impl HttpServer {
ready_tx: Option<tokio::sync::oneshot::Sender<u16>>,
) -> Result<()> {
let (listener, addr) = self.bind().await?;
println!("\n╔═══════════════════════════════════════════════════════════════════════════════════╗");
println!(
eprintln!("\n╔═══════════════════════════════════════════════════════════════════════════════════╗");
eprintln!(
"║ 🌿 NEUROMESH v{} — UI MONITOR & MCP DASHBOARD ACTIVE ║",
env!("CARGO_PKG_VERSION")
);
println!("║ Open in browser: \x1b[1;36mhttp://{}\x1b[0m ║", addr);
println!("╚═══════════════════════════════════════════════════════════════════════════════════╝\n");
eprintln!("║ Open in browser: \x1b[1;36mhttp://{}\x1b[0m ║", addr);
eprintln!("╚═══════════════════════════════════════════════════════════════════════════════════╝\n");
if let Some(tx) = ready_tx {
let _ = tx.send(addr.port());
}
Expand Down
3 changes: 3 additions & 0 deletions crates/neuromesh-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@ embeddings = [
"neuromesh-context/embeddings",
"neuromesh-mcp/embeddings",
]

[dev-dependencies]
tempfile = "3.24"
41 changes: 28 additions & 13 deletions crates/neuromesh-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,26 @@ use std::sync::Arc;

/// Best-effort: open the system's default browser. Never fatal — the caller
/// keeps working (stdio MCP, or the dashboard itself) if this fails.
/// `NEUROMESH_NO_BROWSER` turns it off (tests, headless machines). The child
/// never inherits stdout: in `mcp` mode that stream is the JSON-RPC channel.
fn open_browser(url: &str) {
let result = if cfg!(target_os = "windows") {
std::process::Command::new("cmd")
.args(["/C", "start", "", url])
.spawn()
if std::env::var_os("NEUROMESH_NO_BROWSER").is_some() {
return;
}
let mut cmd = if cfg!(target_os = "windows") {
let mut c = std::process::Command::new("cmd");
c.args(["/C", "start", "", url]);
c
} else if cfg!(target_os = "macos") {
std::process::Command::new("open").arg(url).spawn()
let mut c = std::process::Command::new("open");
c.arg(url);
c
} else {
std::process::Command::new("xdg-open").arg(url).spawn()
let mut c = std::process::Command::new("xdg-open");
c.arg(url);
c
};
let result = cmd.stdout(std::process::Stdio::null()).spawn();
if let Err(e) = result {
eprintln!("NeuroMesh: could not open browser automatically ({e}); open {url} manually.");
}
Expand All @@ -28,16 +38,20 @@ fn open_browser(url: &str) {
/// The `mcp` process is spawned silently and often respawned by the MCP
/// client (Claude Desktop, Cursor, …), so auto-opening a tab on every launch
/// would spam the user. Open the dashboard automatically only the very first
/// time NeuroMesh ever starts on this machine; after that, `neuromesh monitor`
/// time NeuroMesh starts on *this project* (marker lives in that project's
/// own data dir, not a single machine-wide flag) — so a new project, or a
/// teammate cloning the same repo into their own `~/.neuromesh`, still gets
/// the one-time graphical first impression. After that, `neuromesh monitor`
/// opens it on demand.
fn maybe_open_ui_first_run(port: u16) {
let marker = neuromesh_core::neuromesh_home().join(".ui_opened_once");
fn maybe_open_ui_first_run(workspace: &std::path::Path, port: u16) {
let dir = match neuromesh_core::ensure_project_data_dir(workspace) {
Ok(dir) => dir,
Err(_) => return,
};
let marker = dir.join(".ui_opened_once");
if marker.exists() {
return;
}
if std::fs::create_dir_all(neuromesh_core::neuromesh_home()).is_err() {
return;
}
if std::fs::write(&marker, b"1").is_ok() {
open_browser(&format!("http://127.0.0.1:{port}"));
}
Expand Down Expand Up @@ -302,9 +316,10 @@ async fn async_main(command: &str, args: &[String]) -> Result<()> {
});
// Separate task: only open a browser once we know the real
// bound port, but this never delays the stdio server below.
let workspace_for_ui = current_dir.clone();
tokio::spawn(async move {
if let Ok(actual_port) = port_rx.await {
maybe_open_ui_first_run(actual_port);
maybe_open_ui_first_run(&workspace_for_ui, actual_port);
}
});
}
Expand Down
87 changes: 87 additions & 0 deletions crates/neuromesh-cli/tests/mcp_stdout_clean.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//! `neuromesh mcp` speaks line-delimited JSON-RPC on stdout, and the same
//! process also starts the local dashboard. Anything else written to stdout
//! (a banner, a log line) breaks strict MCP clients that parse every line.
//! This spawns the real binary and checks that every stdout line is JSON.

use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::time::{Duration, Instant};

#[test]
fn mcp_stdout_carries_only_json_rpc() {
let home = tempfile::tempdir().unwrap();
let workspace = tempfile::tempdir().unwrap();
std::fs::write(
workspace.path().join("lib.rs"),
"pub fn add(a: i32, b: i32) -> i32 { a + b }\n",
)
.unwrap();

let mut child = Command::new(env!("CARGO_BIN_EXE_neuromesh"))
.arg("mcp")
.current_dir(workspace.path())
.env("NEUROMESH_HOME", home.path())
.env("NEUROMESH_NO_BROWSER", "1")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("spawn neuromesh mcp");

let stdout = child.stdout.take().unwrap();
let (tx, rx) = mpsc::channel::<String>();
std::thread::spawn(move || {
for line in BufReader::new(stdout).lines() {
match line {
Ok(l) => {
if tx.send(l).is_err() {
break;
}
}
Err(_) => break,
}
}
});

let mut stdin = child.stdin.take().unwrap();
let init = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"stdout-test","version":"0"}}}"#;
let list = r#"{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}"#;
writeln!(stdin, "{init}").unwrap();
writeln!(stdin, "{list}").unwrap();
stdin.flush().unwrap();

let deadline = Instant::now() + Duration::from_secs(90);
let mut lines = Vec::new();
let mut saw_list = false;
while Instant::now() < deadline {
match rx.recv_timeout(Duration::from_millis(200)) {
Ok(line) => {
if line.contains("\"id\":2") {
saw_list = true;
}
lines.push(line);
}
Err(mpsc::RecvTimeoutError::Timeout) => {}
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
// Give the dashboard task time to bind and print after the replies.
if saw_list && lines.len() >= 2 {
std::thread::sleep(Duration::from_secs(2));
while let Ok(line) = rx.try_recv() {
lines.push(line);
}
break;
}
}
let _ = child.kill();
let _ = child.wait();

assert!(saw_list, "no tools/list reply on stdout; got {lines:?}");
for line in lines.iter().filter(|l| !l.trim().is_empty()) {
assert!(
serde_json::from_str::<serde_json::Value>(line).is_ok(),
"non-JSON line on MCP stdout: {line:?}"
);
}
}
29 changes: 29 additions & 0 deletions docs/planning/11-roadmap-2026-09-30-concept-queries.fa.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# نقشه راه — بعد از گزارش یوسف (۲۰۲۶-۰۹-۳۰)

ورودی: `handoff-2026-09-30-yoosef-findings.fa.md` — ۳ از ۷ روی ریپوی خودمان، ۲۶ ثانیه تاخیر روی یک query، و باگ stdout در حالت `mcp`.

## تشخیص اولیه (قبل از هر اندازه‌گیری)

ست `self` (۳۰ سوال) تقریباً همه‌اش اسم یک شناسه را در متن دارد (`resolve_cluster_noun_seeds`, `select()`, …). سوال‌های یوسف **مفهومی** هستند و هیچ شناسه‌ای ندارند: «ابزار چطور جلوی ایندکس کردن مسیرهای خطرناک مثل ریشه فایل‌سیستم را می‌گیرد؟». پس ست‌های ما این کلاس را اصلاً اندازه نمی‌گرفتند؛ عدد خوب روی `self` و عدد بد یوسف با هم تناقض ندارند. این کلاس (سوال مفهومی، بدون اسم) رایج‌ترین شکل سوال کاربر واقعی است، پس اولویت اول است.

دومین مشکل: وقتی جواب غلط است، `coverage: no_recorded_gap` می‌گوید (اعتماد کاذب). عامل از روی این عدد تصمیم می‌گیرد جستجوی بیشتری بکند یا نه — اعتماد کاذب از جواب غلط بدتر است.

## فازها

| فاز | کار | معیار خروج |
|---|---|---|
| **A** — پروتکل | بنر داشبورد از stdout به stderr؛ تست یکپارچه: در حالت `mcp` هر خط stdout باید JSON معتبر باشد | تست سبز، روی باینری واقعی چک شده |
| **B** — ست مفهومی (قفل قبل از تیونینگ) | (۱) ست `concept-dev`: ۴ سوال یوسف + ~۱۲ سوال مفهومی دیگر روی همین ریپو، gold با خواندن کد. (۲) ست `concept-holdout`: ~۱۲ سوال مفهومی روی یک ریپوی **Rust شخص ثالث** (ripgrep، revision پین‌شده) که تا پایان فاز D اجرا نمی‌شود. gold هر دو قبل از دیدن هر packet قفل و commit می‌شود | دو فایل gold commit شده؛ baseline روی `concept-dev` اندازه‌گیری شده |
| **C** — تاخیر | بازتولید ۲۶ ثانیه با زمان‌گیری دقیق (باینری release، cache خاموش/روشن)؛ اگر واقعی بود پروفایل و رفع | p95 تک‌query روی ریپوی خودمان < ۲ ثانیه بعد از ایندکس |
| **D** — بازیابی مفهومی | ریشه‌یابی هر miss با `NM_EXPLAIN`؛ فقط قانون‌های عمومی (مثلاً: کلمات سوال ↔ doc-comment و نام فایل/ماژول، هم‌ریشه‌سازی `dangerous→safe/unsafe`, `cap→max/limit`)؛ کالیبره کردن `coverage` تا وقتی seed ضعیف است `partial`/`no_confident_match` بگوید | `concept-dev` ≥ ۰.۸۵ recall@packet، هیچ «no_recorded_gap» روی جواب غلط؛ ۹ ست قبلی بدون افت (ratchet −۰.۰۲) |
| **E** — اثبات روی holdout | یک بار اجرای `concept-holdout` بعد از قفل شدن کد | عدد صادقانه گزارش شود، چه خوب چه بد؛ اگر < ۰.۷ یافته‌ها ثبت و یک دور اصلاح عمومی دیگر |
| **F** — انتشار | v1.0.1: CHANGELOG، `docs/measured.md` (ستون جدید concept)، باینری‌ها از release.yml، جواب انگلیسی برای یوسف با اعداد و ground truth اصلاح‌شده `retry_negative` | tag + سه باینری، سند پاسخ |
| **G** — بک‌لاگ | ستون‌های token_precision/overhead (measure-only)، سپس GitHub Pages rebrand | در `measured.md` و Pages فعال |

## قواعد اجرا

- هر فاز یک branch و یک PR روی `ParsaVictor/code-context-engine`؛ merge بعد از CI سبز.
- فایل‌های تغییرداده‌شده توسط session دیگر (`README.md`, `crates/neuromesh-cli/src/main.rs`, `nm.config.json`) دست نمی‌خورند؛ فقط `git add <file>` با نام.
- gold قبل از packet قفل؛ هیچ قانونی که فقط یک سوال را درست کند (hardcode اسم) پذیرفته نیست.
- ratchetها فقط بالا می‌روند.
- `retry_negative` باگ نیست: این fork واقعاً retry دارد (`crates/neuromesh-provider/src/anthropic.rs`)؛ در ست ما gold آن همان فایل‌های provider است.
Loading
Loading