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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 0.12.5

- Handle the real Serena 1.7/FastMCP structuredContent.result string envelope before parsing symbols or references. Preserve error, empty-result, malformed and shortened-response semantics; do not discard unknown envelope metadata or silently prefer text over unsupported structured data.
- Add an opt-in real Serena acceptance command using an explicitly supplied installed command. It exercises overloaded C# identities, ambiguity, reference locations, direct upstream body evidence, legitimate empty results, process interruption with unavailable restart, inactive projects and owned-process cleanup. It neither installs dependencies nor reconnects the parent Codex client.

## 0.12.4

- Repomix health and packing launch an installed JavaScript bin directly with the current Node executable and separate arguments, eliminating the cmd/npx shell chain. Local package bin discovery and explicit absolute customCliPath are supported; shell wrappers and npx caches are no longer invoked. Missing or invalid entries use the builtin packer without installing anything.
Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ npm run delivery:verify

Reports and bounded stage logs are under `test-tmp/check/<run>/`. CI uploads only the compact report, including failures; it does not upload local workspaces or screenshots. A passing core check does not establish desktop or real-upstream acceptance.

`npm run test:serena-real -- <absolute installed command> [launcher arguments]` performs an opt-in real C# upstream acceptance in a generated `test-tmp/serena-acceptance/` fixture. Supply a command that accepts Serena CLI arguments and an isolated SERENA_HOME launcher; configure/download its C# language server only within an authorized setup. The script uses normal production adapter timeouts, records result/error/cleanup and PID exit, and never installs prerequisites. Body retrieval is a direct upstream oracle, not a claim that WinCode returns whole method bodies. Native reference coordinates may identify the containing class rather than the exact call line. This probe does not register or reload the user's Codex MCP connection.

For opt-in fixed-profile TavernDesk acceptance, use `npm run test:tavern-context -- <repository> --ui-pid=<PID> --ui-hwnd=<HWND>` and `npm run test:product -- <repository> <PID> <HWND>`. The latter checks the fixed profile receipt and performs six navigation-to-source tasks without source filenames supplied in advance. Native candidate discovery is counted, source reads used only as the oracle are separate, and all returned bodies are checked against current file hashes. Source candidates remain distinct from verified runtime bindings. The scripts do not install Serena, activate a language server, launch the target application or use personal databases.

The delivery manifest covers Gateway JavaScript, all published Host files including dependency sidecars, four managed Skill documents, and package/SDK/Host lock configuration. It records the Git revision and toolchains. Timestamps and checkout paths do not participate in content identity. Hashes detect local mismatches; they are not signatures. Run a complete check after changing delivery inputs. Keep a complete previous checkout/artifact set for rollback; do not mix old DLLs with a new Gateway.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ WinCode is a local MCP server built for Windows and .NET engineering. It bridges
- **Inspect the running app:** Enumerate visible windows, query specific controls or subtrees, and capture numbered visual overlays without activating or stealing focus from the target.
- **Review with evidence:** Trace on-screen widgets back to literal XAML declaration tags, line numbers, and file hashes, with transparent reporting for ambiguity, truncation, or degraded upstreams.

Current source version: **0.12.4**. All UI tools are strictly read-only and non-destructive. See [CHANGELOG](CHANGELOG.md) for full version history.
Current source version: **0.12.5**. All UI tools are strictly read-only and non-destructive. See [CHANGELOG](CHANGELOG.md) for full version history.

### Quick start

Expand Down Expand Up @@ -211,7 +211,7 @@ WinCode 是面向 Windows 与 .NET 工程研发的本地 MCP 服务。它将项
- **观察实际界面:**发现系统可见窗口,按条件定向查询目标控件或子树,并在不激活、不抢占前台焦点的前提下获取数字标注截图。
- **源码双向印证:**将运行时抓取的控件关联回 XAML 源码声明的起始行号、代码片段与文件哈希,清晰报告歧义、截断与降级状态。

当前源码版本为 **0.12.4**。所有 UI 取证工具均为纯只读与非侵入设计。版本历史见 [CHANGELOG](CHANGELOG.md)。
当前源码版本为 **0.12.5**。所有 UI 取证工具均为纯只读与非侵入设计。版本历史见 [CHANGELOG](CHANGELOG.md)。

### 快速上手

Expand Down
1 change: 1 addition & 0 deletions WinCode-下一轮工程化迭代计划书.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,4 @@ ToolRouter 是允许知道具体实现的组装位置;Gateway 不再访问适
- 2026-09-08 20:44:51(北京时间):WP4 / 0.12.2 精确 head 的 Node 22/24 与 CodeQL 全通过,合并 [PR #25](https://github.com/linnnn89/WinCode/pull/25),main=ba94c00。D3 仍未决,工程交付完成不等于分支保护已生效。
- WP5 / 0.12.3:已完成新 stdio 精准范围、固定 TavernDesk 六导航→源码候选、真实 WPF 修复闭环和实际截图核对;产品任务 30 次调用/67103 UTF-16 字符,127 显示源码行中 42 行重复,不宣称普遍提速。实际 Codex hello 仍为 0.11.2,安装 Skill 已备份同步。真实 Serena 命令缺失、用户新导入角色卡尚待定位,按本计划保持部分完成;详见工作日志验收矩阵。F12/D3 未决状态不变。
- WP5 新卡补充:用户指明书架角色后,入库、原始 JSON 字段保持、头像文件与书架截图已核对;在新 0.12.3 Gateway/Host 中,同名两文本节点正确返回完整但 ambiguous,不猜测唯一项。含真实卡的六导航取证再度 6/6;详情动作/聊天、真实 Serena、当前 Codex 重连仍未验收。不会把 Computer Use 误归属问题记为 WinCode 故障。
- 2026-09-08 21:43(北京时间)补充:用户同意尝试四项检查后,D3 main 保护已启用并回读,F12 经 PR #27 合并后 CodeQL alert #1 自动 fixed。用户另行批准隔离安装 Serena(1 GB / 15 分钟上限),真实 1.7.0/Roslyn 链暴露并定位 FastMCP result 包装兼容缺陷;0.12.5 修复及七项真实验收已进入最终回归。正文证据为直接上游 oracle,WinCode 符号/引用为真实 adapter;父 Codex 重连仍未完成,详见工作日志。
10 changes: 10 additions & 0 deletions docs/codex_worklog.md
Original file line number Diff line number Diff line change
Expand Up @@ -495,3 +495,13 @@
- Serena 固定 v1.7.0 commit=949a27ef1e5fda1a6e7b561e777bcece345c6ffd,复用现有 Python 3.13.7,venv/uv-cache/SERENA_HOME/语言服务位于 test-tmp/serena-real;安装完成约 632 MB,.NET 10 复用已有环境。C# Roslyn=5.5.0-2.26078.4,下载校验采用上游固定 SHA-256。握手真实成功,第一次查询发现 structuredContent.result 包裹 JSON 字符串被当成符号对象;上游 content 内实际返回三个完整身份。此新缺陷作为后续兼容修复,不把本 PR 写成 Serena 已验收。
- 当前 Codex 管理 CLI 只有 list/get/add/remove/login/logout,没有受支持的 reconnect 命令。保存配置指向正确 I:/WinCode/dist/index.js;没有用重复注册或杀进程替代重连,也未宣称旧父连接更新。
- 作者反证自审:无 shell 不等于已安装脚本可信或有沙盒;缺失显式 CLI 不应悄悄执行另一安装。CodeQL 任务成功仍需合并后读取 alert #1 的 fixed 状态;PR/远端验收待后续回执。

## 2026-09-08 21:43 — 0.12.5 真实 Serena 兼容闭环

- 0.12.4 PR #27 精确 head=b912343 的 Node 22/24 和三语言 CodeQL 均通过,2026-09-08 21:40:05 在启用保护后正常 squash 合并,main=41602e0af4a1738f2542ae513eec1dda1485df29。未使用 admin bypass。main 扫描后 alert #1 已自动 fixed,fixed_at=2026-09-08T13:41:50Z;未手动 dismiss。
- 真实 Serena 原始返回同时包含 content JSON 和 structuredContent={result:JSON字符串}。旧实现优先序列化整个 structuredContent,误判为不支持的符号结构。现在仅解开唯一 result 字符串包装,其余结构仍执行原有校验;不吞掉未知元数据,不将畸形 structuredContent 的 text 作为成功替代。
- 新增六项回归与既有身份/降级套件合计 52/52:真实包装的符号/引用、合法空数组、未激活错误、截断、非 JSON、额外 metadata。引用坐标仍明确 containing-symbol,不伪装为精确调用行。
- 新增显式 test:serena-real,不安装组件、不修改 Codex 注册。复用已批准隔离安装的 Serena 1.7.0 与 Roslyn,按生产默认连接/调用超时执行专用 C# 项目;七项通过:三个同名/重载身份、歧义不猜测、两个重载分别命中各自调用、直接上游正文与源码逐字一致、合法空符号/零引用、终止自有上游后将测试重启命令设为缺失并确认降级、真实未激活项目。保留 direct upstream body oracle 和 WinCode adapter 查询的证据边界,不宣称新增全方法正文产品接口。
- 报告 test-tmp/serena-acceptance/1788874762075-31176/report.json 与 1788874881144-38796/report.json;每次记录真实结果、PID 退出、dispose 成败,测试 C# 源码保持一致。进一步自审加强为核对引用预览中 > 标记所在行,避免“周围文本同时有另一重载”造成假阳性;最终复测回执另存本地。查询已隔离安装目录对应 python/dotnet 进程,没有残留匹配进程。Serena 环境最终约 632 MB,复用 Python 3.13.7,无全局 PATH 修改、无全局 Serena 启用。
- 首轮 0.12.5 全量回归和 stdio 通过,交付阶段因检查运行期间补充 package.json 维护命令触发源码/构建指纹不一致而失败;这是有效的一致性保护。冻结变更后完整重跑,不绕过交付校验。失败报告 2026-09-08T13-40-56-234Z-core。
- 当前 Codex 旧连接仍需客户端重连;没有提供可调用的重连接口,不用结束 Codex/强杀 Gateway 冒充成功。作者自审,未新增独立审查或子代理。
4 changes: 2 additions & 2 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "wincode-mcp",
"version": "0.12.4",
"version": "0.12.5",
"description": "Windows-first MCP gateway: .NET project graph, evidence-bounded context, honest change-impact, long-running process hygiene",
"main": "dist/index.js",
"type": "module",
Expand All @@ -17,6 +17,7 @@
"benchmark:agent": "tsx scripts/benchmark-agent-efficiency.ts",
"test:benchmark": "tsx --test tests/agent-efficiency-benchmark.test.ts",
"test:product": "tsx scripts/verify-product-tasks.ts",
"test:serena-real": "tsx scripts/verify-serena-real.ts",
"test:tavern-context": "tsx scripts/verify-tavern-context.ts",
"test:e2e": "tsx scripts/test-mcp-client.ts",
"typecheck": "tsc -p tsconfig.test.json",
Expand Down
135 changes: 135 additions & 0 deletions scripts/verify-serena-real.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
import { SerenaAdapter } from '../src/Adapters/SerenaAdapter.js';
import { CacheManager } from '../src/Core/Cache.js';
import { getDefaultConfig } from '../src/Core/Config.js';
import { killProcessTree } from '../src/Core/ResourceManager.js';

// Explicit opt-in: use an already installed, isolated Serena command. Never install prerequisites here.
const [command, ...prefixArgs] = process.argv.slice(2);
if (!command || !path.isAbsolute(command)) throw new Error('Usage: test:serena-real -- <absolute command> [launcher arguments]. The command must accept Serena CLI arguments.');
await fs.access(command);
const root = path.resolve('test-tmp/serena-acceptance', `${Date.now()}-${process.pid}`);
await fs.mkdir(path.join(root, '.serena'), { recursive: true });
await fs.copyFile('global.json', path.join(root, 'global.json'));
await fs.writeFile(path.join(root, '.serena/project.yml'), 'project_name: wincode-real-acceptance\nlanguage_servers: [csharp]\nread_only: true\n');
await fs.writeFile(path.join(root, 'Fixture.csproj'), '<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net10.0</TargetFramework></PropertyGroup></Project>');
const source = `namespace Fixture;
public class Service {
public int Save(int value) { return value + 1; }
public string Save(string value) { return value + "!"; }
public int Unused() { return 42; }
}
public class Other {
public int Save(int value) { return value - 1; }
}
public class Caller {
public int Run() { return new Service().Save(7); }
public string Text() { return new Service().Save("y"); }
}
`;
await fs.writeFile(path.join(root, 'Service.cs'), source);
const report: any = { startedAt: new Date().toISOString(), command, prefixArgs, fixture: root, stages: [], passed: false };
const adapters: SerenaAdapter[] = [];
async function create(active: boolean) {
const config = getDefaultConfig(root);
config.adapters.serena.customCommand = command;
config.adapters.serena.customArgs = [...prefixArgs, 'start-mcp-server',
...(active ? ['--project', root] : []), '--enable-web-dashboard', 'false', '--open-web-dashboard', 'false',
'--enable-gui-log-window', 'false', '--log-level', 'WARNING'];
const cache = new CacheManager(path.join(root, active ? 'active-cache' : 'inactive-cache'));
await cache.initialize();
const adapter = new SerenaAdapter(config, cache);
adapters.push(adapter);
await adapter.initialize();
return { adapter, config };
}
async function stage(name: string, run: () => Promise<unknown>) {
const start = Date.now();
try { const result = await run(); report.stages.push({ name, passed: true, ms: Date.now() - start, result }); }
catch (error) { report.stages.push({ name, passed: false, ms: Date.now() - start, error: String(error) }); throw error; }
}
try {
const { adapter, config } = await create(true);
await stage('same-name and overload identities through WinCode adapter', async () => {
const found = await adapter.findSymbolsDetailed('Save', undefined, 'Service.cs');
assert.equal(found.source, 'serena-mcp'); assert.equal(found.queryComplete, true);
assert.deepEqual(found.symbols.map(s => [s.namePath, s.line]), [
['Fixture/Service/Save[0]', 3], ['Fixture/Service/Save[1]', 4], ['Fixture/Other/Save', 8],
]);
assert.equal(adapter.getUpstreamStatus().semanticQueryUsable, true);
return { found, health: adapter.getUpstreamStatus() };
});
await stage('ambiguous name never selects an overload', async () => {
const refs = await adapter.findReferencesDetailed('Save', 'Service.cs');
assert.equal(refs.resolution, 'ambiguous'); assert.equal(refs.candidateCount, 3);
assert.equal(refs.queryComplete, false); return refs;
});
await stage('explicit overload references retain containing-symbol coordinates', async () => {
const results = [];
for (const [name, preview, zeroReferenceLine] of [
['Fixture/Service/Save[0]', 'Save(7)', 10], ['Fixture/Service/Save[1]', 'Save("y")', 11],
] as const) {
const refs = await adapter.findReferencesDetailed(name, 'Service.cs');
assert.equal(refs.source, 'serena-mcp'); assert.equal(refs.queryComplete, true);
assert.equal(refs.resolution, 'resolved'); assert.equal(refs.totalReferences, 1);
assert.equal(refs.target?.namePath, name); assert.ok(refs.references[0].preview.includes(preview));
const marked = refs.references[0].preview.split('\n').find(line => /^\s*>\s*\d+:/.test(line));
assert.ok(marked?.includes(`> ${zeroReferenceLine}:`) && marked.includes(preview), 'marked reference, not merely surrounding context, matches this overload');
assert.equal(refs.references[0].lineKind, 'containing-symbol');
assert.equal(refs.references[0].line, 10); results.push(refs);
}
return results;
});
await stage('real upstream body matches fixture source (direct upstream oracle)', async () => {
// Test-only access to the actual connected upstream; do not add a public product API for this oracle.
const result = await (adapter as any).serenaClient.callTool({ name: 'find_symbol', arguments: {
name_path_pattern: '/Fixture/Service/Save[0]', relative_path: 'Service.cs', include_body: true,
} });
assert.notEqual(result.isError, true);
const body = JSON.parse(result.content[0].text);
assert.equal(body.length, 1); assert.equal(body[0].body, source.split('\n')[2].trim());
return result;
});
await stage('valid empty symbols and references stay semantic', async () => {
const empty = await adapter.findSymbolsDetailed('AbsentSymbol', undefined, 'Service.cs');
const unused = await adapter.findReferencesDetailed('Fixture/Service/Unused', 'Service.cs');
assert.equal(empty.source, 'serena-mcp'); assert.equal(empty.queryComplete, true); assert.equal(empty.totalFound, 0);
assert.equal(unused.source, 'serena-mcp'); assert.equal(unused.queryComplete, true); assert.equal(unused.totalReferences, 0);
return { empty, unused };
});
await stage('actual process interruption and unavailable restart degrade honestly', async () => {
const pid = (adapter as any).serenaPid as number;
assert.ok(pid > 0);
// Only the test-owned Serena tree is terminated. Its restart command is made unavailable in this fixture.
config.adapters.serena.customCommand = path.join(root, 'missing-serena.exe');
await killProcessTree({ pid });
for (let i = 0; i < 100 && adapter.getUpstreamStatus().handshakeOk; i++) await new Promise(r => setTimeout(r, 20));
const fallback = await adapter.findSymbolsDetailed('Unused', undefined, 'Service.cs');
assert.equal(fallback.source, 'serena-adapter-fallback');
assert.equal(adapter.getUpstreamStatus().semanticQueryUsable, false);
assert.throws(() => process.kill(pid, 0), { code: 'ESRCH' });
return { fallback, health: adapter.getUpstreamStatus(), pidExited: pid };
});
await stage('real unactivated project remains inactive', async () => {
const { adapter: inactive } = await create(false);
const found = await inactive.findSymbolsDetailed('Save', undefined, 'Service.cs');
assert.equal(found.source, 'serena-adapter-fallback'); assert.equal(found.queryComplete, false);
assert.equal(inactive.getUpstreamStatus().projectActive, false);
return { found, health: inactive.getUpstreamStatus() };
});
assert.equal(await fs.readFile(path.join(root, 'Service.cs'), 'utf8'), source);
report.passed = true;
} catch (error) { report.error = String(error); process.exitCode = 1; }
finally {
const pids = adapters.map(a => (a as any).serenaPid as number | null).filter((p): p is number => Boolean(p));
const cleanup = await Promise.allSettled(adapters.map(a => a.dispose()));
report.cleanup = cleanup.map(r => r.status === 'fulfilled' ? { closed: true } : { closed: false, error: String(r.reason) });
if (cleanup.some(r => r.status === 'rejected')) { report.passed = false; process.exitCode = 1; }
report.pidExit = pids.map(pid => { try { process.kill(pid, 0); return { pid, exited: false }; } catch (e: any) { return {pid, exited: e.code === 'ESRCH'}; } });
if (report.pidExit.some((p: any) => !p.exited)) { report.passed = false; process.exitCode = 1; }
report.completedAt = new Date().toISOString();
await fs.writeFile(path.join(root, 'report.json'), JSON.stringify(report, null, 2));
console.log(JSON.stringify({ passed: report.passed, stages: report.stages.map((s: any) => ({ name: s.name, passed: s.passed })), report: path.join(root, 'report.json') }));
}
Loading