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

## [Unreleased]

## v1.7.6

### Changed

- Bound session-start and prompt hooks at the manifest boundary and bound prompt-time Git discovery and stale-job identity probes inside the hook budget.
- Align collaboration skill instructions with `fork_turns: "none"`, parent-only `send_message` delivery, computed defaults when no question tool is available, and explicit Fable reasoning effort.

### Fixed

- Keep malformed or oversized lifecycle hook payloads fail-open and retain unread background results when parent delivery fails.

## v1.7.5

### Changed
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ $cc:review --user-mcp-tool mcp__context7__resolve-library-id

**Flags:** `--base <ref>`, `--scope <auto|working-tree|branch>`, `--wait`, `--background`, `--model <model>`, `--effort <low|medium|high|xhigh|max>`, `--user-mcp-tool <mcp__server__tool>`, `--allow-project-mcp-servers`

**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.
**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` have no implicit effort. Explicit Fable effort, including `medium`, is forwarded unchanged. 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.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).

Expand Down Expand Up @@ -228,7 +228,7 @@ $cc:rescue --model sonnet --effort medium investigate the flaky test
| `--fresh` | Force a new task (don't resume) |
| `--write` | Allow file edits (default) |
| `--model <model>` | Claude model (`opus`, `sonnet`, `haiku`, `fable`, or full ID; defaults to `opus`). Aliases are resolved by Claude Code; a full ID pins a version. |
| `--effort <level>` | Reasoning effort: `low`, `medium`, `high`, `xhigh`, `max` (default: `xhigh` for opus, `high` for sonnet, unset for haiku and fable) |
| `--effort <level>` | Reasoning effort: `low`, `medium`, `high`, `xhigh`, `max` (default: `xhigh` for opus, `high` for sonnet, unset for haiku and fable; explicit Fable effort such as `medium` is supported) |
| `--prompt-file <path>` | Read task description from a file |
| `--view-state on-terminal` | Mark the foreground terminal outcome as viewed |
| `--view-state defer` | Leave the terminal outcome unread for later inspection |
Expand Down
2 changes: 2 additions & 0 deletions hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
{
"type": "command",
"command": "node \"${PLUGIN_DATA}/runtime/hook-launcher.mjs\" session-lifecycle",
"timeout": 5,
"statusMessage": "Initializing Claude Code bridge"
}
]
Expand Down Expand Up @@ -43,6 +44,7 @@
{
"type": "command",
"command": "node \"${PLUGIN_DATA}/runtime/hook-launcher.mjs\" unread-result",
"timeout": 5,
"statusMessage": "Checking for unread Claude Code results"
}
]
Expand Down
1 change: 0 additions & 1 deletion hooks/session-lifecycle-hook.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -580,5 +580,4 @@ main().catch((error) => {
process.stderr.write(
`${error instanceof Error ? error.message : String(error)}\n`
);
process.exit(1);
});
42 changes: 28 additions & 14 deletions hooks/unread-result-hook.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const MAX_LISTED_JOBS = 3;
const SKIP_INTERACTIVE_HOOKS_ENV = "CLAUDE_COMPANION_SKIP_INTERACTIVE_HOOKS";
const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const PROMPT_NOTIFICATION_BUDGET_MS = 1_500;
const PROMPT_GIT_TIMEOUT_MS = 200;

function isExplicitClaudeStatusRequest(prompt) {
const text = String(prompt ?? "").toLowerCase();
Expand Down Expand Up @@ -89,12 +90,15 @@ function buildWorkflowContext(workflows) {
].join("\n");
}

function selectUnreadTerminalJobs(workspaceRoot, sessionId) {
function selectUnreadTerminalJobs(workspaceRoot, sessionId, deadlineAt) {
if (!sessionId) {
return [];
}

return listJobs(workspaceRoot)
return listJobs(workspaceRoot, {
deadlineAt,
identityTimeoutMs: PROMPT_GIT_TIMEOUT_MS,
})
.filter((job) => job.sessionId === sessionId)
.filter((job) => !job.workflowId)
.filter((job) => TERMINAL_JOB_STATUSES.has(job.status))
Expand All @@ -117,13 +121,18 @@ function selectUnreadWorkflows(workspaceRoot, sessionId) {
.filter(({ workflow, event }) => !(workflow.viewedEvents ?? []).includes(event));
}

function markJobsNotified(workspaceRoot, jobs) {
function markJobsNotified(workspaceRoot, jobs, deadlineAt) {
const timestamp = nowIso();
for (const job of jobs) {
try {
transitionJob(workspaceRoot, job.id, [job.status], job.status, {
notifiedAt: timestamp,
});
transitionJob(
workspaceRoot,
job.id,
[job.status],
job.status,
{ notifiedAt: timestamp },
{ deadlineAt, skipLockOwnerIdentity: process.platform === "win32" }
);
} catch {
// Notification state is best-effort; still surface the terminal result.
}
Expand Down Expand Up @@ -167,12 +176,14 @@ function markWorkflowsNotified(workspaceRoot, workflows, deadlineAt) {
return claimed;
}

function captureTurnBaseline(workspaceRoot, sessionId, cwd) {
function captureTurnBaseline(workspaceRoot, sessionId, cwd, deadlineAt) {
if (!sessionId) {
return;
}
try {
const fingerprint = getWorkingTreeFingerprint(cwd);
const fingerprint = getWorkingTreeFingerprint(cwd, {
timeout: Math.max(1, Math.min(PROMPT_GIT_TIMEOUT_MS, deadlineAt - performance.now())),
});
writeTurnBaseline(workspaceRoot, sessionId, {
cwd,
workspaceRoot,
Expand Down Expand Up @@ -200,10 +211,14 @@ async function main() {
return;
}
const cwd = input.cwd || process.cwd();
const workspaceRoot = resolveWorkspaceRoot(cwd);
const notificationDeadlineAt = performance.now() + PROMPT_NOTIFICATION_BUDGET_MS;
const workspaceRoot = resolveWorkspaceRoot(cwd, {
filesystemFallbackOnTimeout: true,
gitTimeout: Math.max(1, Math.min(PROMPT_GIT_TIMEOUT_MS, notificationDeadlineAt - performance.now())),
processLocalHandoff: true,
});
const sessionId = input.session_id || process.env[SESSION_ID_ENV] || null;
const prompt = String(input.prompt ?? "");
const notificationDeadlineAt = performance.now() + PROMPT_NOTIFICATION_BUDGET_MS;

if (
process.env[SKIP_INTERACTIVE_HOOKS_ENV] === "1" ||
Expand All @@ -224,20 +239,20 @@ async function main() {
// Best effort: an invalid session id must not fail a user prompt.
}
if (config.stopReviewGate) {
captureTurnBaseline(workspaceRoot, sessionId, cwd);
captureTurnBaseline(workspaceRoot, sessionId, cwd, notificationDeadlineAt);
}

if (isExplicitClaudeStatusRequest(prompt)) {
return;
}

const jobs = selectUnreadTerminalJobs(workspaceRoot, sessionId);
const jobs = selectUnreadTerminalJobs(workspaceRoot, sessionId, notificationDeadlineAt);
const workflows = selectUnreadWorkflows(workspaceRoot, sessionId);
if (jobs.length === 0 && workflows.length === 0) {
return;
}

markJobsNotified(workspaceRoot, jobs);
markJobsNotified(workspaceRoot, jobs, notificationDeadlineAt);
const claimedWorkflows = markWorkflowsNotified(
workspaceRoot,
workflows,
Expand All @@ -255,5 +270,4 @@ main().catch((error) => {
process.stderr.write(
`${error instanceof Error ? error.message : String(error)}\n`
);
process.exit(1);
});
9 changes: 5 additions & 4 deletions internal-skills/cli-runtime/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Command selection:
Routing controls:
- Treat `--cwd`, `--model`, `--effort`, `--resume`, `--resume-last`, `--fresh`, `--prompt-file`, `--view-state`, `--owner-session-id`, and `--job-id` as routing controls, not task text.
- Preserve the parent helper's exact non-empty `workspaceRoot` as `--cwd "<workspaceRoot>"`; never substitute the plugin root or the child's default working directory.
- Leave `--model` and `--effort` unset unless the user explicitly asks for a specific model or effort. The companion command applies these defaults itself: model defaults to `opus`, effort defaults to `xhigh` for opus, `high` for sonnet, and is left unset for haiku and fable.
- Leave `--model` and `--effort` unset unless the user explicitly asks for them, and preserve explicit values exactly. The companion command applies its own defaults; Fable has no implicit effort but supports explicit effort such as `medium`.
- `--view-state on-terminal` means the user will see this companion result in the current turn, so the companion may mark any terminal outcome viewed.
- `--view-state defer` means the parent is not waiting, so the companion must leave the result unread until the user explicitly checks it.
- `--owner-session-id <session-id>` is an internal parent-session routing control. Preserve it when present so tracked jobs remain visible to the parent session's `$cc:status` / `$cc:result`.
Expand All @@ -52,9 +52,10 @@ Task defaults:
- Exit code 0 is the only successful completion.
- Exit code 124 means the job is still running; return the companion output without claiming it finished.
- For any other non-zero exit code or shell-tool error, return the raw companion output or diagnostic without a success notification.
- If the parent supplied a non-empty parent thread id for background completion, allow at most one success-only `send_input` notification before finishing.
- Mention the tool name `send_input` literally when describing that notification path.
- Use the exact tool shape `send_input({ target: <parent-thread-id>, message: <steering-message> })`.
- If the parent supplied a non-empty parent thread id for background completion, allow at most one success-only `send_message` notification before finishing.
- Mention the tool name `send_message` literally when describing that notification path.
- Use the exact tool shape `send_message({ target: <parent-thread-id>, message: <steering-message> })`.
- Keep deferred view state so a failed or unavailable delivery remains available to the unread-result hook.
- Use steering messages that point the parent at `$cc:result` or `$cc:status` instead of embedding the raw Claude result.
- For background rescue, use that same steering message as the child's own final assistant message instead of echoing the raw companion result.
- Do not poll status, fetch results, cancel jobs, or add commentary after the companion output.
Expand Down
10 changes: 5 additions & 5 deletions internal-skills/review-runtime/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ Background contract:
- Exit code 0 is the only successful completion.
- Exit code 124 means the job is still running; return the companion output without claiming it finished.
- For any other non-zero exit code or shell-tool error, return the raw companion output or diagnostic without a success notification.
- use at most one `send_input` completion notification on success
- mention the tool name `send_input` literally in the child instructions
- use the exact tool shape `send_input({ target: <parent-thread-id>, message: <steering-message> })`
- use at most one `send_message` completion notification on success
- mention the tool name `send_message` literally in the child instructions
- use the exact tool shape `send_message({ target: <parent-thread-id>, message: <steering-message> })`
- keep deferred view state so failed delivery remains available to the unread-result hook
- do not silently drop the completion notification path when the parent provided a non-empty parent thread id

Spawn-agent defaults:
- `fork_context: false`
- `fork_turns: "none"`
- `reasoning_effort: "medium"`
- Use the implicit default role and omit `agent_type` when it is optional or absent. If the runtime schema marks `agent_type` required, pass `agent_type: "default"`.
- Omit `model` so the child inherits the current Codex runtime model.
- Do not add a fixed-version model fallback.

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.5",
"version": "1.7.6",
"description": "Claude Code Plugin for Codex (CBEPX fork)",
"type": "module",
"author": {
Expand Down
6 changes: 3 additions & 3 deletions scripts/lib/git.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,15 @@ function hashText(value) {
return createHash("sha256").update(String(value ?? ""), "utf8").digest("hex");
}

export function getWorkingTreeFingerprint(cwd) {
export function getWorkingTreeFingerprint(cwd, options = {}) {
const fingerprintGitEnv = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
const smallGitOptions = {
timeout: FINGERPRINT_GIT_TIMEOUT_MS,
timeout: options.timeout ?? FINGERPRINT_GIT_TIMEOUT_MS,
maxBuffer: FINGERPRINT_SMALL_MAX_BUFFER,
env: fingerprintGitEnv,
};
const pathListGitOptions = {
timeout: FINGERPRINT_GIT_TIMEOUT_MS,
timeout: options.timeout ?? FINGERPRINT_GIT_TIMEOUT_MS,
maxBuffer: FINGERPRINT_PATH_LIST_MAX_BUFFER,
env: fingerprintGitEnv,
};
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 @@ -305,7 +305,7 @@ function stdioProbe(config, timeoutMs) {
params: {
protocolVersion: MCP_PROTOCOL_VERSION,
capabilities: {},
clientInfo: { name: "cc-plugin-codex", version: "1.7.5" },
clientInfo: { name: "cc-plugin-codex", version: "1.7.6" },
},
});
});
Expand Down Expand Up @@ -394,7 +394,7 @@ async function httpProbe(config, timeoutMs) {
params: {
protocolVersion: MCP_PROTOCOL_VERSION,
capabilities: {},
clientInfo: { name: "cc-plugin-codex", version: "1.7.5" },
clientInfo: { name: "cc-plugin-codex", version: "1.7.6" },
},
}, null, deadline);
if (initialized.statusCode === 401 || initialized.statusCode === 403) {
Expand Down
Loading
Loading