From 94fa6d6011c8b5aa1a9afaf73aef3395540c1a0b Mon Sep 17 00:00:00 2001 From: CBEPX <458940+CBEPX@users.noreply.github.com> Date: Thu, 3 Sep 2026 02:51:03 +0300 Subject: [PATCH 1/3] fix(peer): require successful native structured output --- scripts/claude-companion.mjs | 17 ++-- scripts/lib/claude-cli.mjs | 12 ++- tests/claude-cli.test.mjs | 47 ++++++++++ tests/e2e/peer-workflow-e2e.test.mjs | 3 +- tests/peer-companion.test.mjs | 129 ++++++++++++++++++++++++++- 5 files changed, 190 insertions(+), 18 deletions(-) diff --git a/scripts/claude-companion.mjs b/scripts/claude-companion.mjs index 18c3ade..04550f7 100644 --- a/scripts/claude-companion.mjs +++ b/scripts/claude-companion.mjs @@ -3405,15 +3405,14 @@ function submitPeerTargetOneShot(cwd, workflowId, options) { } function parsePeerClaudePayload(result, label) { - if (result.structuredOutput != null) { - if (typeof result.structuredOutput === "object" && !Array.isArray(result.structuredOutput)) { - return result.structuredOutput; - } - } else { - try { - const parsed = JSON.parse(String(result.finalMessage ?? "").trim()); - if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed; - } catch {} + if ( + result.terminalSubtype === "success" && + result.structuredOutput && + typeof result.structuredOutput === "object" && + !Array.isArray(result.structuredOutput) && + Object.getPrototypeOf(result.structuredOutput) === Object.prototype + ) { + return result.structuredOutput; } throw Object.assign( new Error(`EVIDENCE_INCOMPLETE: ${label} did not return one structured JSON object.`), diff --git a/scripts/lib/claude-cli.mjs b/scripts/lib/claude-cli.mjs index 33f39a7..97a5468 100644 --- a/scripts/lib/claude-cli.mjs +++ b/scripts/lib/claude-cli.mjs @@ -596,6 +596,7 @@ export class StreamParser { sessionId: null, finalMessage: "", structuredOutput: null, + terminalSubtype: null, receivedTerminalEvent: false, unknownEvents: [], parseErrors: [], @@ -685,6 +686,8 @@ export class StreamParser { return this._handleSystemEvent(event); case "result": this.state.receivedTerminalEvent = true; + this.state.terminalSubtype = + typeof event.subtype === "string" ? event.subtype : null; { const terminalModel = normalizeObservedModel( extractRawObservedModel(event) @@ -710,9 +713,7 @@ export class StreamParser { this.state.hasTerminalLimitSignal = true; } } - if (Object.prototype.hasOwnProperty.call(event, "structured_output")) { - this.state.structuredOutput = event.structured_output ?? null; - } + this.state.structuredOutput = event.structured_output ?? null; if (event.session_id) this.state.sessionId = event.session_id; return { kind: "result", data: event }; default: @@ -1429,7 +1430,7 @@ export function buildArgs(prompt, options = {}) { /** * Execute a Claude Code turn with streaming progress. - * Returns { status, sessionId, finalMessage, toolUses, touchedFiles, stderr, pid, pidIdentity } + * Returns { status, sessionId, finalMessage, structuredOutput, terminalSubtype, toolUses, touchedFiles, stderr, pid, pidIdentity } */ export async function runClaudeTurn(cwd, prompt, options = {}) { const args = buildArgs(prompt, { @@ -1445,6 +1446,7 @@ export async function runClaudeTurn(cwd, prompt, options = {}) { sessionId: null, finalMessage: "", structuredOutput: null, + terminalSubtype: null, toolUses: [], touchedFiles: [], requestedModel, @@ -1586,6 +1588,7 @@ export async function runClaudeTurn(cwd, prompt, options = {}) { sessionId: parser.state.sessionId, finalMessage: parser.state.finalMessage, structuredOutput: parser.state.structuredOutput, + terminalSubtype: parser.state.terminalSubtype, toolUses: parser.state.toolUses, touchedFiles: parser.state.touchedFiles, requestedModel, @@ -1609,6 +1612,7 @@ export async function runClaudeTurn(cwd, prompt, options = {}) { sessionId: null, finalMessage: "", structuredOutput: null, + terminalSubtype: null, toolUses: [], touchedFiles: [], requestedModel, diff --git a/tests/claude-cli.test.mjs b/tests/claude-cli.test.mjs index baf9536..2ac6514 100644 --- a/tests/claude-cli.test.mjs +++ b/tests/claude-cli.test.mjs @@ -105,6 +105,33 @@ describe("StreamParser", () => { assert.equal(parser.state.finalMessage, ""); }); + it("clears terminal structured output when a later result omits it", () => { + const parser = new StreamParser(); + parser.feed(JSON.stringify({ + type: "result", + subtype: "error", + structured_output: { answer: "stale" }, + }) + "\n"); + parser.feed(JSON.stringify({ + type: "result", + subtype: "success", + }) + "\n"); + + assert.equal(parser.state.terminalSubtype, "success"); + assert.equal(parser.state.structuredOutput, null); + }); + + it("captures the terminal result subtype", () => { + const parser = new StreamParser(); + parser.feed(JSON.stringify({ + type: "result", + subtype: "success", + result: "done", + }) + "\n"); + + assert.equal(parser.state.terminalSubtype, "success"); + }); + it("ignores Claude synthetic error model ids", () => { const parser = new StreamParser(); const resultEvent = JSON.stringify({ @@ -1399,6 +1426,26 @@ describe("classifyClaudeFailure", () => { }); describe("runClaudeTurn", () => { + it("returns the terminal result subtype internally", async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "cc-plugin-claude-subtype-")); + const oldPath = process.env.PATH ?? ""; + try { + createFakeClaudeCommand( + tmpDir, + `const out = JSON.stringify({ type: "result", subtype: "success", result: "done", session_id: "sess-subtype" });\nprocess.stdout.write(out + "\\n", () => process.exit(0));\n` + ); + process.env.PATH = `${tmpDir}${path.delimiter}${oldPath}`; + + const result = await runClaudeTurn(process.cwd(), "prompt"); + + assert.equal(result.status, "completed"); + assert.equal(result.terminalSubtype, "success"); + } finally { + process.env.PATH = oldPath; + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + it("returns bounded parser diagnostics when read-only output has a valid terminal event", async () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "cc-plugin-claude-parse-")); const oldPath = process.env.PATH ?? ""; diff --git a/tests/e2e/peer-workflow-e2e.test.mjs b/tests/e2e/peer-workflow-e2e.test.mjs index 4c09b3a..da3e929 100644 --- a/tests/e2e/peer-workflow-e2e.test.mjs +++ b/tests/e2e/peer-workflow-e2e.test.mjs @@ -114,7 +114,8 @@ async function main() { webCitations: process.env.FAKE_CLAUDE_SPARSE === "1" ? [] : ["https://example.test/primary"], }; const resultLine = () => JSON.stringify({ - type: "result", session_id: sessionId, result: JSON.stringify(payload), + type: "result", session_id: sessionId, subtype: "success", + structured_output: payload, result: JSON.stringify(payload), model: "claude-opus-5", modelUsage: { "claude-opus-5": { inputTokens: 1, outputTokens: 1, contextWindow: 1000000 } }, }) + "\\n"; diff --git a/tests/peer-companion.test.mjs b/tests/peer-companion.test.mjs index 8fe7463..0261511 100644 --- a/tests/peer-companion.test.mjs +++ b/tests/peer-companion.test.mjs @@ -182,20 +182,35 @@ async function main() { }, ...citations, }; - const emitResult = () => process.stdout.write(JSON.stringify({ + const emitResult = () => { + if (process.env.FAKE_CLAUDE_STALE_STRUCTURED_OUTPUT === "1") { + process.stdout.write(JSON.stringify({ + type: "result", + session_id: sessionId, + subtype: "error", + structured_output: payload, + result: "failed", + }) + "\\n"); + } + process.stdout.write(JSON.stringify({ type: "result", session_id: sessionId, ...(process.env.FAKE_CLAUDE_STRUCTURED_ARRAY === "1" ? { structured_output: [payload] } - : process.env.FAKE_CLAUDE_NATIVE_STRUCTURED === "1" - ? { structured_output: payload } - : {}), + : process.env.FAKE_CLAUDE_OMIT_NATIVE_STRUCTURED === "1" || ( + process.env.FAKE_CLAUDE_UNSTRUCTURED === "1" && + process.env.FAKE_CLAUDE_NATIVE_STRUCTURED !== "1" + ) + ? {} + : { structured_output: payload }), + subtype: process.env.FAKE_CLAUDE_TERMINAL_SUBTYPE || "success", result: process.env.FAKE_CLAUDE_UNSTRUCTURED === "1" ? "not structured JSON" : JSON.stringify(payload), model: process.env.FAKE_CLAUDE_FALLBACK === "1" ? "claude-opus-5" : "claude-fable-5-1", modelUsage: { "claude-fable-5-1": { inputTokens: 1, outputTokens: 1, contextWindow: 1000000 } }, }) + "\\n"); + }; if (process.env.FAKE_CLAUDE_RESULT_ON_TERM === "1") { process.on("SIGTERM", () => { emitResult(); @@ -519,6 +534,63 @@ describe("peer companion with fake Claude", () => { assert.equal(result.memo.content.recommendation, "The repository and primary source agree."); }); + it("rejects a memo with JSON text but no native structured output", () => { + const testEnv = createEnvironment(); + const created = createPeer(testEnv); + submitCodexMemo(testEnv, created); + const claudeLease = planLease(created, "_claude_"); + const failed = run(testEnv, [ + "peer-claude-turn", created.workflow.id, "--cwd", testEnv.workspaceDir, + "--brief-hash", created.workflow.briefHash, + "--epoch", String(created.workflow.epoch), "--json", + ], { + input: attemptInput(claudeLease), + env: { FAKE_CLAUDE_OMIT_NATIVE_STRUCTURED: "1" }, + }); + + assert.notEqual(failed.status, 0); + assert.equal(failed.stderr, "EVIDENCE_INCOMPLETE: STRUCTURED_JSON_REQUIRED\n"); + }); + + it("rejects a memo with a non-success terminal subtype", () => { + const testEnv = createEnvironment(); + const created = createPeer(testEnv); + submitCodexMemo(testEnv, created); + const claudeLease = planLease(created, "_claude_"); + const failed = run(testEnv, [ + "peer-claude-turn", created.workflow.id, "--cwd", testEnv.workspaceDir, + "--brief-hash", created.workflow.briefHash, + "--epoch", String(created.workflow.epoch), "--json", + ], { + input: attemptInput(claudeLease), + env: { FAKE_CLAUDE_TERMINAL_SUBTYPE: "error" }, + }); + + assert.notEqual(failed.status, 0); + assert.equal(failed.stderr, "EVIDENCE_INCOMPLETE: STRUCTURED_JSON_REQUIRED\n"); + }); + + it("rejects a success result without native output after failed native output", () => { + const testEnv = createEnvironment(); + const created = createPeer(testEnv); + submitCodexMemo(testEnv, created); + const claudeLease = planLease(created, "_claude_"); + const failed = run(testEnv, [ + "peer-claude-turn", created.workflow.id, "--cwd", testEnv.workspaceDir, + "--brief-hash", created.workflow.briefHash, + "--epoch", String(created.workflow.epoch), "--json", + ], { + input: attemptInput(claudeLease), + env: { + FAKE_CLAUDE_STALE_STRUCTURED_OUTPUT: "1", + FAKE_CLAUDE_OMIT_NATIVE_STRUCTURED: "1", + }, + }); + + assert.notEqual(failed.status, 0); + assert.equal(failed.stderr, "EVIDENCE_INCOMPLETE: STRUCTURED_JSON_REQUIRED\n"); + }); + it("rejects a memo that reflects its live checkpoint lease without mutation or exposure", () => { const testEnv = createEnvironment(); const created = createPeer(testEnv); @@ -1203,6 +1275,55 @@ describe("peer companion with fake Claude", () => { assert.deepEqual(retry.work, [{ kind: "stage", id: "synthesis" }]); }); + it("rejects a critique with JSON text but no native structured output", () => { + const testEnv = createEnvironment(); + const created = createPeer(testEnv); + const memo = (who) => ({ + content: { findings: [`${who} memo`] }, + repoCitations: [{ path: testEnv.repoFile, line: 1 }], + webCitations: [`https://example.test/${who}`], + toolEvents: [{ tool: "repo-read" }, { tool: "web-search" }], + }); + const codexLease = planLease(created, "_codex_", "memo"); + activate(testEnv, created, "memo", "codex", codexLease); + runJson(testEnv, [ + "peer-submit-memo", created.workflow.id, "--cwd", testEnv.workspaceDir, + "--branch", "codex", "--brief-hash", created.workflow.briefHash, + "--epoch", String(created.workflow.epoch), "--json", + ], { input: attemptInput(codexLease, memo("codex")) }); + const claudeLease = planLease(created, "_claude_"); + runJson(testEnv, [ + "peer-claude-turn", created.workflow.id, "--cwd", testEnv.workspaceDir, + "--brief-hash", created.workflow.briefHash, + "--epoch", String(created.workflow.epoch), "--json", + ], { input: attemptInput(claudeLease) }); + const checkpointLease = planLease(created, "_codex_", "checkpoint"); + activate(testEnv, created, "checkpoint", null, checkpointLease); + runJson(testEnv, [ + "peer-checkpoint", created.workflow.id, "--cwd", testEnv.workspaceDir, + "--brief-hash", created.workflow.briefHash, + "--epoch", String(created.workflow.epoch), "--json", + ], { input: attemptInput(checkpointLease, { + agreements: [], disagreements: [], decisionsNeeded: [], + }) }); + const continuation = runJson(testEnv, [ + "peer-resume-plan", created.workflow.id, "--cwd", testEnv.workspaceDir, + "--mode", "design", "--continue", "--owner-session-id", "owner-b", "--json", + ], { input: JSON.stringify({ feedback: "Check both memos." }) }); + const critiqueLease = planLease(continuation, "_critique_"); + const failed = run(testEnv, [ + "peer-claude-critique", created.workflow.id, "--cwd", testEnv.workspaceDir, + "--brief-hash", created.workflow.briefHash, + "--epoch", String(continuation.workflow.epoch), "--json", + ], { + input: attemptInput(critiqueLease), + env: { FAKE_CLAUDE_OMIT_NATIVE_STRUCTURED: "1" }, + }); + + assert.notEqual(failed.status, 0); + assert.equal(failed.stderr, "EVIDENCE_INCOMPLETE: STRUCTURED_JSON_REQUIRED\n"); + }); + it("fails closed with the stable isolation error when Claude cannot start its sandbox", () => { const testEnv = createEnvironment(); const created = createPeer(testEnv); From bbd18cb83f729844f8908f2c361474fe3422770e Mon Sep 17 00:00:00 2001 From: CBEPX <458940+CBEPX@users.noreply.github.com> Date: Thu, 3 Sep 2026 03:00:24 +0300 Subject: [PATCH 2/3] fix(cli): handle subcommand help locally --- scripts/claude-companion.mjs | 35 +++- tests/integration/claude-companion.test.mjs | 200 ++++++++++++++++++++ 2 files changed, 232 insertions(+), 3 deletions(-) diff --git a/scripts/claude-companion.mjs b/scripts/claude-companion.mjs index 04550f7..3903ed8 100644 --- a/scripts/claude-companion.mjs +++ b/scripts/claude-companion.mjs @@ -223,6 +223,7 @@ const PEER_FAILURE_CODES = new Set([ ]); const CODEX_DIR = resolveCodexHome(); const CODEX_CONFIG_TOML = path.join(CODEX_DIR, "config.toml"); +const SUBCOMMAND_HELP_REQUESTED = Symbol("subcommand help requested"); // --------------------------------------------------------------------------- // Usage // --------------------------------------------------------------------------- @@ -436,13 +437,33 @@ function normalizeArgv(argv) { } function parseCommandInput(argv, config = {}) { - return parseArgs(normalizeArgv(argv), { + const normalizedArgv = normalizeArgv(argv); + const normalizedConfig = { ...config, aliasMap: { C: "cwd", ...(config.aliasMap ?? {}) } - }); + }; + const literalSeparator = normalizedArgv.indexOf("--"); + const helpPositionals = parseArgs( + normalizedArgv.slice( + 0, + literalSeparator < 0 ? normalizedArgv.length : literalSeparator + ), + normalizedConfig + ).positionals; + const localHelpPositionals = config.helpAfterPromptIsLiteral + ? helpPositionals.slice(0, 1) + : helpPositionals; + if ( + localHelpPositionals.some( + (positional) => positional === "-h" || positional === "--help" + ) + ) { + throw SUBCOMMAND_HELP_REQUESTED; + } + return parseArgs(normalizedArgv, normalizedConfig); } function resolveCommandCwd(options = {}) { @@ -2580,6 +2601,7 @@ async function resolveLatestResumableSession(cwd, options = {}) { async function handleReviewCommand(argv, config) { const { options, positionals } = parseCommandInput(argv, { + helpAfterPromptIsLiteral: true, valueOptions: [ "base", "scope", @@ -2740,6 +2762,7 @@ async function handleMcpDiagnose(argv) { async function handleTask(argv) { const { options, positionals } = parseCommandInput(argv, { + helpAfterPromptIsLiteral: true, valueOptions: [ "model", "effort", @@ -3633,6 +3656,7 @@ async function executePeerClaudeTurn(cwd, workflowId, options = {}) { async function handlePeerCreate(argv) { const { options, positionals } = parseCommandInput(argv, { + helpAfterPromptIsLiteral: true, valueOptions: [ "cwd", "mode", "owner-session-id", "model", "fallback-model", "effort", "codex-model", "codex-effort", "user-mcp-tool", "auto-mcp-tool", "brief-file", @@ -4464,13 +4488,18 @@ async function main() { } } -async function handleMcpGit(_argv) { +async function handleMcpGit(argv) { + parseCommandInput(argv); const { runMcpGitServer } = await import("./lib/mcp-git.mjs"); const exitCode = await runMcpGitServer(); process.exit(exitCode ?? 0); } main().catch((error) => { + if (error === SUBCOMMAND_HELP_REQUESTED) { + printUsage(); + return; + } const message = error instanceof Error ? error.message : String(error); process.stderr.write(`${message}\n`); process.exitCode = 1; diff --git a/tests/integration/claude-companion.test.mjs b/tests/integration/claude-companion.test.mjs index a94fa66..2c5309c 100644 --- a/tests/integration/claude-companion.test.mjs +++ b/tests/integration/claude-companion.test.mjs @@ -73,6 +73,14 @@ function sanitize(value) { } async function main() { + if (process.env.CLAUDE_INVOCATION_LOG) { + require("node:fs").appendFileSync( + process.env.CLAUDE_INVOCATION_LOG, + JSON.stringify(args) + "\\n", + "utf8" + ); + } + if (args[0] === "--version") { process.stdout.write("2.1.90 (Claude Code)\\n"); return; @@ -871,6 +879,198 @@ describe("claude-companion integration", () => { assert.match(result.stdout, /status \[job-id\].*--wait.*--wait-timeout-ms /); }); + for (const { label, argsFor } of [ + { + label: "--help", + argsFor: (testEnv) => ["task", "--cwd", testEnv.workspaceDir, "--help"], + }, + { + label: "-h", + argsFor: (testEnv) => ["task", "--cwd", testEnv.workspaceDir, "-h"], + }, + { + label: "a normalized raw invocation", + argsFor: (testEnv) => [ + "task", + `--cwd ${JSON.stringify(testEnv.workspaceDir)} --help`, + ], + }, + ]) { + it(`prints global usage for task ${label} without Claude or state`, () => { + const testEnv = createTestEnvironment(); + const invocationLog = path.join(testEnv.rootDir, "claude-invocations.ndjson"); + + try { + const result = runCompanion(argsFor(testEnv), { + env: { + ...testEnv.env, + CLAUDE_INVOCATION_LOG: invocationLog, + }, + }); + + assert.match(result.stdout, /^Usage:/m); + assert.equal(fs.existsSync(invocationLog), false); + assert.equal(fs.existsSync(stateDirFor(testEnv)), false); + assert.deepEqual(listStoredJobs(testEnv), []); + } finally { + cleanupTestEnvironment(testEnv); + } + }); + } + + it("keeps task help after a literal separator as the Claude prompt", () => { + const testEnv = createTestEnvironment(); + const invocationFile = path.join(testEnv.rootDir, "literal-help-invocation.json"); + + try { + const result = runCompanion( + ["task", "--cwd", testEnv.workspaceDir, "--", "--help"], + { + env: { + ...testEnv.env, + CLAUDE_INVOCATION_FILE: invocationFile, + }, + } + ); + + assert.match(result.stdout, /completed:--help/); + assert.equal(JSON.parse(fs.readFileSync(invocationFile, "utf8")).prompt, "--help"); + } finally { + cleanupTestEnvironment(testEnv); + } + }); + + it("does not cancel a running job when trailing --help requests usage", async () => { + const testEnv = createTestEnvironment(); + + try { + const launch = await runCompanionAsyncJson( + [ + "task", + "--cwd", + testEnv.workspaceDir, + "--background", + "--json", + "cancel-help delay=1000", + ], + { env: testEnv.env } + ); + const result = runCompanion( + ["cancel", "--cwd", testEnv.workspaceDir, launch.jobId, "--help"], + { env: testEnv.env } + ); + + assert.match(result.stdout, /^Usage:/m); + assert.equal( + (await waitForTerminalResult(testEnv, launch.jobId, testEnv.env)).job.status, + "completed" + ); + } finally { + cleanupTestEnvironment(testEnv); + } + }); + + it("prints mcp-git usage without processing server input", () => { + const testEnv = createTestEnvironment(); + const result = spawnSync( + process.execPath, + [COMPANION_SCRIPT, "mcp-git", "--help"], + { + cwd: PROJECT_ROOT, + env: { ...testEnv.env, CC_GIT_ROOT: testEnv.workspaceDir }, + input: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} }) + "\n", + encoding: "utf8", + } + ); + + try { + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /^Usage:/m); + assert.doesNotMatch(result.stdout, /serverInfo/); + } finally { + cleanupTestEnvironment(testEnv); + } + }); + + for (const { label, args, prompt } of [ + { + label: "a quoted raw --help prompt", + args: ["task", 'explain "--help"'], + prompt: "explain --help", + }, + { + label: "a multi-token --help prompt", + args: ["task", "explain", "--help"], + prompt: "explain --help", + }, + { + label: "a quoted raw -h prompt", + args: ["task", 'explain "-h"'], + prompt: "explain -h", + }, + { + label: "a multi-token -h prompt", + args: ["task", "explain", "-h"], + prompt: "explain -h", + }, + ]) { + it(`forwards ${label} to Claude`, () => { + const testEnv = createTestEnvironment(); + const invocationFile = path.join(testEnv.rootDir, "prompt-help-invocation.json"); + + try { + runCompanion(args, { + env: { + ...testEnv.env, + CLAUDE_INVOCATION_FILE: invocationFile, + }, + }); + + assert.equal(fs.existsSync(invocationFile), true); + assert.equal(JSON.parse(fs.readFileSync(invocationFile, "utf8")).prompt, prompt); + } finally { + cleanupTestEnvironment(testEnv); + } + }); + } + + it("forwards adversarial review focus containing --help", () => { + const testEnv = createTestEnvironment(); + const invocationFile = path.join(testEnv.rootDir, "adversarial-help-invocation.json"); + + try { + runCompanion(["adversarial-review", "--scope", "working-tree", "focus", "--help"], { + env: { + ...testEnv.env, + CLAUDE_INVOCATION_FILE: invocationFile, + }, + }); + + assert.equal(fs.existsSync(invocationFile), true); + assert.match(JSON.parse(fs.readFileSync(invocationFile, "utf8")).prompt, /focus --help/); + } finally { + cleanupTestEnvironment(testEnv); + } + }); + + it("preserves literal quotes in a one-string raw task prompt", () => { + const testEnv = createTestEnvironment(); + const invocationFile = path.join(testEnv.rootDir, "raw-quote-invocation.json"); + + try { + runCompanion(["task", 'say\\"hi\\"'], { + env: { + ...testEnv.env, + CLAUDE_INVOCATION_FILE: invocationFile, + }, + }); + + assert.equal(JSON.parse(fs.readFileSync(invocationFile, "utf8")).prompt, 'say"hi"'); + } finally { + cleanupTestEnvironment(testEnv); + } + }); + it("setup toggles the review gate on and off for the current workspace", () => { const testEnv = createTestEnvironment(); From af9ccbb8becb977411c4be5d52eb5c9b21782a3f Mon Sep 17 00:00:00 2001 From: CBEPX <458940+CBEPX@users.noreply.github.com> Date: Thu, 3 Sep 2026 03:06:21 +0300 Subject: [PATCH 3/3] chore(release): prepare v1.7.3 --- .codex-plugin/plugin.json | 2 +- CHANGELOG.md | 7 +++++++ README.md | 14 +++++++------- package-lock.json | 4 ++-- package.json | 2 +- scripts/lib/mcp-capabilities.mjs | 4 ++-- 6 files changed, 20 insertions(+), 13 deletions(-) diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 7bcc8ef..bd34026 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "cc", - "version": "1.7.2", + "version": "1.7.3", "description": "Claude Code Plugin for Codex. Run reviews, tracked tasks, and independent Codex-Claude design or research workflows.", "author": { "name": "CBEPX", diff --git a/CHANGELOG.md b/CHANGELOG.md index eb0987e..d0e2997 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## [Unreleased] +## v1.7.3 + +### Fixed + +- Require peer memo and critique phases to return successful native structured output, rejecting JSON text fallbacks (#31). +- Handle subcommand `--help` and `-h` locally before dispatch without changing literal prompts after `--` (#29). + ## v1.7.2 ### Added diff --git a/README.md b/README.md index fb1c391..e7f1023 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ It follows the shape of [openai/codex-plugin-cc](https://github.com/openai/codex Install the fork release from the CBEPX marketplace snapshot: ```bash -codex plugin marketplace add CBEPX/cc-plugin-codex --ref v1.7.2 +codex plugin marketplace add CBEPX/cc-plugin-codex --ref v1.7.3 codex plugin add cc@cbepx ``` @@ -61,8 +61,8 @@ The optional `npx` helper can install this fork release and enable the required ```bash CC_PLUGIN_CODEX_MARKETPLACE_NAME=cbepx \ CC_PLUGIN_CODEX_MARKETPLACE_SOURCE=CBEPX/cc-plugin-codex \ -CC_PLUGIN_CODEX_MARKETPLACE_REF=v1.7.2 \ -npx -y https://github.com/CBEPX/cc-plugin-codex/releases/download/v1.7.2/cc-plugin-codex-1.7.2.tgz install +CC_PLUGIN_CODEX_MARKETPLACE_REF=v1.7.3 \ +npx -y https://github.com/CBEPX/cc-plugin-codex/releases/download/v1.7.3/cc-plugin-codex-1.7.3.tgz install ``` On Windows, prefer the marketplace path or the `npx` helper. The shell-script helper below is POSIX-only. @@ -373,7 +373,7 @@ The review gate is an **optional** stop-time hook. When enabled, pressing Ctrl+C Install from the fork's marketplace snapshot: ```bash -codex plugin marketplace add CBEPX/cc-plugin-codex --ref v1.7.2 +codex plugin marketplace add CBEPX/cc-plugin-codex --ref v1.7.3 codex plugin add cc@cbepx ``` @@ -394,8 +394,8 @@ This fork does not install from the upstream Sendbird marketplace. Use the CBEPX ```bash CC_PLUGIN_CODEX_MARKETPLACE_NAME=cbepx \ CC_PLUGIN_CODEX_MARKETPLACE_SOURCE=CBEPX/cc-plugin-codex \ -CC_PLUGIN_CODEX_MARKETPLACE_REF=v1.7.2 \ -npx -y https://github.com/CBEPX/cc-plugin-codex/releases/download/v1.7.2/cc-plugin-codex-1.7.2.tgz install +CC_PLUGIN_CODEX_MARKETPLACE_REF=v1.7.3 \ +npx -y https://github.com/CBEPX/cc-plugin-codex/releases/download/v1.7.3/cc-plugin-codex-1.7.3.tgz install ``` After install, run: @@ -425,7 +425,7 @@ $cc:setup Re-run the fork marketplace install flow, pinned to the release you want: ```bash -codex plugin marketplace add CBEPX/cc-plugin-codex --ref v1.7.2 +codex plugin marketplace add CBEPX/cc-plugin-codex --ref v1.7.3 codex plugin add cc@cbepx ``` diff --git a/package-lock.json b/package-lock.json index 05b7224..0026ad9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cc-plugin-codex", - "version": "1.7.2", + "version": "1.7.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cc-plugin-codex", - "version": "1.7.2", + "version": "1.7.3", "license": "Apache-2.0", "bin": { "cc-plugin-codex": "scripts/installer-cli.mjs" diff --git a/package.json b/package.json index 0135da1..f057061 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cc-plugin-codex", - "version": "1.7.2", + "version": "1.7.3", "description": "Claude Code Plugin for Codex (CBEPX fork)", "type": "module", "author": { diff --git a/scripts/lib/mcp-capabilities.mjs b/scripts/lib/mcp-capabilities.mjs index 52762b2..8342779 100644 --- a/scripts/lib/mcp-capabilities.mjs +++ b/scripts/lib/mcp-capabilities.mjs @@ -295,7 +295,7 @@ function stdioProbe(config, timeoutMs) { params: { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, - clientInfo: { name: "cc-plugin-codex", version: "1.7.2" }, + clientInfo: { name: "cc-plugin-codex", version: "1.7.3" }, }, }); }); @@ -384,7 +384,7 @@ async function httpProbe(config, timeoutMs) { params: { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, - clientInfo: { name: "cc-plugin-codex", version: "1.7.2" }, + clientInfo: { name: "cc-plugin-codex", version: "1.7.3" }, }, }, null, deadline); if (initialized.statusCode === 401 || initialized.statusCode === 403) {