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.5.1",
"version": "1.5.2",
"description": "Claude Code Plugin for Codex. Delegate code reviews, investigations, tracked tasks, and transcript transfers from inside Codex.",
"author": {
"name": "CBEPX",
Expand Down
54 changes: 36 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
name: CI

permissions:
contents: read

on:
workflow_dispatch:
push:
branches:
- main
pull_request:

jobs:
core-cross-platform:
name: Core checks (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- windows-latest
windows-unit:
name: Unit (windows-latest)
runs-on: windows-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
Expand All @@ -26,11 +26,13 @@ jobs:
- run: npm run check:changelog
- run: npm run lint
- run: npm run typecheck
- run: npm run typecheck:tests
- run: npm run test:cross-platform

macos-full:
name: Full CI (macos-latest)
runs-on: macos-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
Expand All @@ -40,17 +42,12 @@ jobs:
- run: npm install -g @openai/codex
- run: codex --version
- run: npm ci
- run: npm run check:version-sync
- run: npm run check:changelog
- run: npm run lint
- run: npm run typecheck
- run: npm run test
- run: npm run test:integration
- run: npm run test:e2e
- run: npm run check

linux-full:
name: Full CI (ubuntu-latest)
linux-coverage:
name: Coverage (ubuntu-latest)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
Expand All @@ -64,6 +61,27 @@ jobs:
- run: npm run check:changelog
- run: npm run lint
- run: npm run typecheck
- run: npm run typecheck:tests
- run: npm run test:coverage
- if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: coverage
path: reports/coverage/
if-no-files-found: error
retention-days: 14

node18-runtime:
name: Runtime compatibility (Node 18)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 18
cache: npm
- run: npm ci
- run: node --version
- run: npm run test
- run: npm run test:integration
- run: npm run test:e2e
53 changes: 53 additions & 0 deletions .github/workflows/mutation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Mutation

permissions:
contents: read

on:
workflow_dispatch:
pull_request:
schedule:
- cron: "0 3 * * 0"

jobs:
pull-request:
if: github.event_name == 'pull_request'
name: Pull-request mutation
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run test:mutation:pr:force
- if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: mutation-pull-request
path: reports/mutation/
if-no-files-found: error
retention-days: 14

full:
if: github.event_name != 'pull_request'
name: Full mutation
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run test:mutation:full:force
- if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: mutation-full
path: reports/mutation/
if-no-files-found: error
retention-days: 14
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
.claude/
.serena/
node_modules/
.stryker-tmp/
reports/mutation/
reports/coverage/
reports/stryker-incremental.json
reports/stryker-*-incremental.json
stryker.log
*.log
.DS_Store
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,40 @@

## [Unreleased]

## v1.5.2

### Added

- Isolate every test process behind a temporary `CODEX_HOME` preload and add a sentinel regression test proving production state helpers cannot write into the user's real Codex state.
- Add enforced C8 coverage gates with a 89/89/79/96 ratchet, test-source type checking, full macOS/Linux and Node.js 18 unit coverage, a curated Windows-safe suite, and retained CI reports.
- Gate pull requests with Stryker's parser contracts at an 80% break threshold plus managed-cleanup and installer shards at 55%; run all seven shards on the weekly/manual job and fail if line-range scopes drift away from their named functions.

### Changed

- Replace broad prose snapshots with focused executable skill-contract checks while retaining workspace, foreground execution, empty-placeholder, model-inheritance, notification, routing, raw-CLI fallback, and background-launch invariants.
- Expand regression coverage for current Claude Code behavior, Opus aliases, hook block/error paths, installer RPC failures, job selection, managed cleanup, and the existing normalized `contextWindow` JSON contract.

### Fixed

- Refuse managed global cleanup when `hooks.json` is malformed or has an invalid shape, preserving hook and wrapper data instead of partially deleting it.
- Preserve unrelated hook document keys and entries while removing only plugin-managed hooks and wrappers after confirmed official uninstall signals.
- Validate marketplace and hook documents before mutation, tolerate unavailable or unrecognized Codex `plugin/uninstall` failures, and keep explicit RPC permission/auth refusals fail-closed unless the recovery override is set.
- Derive uninstall targets from observed config/cache state, attempt every installed marketplace even when an earlier uninstall RPC is unavailable, and continue validated local cleanup when stale Codex state or future RPC wording would otherwise make uninstall permanently unrepeatable.
- Match only anchored plugin-absence RPC messages so unexpected errors containing words such as `unknown` or `not found` are handled by the explicit recovery policy instead of being misclassified as confirmed absence.
- Keep install/update/uninstall recoverable when foreign hook data is malformed, with an explicit escape hatch that preserves risky legacy hook files while still removing official plugin config and cache state.
- Rewrite retained global `hooks.json`, shared `config.toml`, and personal `marketplace.json` documents through synced same-directory temporary files and atomic renames when inode identity can change; follow existing symlinks, preserve file modes, and recover hard-linked in-place rewrites from synced backups.
- Defer destructive legacy-install cleanup until the replacement Codex marketplace/plugin install succeeds, so an unavailable remote update leaves the working legacy install intact.
- Stop obsolete hooks after confirmed official uninstall even when malformed hook data blocks cleanup, with a resettable one-time repair warning.
- Keep refusal-marker cleanup best-effort so permissions or Windows file locking cannot fail healthy native hook invocations or an otherwise completed uninstall.
- Preserve foreign hook shapes and empty entries, and make the shipped legacy hook installer fail before changing config when cleanup is unsafe.
- Match managed hook paths case-insensitively on Windows, exercise cleanup and line-range guards in Windows CI, and validate complete function spans for mutation scopes.
- Launch both legacy JavaScript and current native Claude Code npm shims directly on Windows, skipping stale or unsupported shims when a later PATH entry is usable, avoiding Node.js `.cmd` spawn failures without routing prompts through a command shell.
- Record Windows process identities through CIM and atomically compare the stored identity before dispatching `taskkill`, re-checking failed terminations so processes that exit during cancellation are reported accurately.
- Keep Windows hooks responsive with a time-bounded, half-open circuit breaker for read-only CIM probes while bypassing it for required spawn-time identity capture, persist failed reaper-probe throttling across one-shot hook processes with a two-second read-only timeout, always attempt atomic identity-checked cancellation for every job, distinguish pre-check absence, CIM failure, and exit during `taskkill`, grant five-minute identity leases only after successful verification, retain the first unavailable-check timestamp without refreshing it, fail open when that timestamp cannot be persisted, stop treating the job as active after a fifteen-minute unverifiable ceiling while preserving late successful results and requiring explicit CIM verification before rendering any destructive Windows cleanup command, bound cancellation identity and termination calls to ten seconds, cap aggregate SessionEnd process cleanup at twenty seconds before preserving remaining jobs for manual recovery, and hide every spawned command window.
- Resolve lock-owner identity before publication, stage the complete ownership record privately and atomically hard-link it into place, fall back once per process to exclusive-create publication on filesystems that reject hard links, use per-owner tokens so stale holders cannot remove replacement locks, retry tagged lock contention without rewriting successful executions as raw filesystem failures, clean up only crash-orphaned staging files whose names exactly match the writer's format, protect legacy malformed locks with a fifteen-second grace period, and recover otherwise unverifiable locks after a two-minute hard ceiling.
- Keep unverifiable live-owner locks fail-closed within that ceiling, treat identity lookup races and timeouts as unverifiable instead of PID mismatches, deliberately keep POSIX job reaping fail-open when identity lookup is unavailable, treat `EPERM` liveness probes as proof that POSIX processes and process groups still exist, preserve recovery PIDs when POSIX cancellation cannot verify a live process group, re-check a surviving leader's identity before SIGKILL while still escalating orphaned child groups after their leader exits, classify already-exited POSIX and Windows process trees accurately, and render platform-correct manual cleanup commands against the same PID that was verified.
- Run the process lifecycle suite in Windows CI with platform-neutral Node.js fixtures, including a real CIM lookup and identity-checked child-process tree termination, and expand mutation shards across the complete changed process, reaper, and lock-recovery functions.

## v1.5.1

### Fixed
Expand Down
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,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.5.1
codex plugin marketplace add CBEPX/cc-plugin-codex --ref v1.5.2
codex plugin add cc@cbepx
```

Expand All @@ -59,13 +59,15 @@ 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.5.1 \
npx -y https://github.com/CBEPX/cc-plugin-codex/releases/download/v1.5.1/cc-plugin-codex-1.5.1.tgz install
CC_PLUGIN_CODEX_MARKETPLACE_REF=v1.5.2 \
npx -y https://github.com/CBEPX/cc-plugin-codex/releases/download/v1.5.2/cc-plugin-codex-1.5.2.tgz install
```

On Windows, prefer the marketplace path or the `npx` helper. The shell-script helper below is POSIX-only.
Codex CLI's official guidance still treats Windows support as experimental and recommends a WSL workspace for the best Codex experience. Claude Code supports both native Windows and WSL.

If install/update/uninstall reports an invalid global `hooks.json` while a legacy cc install is present, repair that JSON before retrying. To continue without touching risky legacy hook files, set `CC_PLUGIN_CODEX_SKIP_LEGACY_CLEANUP=1`; uninstall still removes official plugin config/cache state but deliberately leaves the legacy files for manual repair. An explicit Codex RPC permission/auth refusal remains fail-closed; use `CC_PLUGIN_CODEX_IGNORE_UNINSTALL_RPC=1` only when you intentionally want local uninstall cleanup despite that refusal.

> **Prerequisites:** Node.js 18+, Codex with hook support, and `claude` CLI installed and authenticated.
> If you don't have the Claude CLI yet:
> ```bash
Expand Down Expand Up @@ -332,7 +334,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.5.1
codex plugin marketplace add CBEPX/cc-plugin-codex --ref v1.5.2
codex plugin add cc@cbepx
```

Expand All @@ -353,8 +355,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.5.1 \
npx -y https://github.com/CBEPX/cc-plugin-codex/releases/download/v1.5.1/cc-plugin-codex-1.5.1.tgz install
CC_PLUGIN_CODEX_MARKETPLACE_REF=v1.5.2 \
npx -y https://github.com/CBEPX/cc-plugin-codex/releases/download/v1.5.2/cc-plugin-codex-1.5.2.tgz install
```

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

Expand Down Expand Up @@ -442,17 +444,21 @@ Run the normal project gate before publishing or opening a PR:

```bash
npm run check
npm run test:coverage
```

Mutation testing is available as an advisory local signal for the high-risk CLI parsing and prompt-rendering modules:
`npm run check` requires a working `codex` executable for its real E2E suite. Constrained local environments may opt out explicitly with `CC_PLUGIN_ALLOW_E2E_SKIP=1 npm run check`; CI never uses that opt-out.
Coverage stays a separate instrumented run because `npm run check` already executes the full unit, integration, and Codex E2E suites. CI fails below 89% lines/statements, 79% branches, or 96% functions.

Mutation testing enforces the same pull-request profile used in CI:

```bash
npm run test:mutation:dry-run
npm run test:mutation
npm run test:mutation:force
```

Stryker runs through the native `node:test` command runner, so coverage analysis is disabled and the mutation score does not fail the build. The generated report is written under `reports/mutation/`. Use `test:mutation:force` after changing only tests because command-runner incremental mode cannot reliably detect that. Do not mass-disable surviving mutants; either improve the focused tests or use a `// Stryker disable ...: reason` comment for an intentional equivalent mutant.
`test:mutation` checks the critical parser contracts plus managed cleanup and installer orchestration. `test:mutation:force` runs all seven shards, matching the weekly/manual workflow. Scores below each configured break threshold fail the command, and reports are written under `reports/mutation/`. Use the force variant after changing only tests because command-runner incremental mode cannot reliably detect that. Do not mass-disable surviving mutants; improve the focused tests or use a `// Stryker disable ...: reason` comment only for an intentional equivalent mutant.

Mutation testing requires Node.js 20+ because Stryker 9 has a newer development-time engine requirement. The plugin runtime still supports the Node.js version listed in the prerequisites.

Expand Down
1 change: 1 addition & 0 deletions hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
{
"type": "command",
"command": "node \"$PLUGIN_ROOT/hooks/session-lifecycle-hook.mjs\" SessionEnd",
"timeout": 45,
"statusMessage": "Cleaning up Claude Code bridge jobs"
}
]
Expand Down
57 changes: 53 additions & 4 deletions hooks/lib/plugin-install-guard.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,76 @@
* SPDX-License-Identifier: Apache-2.0
*/

import fs from "node:fs";
import path from "node:path";
import process from "node:process";

import {
cleanupManagedGlobalIntegrations,
getManagedPluginSignals,
} from "../../scripts/lib/managed-global-integration.mjs";
import { resolveCodexHome } from "../../scripts/lib/codex-paths.mjs";

export function cleanupAfterOfficialUninstall(pluginRoot) {
const signals = getManagedPluginSignals();
function clearRefusalMarker(refusalMarker) {
try {
fs.rmSync(refusalMarker, { recursive: true, force: true });
} catch {
// A cosmetic warning marker must never make a native hook fail.
}
}

export function cleanupAfterOfficialUninstall(pluginRoot, codexHome) {
const resolvedCodexHome = codexHome ?? resolveCodexHome();
const signals = getManagedPluginSignals(resolvedCodexHome);
const refusalMarker = path.join(
resolvedCodexHome,
"plugins",
"data",
"cc",
"managed-cleanup-refused"
);

if (signals.configState === "active") {
clearRefusalMarker(refusalMarker);
return false;
}

if (signals.configState !== "inactive" || signals.cachePresent) {
if (signals.cachePresent) {
clearRefusalMarker(refusalMarker);
}
return false;
}

const cleaned = cleanupManagedGlobalIntegrations(
pluginRoot,
resolvedCodexHome,
{ reportRefusal: false }
);
if (!cleaned) {
const refusalReason = `${signals.reason}\n`;
let previousReason = null;
try {
previousReason = fs.readFileSync(refusalMarker, "utf8");
} catch {}
if (previousReason !== refusalReason) {
process.stderr.write(
`[cc] managed hook cleanup refused after explicit uninstall signals (${signals.reason}, cache missing); repair ${path.join(resolvedCodexHome, "hooks.json")}\n`
);
try {
clearRefusalMarker(refusalMarker);
fs.mkdirSync(path.dirname(refusalMarker), { recursive: true });
fs.writeFileSync(refusalMarker, refusalReason, "utf8");
} catch {
// The hook still exits early even when the warning marker cannot be persisted.
}
}
return true;
}

clearRefusalMarker(refusalMarker);
process.stderr.write(
`[cc] removing managed hooks after explicit uninstall signals (${signals.reason}, cache missing)\n`
`[cc] removed managed hooks after explicit uninstall signals (${signals.reason}, cache missing)\n`
);
cleanupManagedGlobalIntegrations(pluginRoot);
return true;
}
Loading
Loading