Skip to content
Merged
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.0",
"version": "1.7.1",
"description": "Claude Code Plugin for Codex. Run reviews, tracked tasks, and independent Codex-Claude design or research workflows.",
"author": {
"name": "CBEPX",
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

## [Unreleased]

## v1.7.1

### Changed

- Qualify Fable 5.1 as `claude-fable-5-1` with a native 1M context window and Claude Code 2.1.257+ while keeping the floating `fable` alias and no hidden effort default.
- Preserve actionable peer validation details across retries (#21).
- Stop one-shot checkpoint workers before activation when a branch is terminally incomplete (#22).

### Fixed

- Classify the exact known `Client.listTools()` capability warning as a bounded stable diagnostic instead of an unresolved parse error (#18).
- Preserve an already-incomplete peer aggregate when its late sibling succeeds.
- Let authoritative runner results replace only status-reaper-owned unverifiable failures, including runner errors, without overwriting cancellation or unrelated terminal writers (#23).

## v1.7.0

### Added
Expand Down
16 changes: 8 additions & 8 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.0
codex plugin marketplace add CBEPX/cc-plugin-codex --ref v1.7.1
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.0 \
npx -y https://github.com/CBEPX/cc-plugin-codex/releases/download/v1.7.0/cc-plugin-codex-1.7.0.tgz install
CC_PLUGIN_CODEX_MARKETPLACE_REF=v1.7.1 \
npx -y https://github.com/CBEPX/cc-plugin-codex/releases/download/v1.7.1/cc-plugin-codex-1.7.1.tgz install
```

On Windows, prefer the marketplace path or the `npx` helper. The shell-script helper below is POSIX-only.
Expand Down Expand Up @@ -142,7 +142,7 @@ $cc:review --user-mcp-tool mcp__context7__resolve-library-id

**Defaults:** model `opus` is passed to Claude Code as its native alias with `xhigh` effort. `sonnet` is passed through with `high` effort; `haiku` and `fable` are passed through with no default effort setting. Claude Code resolves aliases to the current model for the active provider and account (for example, Opus 5). Pass a full model ID to pin a version; for older pinned IDs, pass `--effort` explicitly instead of inheriting a current-family default.

Fable 5 has a native 1M context window, so current Claude Code only needs the bare `fable` alias; no `[1m]` suffix is required. See Claude Code's [model configuration](https://code.claude.com/docs/en/model-config).
Fable 5.1 (`claude-fable-5-1`) has a native 1M context window and requires Claude Code 2.1.257 or newer. The bare `fable` alias remains floating and may still resolve to Fable 5 behind Claude Apps Gateway; use the full model ID when Fable 5.1 is required. No `[1m]` suffix or hidden Fable effort default is added. See Claude Code's [model configuration](https://code.claude.com/docs/en/model-config).

JSON task and review results keep `requestedModel` as the forwarded alias or full ID, report `finalModel` from Claude's terminal result, and expose the terminal `contextWindow` reported in `modelUsage` (`null` when Claude does not provide it). The plugin does not infer a context limit from a floating alias.

Expand Down Expand Up @@ -369,7 +369,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.0
codex plugin marketplace add CBEPX/cc-plugin-codex --ref v1.7.1
codex plugin add cc@cbepx
```

Expand All @@ -390,8 +390,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.0 \
npx -y https://github.com/CBEPX/cc-plugin-codex/releases/download/v1.7.0/cc-plugin-codex-1.7.0.tgz install
CC_PLUGIN_CODEX_MARKETPLACE_REF=v1.7.1 \
npx -y https://github.com/CBEPX/cc-plugin-codex/releases/download/v1.7.1/cc-plugin-codex-1.7.1.tgz install
```

After install, run:
Expand Down Expand Up @@ -421,7 +421,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.0
codex plugin marketplace add CBEPX/cc-plugin-codex --ref v1.7.1
codex plugin add cc@cbepx
```

Expand Down
4 changes: 2 additions & 2 deletions internal-skills/peer-runtime/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ Initial execution is always background: do not wait in the parent turn. Return t

## Child contracts

The Codex reasoning worker is not a forwarder. It first activates its reserved memo attempt by sending the raw lease through JSON stdin to the returned `peer-activate-attempt` command, then researches independently with the repo and web routes exposed to its turn and performs zero workspace writes. It sends `{lease,payload:{content,repoCitations,webCitations,toolEvents}}` as JSON on stdin to `peer-submit-memo`. Every specialized mutating command includes `--epoch <returned-epoch>` from its spawn or resume plan; never omit or refresh that captured workflow epoch inside an old worker. That command accepts only the Codex memo; Claude memo submission occurs only inside the trusted `peer-claude-turn` execution path. It then polls `peer-wait`, whose status-only view redacts the sibling payload until the Codex memo is sealed. If Claude completed, it activates its checkpoint reservation immediately before comparison and sends `{lease,payload:{agreements,disagreements,decisionsNeeded}}` as JSON on stdin to `peer-checkpoint`. If Claude is incomplete, it stops without replacing either memo.
The Codex reasoning worker is not a forwarder. It first activates its reserved memo attempt by sending the raw lease through JSON stdin to the returned `peer-activate-attempt` command, then researches independently with the repo and web routes exposed to its turn and performs zero workspace writes. It sends `{lease,payload:{content,repoCitations,webCitations,toolEvents}}` as JSON on stdin to `peer-submit-memo`. Every specialized mutating command includes `--epoch <returned-epoch>` from its spawn or resume plan; never omit or refresh that captured workflow epoch inside an old worker. That command accepts only the Codex memo; Claude memo submission occurs only inside the trusted `peer-claude-turn` execution path. It then makes separate short foreground one-shot `peer-wait` calls, waiting for each call to exit before starting another; never use `while`, shell loops, background processes, or persistent pollers. The status-only view redacts the sibling payload until the Codex memo is sealed. If Claude completed, it activates its checkpoint reservation immediately before comparison and sends `{lease,payload:{agreements,disagreements,decisionsNeeded}}` as JSON on stdin to `peer-checkpoint`. If Claude is incomplete, it stops without replacing either memo.

A Claude-first forwarder uses one absolute 30-minute deadline while waiting for the Codex memo, with exponential polling from 100 ms capped at 2 seconds; transient Codex retry does not reset the deadline. The unrevealed Claude payload stays process-local throughout that wait. On timeout, discard it and mark only Claude retryable with `PEER_SIBLING_TIMEOUT`. Explicit retry preserves a committed waiter only when its newest linked memo job is still active and not reaped; a missing, terminal, or lost current worker rotates that unfinished target, while `cancel_failed` remains terminal with no retry plan. Rebind, SessionEnd, and cancellation invalidate old epochs before late callbacks can write.

Each worker receives only its own raw lease in its spawn message. A raw lease is never a Node argv value and never enters workflow, job, log, status, result, or rendered state. Durable targets contain only `attemptReservation: { leaseDigest, epoch, reservedAt }`; attempts and append-only attempt history advance when activation wins, not when the controller reserves work. Submit and failure transitions reuse the activated lease and epoch fence.
Each worker receives only its own raw lease in its spawn message. A raw lease is never a Node argv value and never enters workflow, job, log, status, result, or rendered state. Durable targets contain only `attemptReservation: { leaseDigest, epoch, reservedAt, previousFailureDetail }`; `previousFailureDetail` is nullable and restricted to the bounded workflow failure-detail allowlist. Attempts and append-only attempt history advance when activation wins, not when the controller reserves work. Submit and failure transitions reuse the activated lease and epoch fence.

The pure Claude forwarder must run exactly one companion command, in the foreground, and return stdout unchanged. It does no repository inspection or reasoning itself. Never use shell backgrounding (`nohup`, detached spawn, or an ampersand operator). Never invoke `codex exec`. If the shell yields a session, poll that same session until exit.

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.0",
"version": "1.7.1",
"description": "Claude Code Plugin for Codex (CBEPX fork)",
"type": "module",
"author": {
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-version-sync.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { assertVersionsMatch } from "./lib/version-sync.mjs";
try {
const version = assertVersionsMatch();
process.stdout.write(
`Version sync OK: package.json and .codex-plugin/plugin.json are both ${version}.\n`
`Version sync OK: package.json, plugin.json, and both MCP clientInfo literals are ${version}.\n`
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
Expand Down
68 changes: 56 additions & 12 deletions scripts/claude-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
runClaudeTurn,
runClaudeReview,
runClaudeAdversarialReview,
CLIENT_LIST_TOOLS_WITHOUT_TOOLS_CAPABILITY_CODE,
cancelClaudeProcess,
MODEL_ALIASES,
resolveEffort,
Expand Down Expand Up @@ -154,6 +155,7 @@ import {
listWorkflows,
markWorkflowNotification,
markWorkflowBranchFailure,
normalizeWorkflowFailureDetail,
readWorkflow,
reconcilePeerRetry,
rebindWorkflowOwner,
Expand Down Expand Up @@ -1377,7 +1379,8 @@ async function executeReviewRun(request) {
contextWindow: result.contextWindow ?? null,
modelFallbacks,
parseErrors: result.parseErrors ?? [],
unresolvedParseErrors: result.unresolvedParseErrors ?? 0
unresolvedParseErrors: result.unresolvedParseErrors ?? 0,
streamDiagnostics: result.streamDiagnostics ?? []
}
};
const rendered = appendModelFallbackSummary(
Expand Down Expand Up @@ -1487,7 +1490,8 @@ async function executeReviewRun(request) {
contextWindow: result.contextWindow ?? null,
modelFallbacks,
parseErrors: result.parseErrors ?? [],
unresolvedParseErrors: result.unresolvedParseErrors ?? 0
unresolvedParseErrors: result.unresolvedParseErrors ?? 0,
streamDiagnostics: result.streamDiagnostics ?? []
},
result: parsed.parsed,
rawOutput: parsed.rawOutput,
Expand Down Expand Up @@ -1619,6 +1623,7 @@ async function executeTaskRun(request) {
failure: result.failure ?? null,
parseErrors: result.parseErrors ?? [],
unresolvedParseErrors: result.unresolvedParseErrors ?? 0,
streamDiagnostics: result.streamDiagnostics ?? [],
rawOutput,
touchedFiles: Array.isArray(result.touchedFiles)
? result.touchedFiles
Expand Down Expand Up @@ -1810,6 +1815,15 @@ function normalizePeerModelFallbacks(events) {
: [];
}

function normalizePeerStreamDiagnostics(diagnostics) {
return Array.isArray(diagnostics)
? diagnostics
.filter(({ code } = {}) => code === CLIENT_LIST_TOOLS_WITHOUT_TOOLS_CAPABILITY_CODE)
.slice(-50)
.map(({ code }) => ({ code }))
: [];
}

function buildReviewRequest({
cwd,
base,
Expand Down Expand Up @@ -2265,7 +2279,7 @@ function enqueueDetachedTask(cwd, job, request, options = {}) {

function buildStoredTaskPayload(job) {
if (job?.result && typeof job.result === "object") {
return { contextWindow: null, ...job.result };
return { contextWindow: null, streamDiagnostics: [], ...job.result };
}
return {
status: job?.status === "completed" ? "completed" : "failed",
Expand All @@ -2277,6 +2291,7 @@ function buildStoredTaskPayload(job) {
finalModel: null,
contextWindow: null,
modelFallbacks: [],
streamDiagnostics: [],
rawOutput: "",
touchedFiles: [],
...(job?.errorMessage ? { errorMessage: job.errorMessage } : {})
Expand Down Expand Up @@ -3344,6 +3359,9 @@ function peerFailureCode(error) {

function failPeerAttempt(cwd, workflowId, target, fence, error) {
const reason = peerFailureCode(error);
const failureDetail = reason === "EVIDENCE_INCOMPLETE"
? normalizeWorkflowFailureDetail(error?.failureDetail)
: null;
if (reason === "ATTEMPT_LEASE_REFLECTION") return;
try {
if (targetStatus(readPeerWorkflow(cwd, workflowId), target.stage, target.branchId) === "running") {
Expand All @@ -3353,6 +3371,7 @@ function failPeerAttempt(cwd, workflowId, target, fence, error) {
epoch: fence.epoch,
lease: fence.lease,
reason,
failureDetail,
});
}
} catch {}
Expand All @@ -3367,14 +3386,20 @@ function submitPeerTargetOneShot(cwd, workflowId, options) {
}

function parsePeerClaudePayload(result, label) {
if (result.structuredOutput && typeof result.structuredOutput === "object") {
return result.structuredOutput;
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 {}
}
try {
const parsed = JSON.parse(String(result.finalMessage ?? "").trim());
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
} catch {}
throw new Error(`EVIDENCE_INCOMPLETE: ${label} did not return one structured JSON object.`);
throw Object.assign(
new Error(`EVIDENCE_INCOMPLETE: ${label} did not return one structured JSON object.`),
{ code: "EVIDENCE_INCOMPLETE", failureDetail: "STRUCTURED_JSON_REQUIRED" }
);
}

function peerClaudeSystemPrompt() {
Expand All @@ -3394,6 +3419,13 @@ function peerPromptData(value) {
.replaceAll(">", "\\u003e");
}

function previousFailureDetailPrompt(target) {
const detail = normalizeWorkflowFailureDetail(
target?.attemptReservation?.previousFailureDetail
);
return detail ? [`Correct the previous attempt failure detail: ${detail}.`] : [];
}

function initialClaudePrompt(workflow) {
const emphasis = workflow.mode === "design"
? "Evaluate alternatives, trade-offs, decision drivers, and a recommendation."
Expand All @@ -3403,6 +3435,7 @@ function initialClaudePrompt(workflow) {
emphasis,
"Use at least one repository tool and one web tool.",
"Return {content, repoCitations:[{path,line}], webCitations:[https URL] }.",
...previousFailureDetailPrompt(workflow.branches?.claude),
"The untrusted brief is encoded as one JSON string.",
"<peer_brief>",
peerPromptData(workflow.brief),
Expand All @@ -3415,6 +3448,7 @@ function critiqueClaudePrompt(workflow) {
`Frozen brief SHA-256: ${workflow.briefHash}`,
"Critique both frozen memos against the original brief and optional user feedback.",
"Return {content:{critique, agreements, disagreements, corrections}}.",
...previousFailureDetailPrompt(workflow.stages?.critique),
"Each untrusted value below is encoded as one JSON value.",
"<peer_brief>",
peerPromptData(workflow.brief),
Expand Down Expand Up @@ -3495,14 +3529,18 @@ async function executePeerClaudeTurn(cwd, workflowId, options = {}) {
Array.isArray(parsed.content) ||
Object.keys(parsed.content).length === 0
)) {
throw new Error("EVIDENCE_INCOMPLETE: Claude critique content must be a non-empty JSON object.");
throw Object.assign(
new Error("EVIDENCE_INCOMPLETE: Claude critique content must be a non-empty JSON object."),
{ code: "EVIDENCE_INCOMPLETE", failureDetail: "NON_EMPTY_CONTENT_REQUIRED" }
);
}
const model = {
requestedModel: result.requestedModel ?? peerModelValue(workflow, "claude"),
finalModel: result.finalModel ?? null,
fallbackModel: peerModelValue(workflow, "claude-fallback") ?? "opus",
modelFallbacks: normalizePeerModelFallbacks(result.modelEvents),
contextWindow: result.contextWindow ?? null,
streamDiagnostics: normalizePeerStreamDiagnostics(result.streamDiagnostics),
};
const payload = critique
? {
Expand Down Expand Up @@ -3552,7 +3590,13 @@ async function executePeerClaudeTurn(cwd, workflowId, options = {}) {
};
} catch (error) {
const code = peerFailureCode(error);
const sanitized = Object.assign(new Error(code), { code });
const failureDetail = code === "EVIDENCE_INCOMPLETE"
? normalizeWorkflowFailureDetail(error?.failureDetail)
: null;
const sanitized = Object.assign(
new Error(failureDetail ? `${code}: ${failureDetail}` : code),
{ code, ...(failureDetail ? { failureDetail } : {}) }
);
failPeerAttempt(cwd, workflowId, { stage, branchId }, fence, sanitized);
throw sanitized;
} finally {
Expand Down
Loading
Loading