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
10 changes: 6 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ jobs:
ctx-*.zip*

# Plugin/binary version lockstep: plugin.json is generated from the crate
# version by `ctx harness init --mode plugin`, so it matches the tag by
# version by `ctx harness init --mode plugin`, so both target plugins match the tag by
# construction -- this job asserts that (failing the release otherwise)
# and packages the plugin scaffold as a release artifact.
plugin:
Expand All @@ -114,11 +114,13 @@ jobs:
- name: Verify plugin version lockstep and package plugin
run: ./scripts/release-plugin-check.sh "${GITHUB_REF_NAME#v}"

- name: Upload plugin artifact
- name: Upload plugin artifacts
uses: actions/upload-artifact@v4
with:
name: ctx-claude-plugin
path: ctx-claude-plugin-*.zip
name: ctx-agent-plugins
path: |
ctx-claude-plugin-*.zip
ctx-codex-plugin-*.zip

github-release:
needs: [build, plugin]
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ suite into Claude Code as hooks, with the guardrails already set:

```bash
ctx harness init --target claude # local hooks in .claude/ (or --mode plugin for a shareable plugin)
ctx harness init --target codex # local hooks in .codex/ (also supports --mode plugin)
```

This scaffolds three hooks and a starter `.ctx/rules.toml`:
Expand Down
15 changes: 13 additions & 2 deletions docs/commands/harness.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,25 @@ Package ctx as an AI coding harness integration (Claude Code).
## Synopsis

```bash
ctx harness init [--target claude] [--mode local|plugin] [--force] [--json]
ctx harness init [--target claude|codex] [--mode local|plugin] [--force] [--json]
ctx harness compat --require <SEMVER>
ctx harness doctor [--json]
```

## Description

The `harness` command wires the ctx quality suite into an AI coding harness. Today the only supported target is **Claude Code** (`--target claude`, the default); unknown targets are rejected with exit code 2 and a list of supported values.
The `harness` command wires the ctx quality suite into an AI coding harness. Supported targets are **Claude Code** (`--target claude`, the default) and **Codex** (`--target codex`); unknown targets are rejected with exit code 2 and a list of supported values.

## Codex

`ctx harness init --target codex` writes trusted-project lifecycle configuration to
`.codex/hooks.json`, scripts under `.codex/hooks/ctx/`, and the shared starter
`.ctx/rules.toml`. It prints a guidance block for manual inclusion in `AGENTS.md`.
Review and trust new or changed hook definitions with `/hooks` before relying on them.

`ctx harness init --target codex --mode plugin` creates a distributable Codex plugin
with `.codex-plugin/plugin.json`, lifecycle hooks, the ctx skill, an optional MCP
configuration, and `.agents/plugins/marketplace.json` for local installation.

Everything `init` writes comes from templates embedded in the ctx binary, and every generated file carries a header:

Expand Down
26 changes: 26 additions & 0 deletions docs/integrations/codex.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Codex Integration

Wire ctx into Codex with repository-local lifecycle hooks:

```bash
ctx harness init --target codex
```

This generates `.codex/hooks.json`, scripts under `.codex/hooks/ctx/`, and a starter
`.ctx/rules.toml`. Add the printed guidance block to `AGENTS.md`, open `/hooks` in
Codex, and review and trust the generated definitions. Project hooks load only for
trusted repositories.

The session hook supplies a codebase map, the post-edit hook refreshes the index and
checks architecture rules, and the stop hook runs the quality scorecard. Set
`CTX_GATE_BLOCKING=1` to make failed stop-time gates continue the Codex turn.

For a distributable plugin instead, run:

```bash
ctx harness init --target codex --mode plugin
codex plugin marketplace add ./
```

The plugin includes hooks, the ctx skill, and MCP wiring when ctx was built with the
`mcp` feature.
5 changes: 4 additions & 1 deletion docs/json-output.md
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,10 @@ Exit codes: 0 = snapshot written or partition already existed, 2 = operational e

### `harness.init`

`ctx harness init [--mode local|plugin] [--force] --json`
`ctx harness init [--target claude|codex] [--mode local|plugin] [--force] --json`

Codex local mode uses `"target": "codex"` and returns `agents_md_block` in place
of the Claude-specific `settings_snippet` and `claude_md_block` fields.

```json
{
Expand Down
12 changes: 10 additions & 2 deletions docs/website/docs/commands/harness.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,22 @@ Package ctx as an AI coding harness integration (Claude Code).
## Synopsis

```bash
ctx harness init [--target claude] [--mode local|plugin] [--force] [--json]
ctx harness init [--target claude|codex] [--mode local|plugin] [--force] [--json]
ctx harness compat --require <SEMVER>
ctx harness doctor [--json]
```

## Description

The `harness` command wires the ctx quality suite into an AI coding harness. Today the only supported target is **Claude Code** (`--target claude`, the default); unknown targets are rejected with exit code 2 and a list of supported values.
The `harness` command wires the ctx quality suite into an AI coding harness. Supported targets are **Claude Code** (`--target claude`, the default) and **Codex** (`--target codex`); unknown targets are rejected with exit code 2 and a list of supported values.

## Codex

`ctx harness init --target codex` generates `.codex/hooks.json`, hook scripts under
`.codex/hooks/ctx/`, a starter `.ctx/rules.toml`, and an `AGENTS.md` guidance block.
Review and trust the generated hook definitions with `/hooks`. Plugin mode generates
a `.codex-plugin` bundle, ctx skill, lifecycle hooks, local marketplace, and optional
MCP wiring.

Everything `init` writes comes from templates embedded in the ctx binary, and every generated file carries a header:

Expand Down
13 changes: 13 additions & 0 deletions docs/website/docs/integrations/codex.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
id: codex
title: Codex Integration
---

# Codex Integration

Run `ctx harness init --target codex` to generate trusted-project lifecycle hooks in
`.codex/`, a starter architecture policy, and guidance for `AGENTS.md`. Open `/hooks`
in Codex to review and trust the definitions.

Use `ctx harness init --target codex --mode plugin` for a distributable plugin with
hooks, the ctx skill, a local marketplace entry, and MCP wiring when available.
5 changes: 4 additions & 1 deletion docs/website/docs/json-output.md
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,10 @@ Exit codes: 0 = snapshot written or partition already existed, 2 = operational e

### `harness.init`

`ctx harness init [--mode local|plugin] [--force] --json`
`ctx harness init [--target claude|codex] [--mode local|plugin] [--force] --json`

Codex local mode uses `"target": "codex"` and returns `agents_md_block` in place
of the Claude-specific `settings_snippet` and `claude_md_block` fields.

```json
{
Expand Down
1 change: 1 addition & 0 deletions docs/website/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const sidebars: SidebarsConfig = {
label: 'Integrations',
items: [
'integrations/claude',
'integrations/codex',
'integrations/ci-cd',
'integrations/vscode',
],
Expand Down
47 changes: 23 additions & 24 deletions scripts/release-plugin-check.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# Plugin/binary version lockstep check + plugin packaging for releases.
#
# The Claude Code plugin manifest is *generated* from the crate version by
# `ctx harness init --mode plugin`, so plugin and binary versions agree by
# The Claude Code and Codex plugin manifests are generated from the crate version by
# `ctx harness init --mode plugin`, so plugins and binary versions agree by
# construction. This script asserts that invariant for a release: it builds
# the binary, scaffolds the plugin into a scratch directory, and fails when
# `plugin.json`'s version differs from the expected release version. On
Expand All @@ -28,25 +28,24 @@ ctx_bin="$repo_root/target/debug/ctx"
scratch="$(mktemp -d)"
trap 'rm -rf "$scratch"' EXIT

(cd "$scratch" && "$ctx_bin" harness init --mode plugin >/dev/null)

plugin_json="$scratch/.claude-plugin/plugin.json"
if [ ! -f "$plugin_json" ]; then
echo "ERROR: harness init did not generate .claude-plugin/plugin.json" >&2
exit 1
fi

actual="$(jq -r '.version' "$plugin_json")"
if [ "$actual" != "$expected" ]; then
echo "ERROR: plugin.json version '$actual' does not match release version '$expected'" >&2
echo " (plugin.json is generated from the crate version; tag, Cargo.toml," >&2
echo " and the generated manifest must agree)" >&2
exit 1
fi

zip_name="ctx-claude-plugin-$expected.zip"
rm -f "$repo_root/$zip_name"
(cd "$scratch" && zip -qr "$repo_root/$zip_name" .)

echo "OK: plugin.json version $actual matches release version $expected"
echo "Packaged $zip_name"
for target in claude codex; do
target_scratch="$scratch/$target"
mkdir -p "$target_scratch"
(cd "$target_scratch" && "$ctx_bin" harness init --target "$target" --mode plugin >/dev/null)
plugin_json="$target_scratch/.$target-plugin/plugin.json"
if [ ! -f "$plugin_json" ]; then
echo "ERROR: harness init did not generate .$target-plugin/plugin.json" >&2
exit 1
fi
actual="$(jq -r '.version' "$plugin_json")"
if [ "$actual" != "$expected" ]; then
echo "ERROR: $target plugin version '$actual' does not match '$expected'" >&2
exit 1
fi
zip_name="ctx-$target-plugin-$expected.zip"
rm -f "$repo_root/$zip_name"
(cd "$target_scratch" && zip -qr "$repo_root/$zip_name" .)
echo "Packaged $zip_name"
done

echo "OK: Claude and Codex plugin versions match $expected"
10 changes: 5 additions & 5 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,7 @@ EXAMPLES:
churn_window: String,
},

/// Package ctx as an AI coding harness integration (Claude Code)
/// Package ctx as an AI coding harness integration (Claude Code or Codex)
///
/// `init` scaffolds hook scripts, settings, and (in plugin mode) a full
/// Claude Code plugin from templates embedded in this binary. `compat`
Expand Down Expand Up @@ -863,17 +863,17 @@ pub enum HarnessTarget {
/// Claude Code (hooks, settings, skills, plugin manifest)
#[default]
Claude,
/// Codex (project hooks, AGENTS.md guidance, skills, plugin manifest)
Codex,
}

/// Scaffolding mode for `ctx harness init`.
#[derive(ValueEnum, Clone, Copy, Debug, Default, PartialEq)]
pub enum HarnessMode {
/// Hook scripts under `.claude/hooks/ctx/` plus a settings snippet
/// printed for manual inclusion
/// Project-local hooks plus target-specific settings or guidance
#[default]
Local,
/// A distributable Claude Code plugin (`.claude-plugin/`, hooks, skill,
/// marketplace manifest)
/// A distributable target plugin with hooks, skill, and marketplace
Plugin,
}

Expand Down
66 changes: 49 additions & 17 deletions src/commands/harness.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! `ctx harness` -- Claude Code integration packaging CLI.
//! `ctx harness` -- Claude Code and Codex integration packaging CLI.
//!
//! Thin wrapper around the [`ctx::harness`] library: `init` scaffolds
//! generated files (local hooks or a full plugin), `compat` is the version
Expand Down Expand Up @@ -30,14 +30,15 @@ pub fn run_harness(cmd: HarnessCommand, json: bool) -> Result<Outcome> {
force,
} => {
let root = std::env::current_dir()?;
// Single-variant enum today; keep the match so adding a target
// forces a decision here.
let HarnessTarget::Claude = target;
let target = match target {
HarnessTarget::Claude => harness::Target::Claude,
HarnessTarget::Codex => harness::Target::Codex,
};
let mode = match mode {
HarnessMode::Local => Mode::Local,
HarnessMode::Plugin => Mode::Plugin,
};
run_init(&root, mode, force, json)
run_init(&root, target, mode, force, json)
}
HarnessCommand::Compat { require } => run_compat(&require),
HarnessCommand::Doctor => {
Expand All @@ -51,10 +52,18 @@ pub fn run_harness(cmd: HarnessCommand, json: bool) -> Result<Outcome> {
// init
// ============================================================================

fn run_init(root: &Path, mode: Mode, force: bool, json_mode: bool) -> Result<Outcome> {
let plan = match mode {
Mode::Local => harness::plan_local(root),
Mode::Plugin => harness::plan_plugin(root),
fn run_init(
root: &Path,
target: harness::Target,
mode: Mode,
force: bool,
json_mode: bool,
) -> Result<Outcome> {
let plan = match (target, mode) {
(harness::Target::Claude, Mode::Local) => harness::plan_local(root),
(harness::Target::Claude, Mode::Plugin) => harness::plan_plugin(root),
(harness::Target::Codex, Mode::Local) => harness::plan_codex_local(root),
(harness::Target::Codex, Mode::Plugin) => harness::plan_codex_plugin(root),
};
let actions = harness::write_plan(root, &plan, force)?;

Expand All @@ -78,12 +87,15 @@ fn run_init(root: &Path, mode: Mode, force: bool, json_mode: bool) -> Result<Out
}

let snippet = harness::render_settings_snippet();
let guidance = harness::render_claude_md_block(root);
let guidance = match target {
harness::Target::Claude => harness::render_claude_md_block(root),
harness::Target::Codex => harness::render_agents_md_block(root),
};

// In local mode, merge the ctx hooks + permissions into
// .claude/settings.json ourselves (additive + idempotent). Plugin mode
// is unaffected.
let settings_action = if mode == Mode::Local {
let settings_action = if mode == Mode::Local && target == harness::Target::Claude {
Some(harness::wire_local_settings(root)?)
} else {
None
Expand All @@ -96,13 +108,17 @@ fn run_init(root: &Path, mode: Mode, force: bool, json_mode: bool) -> Result<Out
.collect();
let mut data = serde_json::json!({
"mode": match mode { Mode::Local => "local", Mode::Plugin => "plugin" },
"target": "claude",
"target": match target { harness::Target::Claude => "claude", harness::Target::Codex => "codex" },
"force": force,
"files": files,
});
if mode == Mode::Local {
data["settings_snippet"] = serde_json::Value::String(snippet);
data["claude_md_block"] = serde_json::Value::String(guidance);
if target == harness::Target::Claude {
data["settings_snippet"] = serde_json::Value::String(snippet);
data["claude_md_block"] = serde_json::Value::String(guidance);
} else {
data["agents_md_block"] = serde_json::Value::String(guidance);
}
if let Some(action) = &settings_action {
data["settings_action"] =
serde_json::Value::String(settings_action_str(action).to_string());
Expand All @@ -112,8 +128,8 @@ fn run_init(root: &Path, mode: Mode, force: bool, json_mode: bool) -> Result<Out
return Ok(Outcome::Clean);
}

match mode {
Mode::Local => {
match (target, mode) {
(harness::Target::Claude, Mode::Local) => {
eprintln!();
match settings_action.expect("local mode always wires settings") {
harness::SettingsWireAction::Created => {
Expand All @@ -140,7 +156,7 @@ fn run_init(root: &Path, mode: Mode, force: bool, json_mode: bool) -> Result<Out
println!("{guidance}");
eprintln!("Then verify the integration with 'ctx harness doctor'.");
}
Mode::Plugin => {
(harness::Target::Claude, Mode::Plugin) => {
eprintln!();
eprintln!("Plugin scaffold ready. Install it locally with:");
eprintln!(" claude");
Expand All @@ -156,6 +172,22 @@ fn run_init(root: &Path, mode: Mode, force: bool, json_mode: bool) -> Result<Out
);
}
}
(harness::Target::Codex, Mode::Local) => {
eprintln!();
eprintln!("Add this block to your AGENTS.md so Codex knows about ctx:");
eprintln!();
println!("{guidance}");
eprintln!("Review and trust the generated hooks with '/hooks', then run 'ctx harness doctor'.");
}
(harness::Target::Codex, Mode::Plugin) => {
eprintln!();
eprintln!("Codex plugin scaffold ready. Add its marketplace with:");
eprintln!(" codex plugin marketplace add ./");
eprintln!("Install the ctx plugin, then review and trust its hooks with '/hooks'.");
if !cfg!(feature = "mcp") {
eprintln!("note: .mcp.json was not generated because this ctx build lacks the mcp feature.");
}
}
}

Ok(Outcome::Clean)
Expand Down
Loading
Loading