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
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
},
"metadata": {
"description": "Polycli host adapters for Claude Code and related agent CLIs",
"version": "0.6.25"
"version": "0.6.26"
},
"plugins": [
{
"name": "polycli",
"description": "Claude Code adapter for the shared polycli companion",
"version": "0.6.25",
"version": "0.6.26",
"source": "./plugins/polycli"
}
]
Expand Down
4 changes: 2 additions & 2 deletions .github/plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
},
"metadata": {
"description": "Polycli marketplace for GitHub Copilot CLI",
"version": "0.6.25"
"version": "0.6.26"
},
"plugins": [
{
"name": "polycli-copilot",
"description": "Run the shared polycli companion from GitHub Copilot CLI",
"version": "0.6.25",
"version": "0.6.26",
"source": "./plugins/polycli-copilot"
}
]
Expand Down
4 changes: 2 additions & 2 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@

これは **ユーティリティ専用の Path B モノレポ** です。プロバイダ間の差異を偽の抽象化で覆い隠したり、ランタイム基底クラスを発明したりはしません。公式の上流 CLI をサブプロセスとして組み合わせ、単一のコマンド面を公開し、4 状態の timing スキーマで能力の違いを正直に表現します。

## 最新リリース: v0.6.24
## 最新リリース: v0.6.26

現在の公開版は Claude `ask` / `review` の headless `claude -p` 既定経路を維持しつつ、multi-provider release review で見つかった `status --wait` timeout セマンティクスを修正しています。詳細は英語の release notes を参照してください: [`docs/release-notes-v0.6.24.md`](./docs/release-notes-v0.6.24.md)。
v0.6.25 の cc-X エンドポイントレシピを土台に、review で見つかった Grok のバグとリリース文書のドリフトを修正しました。Grok のネストされた error オブジェクト(`{error:{message:...}}`、可視テキストあり)は `ok:true` ではなく正しく失敗として扱われます。cc-X バリデータは `status` を `verified` / `marketplace-unstable` に限定し、文書では構造 + source の裏付けのみを保証し現時点の真正性は保証しないことを明記しました。v0.6.24 を指したままだった README 三言語と roadmap スナップショットも同期しました。Claude `ask` / `review` は引き続き headless `claude -p` 既定経路です。詳細は英語の release notes を参照してください: [`docs/release-notes-v0.6.26.md`](./docs/release-notes-v0.6.26.md)。

## なぜ polycli を使うのか?

Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,17 @@

It is a **utility-only Path B monorepo**: it does not unify provider differences behind fake abstractions, and it does not invent a runtime base class. It composes the official upstream CLIs as subprocesses, exposes one command surface, and surfaces honest capability differences in a four-state timing schema.

## Latest release: v0.6.24
## Latest release: v0.6.26

The latest patch keeps the v0.6.22 Claude `ask`/`review` print-mode defaults and hardens status wait behavior found by multi-provider release review:
The latest patch fixes a Grok review-found bug and release-doc drift on top of v0.6.25's cc-X endpoint recipes:

- `status --all --wait --json` now exits 2 when it times out instead of returning success with `waitTimedOut:true`.
- Invalid `--timeout-ms` values are rejected as positive-integer errors instead of becoming `NaN`.
- Text `status --all --wait` timeout output now says it timed out before rendering the running-jobs snapshot.
- Claude `ask` and `review` still use headless `claude -p` by default with plan/no-tools/no-MCP constraints, while explicit/internal tmux TUI runtime support remains covered.
- Utility packages stay on their independent v1.x cadence.
- Grok: a NESTED error object (`{error:{message:...}}`) with visible text is now correctly reported as failed instead of `ok:true` — the previous recursion missed an error payload that carried no `type`/`is_error` marker.
- cc-X recipe validator now constrains `status` to `verified` / `marketplace-unstable` (rejecting unlabeled entries), and the docs state plainly that it guards structure + source-anchoring, not current-truth.
- Synced the release-state docs (README in three languages, roadmap snapshot) that still pointed at v0.6.24.
- Builds on v0.6.25: the re-verified workflow-review remediation, the tmux test stabilization, and the cc-X domestic-model endpoint recipes (`docs/cc-x-endpoints.md` / `docs/cc-x-recipes.json`) that ride the existing `claude`/`opencode` runtimes — cc-X is documented config, **not** a new provider adapter.
- Claude `ask` and `review` still use headless `claude -p` by default with plan/no-tools/no-MCP constraints; utility packages stay on their independent v1.x cadence.

See [`docs/release-notes-v0.6.24.md`](./docs/release-notes-v0.6.24.md).
See [`docs/release-notes-v0.6.26.md`](./docs/release-notes-v0.6.26.md).

## Why polycli?

Expand Down
4 changes: 2 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@

这是一个 **utility-only 的 Path B monorepo**:不假装能抹平 provider 之间的差异,也不引入 runtime 基类。它把官方上游 CLI 作为子进程组合起来,统一命令面,并通过四态 timing schema 如实暴露能力差异。

## 最新版本:v0.6.24
## 最新版本:v0.6.26

当前公开版本保持 Claude `ask` / `review` 的 headless `claude -p` 默认路径,并修复了多 provider 发布审查发现的 `status --wait` timeout 语义。详情见英文 release notes:[`docs/release-notes-v0.6.24.md`](./docs/release-notes-v0.6.24.md)。
在 v0.6.25 的 cc-X 端点 recipe 基础上,修复了 review 发现的 Grok bug 与发布文档漂移:Grok 嵌套 error 对象(`{error:{message:...}}`,带可见文本)现在正确判为失败而非 `ok:true`;cc-X validator 将 `status` 限定为 `verified` / `marketplace-unstable`,文档明确它只保结构 + source 锚定、不保当前真值;同步了仍指向 v0.6.24 的 README 三语与 roadmap 快照。Claude `ask` / `review` 仍走 headless `claude -p` 默认路径。详情见英文 release notes:[`docs/release-notes-v0.6.26.md`](./docs/release-notes-v0.6.26.md)。

## 为什么要用 polycli?

Expand Down
41 changes: 41 additions & 0 deletions docs/release-notes-v0.6.26.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# polycli v0.6.26

Patch on top of `v0.6.25` that fixes a review-found Grok runtime bug, tightens the cc-X recipe validator, adds execution-layer coverage, and syncs release-state documentation drift.

The v0.6.22 Claude behavior remains unchanged: ordinary Claude `ask` / `review` calls use headless `claude -p` with plan/no-tools/no-MCP constraints, while explicit/internal tmux TUI runtime support remains available.

## What changed

### Grok nested-error detection (High fix)

- `extractTerminalError()` recursed into a nested `error` object but only extracted a message when that object carried a `type:"error"` / `is_error` marker. A real shape like `{text:"partial", error:{message:"permission denied"}}` therefore returned no error, and the visible partial text was wrongly reported as `ok:true`.
- A nested error OBJECT is now treated as a terminal-error signal in its own right: the parser recurses for deeper nesting, then pulls the object's `message`/`data`, and falls back to a generic marker when the object is non-empty but unlabeled. An empty `{}` is still NOT treated as an error (no false positive), and clean successes are unaffected.
- Covers both `parseGrokJsonResult` and `parseGrokStreamText`.

### cc-X recipe validator contract

- `scripts/validate-cc-x-recipes.mjs` now constrains `status` to `verified` / `marketplace-unstable` (non-marketplace entries must be `verified`), so an unlabeled/draft entry no longer passes.
- `docs/roadmap.md` Q10 now states plainly that the validator guards STRUCTURE + source-anchoring (required fields, a `source{url,date}` per entry, the constrained `status`, the marketplace honest-default) — NOT that the endpoints/models are currently accurate, which stays a per-entry `source`-URL re-check.

### Coverage and documentation

- Added a `runQwenPrompt` regression that logs the spawned argv and asserts an explicit `--model` is forwarded end-to-end (previously only the builder layer was covered).
- Added Grok regressions for the nested-error object (json + streaming) and an empty-error-object non-failure.
- Synced release-state docs that still pointed at v0.6.24: `README.md`, `README.zh-CN.md`, `README.ja.md`, and the `docs/roadmap.md` snapshot line.

### Known pre-existing (not fixed here)

- `scripts/validate-fixture-metadata.mjs` does not enforce the `docs/capture-fixtures.md` path/meta consistency rules (provider matches directory, name matches file stem) — a pre-existing Low gap, flagged for a separate change.

## Verification

- Reproduced the Grok bug before the fix (`ok:true`) and confirmed `ok:false` with the error message preserved after, across nested/deep/string/empty/clean shapes.
- Focused: grok + qwen + cc-X validator tests passed (55/55); `node scripts/validate-cc-x-recipes.mjs` ok (9 entries).
- `npm test` and `npm run release:check` green.

## Release artifacts

- GitHub release `v0.6.26`: https://github.com/bbingz/polycli/releases/tag/v0.6.26
- npm `@bbingz/polycli@0.6.26` and `@bbingz/polycli-opencode@0.6.26` (`latest`).

Utility packages stay on the independent v1.x cadence (`@bbingz/polycli-utils@1.0.2`, `@bbingz/polycli-timing@1.0.1`).
4 changes: 2 additions & 2 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Roadmap

Snapshot: 2026-06-16 (v0.6.24 is the latest public release; it keeps Claude ask/review on headless `claude -p` defaults and hardens status wait timeout behavior found by multi-provider release review).
Snapshot: 2026-06-19 (v0.6.26 is the latest public release; it fixes a review-found Grok nested-error bug, tightens the cc-X recipe validator, and syncs release-state docs, on top of v0.6.25's re-verified remediation + cc-X endpoint recipes, while keeping Claude ask/review on headless `claude -p` defaults).

This file lives next to `docs/release.md` (what's shipped) and `CHANGELOG.md` (what happened). It answers the complementary question: **what's open, how it's prioritized, and what we're deliberately not doing.**

Expand Down Expand Up @@ -125,7 +125,7 @@ Items:

Source: 2026-06-19 cc-X research (point Claude Code / opencode at a domestic vendor's Anthropic-compatible endpoint via `ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN` + `ANTHROPIC_MODEL`).

Status: landed as docs + reference data, Path-B-pure. `docs/cc-x-endpoints.md` + machine-readable `docs/cc-x-recipes.json` (guarded by `scripts/validate-cc-x-recipes.mjs` + its paired test) encode the core-lab endpoint matrix, the operational gotchas (silent prompt-cache degradation, `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` pin, `CLAUDE_CODE_AUTO_COMPACT_WINDOW` sizing, marketplace model-identity instability, data-sovereignty gate), and the honest-default refusal to pin a model/version for the Baidu/Tencent marketplace endpoints. cc-X rides the EXISTING `claude`/`opencode` runtimes via standard env vars — verified zero runtime change needed (`claude.js` already forwards the `ANTHROPIC_*` trio on both the `claude -p` and tmux paths). NO cc-X provider/adapter/runtime was added; that refusal is the decision, recorded in Explicit non-goals below.
Status: landed as docs + reference data, Path-B-pure. `docs/cc-x-endpoints.md` + machine-readable `docs/cc-x-recipes.json` record the core-lab endpoint matrix, the operational gotchas (silent prompt-cache degradation, `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` pin, `CLAUDE_CODE_AUTO_COMPACT_WINDOW` sizing, marketplace model-identity instability, data-sovereignty gate), and the honest-default refusal to pin a model/version for the Baidu/Tencent marketplace endpoints. `scripts/validate-cc-x-recipes.mjs` + its paired test guard STRUCTURE only — required fields, a `source{url,date}` per entry, a constrained `status` (`verified` / `marketplace-unstable`), and the marketplace honest-default — NOT that the endpoints/models are currently accurate (that stays a per-entry `source`-URL re-check, since a live probe is out of scope). cc-X rides the EXISTING `claude`/`opencode` runtimes via standard env vars — verified zero runtime change needed (`claude.js` already forwards the `ANTHROPIC_*` trio on both the `claude -p` and tmux paths). NO cc-X provider/adapter/runtime was added; that refusal is the decision, recorded in Explicit non-goals below.

---

Expand Down
9 changes: 8 additions & 1 deletion packages/polycli-runtime/src/grok.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,14 @@ function extractTerminalError(value) {
if (!value || typeof value !== "object") return null;
if (typeof value.error === "string" && value.error.trim()) return value.error.trim();
if (value.error && typeof value.error === "object") {
return extractTerminalError(value.error);
// A nested error OBJECT is itself a terminal-error signal even without a type/is_error marker.
// Recurse for deeper nesting, then pull its message/data, and fall back to a generic marker when
// the object is non-empty but unlabeled. (An empty {} is not treated as an error.)
const nested = extractTerminalError(value.error);
if (nested) return nested;
if (typeof value.error.message === "string" && value.error.message.trim()) return value.error.message.trim();
if (typeof value.error.data === "string" && value.error.data.trim()) return value.error.data.trim();
return Object.keys(value.error).length > 0 ? "grok emitted a terminal error" : null;
}
if (value.is_error === true || value.isError === true || value.type === "error") {
if (typeof value.message === "string" && value.message.trim()) return value.message.trim();
Expand Down
38 changes: 38 additions & 0 deletions packages/polycli-runtime/test/grok.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,31 @@ test("parseGrokJsonResult fails on terminal error metadata even with visible tex
assert.equal(parsed.error, "permission denied");
});

test("parseGrokJsonResult fails on a NESTED error object whose payload is only a message", () => {
// Regression: a nested error object ({error:{message:...}}) carries no type/is_error marker, so
// the old recursion returned null and a visible-text response was wrongly reported ok:true.
const parsed = parseGrokJsonResult(
JSON.stringify({ text: "partial", error: { message: "permission denied" } }),
"",
0
);

assert.equal(parsed.ok, false);
assert.equal(parsed.response, "partial");
assert.equal(parsed.error, "permission denied");
});

test("parseGrokJsonResult does not flag an empty error object as a failure", () => {
const parsed = parseGrokJsonResult(
JSON.stringify({ text: "all good", error: {} }),
"",
0
);

assert.equal(parsed.ok, true);
assert.equal(parsed.error, null);
});

test("parseGrokJsonResult fails on a non-success stopReason alone (no error metadata) and keeps partial text", () => {
// stopReason-only failure: no error field/event, so providerError is null and the failure must be
// driven solely by isNonSuccessStopReason. Reverting that branch would flip ok back to true.
Expand Down Expand Up @@ -130,6 +155,19 @@ test("parseGrokStreamText records terminal error events and non-success stop rea
assert.equal(parsed.providerError, "permission denied");
});

test("parseGrokStreamText captures a nested error object payload (message-only)", () => {
const parsed = parseGrokStreamText(
[
'{"type":"text","data":"partial"}',
'{"type":"error","error":{"message":"permission denied"}}',
'{"type":"end","stopReason":"EndTurn"}',
].join("\n")
);

assert.equal(parsed.response, "partial");
assert.equal(parsed.providerError, "permission denied");
});

test("runGrokPrompt parses json output and ignores transient stderr worker noise on success", () => {
withFakeGrokBin(
`#!/usr/bin/env node
Expand Down
25 changes: 25 additions & 0 deletions packages/polycli-runtime/test/qwen.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,31 @@ process.stdout.write(JSON.stringify({ type: "result", subtype: "success", result
);
});

test("runQwenPrompt forwards an explicit --model through to the spawned argv", () => {
withFakeQwenBin(
`#!/usr/bin/env node
const fs = require("node:fs");
if (process.env.QWEN_ARGV_LOG) fs.writeFileSync(process.env.QWEN_ARGV_LOG, JSON.stringify(process.argv.slice(2)));
process.stdout.write(JSON.stringify({ type: "result", subtype: "success", result: "ok", is_error: false }) + "\\n");
`,
({ root, env }) => {
const argvLog = path.join(root, "argv.json");
const result = runQwenPrompt({
prompt: "ping",
cwd: root,
model: "qwen-explicit-model",
env: { ...env, QWEN_ARGV_LOG: argvLog },
});

assert.equal(result.ok, true);
const argv = JSON.parse(fs.readFileSync(argvLog, "utf8"));
const modelIndex = argv.indexOf("--model");
assert.ok(modelIndex >= 0, "spawned argv should include --model");
assert.equal(argv[modelIndex + 1], "qwen-explicit-model");
}
);
});

test("runQwenPrompt surfaces result-only errors", () => {
withFakeQwenBin(
`#!/usr/bin/env node
Expand Down
6 changes: 5 additions & 1 deletion packages/polycli-terminal/bin/polycli-companion.bundle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3919,7 +3919,11 @@ function extractTerminalError(value) {
if (!value || typeof value !== "object") return null;
if (typeof value.error === "string" && value.error.trim()) return value.error.trim();
if (value.error && typeof value.error === "object") {
return extractTerminalError(value.error);
const nested = extractTerminalError(value.error);
if (nested) return nested;
if (typeof value.error.message === "string" && value.error.message.trim()) return value.error.message.trim();
if (typeof value.error.data === "string" && value.error.data.trim()) return value.error.data.trim();
return Object.keys(value.error).length > 0 ? "grok emitted a terminal error" : null;
}
if (value.is_error === true || value.isError === true || value.type === "error") {
if (typeof value.message === "string" && value.message.trim()) return value.message.trim();
Expand Down
2 changes: 1 addition & 1 deletion packages/polycli-terminal/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bbingz/polycli",
"version": "0.6.25",
"version": "0.6.26",
"description": "Terminal CLI for Polycli provider diagnostics and host-compatible commands.",
"type": "module",
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion plugins/polycli-codex/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "polycli-codex",
"version": "0.6.25",
"version": "0.6.26",
"description": "Codex skill adapter that routes provider CLI work through the shared polycli companion.",
"author": {
"name": "bing"
Expand Down
6 changes: 5 additions & 1 deletion plugins/polycli-codex/scripts/polycli-companion.bundle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3919,7 +3919,11 @@ function extractTerminalError(value) {
if (!value || typeof value !== "object") return null;
if (typeof value.error === "string" && value.error.trim()) return value.error.trim();
if (value.error && typeof value.error === "object") {
return extractTerminalError(value.error);
const nested = extractTerminalError(value.error);
if (nested) return nested;
if (typeof value.error.message === "string" && value.error.message.trim()) return value.error.message.trim();
if (typeof value.error.data === "string" && value.error.data.trim()) return value.error.data.trim();
return Object.keys(value.error).length > 0 ? "grok emitted a terminal error" : null;
}
if (value.is_error === true || value.isError === true || value.type === "error") {
if (typeof value.message === "string" && value.message.trim()) return value.message.trim();
Expand Down
2 changes: 1 addition & 1 deletion plugins/polycli-copilot/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "polycli-copilot",
"description": "GitHub Copilot CLI adapter for the shared polycli companion.",
"version": "0.6.25",
"version": "0.6.26",
"author": {
"name": "bing"
},
Expand Down
6 changes: 5 additions & 1 deletion plugins/polycli-copilot/scripts/polycli-companion.bundle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3919,7 +3919,11 @@ function extractTerminalError(value) {
if (!value || typeof value !== "object") return null;
if (typeof value.error === "string" && value.error.trim()) return value.error.trim();
if (value.error && typeof value.error === "object") {
return extractTerminalError(value.error);
const nested = extractTerminalError(value.error);
if (nested) return nested;
if (typeof value.error.message === "string" && value.error.message.trim()) return value.error.message.trim();
if (typeof value.error.data === "string" && value.error.data.trim()) return value.error.data.trim();
return Object.keys(value.error).length > 0 ? "grok emitted a terminal error" : null;
}
if (value.is_error === true || value.isError === true || value.type === "error") {
if (typeof value.message === "string" && value.message.trim()) return value.message.trim();
Expand Down
Loading