From a2b21701ccee6d713edb0a26c80b190609e26a34 Mon Sep 17 00:00:00 2001 From: DQT-bit Date: Mon, 1 Jun 2026 11:55:02 +0800 Subject: [PATCH 1/2] fix(rx-go): replace nonexistent `reasonix run --preset` with `--model` mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `reasonix run` (reasonix CLI v0.53.x) has no `--preset` flag — its options are `--model / --system / --effort / --budget / --transcript / --mcp`. The current dispatch always passes `--preset "$PRESET"` (default `auto`), so commander aborts with `error: unknown option '--preset'` (exit 1) on every dispatch, breaking the whole rx-go → reasonix pipeline. Map the preset to reasonix's real `--model` flag instead, preserving the flash/pro/auto semantics: - flash → --model deepseek-v4-flash - pro → --model deepseek-v4-pro - auto → (omit --model, use config.json default model) Verified: `bash -n` passes; `reasonix run --preset auto` reproduces the error, the mapped form runs clean. Co-Authored-By: Claude Opus 4.8 --- bin/rx-go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bin/rx-go b/bin/rx-go index 980e684..39fb882 100755 --- a/bin/rx-go +++ b/bin/rx-go @@ -73,11 +73,20 @@ STDOUT_LOG="$LOGS_DIR/${TIMESTAMP}-${SLUG}.out" # --- dispatch 信息 --- echo "rx-go dispatch · 规格: $SPEC_FILE · 预算: \$$BUDGET · preset: $PRESET" +# --- preset → reasonix 模型映射 --- +# reasonix run 没有 --preset 选项,只有 --model;auto 用 config.json 默认模型(不传 --model) +MODEL_ARGS=() +case "$PRESET" in + flash) MODEL_ARGS=(--model deepseek-v4-flash) ;; + pro) MODEL_ARGS=(--model deepseek-v4-pro) ;; + auto|*) MODEL_ARGS=() ;; +esac + # --- dispatch --- TASK_CONTENT=$(cat "$SPEC_FILE") cd "$PROJECT_ROOT" EXIT_CODE=0 -reasonix run --budget "$BUDGET" --preset "$PRESET" --transcript "$TRANSCRIPT" "$TASK_CONTENT" > "$STDOUT_LOG" 2>&1 || EXIT_CODE=$? +reasonix run --budget "$BUDGET" "${MODEL_ARGS[@]}" --transcript "$TRANSCRIPT" "$TASK_CONTENT" > "$STDOUT_LOG" 2>&1 || EXIT_CODE=$? # --- 写 task state(.reasonix-tasks/state.json,#6)--- if [ -d ".reasonix-tasks" ]; then From d7636a89eb353ad16fcbd484bca52a468ac0aea8 Mon Sep 17 00:00:00 2001 From: DQT-bit Date: Mon, 1 Jun 2026 12:49:13 +0800 Subject: [PATCH 2/2] fix(rx-go): dispatch via Reasonix ACP instead of `run`; fix python3 Store shim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two deeper failures beyond the `--preset` flag (kept in this branch's history): 1. `reasonix run` never lands files. In run mode the agent streams tool calls as *text* (e.g. ``) but does not execute them — verified across multiple configs and prompts, files never hit disk. The whole rx-go pipeline silently produced nothing. Fix: dispatch through `reasonix acp` (the Agent Client Protocol agent on stdio NDJSON JSON-RPC) via a new `bin/rx-acp-dispatch.mjs`. ACP actually drives write_file / run_command tools. The client does initialize → session/new → session/prompt, auto-approves session/request_permission, and exits on stopReason=end_turn. 2. `python3` is a Microsoft Store alias stub on many Windows installs: `command -v python3` resolves it, but running it exits 49. That exit code propagated out of rx-go (state.json block) and masked dispatch success. Fix: probe interpreters by actually running `--version`, pick the first that works (python3 → python), in both rx-go and rx-doctor. Verified end-to-end: fresh spec (no reference code in spec) → reasonix writes palindrome.py + test_palindrome.py via ACP → `python -m unittest` passes, rx-go exits 0, state.json written. Co-Authored-By: Claude Opus 4.8 --- bin/rx-acp-dispatch.mjs | 119 ++++++++++++++++++++++++++++++++++++++++ bin/rx-doctor | 12 +++- bin/rx-go | 30 ++++++---- 3 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 bin/rx-acp-dispatch.mjs diff --git a/bin/rx-acp-dispatch.mjs b/bin/rx-acp-dispatch.mjs new file mode 100644 index 0000000..bfe8ed4 --- /dev/null +++ b/bin/rx-acp-dispatch.mjs @@ -0,0 +1,119 @@ +#!/usr/bin/env node +// rx-acp-dispatch — 通过 Reasonix ACP(Agent Client Protocol)派发规格任务。 +// +// 为什么不是 `reasonix run`:run 模式只把工具调用作为文本流式输出,不真正执行, +// 文件永远不会落盘。ACP(stdio NDJSON JSON-RPC)才会真正驱动 write_file / run_command +// 等工具落地改动。 +// +// 用法: node rx-acp-dispatch.mjs [budget] [model] +// 退出码: 0 = end_turn 正常完成;非 0 = 协议错误 / cancelled / 超时。 + +import { spawn } from "node:child_process"; +import { readFileSync } from "node:fs"; + +const [, , PROJECT_ROOT, SPEC_FILE, TRANSCRIPT, BUDGET, MODEL] = process.argv; + +if (!PROJECT_ROOT || !SPEC_FILE || !TRANSCRIPT) { + console.error("用法: rx-acp-dispatch.mjs [budget] [model]"); + process.exit(2); +} + +const TASK = readFileSync(SPEC_FILE, "utf-8"); + +const args = ["acp", "--dir", PROJECT_ROOT, "--transcript", TRANSCRIPT]; +if (BUDGET) args.push("--budget", BUDGET); +if (MODEL) args.push("--model", MODEL); + +const child = spawn("reasonix", args, { + stdio: ["pipe", "pipe", "inherit"], + shell: process.platform === "win32", + env: process.env, +}); + +let buf = ""; +let nextId = 1; +const pending = new Map(); + +function send(method, params) { + const id = nextId++; + child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n"); + return new Promise((res) => pending.set(id, res)); +} +function respond(id, result) { + child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n"); +} + +child.stdout.on("data", (chunk) => { + buf += chunk.toString(); + let nl; + while ((nl = buf.indexOf("\n")) >= 0) { + const line = buf.slice(0, nl).trim(); + buf = buf.slice(nl + 1); + if (!line) continue; + let m; + try { m = JSON.parse(line); } catch { continue; } + + // agent → client:工具执行权限请求。editMode=auto/yolo 下 agent 本应自动放行, + // 但这里仍显式批准,保证非交互管道不卡住。 + if (m.method === "session/request_permission") { + const opts = m.params?.options || []; + const allow = opts.find((o) => /allow/.test(o.optionId)) || opts[0]; + respond(m.id, { outcome: { outcome: "selected", optionId: allow?.optionId || "allow_always" } }); + continue; + } + // agent → client:进度通知。工具调用打印到 stderr(不污染 stdout 协议流)。 + if (m.method === "session/update") { + const u = m.params?.update; + if (u?.sessionUpdate === "tool_call") { + process.stderr.write(` [工具] ${u.title || u.kind || ""}\n`); + } else if (u?.sessionUpdate === "agent_message_chunk") { + process.stderr.write(u.content?.text || ""); + } + continue; + } + // 对本端请求的响应 + if (m.id != null && pending.has(m.id)) { + pending.get(m.id)(m); + pending.delete(m.id); + } + } +}); + +child.on("error", (e) => { + console.error("无法启动 reasonix:", e.message); + process.exit(3); +}); + +const TIMEOUT_MS = 180000; +const timer = setTimeout(() => { + console.error("\n[超时] ACP 派发超过 180s,强制终止"); + child.kill(); + process.exit(4); +}, TIMEOUT_MS); + +(async () => { + try { + await send("initialize", { protocolVersion: 1, clientCapabilities: {} }); + + const ns = await send("session/new", { cwd: PROJECT_ROOT, mcpServers: [] }); + const sid = ns.result?.sessionId; + if (!sid) { + console.error("session/new 失败:", JSON.stringify(ns.error || ns.result)); + clearTimeout(timer); child.kill(); process.exit(5); + } + + const pr = await send("session/prompt", { + sessionId: sid, + prompt: [{ type: "text", text: TASK }], + }); + const stop = pr.result?.stopReason; + process.stderr.write(`\n[stopReason] ${stop}\n`); + + clearTimeout(timer); + child.kill(); + process.exit(stop === "end_turn" ? 0 : 1); + } catch (e) { + console.error("ACP 派发异常:", e.message); + clearTimeout(timer); child.kill(); process.exit(6); + } +})(); diff --git a/bin/rx-doctor b/bin/rx-doctor index 47686e8..4f7d54b 100755 --- a/bin/rx-doctor +++ b/bin/rx-doctor @@ -5,6 +5,14 @@ set -uo pipefail SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) PLUGIN_ROOT=$(dirname "$SCRIPT_DIR") FAIL=0 +# python3 在部分 Windows 环境是 Store 占位符:command -v 能找到、但一运行就退 49。 +# 因此逐个实跑 --version,取第一个真正可用的解释器。 +PYTHON="" +for _py in python3 python; do + if command -v "$_py" >/dev/null 2>&1 && "$_py" --version >/dev/null 2>&1; then + PYTHON="$_py"; break + fi +done echo "═══ Reasonix 链路体检 ═══" echo "" @@ -51,8 +59,8 @@ echo "" # [4] task state(当前工作目录) echo "[4] task state(当前目录)" -if [ -f ".reasonix-tasks/state.json" ]; then - python3 -c " +if [ -f ".reasonix-tasks/state.json" ] && [ -n "$PYTHON" ]; then + "$PYTHON" -c " import json, sys try: with open('.reasonix-tasks/state.json', encoding='utf-8') as f: diff --git a/bin/rx-go b/bin/rx-go index 39fb882..d836d05 100755 --- a/bin/rx-go +++ b/bin/rx-go @@ -74,23 +74,33 @@ STDOUT_LOG="$LOGS_DIR/${TIMESTAMP}-${SLUG}.out" echo "rx-go dispatch · 规格: $SPEC_FILE · 预算: \$$BUDGET · preset: $PRESET" # --- preset → reasonix 模型映射 --- -# reasonix run 没有 --preset 选项,只有 --model;auto 用 config.json 默认模型(不传 --model) -MODEL_ARGS=() +# auto 用 config.json 默认模型(不指定 model);flash/pro 显式指定 +MODEL="" case "$PRESET" in - flash) MODEL_ARGS=(--model deepseek-v4-flash) ;; - pro) MODEL_ARGS=(--model deepseek-v4-pro) ;; - auto|*) MODEL_ARGS=() ;; + flash) MODEL="deepseek-v4-flash" ;; + pro) MODEL="deepseek-v4-pro" ;; + auto|*) MODEL="" ;; esac -# --- dispatch --- -TASK_CONTENT=$(cat "$SPEC_FILE") +# --- dispatch(经 ACP,真正执行工具落盘)--- +# 不用 `reasonix run`:run 只把工具调用作为文本输出、不执行,文件不会落盘。 +# 改走 reasonix acp(stdio JSON-RPC),由 rx-acp-dispatch.mjs 驱动 write_file 等工具真正落地。 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$PROJECT_ROOT" EXIT_CODE=0 -reasonix run --budget "$BUDGET" "${MODEL_ARGS[@]}" --transcript "$TRANSCRIPT" "$TASK_CONTENT" > "$STDOUT_LOG" 2>&1 || EXIT_CODE=$? +node "$SCRIPT_DIR/rx-acp-dispatch.mjs" "$PROJECT_ROOT" "$SPEC_FILE" "$TRANSCRIPT" "$BUDGET" "$MODEL" > "$STDOUT_LOG" 2>&1 || EXIT_CODE=$? # --- 写 task state(.reasonix-tasks/state.json,#6)--- -if [ -d ".reasonix-tasks" ]; then - python3 - "$SLUG" "$BUDGET" "$PRESET" "$EXIT_CODE" "$TRANSCRIPT" <<'PYEOF' +# python3 在部分 Windows 环境是 Store 占位符:command -v 能找到、但一运行就退 49。 +# 因此逐个实跑 --version,取第一个真正可用的解释器。 +PYTHON="" +for _py in python3 python; do + if command -v "$_py" >/dev/null 2>&1 && "$_py" --version >/dev/null 2>&1; then + PYTHON="$_py"; break + fi +done +if [ -d ".reasonix-tasks" ] && [ -n "$PYTHON" ]; then + "$PYTHON" - "$SLUG" "$BUDGET" "$PRESET" "$EXIT_CODE" "$TRANSCRIPT" <<'PYEOF' import json, sys, os, datetime slug, budget, preset, exit_code, transcript = sys.argv[1:6] sf = ".reasonix-tasks/state.json"