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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"name": "code-oz",
"description": "Enforced SDLC gates + cross-family review for AI coding agents. Discovers and invokes the code-oz engine; the binary is the only writer of gates, events, and reviews.",
"version": "0.21.1-alpha.0",
"source": "./code-oz",
"source": "./plugins/code-oz",
"author": {
"name": "Ozzy (Omer Akben)",
"url": "https://github.com/omerakben/code-oz"
Expand All @@ -20,7 +20,7 @@
"name": "code-oz-discipline",
"description": "Advisory SDLC discipline skills (brainstorming, source-check, RED-first) for AI coding agents. Advisory only — not enforced gates; upsells to the code-oz engine for enforced gates and different-model review.",
"version": "0.21.1-alpha.0",
"source": "./code-oz-discipline",
"source": "./plugins/code-oz-discipline",
"author": {
"name": "Ozzy (Omer Akben)",
"url": "https://github.com/omerakben/code-oz"
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,29 @@ Platform support: macOS arm64, macOS x64, Linux x64, Linux arm64. Windows and Sc

If `npm install -g @tuel/code-oz` fails with a 404 or authentication error, your `~/.npmrc` is likely overriding the `@tuel` scope — see [`docs/TRUST.md` § Install gotchas](docs/TRUST.md#install-gotchas-npm-scope-routing-for-tuelcode-oz) for the fix.

## Use it inside Claude Code (plugin)

code-oz ships two Claude Code plugins through a marketplace declared at this repo's root. From a Claude Code session, add the marketplace and install:

```sh
/plugin marketplace add omerakben/code-oz
/plugin install code-oz@code-oz-marketplace
```

The `code-oz` plugin is a thin wrapper. It adds `/code-oz-run`, `/code-oz-init`, `/code-oz-doctor`, and `/code-oz-resume`, and each command discovers the `code-oz` engine on your `PATH`, falling back to `npx @tuel/code-oz` when the binary is absent. Install the engine from the channels above for the fastest path. The plugin never writes gates, events, or reviews — the engine binary is the only writer, so the gate guarantees hold whether you drive code-oz from the CLI or through the plugin.

A second plugin, `code-oz-discipline`, is optional. It installs advisory-only skills (brainstorming, source-check, RED-first) that are guidance, not enforcement:

```sh
/plugin install code-oz-discipline@code-oz-marketplace
```

Adding the marketplace clones this repository into Claude Code's plugin cache. The repo is the full engine source, so from the CLI you can restrict the checkout to just the plugin files:

```sh
claude plugin marketplace add omerakben/code-oz --sparse .claude-plugin plugins
```

## Why not just Claude Code or Codex?

Use Claude Code, Codex, Cursor, Gemini CLI, OpenCode, Roo Code, or Aider directly when you want the fastest possible agent loop.
Expand Down
2 changes: 1 addition & 1 deletion agent-skills/code-oz/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Copy this `code-oz/` folder into your agent's skills directory:
- OpenClaw: your personal/project skills dir.

Then ensure the engine is installed: `npm i -g @tuel/code-oz` or
`brew install omerakben/tap/code-oz`.
`brew install omerakben/code-oz/code-oz`.

The skill drives code-oz in fail-closed operator mode, which bans the fake
provider, blocks SHIP/push, and records operator provenance. Bind the project
Expand Down
2 changes: 1 addition & 1 deletion agent-skills/code-oz/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ write its state.

1. Check it is installed: run `code-oz --version` (or `code-oz doctor`).
2. If it is missing, STOP and tell the user to install it:
`npm i -g @tuel/code-oz` OR `brew install omerakben/tap/code-oz`.
`npm i -g @tuel/code-oz` OR `brew install omerakben/code-oz/code-oz`.
Do NOT auto-run `curl`, `npx`, or `bunx` to install it yourself.

## How to drive it
Expand Down
4 changes: 2 additions & 2 deletions plugins/code-oz/scripts/resolve-code-oz.sh
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ if command -v npx >/dev/null 2>&1; then
printf 'A @tuel scope-routing trap may be 404ing on npm.pkg.github.com.\n' >&2
printf 'To fix:\n' >&2
printf ' Option A — install via Homebrew (bypasses npm scope routing):\n' >&2
printf ' brew install omerakben/tap/code-oz\n' >&2
printf ' brew install omerakben/code-oz/code-oz\n' >&2
printf ' Option B — set the @tuel registry in your .npmrc:\n' >&2
printf ' @tuel:registry=https://registry.npmjs.org/\n' >&2
exit "${NPX_EXIT}"
Expand All @@ -115,5 +115,5 @@ fi
printf 'code-oz is not installed. Install:\n' >&2
printf ' npm i -g @tuel/code-oz\n' >&2
printf ' OR\n' >&2
printf ' brew install omerakben/tap/code-oz\n' >&2
printf ' brew install omerakben/code-oz/code-oz\n' >&2
exit 1
9 changes: 8 additions & 1 deletion tests/plugins/bootstrap-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@ describe('resolve-code-oz.sh — npx failure surfaces scope-routing caveat', ()
expect(result.stderr).toMatch(/@tuel/)
// Must suggest Homebrew as an alternative
expect(result.stderr).toMatch(/[Hh]omebrew|brew/)
// The Homebrew hint must use the real tap (brew resolves omerakben/code-oz
// to github.com/omerakben/homebrew-code-oz), not the nonexistent
// omerakben/homebrew-tap that omerakben/tap/code-oz would target.
expect(result.stderr).toContain('omerakben/code-oz/code-oz')
expect(result.stderr).not.toContain('omerakben/tap/code-oz')
// Must mention the registry workaround
expect(result.stderr).toMatch(/@tuel:registry/)
})
Expand All @@ -204,8 +209,10 @@ describe('resolve-code-oz.sh — hard-stop (no code-oz, no npm/npx)', () => {
expect(result.stdout + result.stderr).toMatch(/npm/)
// Must mention the package name
expect(result.stdout + result.stderr).toMatch(/@tuel\/code-oz/)
// Must mention brew as alternative
// Must mention brew as alternative, with the correct tap form.
expect(result.stdout + result.stderr).toMatch(/brew/)
expect(result.stdout + result.stderr).toContain('omerakben/code-oz/code-oz')
expect(result.stdout + result.stderr).not.toContain('omerakben/tap/code-oz')
})
})

Expand Down
34 changes: 29 additions & 5 deletions tests/plugins/discipline-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ const DISCIPLINE_PLUGIN_JSON_PATH = join(
'plugins/code-oz-discipline/.claude-plugin/plugin.json',
)
const CODE_OZ_PLUGIN_JSON_PATH = join(REPO_ROOT, 'plugins/code-oz/.claude-plugin/plugin.json')
const MARKETPLACE_JSON_PATH = join(REPO_ROOT, 'plugins/.claude-plugin/marketplace.json')
// Repo-root manifest: `claude plugin marketplace add <owner/repo>` only finds
// `.claude-plugin/marketplace.json` at the cloned repo's root.
const MARKETPLACE_JSON_PATH = join(REPO_ROOT, '.claude-plugin/marketplace.json')

describe('plugins/code-oz-discipline manifest shape', () => {
test('plugin.json exists and parses as JSON', async () => {
Expand Down Expand Up @@ -73,22 +75,22 @@ describe('marketplace.json with both sibling plugins', () => {
expect(market.plugins).toHaveLength(2)
})

test('marketplace.json has a code-oz entry with source ./code-oz', async () => {
test('marketplace.json has a code-oz entry with source ./plugins/code-oz', async () => {
const raw = await readFile(MARKETPLACE_JSON_PATH, 'utf8')
const market = JSON.parse(raw) as { plugins: Array<Record<string, unknown>> }

const entry = market.plugins.find((p) => p.name === 'code-oz')
expect(entry).toBeDefined()
expect(entry!.source).toBe('./code-oz')
expect(entry!.source).toBe('./plugins/code-oz')
})

test('marketplace.json has a code-oz-discipline entry with source ./code-oz-discipline', async () => {
test('marketplace.json has a code-oz-discipline entry with source ./plugins/code-oz-discipline', async () => {
const raw = await readFile(MARKETPLACE_JSON_PATH, 'utf8')
const market = JSON.parse(raw) as { plugins: Array<Record<string, unknown>> }

const entry = market.plugins.find((p) => p.name === 'code-oz-discipline')
expect(entry).toBeDefined()
expect(entry!.source).toBe('./code-oz-discipline')
expect(entry!.source).toBe('./plugins/code-oz-discipline')
})

test('both marketplace entries share the same version', async () => {
Expand Down Expand Up @@ -117,4 +119,26 @@ describe('marketplace.json with both sibling plugins', () => {
expect(codeOzEntry!.version).toBe(codeOz.version)
expect(disciplineEntry!.version).toBe(codeOz.version)
})

// Installability guard: every `source` path must resolve (relative to the
// marketplace root = repo root) to a real plugin directory whose plugin.json
// name matches the entry. A manifest that points at a non-existent or
// mismatched directory passes schema checks but fails at `plugin install`.
test('every marketplace source resolves to a real plugin dir with a matching plugin.json', async () => {
const raw = await readFile(MARKETPLACE_JSON_PATH, 'utf8')
const market = JSON.parse(raw) as {
plugins: Array<{ name: string; source: string }>
}

for (const entry of market.plugins) {
// sources are repo-relative, must descend (no `..` traversal).
expect(entry.source.startsWith('./')).toBe(true)
expect(entry.source.includes('..')).toBe(false)

const pluginJsonPath = join(REPO_ROOT, entry.source, '.claude-plugin/plugin.json')
const pluginRaw = await readFile(pluginJsonPath, 'utf8')
const plugin = JSON.parse(pluginRaw) as { name: string }
expect(plugin.name).toBe(entry.name)
}
})
})
9 changes: 7 additions & 2 deletions tests/plugins/manifest-shape.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import { fileURLToPath } from 'node:url'
const REPO_ROOT = fileURLToPath(new URL('../../', import.meta.url)).replace(/\/$/, '')

const PLUGIN_JSON_PATH = join(REPO_ROOT, 'plugins/code-oz/.claude-plugin/plugin.json')
const MARKETPLACE_JSON_PATH = join(REPO_ROOT, 'plugins/.claude-plugin/marketplace.json')
// The marketplace manifest lives at the REPO ROOT so `claude plugin marketplace
// add <owner/repo>` discovers it — Claude Code only reads `.claude-plugin/
// marketplace.json` at the cloned repo's root, never a subdirectory.
const MARKETPLACE_JSON_PATH = join(REPO_ROOT, '.claude-plugin/marketplace.json')
const PACKAGE_JSON_PATH = join(REPO_ROOT, 'package.json')

const EXPECTED_COMMANDS = [
Expand Down Expand Up @@ -69,7 +72,9 @@ describe('plugins/code-oz manifest shape', () => {

const entry = market.plugins.find((p) => p.name === 'code-oz')
expect(entry).toBeDefined()
expect(entry!.source).toBe('./code-oz')
// source is resolved relative to the marketplace root (repo root), so the
// path includes the plugins/ directory the plugin actually lives in.
expect(entry!.source).toBe('./plugins/code-oz')
})

test('marketplace.json code-oz entry version matches plugin.json version', async () => {
Expand Down
Loading