Initial commit: qwen delegation MCP server, tracker setup, research

- qwen_delegate MCP tool: src/qwen-delegate.ts (testable core, subprocess
  spawn/timeout/parse) + src/qwen-delegate-server.ts (thin MCP stdio wiring)
- test/qwen-delegate.test.ts (node:test, mocked spawn)
- docs/agents/* + CLAUDE.md from /setup-matt-pocock-skills (Gitea issue
  tracker via tea CLI, default triage labels, single-context domain docs)
- research/qwen-mcp-delegation.md, corrected after confirming qwen-code
  runs natively on Windows (no WSL) against a local OpenAI-compatible proxy

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRnb5Gqdu7gVTQrwAqFdfJ
This commit is contained in:
2026-09-06 16:43:49 +02:00
co-authored by Claude-Bot
commit c3b6a9d5b5
12 changed files with 1656 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
// Core delegation logic: spawn the qwen-code CLI non-interactively and capture its reply.
// Kept separate from the MCP server wiring (qwen-delegate-server.ts) so it's unit-testable
// without a live MCP connection or a live qwen endpoint.
import { spawn, type ChildProcess } from "node:child_process";
export interface DelegateResult {
ok: boolean;
output: string; // stdout (ok) or combined error detail (!ok)
}
export interface DelegateOptions {
/** Milliseconds before giving up. qwen-code has been observed taking 3-6 min for a trivial
* prompt against a local model proxy, so default generous — see research/qwen-mcp-delegation.md. */
timeoutMs?: number;
/** Injectable for tests. Defaults to node:child_process's spawn. */
spawnFn?: typeof spawn;
}
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000; // 10 min
/**
* Runs `qwen -p "<prompt>"` non-interactively and returns its trimmed stdout.
* qwen-code prints startup warnings (e.g. failed MCP sub-servers) to stderr; those are
* ignored on success and surfaced only when the run itself fails.
*/
export function delegateToQwen(
prompt: string,
options: DelegateOptions = {},
): Promise<DelegateResult> {
const { timeoutMs = DEFAULT_TIMEOUT_MS, spawnFn = spawn } = options;
return new Promise((resolve) => {
const child: ChildProcess = spawnFn("qwen", ["-p", prompt], {
shell: true, // qwen.cmd on Windows needs a shell to resolve
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
let settled = false;
const timer = setTimeout(() => {
if (settled) return;
settled = true;
child.kill();
resolve({ ok: false, output: `qwen timed out after ${timeoutMs}ms` });
}, timeoutMs);
child.stdout?.on("data", (chunk) => (stdout += chunk));
child.stderr?.on("data", (chunk) => (stderr += chunk));
child.on("error", (err) => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve({ ok: false, output: `failed to start qwen: ${err.message}` });
});
child.on("close", (code) => {
if (settled) return;
settled = true;
clearTimeout(timer);
if (code === 0) {
resolve({ ok: true, output: stdout.trim() });
} else {
const detail = stderr.trim() || stdout.trim() || "(no output)";
resolve({ ok: false, output: `qwen exited with code ${code}: ${detail}` });
}
});
});
}