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
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand All @@ -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.
Expand Down Expand Up @@ -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
```

Expand All @@ -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:
Expand Down Expand Up @@ -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
```

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
52 changes: 40 additions & 12 deletions scripts/claude-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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 = {}) {
Expand Down Expand Up @@ -2580,6 +2601,7 @@ async function resolveLatestResumableSession(cwd, options = {}) {

async function handleReviewCommand(argv, config) {
const { options, positionals } = parseCommandInput(argv, {
helpAfterPromptIsLiteral: true,
valueOptions: [
"base",
"scope",
Expand Down Expand Up @@ -2740,6 +2762,7 @@ async function handleMcpDiagnose(argv) {

async function handleTask(argv) {
const { options, positionals } = parseCommandInput(argv, {
helpAfterPromptIsLiteral: true,
valueOptions: [
"model",
"effort",
Expand Down Expand Up @@ -3405,15 +3428,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.`),
Expand Down Expand Up @@ -3634,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",
Expand Down Expand Up @@ -4465,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;
Expand Down
12 changes: 8 additions & 4 deletions scripts/lib/claude-cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,7 @@ export class StreamParser {
sessionId: null,
finalMessage: "",
structuredOutput: null,
terminalSubtype: null,
receivedTerminalEvent: false,
unknownEvents: [],
parseErrors: [],
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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, {
Expand All @@ -1445,6 +1446,7 @@ export async function runClaudeTurn(cwd, prompt, options = {}) {
sessionId: null,
finalMessage: "",
structuredOutput: null,
terminalSubtype: null,
toolUses: [],
touchedFiles: [],
requestedModel,
Expand Down Expand Up @@ -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,
Expand All @@ -1609,6 +1612,7 @@ export async function runClaudeTurn(cwd, prompt, options = {}) {
sessionId: null,
finalMessage: "",
structuredOutput: null,
terminalSubtype: null,
toolUses: [],
touchedFiles: [],
requestedModel,
Expand Down
4 changes: 2 additions & 2 deletions scripts/lib/mcp-capabilities.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
},
});
});
Expand Down Expand Up @@ -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) {
Expand Down
47 changes: 47 additions & 0 deletions tests/claude-cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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 ?? "";
Expand Down
3 changes: 2 additions & 1 deletion tests/e2e/peer-workflow-e2e.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading
Loading