Route Codex main updates to fork flows
All checks were successful
check / check (push) Successful in 36s

This commit is contained in:
matamune 2026-05-18 19:15:43 +00:00
parent 927effa853
commit c14470a8f4
Signed by: matamune
GPG key ID: 3BB8E7D3B968A324
22 changed files with 951 additions and 291 deletions

View file

@ -1,8 +1,12 @@
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
type FlowContext = {
flow: {
name?: string;
root?: string;
step?: string;
config?: Record<string, unknown>;
event: {
id: string;
@ -10,6 +14,9 @@ type FlowContext = {
payload: Record<string, unknown>;
};
};
runtime?: {
workspaceBackendUrl?: string;
};
};
type CommandResult = {
@ -23,7 +30,7 @@ type CommandResult = {
const context = JSON.parse(await Bun.stdin.text()) as FlowContext;
const config = context.flow.config ?? {};
const repoRoot = process.cwd();
const repoRoot = path.resolve(envConfig(stringConfig("codex_flows_repo_env", "")) || stringConfig("codex_flows_repo", process.cwd()));
const commands: CommandResult[] = [];
try {
@ -52,15 +59,20 @@ try {
await updatePackageVersion(packageJsonPath, version);
await run("refresh Bun lockfile", ["bun", "install"]);
const changedStatus = await run("changed file status", ["git", "status", "--short"]);
if (!changedStatus.stdout.trim()) {
finish("skipped", `No generated binding changes for ${tag}.`, { version, tag });
}
const followupTurn = await maybeRunFollowupTurn({ tag, version, changedStatus: changedStatus.stdout });
await run("codex-flows package release check", ["bun", "run", "--filter", packageName, "release:check"]);
await run("workspace typecheck", ["bun", "run", "check:types"]);
await run("workspace tests", ["bun", "run", "test"]);
await run("git diff check", ["git", "diff", "--check"]);
const status = await run("final git status", ["git", "status", "--short"]);
if (!status.stdout.trim()) {
finish("skipped", `No generated binding changes for ${tag}.`, { version, tag });
}
if (enabled("commit", true)) {
await run("stage binding update", ["git", "add", "--", generatedDir, packageJsonPath, path.join(repoRoot, "bun.lock")]);
@ -96,6 +108,7 @@ try {
committed: enabled("commit", true),
pushed: enabled("push", false),
published: enabled("publish", false),
followupTurn,
});
} catch (error) {
finish("failed", error instanceof Error ? error.message : String(error));
@ -127,6 +140,89 @@ async function updatePackageVersion(packageJsonPath: string, version: string): P
await writeFile(packageJsonPath, `${JSON.stringify(parsed, null, "\t")}\n`);
}
async function maybeRunFollowupTurn(input: {
tag: string;
version: string;
changedStatus: string;
}): Promise<Record<string, unknown> | undefined> {
if (!enabled("followup_turn", true)) {
return undefined;
}
const runCodexAgentTurnFromFlow = await loadRunCodexAgentTurnFromFlow();
const threadJson = stringConfig("followup_thread_json", ".codex/flow-artifacts/openai-codex-bindings-thread.json");
const prompt = [
`OpenAI Codex ${input.tag} regenerated app-server TypeScript bindings for @peezy.tech/codex-flows ${input.version}.`,
"",
"Review the changed files and make any source, test, or export updates needed so the package still builds against the regenerated bindings.",
"Keep changes focused on codex-flows compatibility with the regenerated app-server surface.",
"Do not push, publish, or edit release metadata beyond what this flow already changed.",
"",
"Current changed files:",
input.changedStatus.trim(),
].join("\n");
const turn = await runCodexAgentTurnFromFlow(
{
flow: {
name: context.flow.name ?? "openai-codex-bindings",
root: repoRoot,
step: context.flow.step ?? "regenerate-bindings",
event: context.flow.event,
},
runtime: context.runtime,
},
{
prompt,
cwd: repoRoot,
approvalPolicy: "never",
sandbox: "danger-full-access",
appServerUrl: process.env.CODEX_APP_SERVER_URL,
requestTimeoutMs: numberConfig("followup_request_timeout_ms", 900000),
wait: {
timeoutMs: numberConfig("followup_wait_timeout_ms", 900000),
throwOnFailure: true,
},
exportThreadJson: threadJson || false,
},
) as {
artifacts?: Record<string, unknown>;
threadId?: string;
turnId?: string;
threadJsonPath?: string;
};
return {
threadId: turn.threadId,
turnId: turn.turnId,
threadJsonPath: turn.threadJsonPath,
...(turn.artifacts ?? {}),
};
}
async function loadRunCodexAgentTurnFromFlow(): Promise<(
context: unknown,
options: Record<string, unknown>,
) => Promise<unknown>> {
const candidates = [
"@peezy.tech/codex-flows/flows",
pathToFileURL(path.join(repoRoot, "packages/codex-client/src/app-server/flows.ts")).href,
];
const errors: string[] = [];
for (const candidate of candidates) {
try {
const module = await import(candidate) as { runCodexAgentTurnFromFlow?: unknown };
if (typeof module.runCodexAgentTurnFromFlow === "function") {
return module.runCodexAgentTurnFromFlow as (
context: unknown,
options: Record<string, unknown>,
) => Promise<unknown>;
}
errors.push(`${candidate}: runCodexAgentTurnFromFlow is not exported`);
} catch (error) {
errors.push(`${candidate}: ${error instanceof Error ? error.message : String(error)}`);
}
}
throw new Error(`Could not load codex-flows follow-up turn helper:\n${errors.join("\n")}`);
}
async function run(
label: string,
cmd: string[],
@ -171,11 +267,23 @@ function enabled(name: string, fallback: boolean): boolean {
return typeof value === "boolean" ? value : fallback;
}
function envConfig(name: string): string | undefined {
return name.trim() ? process.env[name]?.trim() || undefined : undefined;
}
function stringConfig(name: string, fallback: string): string {
const value = config[name];
return typeof value === "string" && value.trim() ? value : fallback;
}
function numberConfig(name: string, fallback: number): number {
const envName = `CODEX_FLOW_${name.toUpperCase()}`;
const envValue = process.env[envName];
const raw = envValue !== undefined ? envValue : config[name];
const value = typeof raw === "number" ? raw : typeof raw === "string" ? Number(raw) : fallback;
return Number.isFinite(value) ? value : fallback;
}
function stringValue(value: unknown, name: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new Error(`${name} must be a non-empty string`);

View file

@ -4,9 +4,13 @@ description = "Regenerate @peezy.tech/codex-flows bindings from a canonical open
[config]
package_name = "@peezy.tech/codex-flows"
codex_flows_repo_env = "PEEZY_CODEX_FLOWS_REPO"
codex_flows_repo = "/home/peezy/meta-workspace/codex-flows"
generated_dir = "packages/codex-client/src/app-server/generated"
package_json = "packages/codex-client/package.json"
commit = true
followup_turn = true
followup_thread_json = ".codex/flow-artifacts/openai-codex-bindings-thread.json"
push = false
publish = false
github_repo = "peezy-tech/codex-flows"

View file

@ -1,5 +1,6 @@
const config = flow.config || {};
const payload = flow.event.payload || {};
const eventType = String(flow.event.type || "");
const commands = [];
function q(value) {
@ -88,16 +89,16 @@ async function run(label, cmd, options = {}) {
yield_time_ms: options.yield_time_ms || 1000,
max_output_tokens: options.max_output_tokens || 12000
});
const result = {
const command = {
label,
cmd,
workdir,
exit_code: exitCodeOf(raw),
output: outputOf(raw)
};
commands.push({ ...result, output: truncate(result.output, 4000) });
text("exit_code=" + String(result.exit_code) + "\n" + truncate(result.output, options.textLimit || 12000) + "\n");
return result;
commands.push({ ...command, output: truncate(command.output, 4000) });
text("exit_code=" + String(command.exit_code) + "\n" + truncate(command.output, options.textLimit || 12000) + "\n");
return command;
}
function finish(status, message, artifacts = {}) {
@ -105,56 +106,541 @@ function finish(status, message, artifacts = {}) {
status,
message,
artifacts: {
eventType,
operation,
releaseTag,
version,
codexRepo,
targetBranch,
upstreamBranch,
patchPrefix,
commands,
...artifacts
}
});
}
async function collectRebaseContext(rebaseOutput, beforeSha) {
const status = await run("rebase conflict status", "git status --short --branch", { max_output_tokens: 12000 });
async function collectConflictContext(params) {
const status = await run("patch rebuild conflict status", "git status --short --branch", { max_output_tokens: 12000 });
const unmerged = await run("unmerged files", "git diff --name-only --diff-filter=U", { max_output_tokens: 12000 });
const diffStat = await run("conflict diff stat", "git diff --cc --stat", { max_output_tokens: 12000 });
const conflictDiff = await run("conflict diff", "git diff --cc", { max_output_tokens: 30000, textLimit: 20000 });
const currentPatch = await run("current rebase patch", "git rebase --show-current-patch", { max_output_tokens: 20000, textLimit: 12000 });
const patchDetails = params.failedPatch
? await run(
"failed patch details",
"git show --stat --oneline --decorate --no-renames " + q(params.failedPatch.sha),
{ max_output_tokens: 16000, textLimit: 12000 }
)
: undefined;
return {
beforeSha,
rebaseOutput,
beforeSha: params.beforeSha,
baseRef: params.baseRef,
baseSha: params.baseSha,
rebuildOutput: params.rebuildOutput,
statusOutput: status.output,
unmergedFiles: unmerged.output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean),
diffStat: diffStat.output,
conflictDiff: truncate(conflictDiff.output, 20000),
currentPatch: truncate(currentPatch.output, 12000),
interventionPrompt: "Continue this same Code Mode thread to resolve the paused rebase. Preserve the fork patch stack, do not abort or reset unless explicitly instructed, then run the configured verification commands."
failedPatch: params.failedPatch,
patchDetails: patchDetails ? truncate(patchDetails.output, 12000) : undefined,
applied: params.applied,
interventionPrompt: [
"Continue this same Code Mode thread to resolve the paused cherry-pick.",
"Preserve the fork patch branch semantics: each patch/* branch remains one logical patch commit, main is rebuilt output, and upstream follows upstream/main.",
"After resolving conflicts, continue the cherry-pick, update main to the rebuilt HEAD, switch back to main, and run the configured verification commands."
].join(" ")
};
}
async function requireCleanWorktree() {
const status = await run("dirty check", "git status --porcelain=v1", { max_output_tokens: 12000 });
if (trim(status.output)) {
finish("blocked", "Codex checkout has local changes before fork maintenance.", {
dirtyStatus: status.output
});
}
}
async function requireNoPausedGitOperation() {
const cherryPick = await run(
"check existing cherry-pick state",
"test -f \"$(git rev-parse --git-path CHERRY_PICK_HEAD)\"",
{ max_output_tokens: 4000 }
);
if (cherryPick.exit_code === 0) {
const context = await collectConflictContext({
beforeSha: undefined,
baseRef: undefined,
baseSha: undefined,
rebuildOutput: "A cherry-pick was already in progress before this flow started.",
applied: []
});
finish("blocked", "A cherry-pick is already in progress in the Codex checkout.", context);
}
const rebase = await run(
"check existing rebase state",
"test -d \"$(git rev-parse --git-path rebase-merge)\" -o -d \"$(git rev-parse --git-path rebase-apply)\"",
{ max_output_tokens: 4000 }
);
if (rebase.exit_code === 0) {
finish("blocked", "A rebase is already in progress in the Codex checkout.", {
statusOutput: (await run("rebase status", "git status --short --branch", { max_output_tokens: 12000 })).output
});
}
}
async function ensureUpstreamRemote() {
const remote = await run(
"ensure upstream openai/codex remote",
"git remote get-url " + q(upstreamRemote) + " >/dev/null 2>&1 && git remote set-url " + q(upstreamRemote) + " " + q(upstreamRepoUrl) + " || git remote add " + q(upstreamRemote) + " " + q(upstreamRepoUrl),
{ max_output_tokens: 12000 }
);
if (!ok(remote)) {
finish("failed", "Could not configure upstream remote.", { remoteOutput: remote.output });
}
}
async function fetchUpstreamMainAndTags() {
const refspec = "+refs/heads/" + upstreamMainRef + ":refs/remotes/" + upstreamRemote + "/" + upstreamMainRef;
const fetch = await run(
"fetch upstream main and tags",
"git fetch " + q(upstreamRemote) + " --tags --prune " + q(refspec),
{ max_output_tokens: 20000, textLimit: 16000 }
);
if (!ok(fetch)) {
finish("failed", "Could not fetch upstream main and release tags.", { fetchOutput: fetch.output });
}
const remoteMain = await run(
"resolve upstream main",
"git rev-parse --verify " + q("refs/remotes/" + upstreamRemote + "/" + upstreamMainRef + "^{commit}"),
{ max_output_tokens: 4000 }
);
if (!ok(remoteMain)) {
finish("failed", "Could not resolve fetched upstream main.", { upstreamMainOutput: remoteMain.output });
}
const current = await currentBranch();
if (current === upstreamBranch) {
const switched = await run("switch away from upstream branch", "git switch " + q(targetBranch), {
max_output_tokens: 12000
});
if (!ok(switched)) {
const detached = await run("detach from upstream branch", "git switch --detach", { max_output_tokens: 12000 });
if (!ok(detached)) {
finish("failed", "Could not leave the upstream branch before updating it.", {
switchOutput: switched.output,
detachOutput: detached.output
});
}
}
}
const update = await run(
"update local upstream branch",
"git update-ref " + q("refs/heads/" + upstreamBranch) + " " + q(trim(remoteMain.output)),
{ max_output_tokens: 12000 }
);
if (!ok(update)) {
finish("failed", "Could not update local upstream branch.", { updateOutput: update.output });
}
return trim(remoteMain.output);
}
async function resolveReleaseBase() {
const release = await run(
"resolve release tag",
"git rev-parse --verify " + q("refs/tags/" + releaseTag + "^{commit}"),
{ max_output_tokens: 4000 }
);
if (!ok(release)) {
finish("failed", "Could not resolve upstream release tag after fetch.", {
releaseTag,
resolveOutput: release.output
});
}
return trim(release.output);
}
async function currentBranch() {
const branch = await run("current branch", "git rev-parse --abbrev-ref HEAD", { max_output_tokens: 4000 });
return ok(branch) ? trim(branch.output) : "";
}
async function resolveCommit(ref) {
const result = await run(
"resolve " + ref,
"git rev-parse --verify " + q(ref + "^{commit}"),
{ max_output_tokens: 4000 }
);
return ok(result) ? trim(result.output) : "";
}
async function resolveTree(ref) {
const result = await run(
"resolve tree " + ref,
"git rev-parse --verify " + q(ref + "^{tree}"),
{ max_output_tokens: 4000 }
);
return ok(result) ? trim(result.output) : "";
}
async function listPatchBranches() {
const refRoot = "refs/heads/" + patchPrefix.replace(/\/+$/, "");
const result = await run(
"list patch branches",
"git for-each-ref --format='%(refname:short)%09%(objectname)%09%(contents:subject)' " + q(refRoot),
{ max_output_tokens: 20000, textLimit: 16000 }
);
if (!ok(result)) {
finish("failed", "Could not list patch branches.", { patchListOutput: result.output });
}
const patches = trim(result.output)
.split(/\r?\n/)
.map((line) => {
const parts = line.split("\t");
return {
name: parts[0] || "",
sha: parts[1] || "",
subject: parts[2] || ""
};
})
.filter((patch) => patch.name.startsWith(patchPrefix) && patch.sha)
.sort((left, right) => left.name.localeCompare(right.name));
if (patches.length === 0) {
finish("blocked", "No patch branches were found for prefix " + patchPrefix + ".");
}
return patches;
}
async function rebuildMainFromBase(baseRef, baseSha, beforeSha) {
const beforeTree = beforeSha ? await resolveTree(targetBranch) : "";
const patches = await listPatchBranches();
const detach = await run("checkout rebuild base", "git switch --detach " + q(baseSha), {
max_output_tokens: 12000
});
if (!ok(detach)) {
finish("failed", "Could not switch to the rebuild base.", { checkoutOutput: detach.output });
}
const applied = [];
for (const patch of patches) {
const pick = await run("apply " + patch.name, "git cherry-pick " + q(patch.sha), {
max_output_tokens: 30000,
textLimit: 20000
});
if (!ok(pick)) {
const context = await collectConflictContext({
beforeSha,
baseRef,
baseSha,
rebuildOutput: pick.output,
failedPatch: patch,
applied
});
finish("needs_intervention", "Patch workspace rebuild paused on " + patch.name + ".", context);
}
applied.push(patch);
}
const afterHead = await run("rebuilt head", "git rev-parse HEAD", { max_output_tokens: 4000 });
const afterSha = trim(afterHead.output);
const afterTree = await resolveTree("HEAD");
if (beforeTree && afterTree && beforeTree === afterTree) {
const restore = await run("restore target branch", "git switch " + q(targetBranch), { max_output_tokens: 12000 });
if (!ok(restore)) {
finish("failed", "Rebuilt tree matched target, but switching back failed.", { restoreOutput: restore.output });
}
return {
changed: false,
beforeSha,
afterSha: beforeSha,
rebuiltSha: afterSha,
applied
};
}
const update = await run("update target branch", "git branch -f " + q(targetBranch) + " " + q(afterSha), {
max_output_tokens: 12000
});
if (!ok(update)) {
finish("failed", "Could not update target branch after rebuild.", { updateOutput: update.output });
}
const target = await run("switch target branch", "git switch " + q(targetBranch), { max_output_tokens: 12000 });
if (!ok(target)) {
finish("failed", "Could not switch to target branch after rebuild.", { switchOutput: target.output });
}
return {
changed: beforeSha !== afterSha,
beforeSha,
afterSha,
rebuiltSha: afterSha,
applied
};
}
async function verifyBranchUpdateCandidate(rebuild) {
const diffCheck = await run("codex diff whitespace check", "git diff --check", { max_output_tokens: 12000 });
if (!ok(diffCheck)) {
finish("failed", "Codex git diff --check failed after branch update.", {
beforeSha: rebuild.beforeSha,
afterSha: rebuild.afterSha,
diffCheckOutput: diffCheck.output
});
}
const cargoCheck = await run(
"cargo check code mode packages",
"CARGO_TARGET_DIR=" + q(cargoTargetDir) + " cargo check -p codex-app-server -p codex-core -p codex-app-server-protocol",
{ workdir: codexRustDir, max_output_tokens: 30000, textLimit: 20000 }
);
if (!ok(cargoCheck)) {
finish("failed", "Cargo check failed after branch update.", {
beforeSha: rebuild.beforeSha,
afterSha: rebuild.afterSha,
cargoCheckOutput: cargoCheck.output
});
}
}
async function verifyReleaseCandidate(rebuild) {
const cargoVersion = await run(
"validate Cargo.toml version",
"grep -m1 '^version' " + q(codexRustDir + "/Cargo.toml") + " | sed -E 's/version *= *\"([^\"]+)\".*/\\1/'",
{ max_output_tokens: 4000 }
);
if (!ok(cargoVersion) || trim(cargoVersion.output) !== version) {
finish("failed", "Release tag does not match codex-rs/Cargo.toml version.", {
releaseTag,
version,
cargoVersion: trim(cargoVersion.output)
});
}
const build = await run(
"build fork binary",
"CARGO_TARGET_DIR=" + q(cargoTargetDir) + " cargo build -p codex-cli --bin codex",
{ workdir: codexRustDir, max_output_tokens: 30000, textLimit: 20000 }
);
if (!ok(build)) {
finish("failed", "Fork binary build failed.", {
beforeSha: rebuild.beforeSha,
afterSha: rebuild.afterSha,
buildOutput: build.output
});
}
const versionCheck = await run("verify fork binary", q(codexBinary) + " --version", { max_output_tokens: 4000 });
if (!ok(versionCheck)) {
finish("failed", "Built fork binary did not run.", {
beforeSha: rebuild.beforeSha,
afterSha: rebuild.afterSha,
versionOutput: versionCheck.output
});
}
const cargoCheck = await run(
"cargo check code mode packages",
"CARGO_TARGET_DIR=" + q(cargoTargetDir) + " cargo check -p codex-app-server -p codex-core -p codex-app-server-protocol",
{ workdir: codexRustDir, max_output_tokens: 30000, textLimit: 20000 }
);
if (!ok(cargoCheck)) {
finish("failed", "Cargo check failed after release rebuild.", {
beforeSha: rebuild.beforeSha,
afterSha: rebuild.afterSha,
cargoCheckOutput: cargoCheck.output
});
}
const protocolTest = await run(
"protocol code mode execute test",
"CARGO_TARGET_DIR=" + q(cargoTargetDir) + " cargo test -p codex-app-server-protocol thread_code_mode_execute -- --nocapture",
{ workdir: codexRustDir, max_output_tokens: 30000, textLimit: 20000 }
);
if (!ok(protocolTest)) {
finish("failed", "Protocol Code Mode API test failed after release rebuild.", {
beforeSha: rebuild.beforeSha,
afterSha: rebuild.afterSha,
protocolTestOutput: protocolTest.output
});
}
const fmt = await run("cargo fmt check", "cargo fmt --check", {
workdir: codexRustDir,
max_output_tokens: 20000
});
if (!ok(fmt)) {
finish("failed", "Cargo fmt --check failed after release rebuild.", {
beforeSha: rebuild.beforeSha,
afterSha: rebuild.afterSha,
fmtOutput: fmt.output
});
}
const diffCheck = await run("codex diff whitespace check", "git diff --check", { max_output_tokens: 12000 });
if (!ok(diffCheck)) {
finish("failed", "Codex git diff --check failed after release rebuild.", {
beforeSha: rebuild.beforeSha,
afterSha: rebuild.afterSha,
diffCheckOutput: diffCheck.output
});
}
const artifacts = {
codexBinary,
codexVersion: trim(versionCheck.output)
};
if (enabled("stage_npm_wrapper", true)) {
Object.assign(artifacts, await stageLocalNpmWrapper());
}
return artifacts;
}
async function detectLocalTargetTriple() {
const target = await run(
"detect local npm target triple",
[
"case \"$(uname -s):$(uname -m)\" in",
" Linux:x86_64) printf x86_64-unknown-linux-musl ;;",
" Linux:aarch64|Linux:arm64) printf aarch64-unknown-linux-musl ;;",
" Darwin:x86_64) printf x86_64-apple-darwin ;;",
" Darwin:arm64) printf aarch64-apple-darwin ;;",
" *) exit 1 ;;",
"esac"
].join("\n"),
{ max_output_tokens: 4000 }
);
if (!ok(target) || !trim(target.output)) {
finish("failed", "Could not infer local target triple for npm wrapper staging.", {
targetOutput: target.output
});
}
return trim(target.output);
}
async function stageLocalNpmWrapper() {
const targetTriple = await detectLocalTargetTriple();
const binaryName = targetTriple.includes("windows") ? "codex.exe" : "codex";
const stage = await run(
"stage local npm wrapper",
[
"set -euo pipefail",
"stage_root=$(mktemp -d /tmp/peezy-codex-npm-stage.XXXXXX)",
"package_dir=\"$stage_root/package\"",
"python3 " + q(codexRepo + "/codex-cli/scripts/build_npm_package.py") + " --package codex --version " + q(version) + " --staging-dir \"$package_dir\"",
"mkdir -p \"$package_dir/vendor/" + targetTriple + "/codex\"",
"cp " + q(codexBinary) + " \"$package_dir/vendor/" + targetTriple + "/codex/" + binaryName + "\"",
"chmod 0755 \"$package_dir/vendor/" + targetTriple + "/codex/" + binaryName + "\"",
"node \"$package_dir/bin/codex.js\" --version",
"printf '\\nSTAGED_PACKAGE=%s\\n' \"$package_dir\""
].join("\n"),
{ max_output_tokens: 20000, textLimit: 16000 }
);
if (!ok(stage)) {
finish("failed", "Local npm wrapper staging failed.", { stageOutput: stage.output });
}
const match = stage.output.match(/STAGED_PACKAGE=(.+)/);
const stagedPackage = match ? trim(match[1]) : "";
if (enabled("link_local_package", false) && stagedPackage) {
const link = await run("link local npm wrapper with Bun", "bun pm link", {
workdir: stagedPackage,
max_output_tokens: 12000
});
if (!ok(link)) {
finish("failed", "Bun link of local Codex package failed.", { linkOutput: link.output, stagedPackage });
}
}
return {
targetTriple,
stagedPackage,
linked: enabled("link_local_package", false)
};
}
async function maybePushTargetBranch(afterSha) {
if (!enabled("push", false)) {
return false;
}
const push = await run("push fork branch", "git push origin HEAD:" + q(targetBranch) + " --force-with-lease", {
max_output_tokens: 20000
});
if (!ok(push)) {
finish("failed", "Could not push rebuilt fork branch.", { pushOutput: push.output });
}
return true;
}
async function maybePublishReleaseTag() {
if (!enabled("publish", false)) {
return false;
}
const tagName = "rust-v" + version;
const existing = await run("check release tag", "git rev-parse --verify " + q("refs/tags/" + tagName), {
max_output_tokens: 4000
});
if (existing.exit_code !== 0) {
const tag = await run("create release tag", "git tag -a " + q(tagName) + " -m " + q("Release " + version), {
max_output_tokens: 12000
});
if (!ok(tag)) {
finish("failed", "Could not create release tag.", { tagOutput: tag.output });
}
}
const pushTag = await run("push release tag", "git push origin " + q(tagName), {
max_output_tokens: 20000
});
if (!ok(pushTag)) {
finish("failed", "Could not push release tag.", { pushTagOutput: pushTag.output });
}
return true;
}
function candidateRef(afterSha, pushed) {
return {
kind: "branch",
repo: "peezy-tech/codex",
remote: pushed ? "origin" : "local",
ref: "refs/heads/" + targetBranch,
sha: afterSha,
pushed
};
}
const releaseTag = String(payload.tag || "");
const version = versionFromTag(releaseTag);
const flowFlagOverrides = {
force: await envFlag("CODEX_FLOW_FORCE"),
push: await envFlag("CODEX_FLOW_PUSH"),
publish: await envFlag("CODEX_FLOW_PUBLISH"),
squash_patch_stack: await envFlag("CODEX_FLOW_SQUASH_PATCH_STACK")
stage_npm_wrapper: await envFlag("CODEX_FLOW_STAGE_NPM_WRAPPER"),
link_local_package: await envFlag("CODEX_FLOW_LINK_LOCAL_PACKAGE")
};
const releaseTag = eventType === "upstream.release" ? String(payload.tag || "") : "";
const version = releaseTag ? versionFromTag(releaseTag) : "";
const operation = eventType === "upstream.release" ? "release-cycle" : "main-branch-update";
const packageName = cfg("package_name", "@peezy.tech/codex");
const targetBranch = (await env(cfg("target_branch_env", ""))) || cfg("target_branch", "main");
const upstreamBranch = cfg("upstream_branch", "upstream");
const patchPrefix = cfg("patch_prefix", "patch/");
const upstreamRemote = cfg("upstream_remote", "upstream");
const upstreamMainRef = cfg("upstream_main_ref", "main");
const upstreamRepoUrl = cfg("upstream_repo_url", "https://github.com/openai/codex.git");
const cargoTargetDir = (await env(cfg("cargo_target_dir_env", ""))) || cfg("cargo_target_dir", "/tmp/peezy-codex-flow-target");
const codexRepo = (await env(cfg("codex_repo_env", ""))) || cfg("codex_repo", "");
const codexRustDir = codexRepo + "/codex-rs";
const codexBinary = cargoTargetDir + "/debug/codex";
if (!releaseTag) {
if (eventType !== "upstream.release" && eventType !== "upstream.branch_update") {
finish("failed", "Unsupported event type " + eventType + ".");
}
if (String(payload.repo || "") !== "openai/codex") {
finish("skipped", "Ignoring upstream event for " + String(payload.repo || "unknown") + ".");
}
if (eventType === "upstream.release" && !releaseTag) {
finish("failed", "Release payload is missing tag.");
}
if (!version) {
finish("failed", "Could not infer semantic version from release tag " + releaseTag);
if (eventType === "upstream.release" && !version) {
finish("failed", "Could not infer semantic version from release tag " + releaseTag + ".");
}
if (!codexRepo) {
finish("blocked", "No Codex fork checkout configured. Set codex_repo or codex_repo_env in flow.toml.");
@ -163,230 +649,76 @@ if (!codexRepo) {
text([
"Peezy Codex fork update flow",
"",
"Release: " + releaseTag,
"Version: " + version,
"Event type: " + eventType,
"Operation: " + operation,
releaseTag ? "Release: " + releaseTag : "Upstream ref: " + String(payload.ref || "refs/heads/" + upstreamMainRef),
version ? "Version: " + version : undefined,
"Target branch: " + targetBranch,
"Upstream branch: " + upstreamBranch,
"Patch prefix: " + patchPrefix,
"Codex repo: " + codexRepo,
"Upstream remote: " + upstreamRemote + " -> " + upstreamRepoUrl,
"Cargo target dir: " + cargoTargetDir
].join("\n") + "\n");
].filter(Boolean).join("\n") + "\n");
const published = await run("published fork package check", "npm view " + q(packageName + "@" + version) + " version --json", {
max_output_tokens: 4000
});
if (ok(published) && !enabled("force", false)) {
finish("skipped", packageName + "@" + version + " is already published.");
if (eventType === "upstream.release") {
const published = await run("published fork package check", "npm view " + q(packageName + "@" + version) + " version --json", {
max_output_tokens: 4000
});
if (ok(published) && !enabled("force", false)) {
finish("skipped", packageName + "@" + version + " is already published.");
}
}
const repoCheck = await run("verify codex repo", "git rev-parse --show-toplevel");
if (!ok(repoCheck)) {
finish("failed", "codex repo is not a git checkout", { repoCheck: repoCheck.output });
finish("failed", "Codex repo is not a git checkout.", { repoCheck: repoCheck.output });
}
const rustWorkspaceCheck = await run("verify codex Rust workspace", "test -f " + q(codexRustDir + "/Cargo.toml"), {
max_output_tokens: 4000
});
if (!ok(rustWorkspaceCheck)) {
finish("failed", "codex Rust workspace was not found at the expected codex-rs path.", {
finish("failed", "Codex Rust workspace was not found at the expected codex-rs path.", {
codexRustDir,
rustWorkspaceCheck: rustWorkspaceCheck.output
});
}
const existingRebase = await run(
"check existing rebase state",
"test -d \"$(git rev-parse --git-path rebase-merge)\" -o -d \"$(git rev-parse --git-path rebase-apply)\"",
{ max_output_tokens: 4000 }
);
if (existingRebase.exit_code === 0) {
const context = await collectRebaseContext("A rebase was already in progress before this flow started.", undefined);
finish("blocked", "A rebase is already in progress in the Codex checkout.", context);
}
await run("codex status before update", "git status --short --branch", { max_output_tokens: 12000 });
const branch = await run("current branch", "git rev-parse --abbrev-ref HEAD", { max_output_tokens: 4000 });
if (!ok(branch)) {
finish("failed", "could not read current branch", { branchOutput: branch.output });
}
if (trim(branch.output) !== targetBranch) {
const dirtyBeforeSwitch = await run("dirty check before branch switch", "git status --porcelain=v1", { max_output_tokens: 12000 });
if (trim(dirtyBeforeSwitch.output)) {
finish("blocked", "codex checkout has local changes before switching branches.", {
dirtyStatus: dirtyBeforeSwitch.output
});
}
const switched = await run("switch target branch", "git switch " + q(targetBranch), { max_output_tokens: 12000 });
if (!ok(switched)) {
finish("failed", "could not switch to target branch", { switchOutput: switched.output });
}
}
const dirty = await run("dirty check on target branch", "git status --porcelain=v1", { max_output_tokens: 12000 });
if (trim(dirty.output)) {
finish("blocked", "codex target branch has local changes. Resolve or stash them before updating.", {
dirtyStatus: dirty.output
});
}
const remote = await run(
"ensure upstream openai/codex remote",
"git remote get-url " + q(upstreamRemote) + " >/dev/null 2>&1 && git remote set-url " + q(upstreamRemote) + " " + q(upstreamRepoUrl) + " || git remote add " + q(upstreamRemote) + " " + q(upstreamRepoUrl),
{ max_output_tokens: 12000 }
);
if (!ok(remote)) {
finish("failed", "could not configure upstream remote", { remoteOutput: remote.output });
}
const fetch = await run("fetch upstream tags", "git fetch " + q(upstreamRemote) + " --tags --prune", {
max_output_tokens: 20000
});
if (!ok(fetch)) {
finish("failed", "could not fetch upstream release tags", { fetchOutput: fetch.output });
}
const releaseCommit = await run("resolve release tag", "git rev-parse --verify " + q("refs/tags/" + releaseTag + "^{commit}"), {
max_output_tokens: 4000
});
if (!ok(releaseCommit)) {
finish("failed", "could not resolve upstream release tag after fetch", {
releaseTag,
resolveOutput: releaseCommit.output
});
}
const beforeHead = await run("codex head before rebase", "git rev-parse HEAD", { max_output_tokens: 4000 });
const rebase = await run("rebase target branch onto upstream release", "git rebase " + q(releaseTag), {
max_output_tokens: 30000,
textLimit: 20000
});
if (!ok(rebase)) {
const context = await collectRebaseContext(rebase.output, trim(beforeHead.output));
finish("needs_intervention", "Rebase paused with conflicts.", context);
}
if (enabled("squash_patch_stack", true)) {
const count = await run("count fork patch commits", "git rev-list --count " + q(releaseTag) + "..HEAD", {
max_output_tokens: 4000
});
const commitCount = Number(trim(count.output));
if (Number.isFinite(commitCount) && commitCount > 1) {
const reset = await run("squash patch stack reset", "git reset --soft " + q(releaseTag), { max_output_tokens: 12000 });
if (!ok(reset)) {
finish("failed", "could not soft reset patch stack for squashing", { resetOutput: reset.output });
}
const commit = await run("squash patch stack commit", "git commit -m " + q("peezy: codex fork patches for " + releaseTag), {
max_output_tokens: 20000
});
if (!ok(commit)) {
finish("failed", "could not commit squashed patch stack", { commitOutput: commit.output });
}
}
}
const afterHead = await run("codex head after rebase", "git rev-parse HEAD", { max_output_tokens: 4000 });
await run("codex status after rebase", "git status --short --branch", { max_output_tokens: 12000 });
const build = await run(
"build fork binary",
"CARGO_TARGET_DIR=" + q(cargoTargetDir) + " cargo build -p codex-cli --bin codex",
{ workdir: codexRustDir, max_output_tokens: 30000, textLimit: 20000 }
);
if (!ok(build)) {
finish("failed", "fork binary build failed", {
beforeSha: trim(beforeHead.output),
afterSha: trim(afterHead.output),
buildOutput: build.output
});
}
const versionCheck = await run("verify fork binary", q(codexBinary) + " --version", { max_output_tokens: 4000 });
if (!ok(versionCheck)) {
finish("failed", "built fork binary did not run", {
beforeSha: trim(beforeHead.output),
afterSha: trim(afterHead.output),
versionOutput: versionCheck.output
});
}
const cargoCheck = await run(
"cargo check code mode packages",
"CARGO_TARGET_DIR=" + q(cargoTargetDir) + " cargo check -p codex-app-server -p codex-core -p codex-app-server-protocol",
{ workdir: codexRustDir, max_output_tokens: 30000, textLimit: 20000 }
);
if (!ok(cargoCheck)) {
finish("failed", "cargo check failed after rebase", {
beforeSha: trim(beforeHead.output),
afterSha: trim(afterHead.output),
cargoCheckOutput: cargoCheck.output
});
}
const protocolTest = await run(
"protocol code mode execute test",
"CARGO_TARGET_DIR=" + q(cargoTargetDir) + " cargo test -p codex-app-server-protocol thread_code_mode_execute -- --nocapture",
{ workdir: codexRustDir, max_output_tokens: 30000, textLimit: 20000 }
);
if (!ok(protocolTest)) {
finish("failed", "protocol Code Mode API test failed after rebase", {
beforeSha: trim(beforeHead.output),
afterSha: trim(afterHead.output),
protocolTestOutput: protocolTest.output
});
}
const fmt = await run("cargo fmt check", "cargo fmt --check", {
workdir: codexRustDir,
max_output_tokens: 20000
});
if (!ok(fmt)) {
finish("failed", "cargo fmt --check failed after rebase", {
beforeSha: trim(beforeHead.output),
afterSha: trim(afterHead.output),
fmtOutput: fmt.output
});
}
const diffCheck = await run("codex diff whitespace check", "git diff --check", { max_output_tokens: 12000 });
if (!ok(diffCheck)) {
finish("failed", "codex git diff --check failed after rebase", {
beforeSha: trim(beforeHead.output),
afterSha: trim(afterHead.output),
diffCheckOutput: diffCheck.output
});
}
if (enabled("push", false)) {
const push = await run("push fork branch", "git push origin HEAD:" + q(targetBranch) + " --force-with-lease", {
max_output_tokens: 20000
});
if (!ok(push)) {
finish("failed", "could not push rebased fork branch", { pushOutput: push.output });
}
}
if (enabled("publish", false)) {
const tagCommand = "git tag -a " + q("rust-v" + version) + " -m " + q("Release " + version);
const tag = await run("create release tag", tagCommand, { max_output_tokens: 12000 });
if (!ok(tag)) {
finish("failed", "could not create release tag", { tagOutput: tag.output });
}
const pushTag = await run("push release tag", "git push origin " + q("rust-v" + version), {
max_output_tokens: 20000
});
if (!ok(pushTag)) {
finish("failed", "could not push release tag", { pushTagOutput: pushTag.output });
}
}
await requireCleanWorktree();
await requireNoPausedGitOperation();
await ensureUpstreamRemote();
const upstreamMainSha = await fetchUpstreamMainAndTags();
const baseSha = eventType === "upstream.release" ? await resolveReleaseBase() : upstreamMainSha;
const baseRef = eventType === "upstream.release" ? releaseTag : upstreamBranch;
const beforeSha = await resolveCommit(targetBranch);
const rebuild = await rebuildMainFromBase(baseRef, baseSha, beforeSha);
const verificationArtifacts = eventType === "upstream.release"
? await verifyReleaseCandidate(rebuild)
: await verifyBranchUpdateCandidate(rebuild).then(() => ({}));
const pushed = await maybePushTargetBranch(rebuild.afterSha);
const published = eventType === "upstream.release" ? await maybePublishReleaseTag() : false;
const finalStatus = await run("final codex status", "git status --short --branch", { max_output_tokens: 12000 });
finish("changed", "Peezy Codex fork rebased onto upstream release and verified.", {
beforeSha: trim(beforeHead.output),
afterSha: trim(afterHead.output),
codexHead: trim(afterHead.output),
codexBinary,
codexVersion: trim(versionCheck.output),
finalStatus: finalStatus.output,
pushed: enabled("push", false),
published: enabled("publish", false)
});
const status = rebuild.changed ? "changed" : "completed";
finish(
status,
eventType === "upstream.release"
? "Peezy Codex fork rebuilt from upstream release and verified."
: "Peezy Codex fork main rebuilt from upstream/main and verified.",
{
beforeSha: rebuild.beforeSha,
afterSha: rebuild.afterSha,
rebuiltSha: rebuild.rebuiltSha,
baseRef,
baseSha,
upstreamMainSha,
applied: rebuild.applied,
finalStatus: finalStatus.output,
pushed,
published,
candidateRefs: [candidateRef(rebuild.afterSha, pushed)],
...verificationArtifacts
}
);

View file

@ -1,26 +1,30 @@
name = "peezy-codex-fork"
version = 1
description = "Rebase the Peezy Codex fork patch stack onto a canonical openai/codex release."
description = "Maintain the Peezy Codex fork patch workspace from openai/codex releases and main updates."
[config]
package_name = "@peezy.tech/codex"
codex_repo_env = "PEEZY_CODEX_REPO"
codex_repo = "/home/peezy/meta-workspace/codex"
target_branch_env = "PEEZY_CODEX_TARGET_BRANCH"
target_branch = "code-mode-exec-hooks"
target_branch = "main"
upstream_branch = "upstream"
patch_prefix = "patch/"
upstream_remote = "upstream"
upstream_main_ref = "main"
upstream_repo_url = "https://github.com/openai/codex.git"
cargo_target_dir_env = "PEEZY_CODEX_CARGO_TARGET_DIR"
cargo_target_dir = "/tmp/peezy-codex-flow-target"
squash_patch_stack = true
stage_npm_wrapper = true
link_local_package = false
push = false
publish = false
[guidance]
skills = ["jojo-development-flow"]
skills = ["jojo-development-flow", "code-mode-flow-author"]
[[steps]]
name = "rebase-patch-stack"
name = "release-cycle"
runner = "code-mode"
script = "exec/update-fork.code-mode.js"
timeout_ms = 3600000
@ -28,3 +32,13 @@ timeout_ms = 3600000
[steps.trigger]
type = "upstream.release"
schema = "schemas/upstream-release.schema.json"
[[steps]]
name = "main-branch-update"
runner = "code-mode"
script = "exec/update-fork.code-mode.js"
timeout_ms = 3600000
[steps.trigger]
type = "upstream.branch_update"
schema = "schemas/upstream-branch-update.schema.json"

View file

@ -0,0 +1,16 @@
{
"type": "object",
"required": ["repo", "ref"],
"properties": {
"provider": { "type": "string" },
"repo": { "type": "string", "enum": ["openai/codex"] },
"repoOwner": { "type": "string" },
"repoName": { "type": "string" },
"ref": { "type": "string" },
"sha": { "type": "string" },
"url": { "type": "string" },
"publishedAt": { "type": "string" },
"sourceId": { "type": "string" },
"entryId": { "type": "string" }
}
}

View file

@ -7,12 +7,12 @@
"source": {
"input": "../codex-flows",
"type": "local",
"commit": "7083213cd6e8a3a3a175ca63529134f7807e2afe"
"commit": "c3905c73feff72add4f88ba8ec5c11cb1921c386"
},
"sourcePath": "flows/openai-codex-bindings",
"destinationPath": ".codex/flows/openai-codex-bindings",
"contentHash": "sha256:5fcb04e9525e94c24ca319fcefc99fd05ed973e9c640a5e27629e263f13ca968",
"installedAt": "2026-05-18T18:57:28.444Z"
"contentHash": "sha256:6bfd8118be031c145813dcacbf47f6939b2460aeeb0d8959075e6018297d0403",
"installedAt": "2026-05-18T19:14:31.421Z"
},
{
"name": "peezy-codex-fork",
@ -20,12 +20,12 @@
"source": {
"input": "../codex-flows",
"type": "local",
"commit": "7083213cd6e8a3a3a175ca63529134f7807e2afe"
"commit": "c3905c73feff72add4f88ba8ec5c11cb1921c386"
},
"sourcePath": "flows/peezy-codex-fork",
"destinationPath": ".codex/flows/peezy-codex-fork",
"contentHash": "sha256:521e7766cd1888fd54fc0c10a6d2a3a7f235d9a7c8aa2577b4f4681e3a8fdabe",
"installedAt": "2026-05-18T18:57:28.444Z"
"contentHash": "sha256:613f0733ebea22b191ed6e706ad09e05a2dc97aad16cdbe14db01065a1b06582",
"installedAt": "2026-05-18T19:14:31.421Z"
}
]
}

View file

@ -53,10 +53,15 @@
"defaultBranch": "main"
},
"target": {
"provider": "github",
"repoFullName": "peezy-tech/codex",
"branch": "main",
"mode": "notify_only"
"mode": "workspace_flow",
"eventType": "upstream.branch_update",
"workspaceUrlEnv": "PATCH_WORKSPACE_BACKEND_URL",
"workspaceSecretEnv": "PATCH_WORKSPACE_BACKEND_SECRET",
"payload": {
"provider": "github",
"repo": "openai/codex",
"ref": "refs/heads/main"
}
},
"pollIntervalSeconds": 300
},

View file

@ -7,6 +7,7 @@ import { discoverFlows, matchingSteps, type FlowEvent as RuntimeFlowEvent } from
import {
dispatchWorkspaceEventDetailed,
maintenanceAttemptForWorkspaceDispatch,
patchUpstreamBranchUpdateEvent,
patchUpstreamReleaseEvent,
replayWorkspaceEventDetailed,
type WorkspaceDispatchConfig,
@ -53,6 +54,7 @@ Usage:
patch.moi attempts [--event-id ID] [--status STATUS] [--limit N] [--data-dir DIR] [--json]
patch.moi run harness [--event FILE] [--workspace-root DIR] [--data-dir DIR] [--dry-run] [--json]
patch.moi run codex-release --tag TAG [--repo openai/codex] [--workspace-root DIR] [--data-dir DIR] [--dry-run] [--record-only] [--allow-local] [--json]
patch.moi run codex-main [--sha SHA] [--repo openai/codex] [--ref refs/heads/main] [--workspace-root DIR] [--data-dir DIR] [--dry-run] [--record-only] [--allow-local] [--json]
patch.moi run event --file FILE [--workspace-root DIR] [--data-dir DIR] [--dry-run] [--record-only] [--json]
patch.moi patch doctor [--repo DIR] [--main BRANCH] [--upstream BRANCH] [--json]
patch.moi patch list [--repo DIR] [--prefix patch/] [--json]
@ -252,6 +254,19 @@ async function handleRun(positionals: string[], context: CliContext): Promise<nu
}
return await runEvent(event, context);
}
if (target === "codex-main") {
const repo = flagValue(context.parsed, "repo") ?? "openai/codex";
const ref = flagValue(context.parsed, "ref") ?? "refs/heads/main";
const event = patchUpstreamBranchUpdateEvent({
repo,
ref,
sha: flagValue(context.parsed, "sha"),
});
if (!flagBool(context.parsed, "dry-run") && !flagBool(context.parsed, "record-only")) {
assertCodexDispatchAllowed(context, "codex-main");
}
return await runEvent(event, context);
}
if (target === "event") {
const eventFile = flagValue(context.parsed, "file");
if (!eventFile) {
@ -431,7 +446,7 @@ async function handleSetup(positionals: string[], context: CliContext): Promise<
const repoPath = resolvePath(context.workspaceRoot, flagValue(context.parsed, "repo") ?? context.env.PEEZY_CODEX_REPO ?? "../codex");
const upstreamRemote = flagValue(context.parsed, "upstream-remote") ?? "upstream";
const upstreamUrl = flagValue(context.parsed, "upstream-url") ?? "https://github.com/openai/codex.git";
const targetBranch = flagValue(context.parsed, "target-branch") ?? "code-mode-exec-hooks";
const targetBranch = flagValue(context.parsed, "target-branch") ?? "main";
const apply = flagBool(context.parsed, "apply");
const repo = await inspectGitRepo(repoPath, {
upstreamRemote,
@ -560,7 +575,7 @@ async function appendFlowEventIfMissing(store: EventStore, event: FlowEvent): Pr
return true;
}
function assertCodexDispatchAllowed(context: CliContext): void {
function assertCodexDispatchAllowed(context: CliContext, command = "codex-release"): void {
if (
flagBool(context.parsed, "allow-local") ||
workspaceBackendConfigured(context.env) ||
@ -569,7 +584,7 @@ function assertCodexDispatchAllowed(context: CliContext): void {
return;
}
throw new UsageError(
"codex-release dispatch requires PATCH_WORKSPACE_BACKEND_URL, CODEX_WORKSPACE_MODE=actions, or --allow-local; use --dry-run to verify matching without executing release work",
`${command} dispatch requires PATCH_WORKSPACE_BACKEND_URL, CODEX_WORKSPACE_MODE=actions, or --allow-local; use --dry-run to verify matching without executing maintenance work`,
);
}

View file

@ -111,6 +111,25 @@ export function patchUpstreamReleaseEvent(input: {
};
}
export function patchUpstreamBranchUpdateEvent(input: {
repo: string;
ref: string;
sha?: string;
receivedAt?: string;
}): FlowEvent<Record<string, unknown>> {
return {
id: `${serviceSource}:upstream.branch_update:${input.repo}:${input.ref}${input.sha ? `:${input.sha}` : ""}`,
type: "upstream.branch_update",
source: serviceSource,
receivedAt: input.receivedAt ?? new Date().toISOString(),
payload: {
repo: input.repo,
ref: input.ref,
...(input.sha ? { sha: input.sha } : {}),
},
};
}
export async function dispatchFlowEvent(
event: FlowEvent,
target: Partial<FeedWorkspaceFlowTarget> = {},

View file

@ -84,10 +84,38 @@ describe("patch.moi CLI", () => {
expect(dryRun.code).toBe(0);
expect(JSON.parse(dryRun.stdout).matches).toEqual([
{ flow: "openai-codex-bindings", step: "regenerate-bindings", runner: "bun" },
{ flow: "peezy-codex-fork", step: "rebase-patch-stack", runner: "code-mode" },
{ flow: "peezy-codex-fork", step: "release-cycle", runner: "code-mode" },
]);
});
test("dry-runs Codex main branch update matching", async () => {
const dryRun = await invoke([
"run",
"codex-main",
"--sha",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"--workspace-root",
workspaceRoot,
"--dry-run",
"--json",
]);
expect(dryRun.code).toBe(0);
expect(JSON.parse(dryRun.stdout)).toMatchObject({
event: {
type: "upstream.branch_update",
payload: {
repo: "openai/codex",
ref: "refs/heads/main",
sha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
},
matches: [
{ flow: "peezy-codex-fork", step: "main-branch-update", runner: "code-mode" },
],
});
});
test("syncs a maintenance attempt from workspace run state", async () => {
const dataDir = await mkdtemp(join(tmpdir(), "patch-cli-"));
const store = new EventStore(dataDir);
@ -155,7 +183,7 @@ describe("patch.moi CLI", () => {
test("sets up the Codex upstream remote when explicitly applied", async () => {
const repo = await mkdtemp(join(tmpdir(), "patch-cli-codex-"));
await mkdir(repo, { recursive: true });
await git(repo, ["init", "-b", "code-mode-exec-hooks"]);
await git(repo, ["init", "-b", "main"]);
await git(repo, ["remote", "add", "origin", "https://github.com/peezy-tech/codex"]);
const result = await invoke([
@ -170,7 +198,7 @@ describe("patch.moi CLI", () => {
expect(result.code).toBe(0);
expect(JSON.parse(result.stdout)).toMatchObject({
path: repo,
branch: "code-mode-exec-hooks",
branch: "main",
upstream: "https://github.com/openai/codex.git",
addedUpstream: true,
clean: true,

View file

@ -89,6 +89,10 @@ describe("feed watcher", () => {
"github-openai-codex-main",
"github-openai-codex-releases",
]);
expect(sources.find((item) => item.id === "github-openai-codex-main")?.target).toMatchObject({
mode: "workspace_flow",
eventType: "upstream.branch_update",
});
});
test("first poll primes state without emitting old entries", async () => {
@ -210,6 +214,66 @@ describe("feed watcher", () => {
});
});
test("later main commit polls dispatch upstream branch update events", async () => {
const dataDir = await mkdtemp(join(tmpdir(), "patch-feed-"));
const sourcesPath = join(dataDir, "sources.json");
const branchSource: FeedSourceConfig = {
...source,
target: {
mode: "workspace_flow",
eventType: "upstream.branch_update",
workspaceUrlEnv: "WORKSPACE_URL",
payload: {
repo: "openai/codex",
provider: "github",
ref: "refs/heads/main",
},
},
};
await writeFile(sourcesPath, JSON.stringify({ sources: [branchSource] }), "utf8");
await writeFile(join(dataDir, "feed-state.json"), JSON.stringify({
"github-openai-codex-main": {
lastSeenId: "tag:github.com,2008:Grit::Commit/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
lastCheckedAt: "2026-05-12T09:00:00.000Z",
},
}), "utf8");
let dispatchedBody = "";
await pollFeedsOnce({
dataDir,
sourcesPath,
discord: { enabled: false, notifyEvents: new Set(["push"]) },
flowDispatch: {
env: {
WORKSPACE_URL: "https://workspace.example/events",
},
fetchImpl: async (_url, init) => {
dispatchedBody = String(init.body);
const eventId = JSON.parse(String(init.body ?? "{}")).id;
return Response.json({ status: "accepted", eventId, runIds: [], matched: 1 }, { status: 202 });
},
},
}, async () => {
return new Response(atom, { status: 200 });
});
const flowEventText = await readFile(join(dataDir, "flow-events.jsonl"), "utf8");
const flowEvent = JSON.parse(flowEventText.trim()) as Record<string, any>;
expect(flowEvent.type).toBe("upstream.branch_update");
expect(flowEvent.payload.repo).toBe("openai/codex");
expect(flowEvent.payload.ref).toBe("refs/heads/main");
expect(flowEvent.payload.sha).toBe("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
expect(JSON.parse(dispatchedBody).id).toBe(flowEvent.id);
const attempt = JSON.parse((await readFile(join(dataDir, "maintenance-attempts.jsonl"), "utf8")).trim());
expect(attempt).toMatchObject({
eventType: "upstream.branch_update",
status: "started",
upstreamRepo: "openai/codex",
upstreamRef: "refs/heads/main",
upstreamSha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
});
});
test("workspace dispatch uses default workspace backend env names", async () => {
let dispatchedUrl = "";
let dispatchedSignature = "";

View file

@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test";
import type { FlowRunView } from "@peezy.tech/codex-flows/flow-runtime/client";
import {
patchUpstreamBranchUpdateEvent,
maintenanceAttemptForWorkspaceDispatch,
maintenanceAttemptWithWorkspaceRuns,
patchUpstreamReleaseEvent,
@ -88,6 +89,25 @@ describe("maintenance attempt sync", () => {
{ ref: "refs/heads/codex-candidate", sha: "def" },
]);
});
test("Patch upstream branch helper creates deterministic product events", () => {
expect(patchUpstreamBranchUpdateEvent({
repo: "openai/codex",
ref: "refs/heads/main",
sha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
receivedAt: "2026-05-16T00:00:00.000Z",
})).toEqual({
id: "patch:upstream.branch_update:openai/codex:refs/heads/main:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
type: "upstream.branch_update",
source: "patch",
receivedAt: "2026-05-16T00:00:00.000Z",
payload: {
repo: "openai/codex",
ref: "refs/heads/main",
sha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
});
});
});
function statusFor(statuses: string[]): string {

View file

@ -66,7 +66,7 @@ describe("patch.moi harness flow", () => {
expect(matches.map(({ flow, step }) => `${flow.manifest.name}/${step.name}`)).toEqual([
"openai-codex-bindings/regenerate-bindings",
"peezy-codex-fork/rebase-patch-stack",
"peezy-codex-fork/release-cycle",
]);
const codeModeMatch = matches.find((entry) => entry.flow.manifest.name === "peezy-codex-fork");

View file

@ -68,8 +68,8 @@ The Codex fork makes the desired model concrete:
- `origin/main` is a useful local comparison baseline, but a real upstream
remote is still needed for canonical release tags.
- `code-mode-exec-hooks` is the maintained patch branch, but internal use is
not simply "run from this branch."
- `main` is the maintained output branch; `code-mode-exec-hooks` remains only a
legacy reference for the initial conversion.
- `rust-v0.130.0` at the branch head is a downstream candidate or release tag.
- the npm package rename to `@peezy.tech/*` is part of the patch stack, not a
Patch service setting.
@ -91,12 +91,16 @@ git remote get-url origin
git remote get-url upstream || git remote add upstream https://github.com/openai/codex.git
git fetch upstream --tags --prune
git fetch origin --prune
git rev-list --oneline origin/main..code-mode-exec-hooks
git branch --list 'patch/*'
git status --short --branch
```
For an upstream release event, the workspace can resolve the upstream tag,
rebase `code-mode-exec-hooks`, run Codex-specific checks, and push a candidate
branch or tag when policy allows.
rebuild `main` from that tag plus ordered `patch/*` branches, run Codex-specific
checks, and push a candidate branch or tag when policy allows. For an upstream
main commit event, the workspace updates the local `upstream` branch from
`upstream/main` and rebuilds `main` from that branch plus the same patch
inventory.
In service mode, that work should be triggered through the remote fork host. For
example, patch.moi can create or update a maintenance branch on GitHub, trigger
@ -109,7 +113,7 @@ service's durable execution surface.
Feature development should happen in a separate workspace or branch. A new
custom feature starts from the current maintained branch, produces commits, and
only becomes part of the maintained patch stack after it is intentionally merged
or rebased into `code-mode-exec-hooks`.
or captured into a single logical `patch/*` branch.
## Channel Split

View file

@ -5,26 +5,27 @@ description: How patch.moi applies to Codex fork maintenance.
# Codex Use Case
patch.moi watches OpenAI Codex branch and release feeds. Branch activity can
notify operators. Release activity can emit a deterministic `upstream.release`
flow event that starts Codex fork maintenance.
patch.moi watches OpenAI Codex branch and release feeds. Branch activity emits
deterministic `upstream.branch_update` events for main-branch maintenance.
Release activity emits deterministic `upstream.release` events for release-cycle
maintenance.
The concrete local model is the neighboring `../codex` checkout:
- `origin` points at `https://github.com/peezy-tech/codex`.
- `code-mode-exec-hooks` is the maintained patch branch.
- `origin/main` is the current comparison branch.
- `rust-v0.130.0` is a downstream tag on the maintained branch head.
- the patch stack contains Code Mode exec/replay work and Peezy npm release
changes.
- `main` is the rebuildable maintained fork output.
- `upstream` mirrors `upstream/main`.
- ordered `patch/*` branches hold the logical patch commits.
- `rust-v0.130.0` is a downstream tag on the legacy maintained branch head.
- the current patch inventory contains Code Mode exec/replay work and Peezy npm
release changes.
That checkout currently lacks an `upstream` remote, so a patch.moi setup flow
should add or confirm `https://github.com/openai/codex.git` before a release
rebase.
That checkout has the canonical `upstream` remote, so maintenance can fetch
OpenAI Codex main and release tags before rebuilding the fork output branch.
The Codex maintenance flow rebases the Peezy Codex fork patch stack onto a
canonical upstream release tag. That is a patch application workspace job, not
a public release by itself.
The Codex maintenance flow rebuilds the Peezy Codex fork from a canonical
upstream base plus ordered patch branches. That is a patch application workspace
job, not a public release by itself.
Internal Codex use can track a fast-moving branch for local work. Public npm
release can follow upstream release tags and trusted publishing. Those channels

View file

@ -38,9 +38,9 @@ The workspace may be local or runner-managed. The patch stack itself still lives
in Git. Repo-native `codex-flows workspace` tasks are operator automation for
running known commands; they are not a separate patch-stack database.
In the current Codex fork, this means carrying the commits on
`code-mode-exec-hooks` ahead of `origin/main` onto a canonical upstream release
tag from `openai/codex`.
In the current Codex fork, this means rebuilding `main` from a canonical
OpenAI Codex release tag or `upstream/main` plus the ordered local `patch/*`
branches.
## Feature Development Workspace

View file

@ -46,8 +46,9 @@ backend adapter:
```
For patch-stack maintenance, prefer `workspace_flow` to create an
`upstream.release` or `upstream.update` trigger. Let the receiving workspace read
Git to discover the maintained patch branch and candidate refs.
`upstream.release` or `upstream.branch_update` trigger. Let the receiving
workspace read Git to discover the maintained branch, patch inventory, and
candidate refs.
Use explicit payload fields for patch.moi-dispatched events. Do not rely on
implicit workspace flow defaults for maintenance events, because patch.moi uses

View file

@ -98,7 +98,18 @@ bun run patch.moi -- run codex-release \
The output should show the flow steps that will receive the event. For the
current Codex release package, the expected fanout is the bindings update flow
and the fork rebase flow.
and the Codex fork release-cycle flow.
Dry-run the upstream main branch update path separately:
```bash
bun run patch.moi -- run codex-main \
--sha '<upstream-main-sha>' \
--dry-run \
--json
```
That event should match the Codex fork `main-branch-update` Code Mode step.
## 5. Dispatch the maintenance attempt
@ -195,7 +206,8 @@ bun run patch.moi -- replay '<event-id>' --json
Flow packages decide exactly what `CODEX_FLOW_PUSH=1` means. The harness flow
pushes configured branch refs with `--force-with-lease`. The Codex fork flow
pushes the maintained branch and, when configured to publish, release tags.
pushes the maintained `main` branch and, when configured to publish, release
tags.
Keep `CODEX_FLOW_PUBLISH=0` unless you are intentionally entering the public
release channel.

View file

@ -90,6 +90,14 @@ Verify Codex release flow matching without executing release work:
bun run patch.moi -- run codex-release --tag rust-v0.130.0 --dry-run
```
Verify Codex upstream main update matching without executing branch maintenance:
```bash
bun run patch.moi -- run codex-main \
--sha '<upstream-main-sha>' \
--dry-run
```
Dispatching the Codex release task requires an explicit execution surface. Use
Actions/local mode when no workspace backend is running:

View file

@ -51,6 +51,7 @@ adapter. The flow payload includes provider, event, source id, entry id, title,
URL, author, published time, repository fields, ref, SHA, tag, and raw feed
metadata. Values from `target.payload` are merged last.
For release maintenance, use a stable event type such as `upstream.release` and
include only routing hints in `payload`. Avoid copying branch topology into the
feed source when it can be read from the repository.
For release maintenance, use a stable event type such as `upstream.release`.
For upstream main movement, use `upstream.branch_update`. Include only routing
hints in `payload`; avoid copying branch topology into the feed source when it
can be read from the repository.

View file

@ -60,10 +60,11 @@ maintenance flows from the sibling `../codex-flows` repository:
codex-flows pack doctor --json
```
`openai-codex-bindings` and `peezy-codex-fork` both match
`upstream.release` events for `openai/codex`. They are installed capabilities,
not patch.moi product state. patch.moi still records feed-owned flow events,
workspace dispatches, and maintenance attempts under `DATA_DIR`.
`openai-codex-bindings` matches `upstream.release` events for `openai/codex`.
`peezy-codex-fork` matches both `upstream.release` and
`upstream.branch_update` events. They are installed capabilities, not patch.moi
product state. patch.moi still records feed-owned flow events, workspace
dispatches, and maintenance attempts under `DATA_DIR`.
## Related Runtime Package

View file

@ -7,8 +7,9 @@ description: Connect the OpenAI Codex release feed to a Codex patch-stack mainte
This tutorial connects upstream OpenAI Codex releases to Codex fork
maintenance. Patch records the upstream release and dispatches a deterministic
event. The receiving workspace or runner rebases the maintained patch stack
onto the upstream release tag and verifies the candidate.
event. The receiving workspace or runner rebuilds the maintained `main` branch
from the upstream release tag plus the ordered `patch/*` branches, then
verifies the candidate.
## 1. Use the release source
@ -18,7 +19,13 @@ the upstream repository and release tag in the payload.
The maintained Codex fork should still be modeled in Git. In the neighboring
`../codex` checkout, `origin` is `https://github.com/peezy-tech/codex` and
`code-mode-exec-hooks` is the maintained patch branch.
the local branch shape is:
```text
main rebuildable maintained fork output
upstream local mirror of upstream/main
patch/* ordered one-commit logical patches
```
Before running release maintenance, make sure the checkout has a canonical
upstream remote:
@ -26,13 +33,13 @@ upstream remote:
```bash
cd ../codex
git remote get-url upstream || git remote add upstream https://github.com/openai/codex.git
git fetch upstream --tags --prune
git fetch upstream --tags --prune main
git fetch origin --prune
git status --short --branch
```
If `git status` shows local changes or untracked files, resolve them before an
automated rebase.
automated rebuild.
## 2. Install the Codex release capabilities
@ -55,8 +62,8 @@ repo flow.
Safe local verification stops at event matching and runner gating. The test
suite confirms that a stored `upstream.release` event for `openai/codex`
selects both installed Codex release steps, and that the Code Mode step still
requires `CODEX_FLOWS_MODE=code-mode`. Do not fabricate a full
selects both installed release steps, and that the Code Mode step still requires
`CODEX_FLOWS_MODE=code-mode`. Do not fabricate a full
`openai/codex` release lifecycle just to exercise the flow.
You can run the same safe match check through the CLI:
@ -113,10 +120,10 @@ or `X-Patch-Admin-Token: <token>`.
Patch dispatches the generic event. The installed Codex release flow or
workspace owns the work that happens next:
- fetch upstream tags
- fetch upstream main and tags
- resolve the release tag
- rebase the maintained patch branch
- collect conflict context when the rebase stops
- rebuild `main` from the release tag plus ordered `patch/*` branches
- collect conflict context when a cherry-pick stops
- run the configured checks
- optionally push a candidate ref