diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index e552908..f749018 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -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" } ] diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index cd5f72d..6dab3b2 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -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" } ] diff --git a/README.ja.md b/README.ja.md index 4c5ca4c..b41987f 100644 --- a/README.ja.md +++ b/README.ja.md @@ -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 を使うのか? diff --git a/README.md b/README.md index 24e1ec4..d2dfe7d 100644 --- a/README.md +++ b/README.md @@ -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? diff --git a/README.zh-CN.md b/README.zh-CN.md index 4133fa1..38144a1 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -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? diff --git a/docs/release-notes-v0.6.26.md b/docs/release-notes-v0.6.26.md new file mode 100644 index 0000000..a5f4fb2 --- /dev/null +++ b/docs/release-notes-v0.6.26.md @@ -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`). diff --git a/docs/roadmap.md b/docs/roadmap.md index 37ffb45..96cf95e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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.** @@ -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. --- diff --git a/packages/polycli-runtime/src/grok.js b/packages/polycli-runtime/src/grok.js index fa39ac9..d936595 100644 --- a/packages/polycli-runtime/src/grok.js +++ b/packages/polycli-runtime/src/grok.js @@ -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(); diff --git a/packages/polycli-runtime/test/grok.test.js b/packages/polycli-runtime/test/grok.test.js index a5bee6b..2f79a80 100644 --- a/packages/polycli-runtime/test/grok.test.js +++ b/packages/polycli-runtime/test/grok.test.js @@ -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. @@ -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 diff --git a/packages/polycli-runtime/test/qwen.test.js b/packages/polycli-runtime/test/qwen.test.js index aea3e1b..404ce0f 100644 --- a/packages/polycli-runtime/test/qwen.test.js +++ b/packages/polycli-runtime/test/qwen.test.js @@ -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 diff --git a/packages/polycli-terminal/bin/polycli-companion.bundle.mjs b/packages/polycli-terminal/bin/polycli-companion.bundle.mjs index 9afefb0..a501ec0 100755 --- a/packages/polycli-terminal/bin/polycli-companion.bundle.mjs +++ b/packages/polycli-terminal/bin/polycli-companion.bundle.mjs @@ -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(); diff --git a/packages/polycli-terminal/package.json b/packages/polycli-terminal/package.json index 4551328..c17da77 100644 --- a/packages/polycli-terminal/package.json +++ b/packages/polycli-terminal/package.json @@ -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": { diff --git a/plugins/polycli-codex/.codex-plugin/plugin.json b/plugins/polycli-codex/.codex-plugin/plugin.json index c47b6bd..17e14ce 100644 --- a/plugins/polycli-codex/.codex-plugin/plugin.json +++ b/plugins/polycli-codex/.codex-plugin/plugin.json @@ -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" diff --git a/plugins/polycli-codex/scripts/polycli-companion.bundle.mjs b/plugins/polycli-codex/scripts/polycli-companion.bundle.mjs index 9afefb0..a501ec0 100755 --- a/plugins/polycli-codex/scripts/polycli-companion.bundle.mjs +++ b/plugins/polycli-codex/scripts/polycli-companion.bundle.mjs @@ -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(); diff --git a/plugins/polycli-copilot/plugin.json b/plugins/polycli-copilot/plugin.json index 1e16bfa..6a07854 100644 --- a/plugins/polycli-copilot/plugin.json +++ b/plugins/polycli-copilot/plugin.json @@ -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" }, diff --git a/plugins/polycli-copilot/scripts/polycli-companion.bundle.mjs b/plugins/polycli-copilot/scripts/polycli-companion.bundle.mjs index 9afefb0..a501ec0 100755 --- a/plugins/polycli-copilot/scripts/polycli-companion.bundle.mjs +++ b/plugins/polycli-copilot/scripts/polycli-companion.bundle.mjs @@ -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(); diff --git a/plugins/polycli-opencode/package.json b/plugins/polycli-opencode/package.json index f73e550..5a96d3d 100644 --- a/plugins/polycli-opencode/package.json +++ b/plugins/polycli-opencode/package.json @@ -1,6 +1,6 @@ { "name": "@bbingz/polycli-opencode", - "version": "0.6.25", + "version": "0.6.26", "type": "module", "main": "./index.mjs", "exports": { diff --git a/plugins/polycli-opencode/scripts/polycli-companion.bundle.mjs b/plugins/polycli-opencode/scripts/polycli-companion.bundle.mjs index 9afefb0..a501ec0 100755 --- a/plugins/polycli-opencode/scripts/polycli-companion.bundle.mjs +++ b/plugins/polycli-opencode/scripts/polycli-companion.bundle.mjs @@ -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(); diff --git a/plugins/polycli/.claude-plugin/plugin.json b/plugins/polycli/.claude-plugin/plugin.json index a3b994f..2267333 100644 --- a/plugins/polycli/.claude-plugin/plugin.json +++ b/plugins/polycli/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "polycli", - "version": "0.6.25", + "version": "0.6.26", "description": "Use multiple provider CLIs from one Claude Code plugin, including background job lifecycle.", "author": { "name": "bing" diff --git a/plugins/polycli/scripts/polycli-companion.bundle.mjs b/plugins/polycli/scripts/polycli-companion.bundle.mjs index 9afefb0..a501ec0 100755 --- a/plugins/polycli/scripts/polycli-companion.bundle.mjs +++ b/plugins/polycli/scripts/polycli-companion.bundle.mjs @@ -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(); diff --git a/scripts/tests/validate-cc-x-recipes.test.mjs b/scripts/tests/validate-cc-x-recipes.test.mjs index 0153a53..825975b 100644 --- a/scripts/tests/validate-cc-x-recipes.test.mjs +++ b/scripts/tests/validate-cc-x-recipes.test.mjs @@ -58,6 +58,12 @@ test("validateCcXRecipes rejects a marketplace entry with a fabricated autoCompa assert.throws(() => validateCcXRecipes({ recipesPath: writeDoc(baseDoc(recipes)) }), /autoCompactWindow null/); }); +test("validateCcXRecipes rejects an unknown status (e.g. draft)", () => { + const recipes = Array.from({ length: 7 }, (_, i) => baseRecipe({ vendor: `Vendor${i}` })); + recipes[0] = baseRecipe({ vendor: "Draft", status: "draft" }); + assert.throws(() => validateCcXRecipes({ recipesPath: writeDoc(baseDoc(recipes)) }), /status must be "verified" or "marketplace-unstable"/); +}); + test("validateCcXRecipes requires at least the 7 verified core-lab recipes", () => { const recipes = Array.from({ length: 6 }, (_, i) => baseRecipe({ vendor: `Vendor${i}` })); assert.throws(() => validateCcXRecipes({ recipesPath: writeDoc(baseDoc(recipes)) }), /at least the 7 verified/); diff --git a/scripts/validate-cc-x-recipes.mjs b/scripts/validate-cc-x-recipes.mjs index d64abef..e350848 100644 --- a/scripts/validate-cc-x-recipes.mjs +++ b/scripts/validate-cc-x-recipes.mjs @@ -25,6 +25,7 @@ function validateRecipe(index, recipe) { assert.equal(typeof recipe.marketplace, "boolean", `${at}: marketplace must be boolean`); assertNonEmptyString(recipe.cachingNote, `${at}: cachingNote`); assertNonEmptyString(recipe.status, `${at}: status`); + assert.ok(["verified", "marketplace-unstable"].includes(recipe.status), `${at}: status must be "verified" or "marketplace-unstable" (got "${recipe.status}")`); assert.ok(recipe.source && typeof recipe.source === "object", `${at}: source must be an object`); assertNonEmptyString(recipe.source.url, `${at}: source.url`); assertNonEmptyString(recipe.source.date, `${at}: source.date`); @@ -35,8 +36,11 @@ function validateRecipe(index, recipe) { if (recipe.marketplace === true) { assert.equal(recipe.autoCompactWindow, null, `${at}: marketplace recipes must leave autoCompactWindow null (no fabricated pin)`); assert.equal(recipe.status, "marketplace-unstable", `${at}: marketplace recipes must declare status "marketplace-unstable"`); - } else if (recipe.autoCompactWindow !== null) { - assert.ok(Number.isInteger(recipe.autoCompactWindow) && recipe.autoCompactWindow > 0, `${at}: autoCompactWindow must be null or a positive integer`); + } else { + assert.equal(recipe.status, "verified", `${at}: non-marketplace recipes must declare status "verified"`); + if (recipe.autoCompactWindow !== null) { + assert.ok(Number.isInteger(recipe.autoCompactWindow) && recipe.autoCompactWindow > 0, `${at}: autoCompactWindow must be null or a positive integer`); + } } }