diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..bf71fb8 --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "torque-loop", + "interface": { + "displayName": "Torque Loop" + }, + "plugins": [ + { + "name": "torque-loop", + "source": { + "source": "local", + "path": "./" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Developer Tools" + } + ] +} diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json new file mode 100644 index 0000000..a549789 --- /dev/null +++ b/.codex-plugin/plugin.json @@ -0,0 +1,41 @@ +{ + "name": "torque-loop", + "version": "0.2.0", + "description": "Torque Loop evolves ambiguous work into shipped, tested, serialized artifacts through evidence-gated loops.", + "author": { + "name": "Danny Gillespie", + "url": "https://github.com/TheLucidTech" + }, + "homepage": "https://github.com/TheLucidTech/torque-loop", + "repository": "https://github.com/TheLucidTech/torque-loop", + "license": "MIT", + "keywords": [ + "workflow", + "quality", + "validation", + "skills", + "state", + "qa", + "decision" + ], + "skills": "./skills/", + "interface": { + "displayName": "Torque Loop", + "shortDescription": "Evidence-gated workflow skills and ratchet state helpers", + "longDescription": "Use Torque Loop to lock vague goals, rank blockers, build small artifacts, attack assumptions, patch failures, serialize state, and run bounded evidence-gated evolution loops.", + "developerName": "Danny Gillespie", + "category": "Developer Tools", + "capabilities": [ + "Interactive", + "Read", + "Write" + ], + "defaultPrompt": [ + "Use Torque Loop to lock and ship this task.", + "Run an evidence-gated evolve pass on this file.", + "Attack this plan and patch only what fails." + ], + "websiteURL": "https://github.com/TheLucidTech/torque-loop", + "brandColor": "#111827" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index b57ee82..1532000 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Codex install metadata: `.codex-plugin/plugin.json` plus a repo-local + `.agents/plugins/marketplace.json`, so the plugin can be registered and installed by + Codex CLI and surfaced in the Codex app. + +### Changed + +- Package metadata, `ratchet doctor`, and plugin-shape tests now cover both Claude Code + and Codex manifests. +- npm packaging includes the Codex marketplace file explicitly as + `.agents/plugins/marketplace.json`, without packaging the whole `.agents` tree. +- Repo snapshots now surface `.agents` and `.codex-plugin` alongside the existing + plugin-critical dot directories. + ## [0.2.0] - 2026-07-03 — Proof Gate Hardening, not expansion. This release makes the loop's philosophy mechanically true: diff --git a/README.md b/README.md index af0687b..fe83261 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,10 @@ **Mutate. Test. Keep the delta.** -A Claude Code plugin for evolving one artifact through evidence-gated improvement loops. +A Claude Code and Codex plugin for evolving one artifact through evidence-gated improvement +loops. -> Not affiliated with, endorsed by, or sponsored by Anthropic. +> Not affiliated with, endorsed by, or sponsored by Anthropic or OpenAI. Torque Loop is **not a prompt library.** A prompt library gives Claude better words. Torque Loop gives Claude a job: @@ -36,20 +37,19 @@ restarting. *Ambiguity in. Artifact out. Failure tested. State advanced.* ## Install -Torque Loop has two halves: the **Claude Code plugin** (the slash commands + agents + hooks) -and the **`ratchet` CLI** (the state engine the commands call). Installing the plugin is the -main path; the CLI ships inside it and is put on `PATH` automatically while the plugin is -enabled. The plugin's command namespace stays `/ratchet:*` — the brand is Torque Loop, the -commands keep the wrench energy. +Torque Loop has two halves: the **agent plugin** (skills/commands, agents, and Claude-only +hooks) and the **`ratchet` CLI** (the state engine the skills call). Claude Code exposes the +skills as `/ratchet:*` slash commands. Codex installs the same skills under the +`torque-loop` plugin and can use them from Codex CLI or the Codex app. ### Requirements -- [Claude Code](https://claude.com/claude-code) +- Claude Code or Codex CLI/app - Node.js ≥ 18 (`node --version`) - On Windows, the bundled hooks call the CLI via `node`, so no shell-specific setup is needed. -### A. Install as a Claude Code plugin — global (available in every project) +### A. Install as a Claude Code plugin — global The repo doubles as a single-plugin marketplace, so you can install it directly. @@ -93,9 +93,44 @@ Or vendor it directly inside the project and reference the local path. Either wa commands appear only when that project is open. (Prefer an absolute path, or a path relative to the settings file, so it resolves on every machine.) -### C. Install the `ratchet` CLI on its own (optional) +### C. Install as a Codex plugin — CLI -The commands find the CLI automatically, but you can also use `ratchet` from any terminal. +The repo also contains a Codex marketplace manifest at +`.agents/plugins/marketplace.json` and a Codex plugin manifest at +`.codex-plugin/plugin.json`. + +```bash +# 1. get the repo +git clone https://github.com/TheLucidTech/torque-loop.git + +# 2. register this repo as a Codex marketplace +codex plugin marketplace add /absolute/path/to/torque-loop + +# 3. install the plugin from that marketplace +codex plugin add torque-loop@torque-loop + +# 4. verify Codex can see it +codex plugin list --marketplace torque-loop +``` + +For local development, re-run `codex plugin add torque-loop@torque-loop` after manifest +changes, then start a new Codex thread so the refreshed skills are loaded. + +### D. Install as a Codex plugin — app + +Register the marketplace once with the Codex CLI: + +```bash +codex plugin marketplace add /absolute/path/to/torque-loop +``` + +Then open the Codex app, go to **Plugins**, find **Torque Loop** under Developer Tools, +and install it. The app and CLI share the configured marketplace source. + +### E. Install the `ratchet` CLI on its own (optional) + +Claude Code puts the bundled CLI on `PATH` while the plugin is enabled. Codex skills can +use a globally installed `ratchet`, or you can run the bundled CLI from the plugin root. ```bash cd torque-loop @@ -122,7 +157,7 @@ ratchet status State survives plugin updates. It is written to, in order of preference: -1. `$CLAUDE_PLUGIN_DATA` — set by Claude Code for enabled plugins (recommended). +1. `$CLAUDE_PLUGIN_DATA` — set by Claude Code for enabled plugins. 2. `$RATCHET_DATA_DIR` — override it yourself. 3. `~/.ratchet` — fallback. @@ -133,7 +168,9 @@ one shared data directory. ## Commands -Run `/ratchet:ignite` when you don't know which command to run — it drives the full loop. +In Claude Code, run `/ratchet:ignite` when you don't know which command to run — it drives +the full loop. In Codex, ask it to use the matching Torque Loop skill, for example +`torque-loop:ignite` or `torque-loop:evolve`. ### Core loop @@ -191,10 +228,9 @@ absolute: **no proof → no keep; no keep → no progress claim.** It is never a this better" — it evolves along one chosen pressure vector and records every verdict to `.ratchet/evolve-log.jsonl` via the `ratchet-evolve` helper CLI. -> Note: because plugin skills are namespaced by the plugin, the command is invoked as -> `/ratchet:evolve` (renamed from the older `ratchet-evolve` skill in v0.2.0 — no alias is -> kept). Drop the `SKILL.md` into `~/.claude/skills/evolve/` to use it as a bare `/evolve` -> outside the plugin. +> Note: in Claude Code, the plugin command is invoked as `/ratchet:evolve` (renamed from +> the older `ratchet-evolve` skill in v0.2.0 — no alias is kept). In Codex, use the +> installed Torque Loop skill, typically surfaced as `torque-loop:evolve`. #### One run, end to end @@ -222,10 +258,10 @@ refused at write time — the loop cannot record progress it did not prove. ## How it works ``` -skills/*/SKILL.md → Claude-facing operating discipline (the slash commands) +skills/*/SKILL.md → agent-facing operating discipline (Claude slash commands / Codex skills) agents/*.md → ratchet-builder · ratchet-auditor · ratchet-scribe -hooks/hooks.json → session-start init · post-edit tracking · stop-compile reminder -bin/ratchet → the state CLI (added to PATH while the plugin is enabled) +hooks/hooks.json → Claude-only session-start init · post-edit tracking · stop reminder +bin/ratchet → the state CLI (PATH in Claude, global or plugin-root path in Codex) bin/ratchet-evolve → the evolution-loop helper CLI (snapshot · score · verify · log) src/*.js → state, scoring, ledger, artifact indexing, snapshots, rendering src/evolve/*.js → snapshot · pressure · mutation scoring · verify runner · journal @@ -283,8 +319,8 @@ public issue tracker. MIT © 2026 Danny Gillespie -Not affiliated with, endorsed by, or sponsored by Anthropic. "Claude" and "Claude Code" are -trademarks of Anthropic. +Not affiliated with, endorsed by, or sponsored by Anthropic or OpenAI. "Claude" and +"Claude Code" are trademarks of Anthropic. "Codex" is a trademark of OpenAI. --- diff --git a/package.json b/package.json index a74c8ff..110cbaa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@torque-loop/plugin", "version": "0.2.0", - "description": "Torque Loop — a Claude Code plugin that evolves ambiguity into shipped, tested, serialized artifacts through evidence-gated loops. Not affiliated with Anthropic.", + "description": "Torque Loop — a Claude Code and Codex plugin that evolves ambiguity into shipped, tested, serialized artifacts through evidence-gated loops.", "license": "MIT", "author": "Danny Gillespie", "type": "commonjs", @@ -25,11 +25,14 @@ "agents", "hooks", "templates", - ".claude-plugin" + ".claude-plugin", + ".codex-plugin", + ".agents/plugins/marketplace.json" ], "keywords": [ "torque-loop", "claude-code", + "codex", "plugin", "workflow", "quality", diff --git a/skills/evolve/SKILL.md b/skills/evolve/SKILL.md index be78499..4e843e2 100644 --- a/skills/evolve/SKILL.md +++ b/skills/evolve/SKILL.md @@ -44,7 +44,9 @@ without it, propose patches but do not modify files). If the target or goal is m straight to **ASK**. The deterministic helpers live in the `ratchet-evolve` CLI (call via Bash; fall back to -`node "$CLAUDE_PLUGIN_ROOT/bin/ratchet-evolve"` if not on PATH). +`node "/bin/ratchet-evolve"` if not on PATH). In Claude Code, +`` is `$CLAUDE_PLUGIN_ROOT`; in Codex local development, use this repo root +or the installed plugin path shown by `codex plugin list --json`. ## The loop diff --git a/skills/ignite/SKILL.md b/skills/ignite/SKILL.md index ca42a7d..06d842b 100644 --- a/skills/ignite/SKILL.md +++ b/skills/ignite/SKILL.md @@ -22,7 +22,10 @@ ratchet status ratchet snapshot repo ``` -If `ratchet` is not on PATH, call it as `node "$CLAUDE_PLUGIN_ROOT/bin/ratchet"`. +If `ratchet` is not on PATH, call the plugin-local CLI as +`node "/bin/ratchet"`. In Claude Code, `` is +`$CLAUDE_PLUGIN_ROOT`; in Codex local development, use this repo root or the installed +plugin path shown by `codex plugin list --json`. ## The pipeline (run all seven; do not skip) diff --git a/src/cli.js b/src/cli.js index fb63b4b..395115d 100644 --- a/src/cli.js +++ b/src/cli.js @@ -314,8 +314,8 @@ function frontmatter(text) { } // "Is my plugin healthy?" — validate the plugin shape against what Claude Code -// expects, plus version alignment and a live state/snapshot check. Exits non-zero -// on any failure so CI and `npm test` can gate a release on it. +// and Codex expect, plus version alignment and a live state/snapshot check. +// Exits non-zero on any failure so CI and `npm test` can gate a release on it. function cmdDoctor(cwd, asJson) { const root = PLUGIN_ROOT; const checks = []; @@ -333,23 +333,48 @@ function cmdDoctor(cwd, asJson) { const pkg = readJsonFile('package.json'); add('package.json parses', pkg.ok, pkg.ok ? '' : pkg.error); - const plugin = readJsonFile('.claude-plugin/plugin.json'); - add('.claude-plugin/plugin.json parses', plugin.ok, plugin.ok ? '' : plugin.error); - const market = readJsonFile('.claude-plugin/marketplace.json'); - add('.claude-plugin/marketplace.json parses', market.ok, market.ok ? '' : market.error); - - if (pkg.ok && plugin.ok && market.ok) { + const claudePlugin = readJsonFile('.claude-plugin/plugin.json'); + add('.claude-plugin/plugin.json parses', claudePlugin.ok, claudePlugin.ok ? '' : claudePlugin.error); + const claudeMarket = readJsonFile('.claude-plugin/marketplace.json'); + add('.claude-plugin/marketplace.json parses', claudeMarket.ok, claudeMarket.ok ? '' : claudeMarket.error); + const codexPlugin = readJsonFile('.codex-plugin/plugin.json'); + add('.codex-plugin/plugin.json parses', codexPlugin.ok, codexPlugin.ok ? '' : codexPlugin.error); + const codexMarket = readJsonFile('.agents/plugins/marketplace.json'); + add('.agents/plugins/marketplace.json parses', codexMarket.ok, codexMarket.ok ? '' : codexMarket.error); + + if (pkg.ok && claudePlugin.ok && claudeMarket.ok && codexPlugin.ok) { const pv = pkg.data.version; const others = [ - ['plugin.json', plugin.data.version], - ['marketplace.metadata', market.data.metadata && market.data.metadata.version], - ['marketplace.plugins[0]', market.data.plugins && market.data.plugins[0] && market.data.plugins[0].version], + ['.claude-plugin/plugin.json', claudePlugin.data.version], + ['.claude-plugin/marketplace.metadata', claudeMarket.data.metadata && claudeMarket.data.metadata.version], + ['.claude-plugin/marketplace.plugins[0]', claudeMarket.data.plugins && claudeMarket.data.plugins[0] && claudeMarket.data.plugins[0].version], + ['.codex-plugin/plugin.json', codexPlugin.data.version], ]; const mismatch = others.filter(([, v]) => v !== pv).map(([k, v]) => `${k}=${v}`); add('versions aligned', mismatch.length === 0, mismatch.length ? `package=${pv} but ${mismatch.join(', ')}` : `all ${pv}`); } - for (const d of ['skills', 'agents', 'hooks', 'bin', 'src']) { + if (codexPlugin.ok) { + const iface = codexPlugin.data.interface || {}; + const required = ['displayName', 'shortDescription', 'longDescription', 'developerName', 'category']; + const missing = required.filter((field) => !iface[field]); + add('Codex manifest required interface fields exist', missing.length === 0, missing.join(', ')); + add('Codex manifest points at skills', codexPlugin.data.skills === './skills/'); + } + + if (codexPlugin.ok && codexMarket.ok) { + const plugins = Array.isArray(codexMarket.data.plugins) ? codexMarket.data.plugins : []; + const entry = plugins.find((p) => p && p.name === codexPlugin.data.name); + add('Codex marketplace entry exists', Boolean(entry), codexPlugin.data.name); + if (entry) { + const sourceOk = entry.source && entry.source.source === 'local' && entry.source.path === './'; + const policyOk = entry.policy && entry.policy.installation === 'AVAILABLE' && entry.policy.authentication === 'ON_INSTALL'; + add('Codex marketplace source is local repo root', sourceOk); + add('Codex marketplace policy is installable', policyOk); + } + } + + for (const d of ['.agents', '.claude-plugin', '.codex-plugin', 'skills', 'agents', 'hooks', 'bin', 'src']) { const p = path.join(root, d); add(`dir ${d}/ exists`, fs.existsSync(p) && fs.statSync(p).isDirectory()); } diff --git a/src/repoSnapshot.js b/src/repoSnapshot.js index 2ec10d3..f389c61 100644 --- a/src/repoSnapshot.js +++ b/src/repoSnapshot.js @@ -28,9 +28,8 @@ const IGNORE_DIRS = new Set([ ]); // Dot-directories that ARE meaningful to a plugin/repo and must not be hidden. -// Without this, a blanket dot-dir skip loses .claude-plugin, .github, etc. — the -// exact directories a Claude Code plugin lives in. -const DOT_DIR_ALLOWLIST = new Set(['.claude', '.claude-plugin', '.github', '.ratchet']); +// Without this, a blanket dot-dir skip loses plugin manifests and CI metadata. +const DOT_DIR_ALLOWLIST = new Set(['.agents', '.claude', '.claude-plugin', '.codex-plugin', '.github', '.ratchet']); function shouldSkipDir(name) { if (IGNORE_DIRS.has(name)) return true; diff --git a/test/cli.test.js b/test/cli.test.js index 37e4a44..0ca6d5f 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -140,11 +140,13 @@ ok('corrupt ledger.json is backed up, not silently lost', () => { ok('repo snapshot sees allowlisted dot dirs, skips .git / node_modules', () => { const proj = path.join(tmp, 'snap-fixture'); - for (const d of ['.claude-plugin', '.github', '.git', 'node_modules', 'src']) { + for (const d of ['.agents', '.claude-plugin', '.codex-plugin', '.github', '.git', 'node_modules', 'src']) { fs.mkdirSync(path.join(proj, d), { recursive: true }); } const snap = repo.snapshot(proj); + assert.ok(snap.dirs.includes('.agents'), '.agents is visible'); assert.ok(snap.dirs.includes('.claude-plugin'), '.claude-plugin is visible'); + assert.ok(snap.dirs.includes('.codex-plugin'), '.codex-plugin is visible'); assert.ok(snap.dirs.includes('.github'), '.github is visible'); assert.ok(!snap.dirs.includes('.git'), '.git is skipped'); assert.ok(!snap.dirs.includes('node_modules'), 'node_modules is skipped'); diff --git a/test/plugin-shape.test.js b/test/plugin-shape.test.js index fc314b1..e0ac609 100644 --- a/test/plugin-shape.test.js +++ b/test/plugin-shape.test.js @@ -1,7 +1,7 @@ 'use strict'; // Zero-dependency plugin-shape test. Run: node test/plugin-shape.test.js -// Validates the packaging surface Claude Code expects — so a broken plugin +// Validates the packaging surface Claude Code and Codex expect — so a broken plugin // package (missing SKILL.md, drifted version, stale command name) fails CI // before a user ever installs it. Reads the real repo, writes nothing. @@ -26,21 +26,46 @@ function ok(name, fn) { } const pkg = readJson('package.json'); -const plugin = readJson('.claude-plugin/plugin.json'); -const market = readJson('.claude-plugin/marketplace.json'); +const claudePlugin = readJson('.claude-plugin/plugin.json'); +const claudeMarket = readJson('.claude-plugin/marketplace.json'); +const codexPlugin = readJson('.codex-plugin/plugin.json'); +const codexMarket = readJson('.agents/plugins/marketplace.json'); ok('manifests exist and parse', () => { assert.ok(pkg.version, 'package.json has a version'); - assert.ok(plugin.version, 'plugin.json has a version'); - assert.ok(market.metadata && market.metadata.version, 'marketplace metadata has a version'); - assert.ok(Array.isArray(market.plugins) && market.plugins.length, 'marketplace lists plugins'); + assert.ok(claudePlugin.version, 'Claude plugin.json has a version'); + assert.ok(claudeMarket.metadata && claudeMarket.metadata.version, 'Claude marketplace metadata has a version'); + assert.ok(Array.isArray(claudeMarket.plugins) && claudeMarket.plugins.length, 'Claude marketplace lists plugins'); + assert.ok(codexPlugin.version, 'Codex plugin.json has a version'); + assert.ok(Array.isArray(codexMarket.plugins) && codexMarket.plugins.length, 'Codex marketplace lists plugins'); }); ok('versions are aligned across every surface', () => { const v = pkg.version; - assert.strictEqual(plugin.version, v, 'plugin.json version matches package.json'); - assert.strictEqual(market.metadata.version, v, 'marketplace metadata version matches package.json'); - assert.strictEqual(market.plugins[0].version, v, 'marketplace plugin version matches package.json'); + assert.strictEqual(claudePlugin.version, v, 'Claude plugin.json version matches package.json'); + assert.strictEqual(claudeMarket.metadata.version, v, 'Claude marketplace metadata version matches package.json'); + assert.strictEqual(claudeMarket.plugins[0].version, v, 'Claude marketplace plugin version matches package.json'); + assert.strictEqual(codexPlugin.version, v, 'Codex plugin.json version matches package.json'); +}); + +ok('Codex manifest has required install metadata', () => { + assert.strictEqual(codexPlugin.name, 'torque-loop', 'Codex plugin name matches repo identity'); + assert.strictEqual(codexPlugin.skills, './skills/', 'Codex manifest points at skills'); + assert.ok(codexPlugin.author && codexPlugin.author.name, 'Codex manifest has author.name'); + assert.ok(codexPlugin.interface, 'Codex manifest has interface metadata'); + for (const field of ['displayName', 'shortDescription', 'longDescription', 'developerName', 'category']) { + assert.ok(codexPlugin.interface[field], `Codex interface.${field} exists`); + } + assert.ok(Array.isArray(codexPlugin.interface.capabilities), 'Codex interface.capabilities is an array'); + assert.ok(Array.isArray(codexPlugin.interface.defaultPrompt), 'Codex interface.defaultPrompt is an array'); +}); + +ok('Codex marketplace installs this repo as a local plugin source', () => { + const entry = codexMarket.plugins.find((p) => p.name === codexPlugin.name); + assert.ok(entry, 'Codex marketplace has a torque-loop entry'); + assert.deepStrictEqual(entry.source, { source: 'local', path: './' }); + assert.deepStrictEqual(entry.policy, { installation: 'AVAILABLE', authentication: 'ON_INSTALL' }); + assert.strictEqual(entry.category, 'Developer Tools'); }); ok('CLI VERSION constants match package.json', () => { @@ -64,7 +89,7 @@ ok('every bin target from package.json exists', () => { }); ok('required plugin directories exist', () => { - for (const d of ['skills', 'agents', 'hooks', 'bin', 'src']) { + for (const d of ['.agents', '.claude-plugin', '.codex-plugin', 'skills', 'agents', 'hooks', 'bin', 'src']) { assert.ok(fs.existsSync(path.join(root, d)), `${d}/ exists`); } });