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
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
},
"metadata": {
"description": "Parallel Codex workers inside Claude Code.",
"version": "0.5.1"
"version": "0.5.2"
},
"plugins": [
{
"name": "magic-codex",
"source": "./plugin",
"description": "Parallel Codex workers inside Claude Code — multi-agent orchestration with git worktree isolation, resumable sessions, and dual-model PR review.",
"version": "0.5.1",
"version": "0.5.2",
"author": {
"name": "Wenqing Yu"
},
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

All notable changes documented here. Format follows [Keep a Changelog](https://keepachangelog.com/).

## [0.5.2] — 2026-04-29

Tooling-only release.

### Added
- **`npm run release` script** (`scripts/release.mjs`) automates the full release procedure: validates clean `main`, refuses to clobber existing tags, verifies the CHANGELOG has an entry for the version in `package.json`, runs build (with the version-drift guard) + tests, creates the annotated tag, pushes it, and creates the GitHub Release with the CHANGELOG section as the body and `--latest` flag. Closes the gap from 0.5.0/0.5.1 where tags got pushed but GitHub Releases didn't, leaving the project's release page stuck at 0.3.9.
- **`npm run release:dry-run`** prints what would happen without mutating anything.
- **CONTRIBUTING.md** documents the maintainer release procedure end to end, calling out the five duplicated version literals (and that `plugin/.claude-plugin/plugin.json` is the one Claude Code's plugin loader actually reads).

### Internal
- Backfilled GitHub Releases for v0.4.0 / v0.4.1 / v0.4.2 / v0.5.0 / v0.5.1 with their CHANGELOG bodies.

## [0.5.1] — 2026-04-29

Docs-only release.
Expand Down
31 changes: 31 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,37 @@ Open a GitHub issue with:

Maintainers look for: clear test coverage, strict types, docs updated if behavior visible to users, no secret-bearing files committed.

## Cutting a release (maintainers)

We use one tag per release, an annotated GitHub Release for each, and the version literal duplicated in 5 files (the build's drift guard catches drift). A small script handles the tag + push + GitHub Release flow so you don't forget any step.

**Procedure:**

1. **On a feature branch:** bump the version in `package.json` and let the build's drift guard tell you the other 4 files to bump in lockstep:
- `.claude-plugin/marketplace.json` (two version fields — `metadata.version` and `plugins[0].version`)
- `plugin/.claude-plugin/plugin.json` ← **easy to forget; this is the file Claude Code's loader actually reads**
- `src/index.ts` MCP banner
- `src/mcp/codex-client.ts` MCP banner
2. **Add a CHANGELOG entry** with `## [X.Y.Z] — YYYY-MM-DD` heading and the relevant `### Added / ### Fixed / ### Changed / ### Internal` subsections.
3. **PR + merge to `main`** (the release script refuses to run from a feature branch).
4. **From a clean `main` checked out at the merge commit:**

```bash
npm run release:dry-run # show what would happen
npm run release # tag, push, create GitHub Release, mark latest
```

The script:
- Verifies you're on `main` and synced with origin
- Verifies the tag doesn't already exist on origin (refuses to clobber)
- Verifies the CHANGELOG has a section for the version in `package.json`
- Runs `npm run build` (drift guard validates all 5 version literals match)
- Runs `npm test`
- Creates an annotated tag, pushes it
- Creates the GitHub Release with the CHANGELOG section as the body, marks it `latest`

If `gh release create` fails because the release already exists for a tag, delete it first (`gh release delete vX.Y.Z`) — the script intentionally won't overwrite.

## Licensing of your contributions

By submitting a pull request you agree that your contribution will be distributed under the project's current license ([PolyForm Noncommercial 1.0.0](./LICENSE)). You retain copyright to your contribution; you grant the project maintainer a license broad enough to redistribute the combined work under the project license and any future dual / commercial license we may offer.
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "magic-cc-codex-worker",
"version": "0.5.1",
"version": "0.5.2",
"description": "Parallel Codex workers inside Claude Code — multi-agent orchestration.",
"private": true,
"license": "SEE LICENSE IN LICENSE",
Expand All @@ -15,7 +15,9 @@
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"clean": "rm -rf plugin/dist"
"clean": "rm -rf plugin/dist",
"release": "node scripts/release.mjs",
"release:dry-run": "node scripts/release.mjs --dry-run"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4",
Expand Down
2 changes: 1 addition & 1 deletion plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "magic-codex",
"version": "0.5.1",
"version": "0.5.2",
"description": "Parallel Codex workers inside Claude Code — multi-agent orchestration with git worktree isolation, resumable sessions, and dual-model PR review.",
"author": {
"name": "Wenqing Yu",
Expand Down
4 changes: 2 additions & 2 deletions plugin/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27716,7 +27716,7 @@ var CodexChild = class {
stderr: "pipe"
});
this.client = new Client(
{ name: "magic-codex", version: "0.5.1" },
{ name: "magic-codex", version: "0.5.2" },
{ capabilities: {} }
);
await this.client.connect(this.transport);
Expand Down Expand Up @@ -28278,7 +28278,7 @@ async function main() {
mfConventions
});
const server = new Server(
{ name: "magic-codex", version: "0.5.1" },
{ name: "magic-codex", version: "0.5.2" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
Expand Down
158 changes: 158 additions & 0 deletions scripts/release.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/env node
// Cut a release: validate, tag, push, and create the GitHub Release with
// notes extracted from CHANGELOG.md. Idempotent — re-running on a tag
// that already exists locally or on origin will skip the create step
// for that side and proceed; re-running after the GitHub Release also
// exists is a hard error (refuses to clobber).
//
// Usage:
// node scripts/release.mjs # cut release for whatever package.json says
// node scripts/release.mjs --dry-run # show what would happen, change nothing
//
// Preconditions (script enforces):
// 1. Working tree clean on `main`, up to date with origin.
// 2. package.json version not already tagged on origin.
// 3. CHANGELOG.md has a `## [<version>]` section (notes body).
// 4. `npm run build` passes — drift guard validates all 5 version
// literals match package.json.
// 5. `gh auth status` is authenticated.
//
// What it does:
// 1. Builds + tests (sanity check; same as CI would do).
// 2. Annotated tag `vX.Y.Z` with the CHANGELOG section's first prose
// line as the message (becomes the GitHub Release title via
// gh release create --verify-tag).
// 3. Pushes the tag to origin.
// 4. Creates the GitHub Release with --latest, body = CHANGELOG
// section.
//
// To make a future release: bump version literals (build will fail if
// any is missed), add a CHANGELOG entry, commit/PR/merge to main, then
// run `npm run release`. That's the whole procedure.

import { readFile } from "node:fs/promises";
import { execSync } from "node:child_process";
import { writeFileSync, unlinkSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

const dryRun = process.argv.includes("--dry-run");

function sh(cmd, opts = {}) {
if (dryRun && opts.mutating) {
console.log(`[dry-run] would run: ${cmd}`);
return "";
}
return execSync(cmd, { stdio: opts.silent ? "pipe" : "inherit", encoding: "utf8" });
}

function shCapture(cmd) {
return execSync(cmd, { encoding: "utf8" }).trim();
}

function fail(msg) {
console.error(`\n✗ ${msg}\n`);
process.exit(1);
}

// 1. Read package version
const pkg = JSON.parse(await readFile("package.json", "utf8"));
const version = pkg.version;
const tag = `v${version}`;
console.log(`→ release target: ${tag}`);

// 2. Sanity: clean tree on main, synced
const branch = shCapture("git rev-parse --abbrev-ref HEAD");
if (branch !== "main") {
fail(`must release from main; currently on ${branch}`);
}
const status = shCapture("git status --porcelain");
if (status) {
fail(`working tree not clean:\n${status}`);
}
sh("git fetch origin main --quiet", { silent: true });
const localHead = shCapture("git rev-parse HEAD");
const remoteHead = shCapture("git rev-parse origin/main");
if (localHead !== remoteHead) {
fail(`local main (${localHead.slice(0, 8)}) is not in sync with origin/main (${remoteHead.slice(0, 8)})`);
}

// 3. Verify tag doesn't already exist on origin
const remoteTags = shCapture("git ls-remote --tags origin").split("\n");
if (remoteTags.some((line) => line.endsWith(`refs/tags/${tag}`))) {
fail(`tag ${tag} already exists on origin — refusing to clobber. If you need to redo, delete it first: git push origin :refs/tags/${tag}`);
}

// 4. Verify CHANGELOG has an entry for this version
const changelog = await readFile("CHANGELOG.md", "utf8");
const sectionRe = new RegExp(
`^## \\[${version.replace(/\./g, "\\.")}\\][^\\n]*\\n([\\s\\S]*?)(?=^## \\[|\\Z)`,
"m",
);
const match = sectionRe.exec(changelog);
if (!match) {
fail(`CHANGELOG.md has no entry for [${version}]`);
}
const notesBody = match[1].trim() + "\n";

// 5. Derive a release title. Use the first non-blank, non-heading line
// from the CHANGELOG section as the summary. Fall back to "vX.Y.Z".
const summaryLine = notesBody
.split("\n")
.find((line) => line.trim() && !line.startsWith("#") && !line.startsWith("-") && !line.startsWith(">"));
const titleSuffix = summaryLine
? summaryLine.replace(/^\*\*/, "").replace(/\*\*\.?$/, "").replace(/\.$/, "").slice(0, 80)
: "";
const title = titleSuffix ? `${tag} — ${titleSuffix}` : tag;
console.log(`→ title: ${title}`);

// 6. Run build (drift guard validates version literals match)
console.log("\n→ npm run build (drift guard)");
sh("npm run build");

// 7. Run tests (sanity)
console.log("\n→ npm test");
sh("npm test");

// 8. Verify gh is authenticated
try {
sh("gh auth status", { silent: true });
} catch {
fail("gh CLI not authenticated; run `gh auth login`");
}

// 9. Tag locally (annotated, message = title)
console.log(`\n→ git tag -a ${tag}`);
sh(`git tag -a ${tag} -m "${title.replace(/"/g, '\\"')}"`, { mutating: true });

// 10. Push tag
console.log(`→ git push origin ${tag}`);
sh(`git push origin ${tag}`, { mutating: true });

// 11. Create GitHub Release. --verify-tag makes gh use the existing
// pushed tag; --latest marks this as the latest release on the
// project page.
const notesPath = join(tmpdir(), `release-notes-${tag}.md`);
writeFileSync(notesPath, notesBody, "utf8");
try {
console.log(`→ gh release create ${tag}`);
if (!dryRun) {
sh(
`gh release create ${tag} --verify-tag --latest --title "${title.replace(/"/g, '\\"')}" --notes-file "${notesPath}"`,
{ mutating: true },
);
} else {
console.log(`[dry-run] would gh release create ${tag} --notes-file ${notesPath}`);
}
} finally {
if (!dryRun) {
try {
unlinkSync(notesPath);
} catch {
// ignore
}
}
}

console.log(`\n✓ released ${tag}`);
console.log(` https://github.com/wenqingyu/magic-cc-codex-worker/releases/tag/${tag}`);
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ async function main() {
});

const server = new Server(
{ name: "magic-codex", version: "0.5.1" },
{ name: "magic-codex", version: "0.5.2" },
{ capabilities: { tools: {} } },
);

Expand Down
2 changes: 1 addition & 1 deletion src/mcp/codex-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class CodexChild {
stderr: "pipe",
});
this.client = new Client(
{ name: "magic-codex", version: "0.5.1" },
{ name: "magic-codex", version: "0.5.2" },
{ capabilities: {} },
);
await this.client.connect(this.transport);
Expand Down