feat: install codex release flows
This commit is contained in:
parent
27979cb621
commit
39e843fb29
12 changed files with 922 additions and 3 deletions
196
.codex/flows/openai-codex-bindings/exec/update-bindings.ts
Normal file
196
.codex/flows/openai-codex-bindings/exec/update-bindings.ts
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
type FlowContext = {
|
||||
flow: {
|
||||
config?: Record<string, unknown>;
|
||||
event: {
|
||||
id: string;
|
||||
type: string;
|
||||
payload: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type CommandResult = {
|
||||
label: string;
|
||||
cmd: string[];
|
||||
cwd: string;
|
||||
exitCode: number | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
const context = JSON.parse(await Bun.stdin.text()) as FlowContext;
|
||||
const config = context.flow.config ?? {};
|
||||
const repoRoot = process.cwd();
|
||||
const commands: CommandResult[] = [];
|
||||
|
||||
try {
|
||||
const tag = stringValue(context.flow.event.payload.tag, "payload.tag");
|
||||
const version = versionFromTag(tag);
|
||||
const packageName = stringConfig("package_name", "@peezy.tech/codex-flows");
|
||||
const generatedDir = path.resolve(repoRoot, stringConfig("generated_dir", "packages/codex-client/src/app-server/generated"));
|
||||
const packageJsonPath = path.resolve(repoRoot, stringConfig("package_json", "packages/codex-client/package.json"));
|
||||
|
||||
const published = await npmPackageExists(packageName, version);
|
||||
if (published && !enabled("force", false)) {
|
||||
finish("skipped", `${packageName}@${version} is already published.`, { version, tag });
|
||||
}
|
||||
|
||||
await requireCleanWorktree();
|
||||
await run("regenerate app-server TypeScript bindings", [
|
||||
"npx",
|
||||
"-y",
|
||||
`@openai/codex@${version}`,
|
||||
"app-server",
|
||||
"generate-ts",
|
||||
"--experimental",
|
||||
"--out",
|
||||
generatedDir,
|
||||
]);
|
||||
|
||||
await updatePackageVersion(packageJsonPath, version);
|
||||
await run("refresh Bun lockfile", ["bun", "install"]);
|
||||
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")]);
|
||||
await run("commit binding update", [
|
||||
"git",
|
||||
"commit",
|
||||
"-m",
|
||||
`flow: update codex-flows for openai codex ${version}`,
|
||||
]);
|
||||
}
|
||||
|
||||
if (enabled("push", false)) {
|
||||
await run("push jojo main", ["git", "push", "origin", "HEAD:main"]);
|
||||
}
|
||||
|
||||
if (enabled("publish", false)) {
|
||||
await run("push GitHub main", ["git", "push", "github", "HEAD:main"]);
|
||||
await run("trigger GitHub trusted publish", [
|
||||
"gh",
|
||||
"workflow",
|
||||
"run",
|
||||
stringConfig("github_publish_workflow", "publish-codex-flows.yml"),
|
||||
"--repo",
|
||||
stringConfig("github_repo", "peezy-tech/codex-flows"),
|
||||
"-f",
|
||||
`confirm_package=${packageName}`,
|
||||
]);
|
||||
}
|
||||
|
||||
finish("changed", `${packageName} regenerated for openai/codex ${tag}.`, {
|
||||
version,
|
||||
tag,
|
||||
committed: enabled("commit", true),
|
||||
pushed: enabled("push", false),
|
||||
published: enabled("publish", false),
|
||||
});
|
||||
} catch (error) {
|
||||
finish("failed", error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
|
||||
async function requireCleanWorktree(): Promise<void> {
|
||||
const status = await run("dirty worktree check", ["git", "status", "--porcelain=v1"]);
|
||||
if (status.stdout.trim()) {
|
||||
finish("blocked", "codex-flows checkout has local changes before the release update.", {
|
||||
dirtyStatus: status.stdout,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function npmPackageExists(packageName: string, version: string): Promise<boolean> {
|
||||
const result = await run("published package check", [
|
||||
"npm",
|
||||
"view",
|
||||
`${packageName}@${version}`,
|
||||
"version",
|
||||
"--json",
|
||||
], { allowFailure: true });
|
||||
return result.exitCode === 0 && result.stdout.includes(version);
|
||||
}
|
||||
|
||||
async function updatePackageVersion(packageJsonPath: string, version: string): Promise<void> {
|
||||
const parsed = JSON.parse(await readFile(packageJsonPath, "utf8")) as Record<string, unknown>;
|
||||
parsed.version = version;
|
||||
await writeFile(packageJsonPath, `${JSON.stringify(parsed, null, "\t")}\n`);
|
||||
}
|
||||
|
||||
async function run(
|
||||
label: string,
|
||||
cmd: string[],
|
||||
options: { allowFailure?: boolean; cwd?: string } = {},
|
||||
): Promise<CommandResult> {
|
||||
const child = Bun.spawn(cmd, {
|
||||
cwd: options.cwd ?? repoRoot,
|
||||
env: process.env,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
child.stdout.text(),
|
||||
child.stderr.text(),
|
||||
child.exited,
|
||||
]);
|
||||
const result = { label, cmd, cwd: options.cwd ?? repoRoot, exitCode, stdout, stderr };
|
||||
commands.push(result);
|
||||
if (exitCode !== 0 && !options.allowFailure) {
|
||||
throw new Error(`${label} failed with exit ${exitCode}:\n${stderr || stdout}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function finish(status: string, message: string, artifacts: Record<string, unknown> = {}): never {
|
||||
const trimmedCommands = commands.map((command) => ({
|
||||
...command,
|
||||
stdout: truncate(command.stdout),
|
||||
stderr: truncate(command.stderr),
|
||||
}));
|
||||
console.log(`FLOW_RESULT ${JSON.stringify({ status, message, artifacts: { ...artifacts, commands: trimmedCommands } })}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function enabled(name: string, fallback: boolean): boolean {
|
||||
const envName = `CODEX_FLOW_${name.toUpperCase()}`;
|
||||
const envValue = process.env[envName];
|
||||
if (envValue !== undefined) {
|
||||
return ["1", "true", "yes", "on"].includes(envValue.trim().toLowerCase());
|
||||
}
|
||||
const value = config[name];
|
||||
return typeof value === "boolean" ? value : fallback;
|
||||
}
|
||||
|
||||
function stringConfig(name: string, fallback: string): string {
|
||||
const value = config[name];
|
||||
return typeof value === "string" && value.trim() ? 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`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function versionFromTag(tag: string): string {
|
||||
const match = tag.match(/[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?/);
|
||||
if (!match) {
|
||||
throw new Error(`Could not infer semantic version from release tag ${tag}`);
|
||||
}
|
||||
return match[0];
|
||||
}
|
||||
|
||||
function truncate(value: string, max = 4000): string {
|
||||
return value.length <= max ? value : `${value.slice(0, max)}\n...[truncated ${value.length - max} chars]`;
|
||||
}
|
||||
24
.codex/flows/openai-codex-bindings/flow.toml
Normal file
24
.codex/flows/openai-codex-bindings/flow.toml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
name = "openai-codex-bindings"
|
||||
version = 1
|
||||
description = "Regenerate @peezy.tech/codex-flows bindings from a canonical openai/codex release."
|
||||
|
||||
[config]
|
||||
package_name = "@peezy.tech/codex-flows"
|
||||
generated_dir = "packages/codex-client/src/app-server/generated"
|
||||
package_json = "packages/codex-client/package.json"
|
||||
commit = true
|
||||
push = false
|
||||
publish = false
|
||||
github_repo = "peezy-tech/codex-flows"
|
||||
github_publish_workflow = "publish-codex-flows.yml"
|
||||
|
||||
[[steps]]
|
||||
name = "regenerate-bindings"
|
||||
runner = "bun"
|
||||
script = "exec/update-bindings.ts"
|
||||
cwd = "../.."
|
||||
timeout_ms = 1200000
|
||||
|
||||
[steps.trigger]
|
||||
type = "upstream.release"
|
||||
schema = "schemas/upstream-release.schema.json"
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"type": "object",
|
||||
"required": ["repo", "tag"],
|
||||
"properties": {
|
||||
"provider": { "type": "string" },
|
||||
"repo": { "type": "string", "enum": ["openai/codex"] },
|
||||
"tag": { "type": "string" },
|
||||
"url": { "type": "string" },
|
||||
"publishedAt": { "type": "string" }
|
||||
}
|
||||
}
|
||||
392
.codex/flows/peezy-codex-fork/exec/update-fork.code-mode.js
Normal file
392
.codex/flows/peezy-codex-fork/exec/update-fork.code-mode.js
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
const config = flow.config || {};
|
||||
const payload = flow.event.payload || {};
|
||||
const commands = [];
|
||||
|
||||
function q(value) {
|
||||
return "'" + String(value).replaceAll("'", "'\\''") + "'";
|
||||
}
|
||||
|
||||
function trim(value) {
|
||||
return String(value || "").trim();
|
||||
}
|
||||
|
||||
function truncate(value, max) {
|
||||
const textValue = String(value || "");
|
||||
if (textValue.length <= max) {
|
||||
return textValue;
|
||||
}
|
||||
return textValue.slice(0, max) + "\n...[truncated " + String(textValue.length - max) + " chars]";
|
||||
}
|
||||
|
||||
function outputOf(result) {
|
||||
if (typeof result?.output === "string") {
|
||||
return result.output;
|
||||
}
|
||||
return JSON.stringify(result ?? {});
|
||||
}
|
||||
|
||||
function exitCodeOf(result) {
|
||||
if (typeof result?.exit_code === "number") {
|
||||
return result.exit_code;
|
||||
}
|
||||
if (typeof result?.exitCode === "number") {
|
||||
return result.exitCode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function ok(result) {
|
||||
return result.exit_code === 0;
|
||||
}
|
||||
|
||||
function cfg(name, fallback) {
|
||||
const value = config[name];
|
||||
return typeof value === "string" && value.trim() ? value : fallback;
|
||||
}
|
||||
|
||||
function enabled(name, fallback) {
|
||||
const override = flowFlagOverrides[name];
|
||||
if (typeof override === "boolean") {
|
||||
return override;
|
||||
}
|
||||
const value = config[name];
|
||||
return typeof value === "boolean" ? value : fallback;
|
||||
}
|
||||
|
||||
function versionFromTag(tag) {
|
||||
const match = String(tag).match(/[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?/);
|
||||
return match ? match[0] : "";
|
||||
}
|
||||
|
||||
async function env(name) {
|
||||
if (!name) {
|
||||
return "";
|
||||
}
|
||||
const result = await tools.exec_command({
|
||||
cmd: "printf %s \"${" + name + ":-}\"",
|
||||
workdir: flow.root,
|
||||
yield_time_ms: 1000,
|
||||
max_output_tokens: 2000
|
||||
});
|
||||
return trim(outputOf(result));
|
||||
}
|
||||
|
||||
async function envFlag(name) {
|
||||
const value = (await env(name)).trim().toLowerCase();
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
return ["1", "true", "yes", "on"].includes(value);
|
||||
}
|
||||
|
||||
async function run(label, cmd, options = {}) {
|
||||
const workdir = options.workdir || codexRepo;
|
||||
text("\n### " + label + "\n$ " + cmd + "\n");
|
||||
const raw = await tools.exec_command({
|
||||
cmd,
|
||||
workdir,
|
||||
yield_time_ms: options.yield_time_ms || 1000,
|
||||
max_output_tokens: options.max_output_tokens || 12000
|
||||
});
|
||||
const result = {
|
||||
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;
|
||||
}
|
||||
|
||||
function finish(status, message, artifacts = {}) {
|
||||
result({
|
||||
status,
|
||||
message,
|
||||
artifacts: {
|
||||
releaseTag,
|
||||
version,
|
||||
codexRepo,
|
||||
targetBranch,
|
||||
commands,
|
||||
...artifacts
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function collectRebaseContext(rebaseOutput, beforeSha) {
|
||||
const status = await run("rebase 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 });
|
||||
return {
|
||||
beforeSha,
|
||||
rebaseOutput,
|
||||
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."
|
||||
};
|
||||
}
|
||||
|
||||
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")
|
||||
};
|
||||
const packageName = cfg("package_name", "@peezy.tech/codex");
|
||||
const targetBranch = (await env(cfg("target_branch_env", ""))) || cfg("target_branch", "main");
|
||||
const upstreamRemote = cfg("upstream_remote", "upstream");
|
||||
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) {
|
||||
finish("failed", "Release payload is missing tag.");
|
||||
}
|
||||
if (!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.");
|
||||
}
|
||||
|
||||
text([
|
||||
"Peezy Codex fork update flow",
|
||||
"",
|
||||
"Release: " + releaseTag,
|
||||
"Version: " + version,
|
||||
"Target branch: " + targetBranch,
|
||||
"Codex repo: " + codexRepo,
|
||||
"Upstream remote: " + upstreamRemote + " -> " + upstreamRepoUrl,
|
||||
"Cargo target dir: " + cargoTargetDir
|
||||
].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.");
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
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.", {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
});
|
||||
30
.codex/flows/peezy-codex-fork/flow.toml
Normal file
30
.codex/flows/peezy-codex-fork/flow.toml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
name = "peezy-codex-fork"
|
||||
version = 1
|
||||
description = "Rebase the Peezy Codex fork patch stack onto a canonical openai/codex release."
|
||||
|
||||
[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"
|
||||
upstream_remote = "upstream"
|
||||
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
|
||||
push = false
|
||||
publish = false
|
||||
|
||||
[guidance]
|
||||
skills = ["jojo-development-flow"]
|
||||
|
||||
[[steps]]
|
||||
name = "rebase-patch-stack"
|
||||
runner = "code-mode"
|
||||
script = "exec/update-fork.code-mode.js"
|
||||
timeout_ms = 3600000
|
||||
|
||||
[steps.trigger]
|
||||
type = "upstream.release"
|
||||
schema = "schemas/upstream-release.schema.json"
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"type": "object",
|
||||
"required": ["repo", "tag"],
|
||||
"properties": {
|
||||
"provider": { "type": "string" },
|
||||
"repo": { "type": "string", "enum": ["openai/codex"] },
|
||||
"tag": { "type": "string" },
|
||||
"url": { "type": "string" },
|
||||
"publishedAt": { "type": "string" }
|
||||
}
|
||||
}
|
||||
31
.codex/pack-lock.json
Normal file
31
.codex/pack-lock.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"version": 1,
|
||||
"items": [
|
||||
{
|
||||
"name": "openai-codex-bindings",
|
||||
"kind": "flow",
|
||||
"source": {
|
||||
"input": "../codex-flows",
|
||||
"type": "local",
|
||||
"commit": "5be9b571578409a31af4693caac8c949c06fe388"
|
||||
},
|
||||
"sourcePath": "flows/openai-codex-bindings",
|
||||
"destinationPath": ".codex/flows/openai-codex-bindings",
|
||||
"contentHash": "sha256:5fcb04e9525e94c24ca319fcefc99fd05ed973e9c640a5e27629e263f13ca968",
|
||||
"installedAt": "2026-05-16T19:56:06.687Z"
|
||||
},
|
||||
{
|
||||
"name": "peezy-codex-fork",
|
||||
"kind": "flow",
|
||||
"source": {
|
||||
"input": "../codex-flows",
|
||||
"type": "local",
|
||||
"commit": "5be9b571578409a31af4693caac8c949c06fe388"
|
||||
},
|
||||
"sourcePath": "flows/peezy-codex-fork",
|
||||
"destinationPath": ".codex/flows/peezy-codex-fork",
|
||||
"contentHash": "sha256:521e7766cd1888fd54fc0c10a6d2a3a7f235d9a7c8aa2577b4f4681e3a8fdabe",
|
||||
"installedAt": "2026-05-16T19:56:06.687Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
147
apps/patch/test/flow.test.ts
Normal file
147
apps/patch/test/flow.test.ts
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import { describe, expect, test } from "bun:test";
|
||||
import type { FlowRunView } from "@peezy.tech/flow-runtime/client";
|
||||
import {
|
||||
maintenanceAttemptForWorkspaceDispatch,
|
||||
maintenanceAttemptWithWorkspaceRuns,
|
||||
patchUpstreamReleaseEvent,
|
||||
} from "../src/flow";
|
||||
import type {
|
||||
CandidateRefRecord,
|
||||
FlowDispatchRecord,
|
||||
MaintenanceAttemptRecord,
|
||||
} from "../src/types";
|
||||
|
||||
describe("maintenance attempt sync", () => {
|
||||
test("aggregates multi-run fanout statuses and candidate refs", () => {
|
||||
const attempt = baseAttempt();
|
||||
const next = maintenanceAttemptWithWorkspaceRuns(attempt, [
|
||||
flowRun("run-completed", "completed", [candidate("refs/heads/a", "aaa")]),
|
||||
flowRun("run-changed", "changed", [candidate("refs/heads/b", "bbb")]),
|
||||
], "2026-05-16T00:10:00.000Z");
|
||||
|
||||
expect(next.status).toBe("changed");
|
||||
expect(next.workspaceRunIds).toEqual(["run-completed", "run-changed"]);
|
||||
expect(next.workspaceRunStatuses).toEqual({
|
||||
"run-completed": "completed",
|
||||
"run-changed": "changed",
|
||||
});
|
||||
expect(next.candidateRefs).toMatchObject([
|
||||
{ ref: "refs/heads/a", sha: "aaa" },
|
||||
{ ref: "refs/heads/b", sha: "bbb" },
|
||||
]);
|
||||
expect(next.completedAt).toBe("2026-05-16T00:10:00.000Z");
|
||||
});
|
||||
|
||||
test("uses failure precedence across partial fanout", () => {
|
||||
expect(statusFor(["completed", "skipped"])).toBe("completed");
|
||||
expect(statusFor(["skipped", "skipped"])).toBe("skipped");
|
||||
expect(statusFor(["completed", "changed"])).toBe("changed");
|
||||
expect(statusFor(["changed", "failed"])).toBe("failed");
|
||||
expect(statusFor(["failed", "blocked"])).toBe("blocked");
|
||||
expect(statusFor(["blocked", "needs_intervention"])).toBe("needs_intervention");
|
||||
});
|
||||
|
||||
test("preserves successful candidates when another run fails", () => {
|
||||
const next = maintenanceAttemptWithWorkspaceRuns(baseAttempt(), [
|
||||
flowRun("run-ok", "completed", [candidate("refs/heads/candidate", "abc")]),
|
||||
flowRun("run-failed", "failed", [], "release verification failed"),
|
||||
]);
|
||||
|
||||
expect(next.status).toBe("failed");
|
||||
expect(next.error).toBe("release verification failed");
|
||||
expect(next.candidateRefs).toMatchObject([
|
||||
{ ref: "refs/heads/candidate", sha: "abc" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("records replay attempts with workspace run results", () => {
|
||||
const event = patchUpstreamReleaseEvent({
|
||||
repo: "openai/codex",
|
||||
tag: "rust-v0.130.0",
|
||||
receivedAt: "2026-05-16T00:00:00.000Z",
|
||||
});
|
||||
const record: FlowDispatchRecord = {
|
||||
eventId: event.id,
|
||||
eventType: event.type,
|
||||
operation: "replay",
|
||||
target: "workspace-backend",
|
||||
transport: "workspace-ws",
|
||||
workspaceBackendUrl: "ws://127.0.0.1:3586",
|
||||
status: "dispatched",
|
||||
runIds: ["run-a", "run-b"],
|
||||
matched: 2,
|
||||
createdAt: "2026-05-16T00:05:00.000Z",
|
||||
};
|
||||
|
||||
const attempt = maintenanceAttemptForWorkspaceDispatch(event, record, [
|
||||
flowRun("run-a", "completed"),
|
||||
flowRun("run-b", "changed", [candidate("refs/heads/codex-candidate", "def")]),
|
||||
]);
|
||||
|
||||
expect(attempt.id).toBe(`${event.id}:replay:${record.createdAt}`);
|
||||
expect(attempt.operation).toBe("replay");
|
||||
expect(attempt.status).toBe("changed");
|
||||
expect(attempt.upstreamRepo).toBe("openai/codex");
|
||||
expect(attempt.upstreamTag).toBe("rust-v0.130.0");
|
||||
expect(attempt.workspaceRunIds).toEqual(["run-a", "run-b"]);
|
||||
expect(attempt.candidateRefs).toMatchObject([
|
||||
{ ref: "refs/heads/codex-candidate", sha: "def" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
function statusFor(statuses: string[]): string {
|
||||
return maintenanceAttemptWithWorkspaceRuns(
|
||||
baseAttempt(),
|
||||
statuses.map((status, index) => flowRun(`run-${index}`, status)),
|
||||
).status;
|
||||
}
|
||||
|
||||
function baseAttempt(): MaintenanceAttemptRecord {
|
||||
return {
|
||||
id: "attempt-1",
|
||||
eventId: "event-1",
|
||||
eventType: "upstream.release",
|
||||
operation: "dispatch",
|
||||
status: "started",
|
||||
upstreamRepo: "openai/codex",
|
||||
upstreamTag: "rust-v0.130.0",
|
||||
workspaceRunIds: [],
|
||||
candidateRefs: [],
|
||||
createdAt: "2026-05-16T00:00:00.000Z",
|
||||
updatedAt: "2026-05-16T00:00:00.000Z",
|
||||
};
|
||||
}
|
||||
|
||||
function flowRun(
|
||||
id: string,
|
||||
status: string,
|
||||
candidateRefs: CandidateRefRecord[] = [],
|
||||
message = `${id} ${status}`,
|
||||
): FlowRunView {
|
||||
return {
|
||||
id,
|
||||
eventId: "event-1",
|
||||
flowName: "test-flow",
|
||||
stepName: id,
|
||||
status,
|
||||
effectiveStatus: status,
|
||||
completedAt: "2026-05-16T00:10:00.000Z",
|
||||
resultPayload: {
|
||||
status,
|
||||
message,
|
||||
artifacts: { candidateRefs },
|
||||
},
|
||||
} as FlowRunView;
|
||||
}
|
||||
|
||||
function candidate(ref: string, sha: string): CandidateRefRecord {
|
||||
return {
|
||||
kind: "branch",
|
||||
repo: "peezy-tech/codex",
|
||||
remote: "local",
|
||||
ref,
|
||||
sha,
|
||||
pushed: false,
|
||||
};
|
||||
}
|
||||
|
|
@ -53,6 +53,41 @@ describe("patch.moi harness flow", () => {
|
|||
expect(afterHead).toBe(beforeHead);
|
||||
expect(await git(["status", "--porcelain=v1"])).toBe("");
|
||||
});
|
||||
|
||||
test("matches installed Codex release flows without executing release work", async () => {
|
||||
const flows = await discoverFlows({ cwd: workspaceRoot });
|
||||
const matches = await matchingSteps(flows, {
|
||||
id: "patch:upstream.release:openai/codex:rust-v0.130.0",
|
||||
type: "upstream.release",
|
||||
source: "patch",
|
||||
receivedAt: "2026-05-16T00:00:00.000Z",
|
||||
payload: { repo: "openai/codex", tag: "rust-v0.130.0" },
|
||||
});
|
||||
|
||||
expect(matches.map(({ flow, step }) => `${flow.manifest.name}/${step.name}`)).toEqual([
|
||||
"openai-codex-bindings/regenerate-bindings",
|
||||
"peezy-codex-fork/rebase-patch-stack",
|
||||
]);
|
||||
|
||||
const codeModeMatch = matches.find((entry) => entry.flow.manifest.name === "peezy-codex-fork");
|
||||
expect(codeModeMatch?.step.runner).toBe("code-mode");
|
||||
if (!codeModeMatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
await expect(runFlowStep({
|
||||
flow: codeModeMatch.flow,
|
||||
step: codeModeMatch.step,
|
||||
event: {
|
||||
id: "patch:upstream.release:openai/codex:rust-v0.130.0",
|
||||
type: "upstream.release",
|
||||
source: "patch",
|
||||
receivedAt: "2026-05-16T00:00:00.000Z",
|
||||
payload: { repo: "openai/codex", tag: "rust-v0.130.0" },
|
||||
},
|
||||
env: {},
|
||||
})).rejects.toThrow("requires CODEX_FLOWS_ENABLE_CODE_MODE=1");
|
||||
});
|
||||
});
|
||||
|
||||
async function git(args: string[]): Promise<string> {
|
||||
|
|
|
|||
|
|
@ -104,6 +104,8 @@ DATA_DIR=./data FEED_SOURCES_PATH=./feed-sources.json bun run --filter @peezy.te
|
|||
- `apps/patch`: Patch service, feed poller, JSONL store, admin API, Discord
|
||||
output, and workspace backend adapter.
|
||||
- `flows/patch-moi-harness`: executable maintenance flow for the harness repos.
|
||||
- `.codex/flows`: installed external flow capabilities, currently the Codex
|
||||
release maintenance flows from the neighboring `../codex-flows` pack.
|
||||
- `harness`: upstream and maintained fork repositories used for rehearsal.
|
||||
- `.codex/workspace.toml`: optional repo-native workspace automation config.
|
||||
- `docs`: this Tome documentation site.
|
||||
|
|
|
|||
|
|
@ -44,6 +44,21 @@ bun run workspace:run:harness
|
|||
Those commands are operator automation around the repo. They do not replace the
|
||||
Patch service package or its `DATA_DIR` state.
|
||||
|
||||
## Installed Flow Capabilities
|
||||
|
||||
External flow capabilities are installed under `.codex/flows` and tracked in
|
||||
`.codex/pack-lock.json`. The current install brings in the Codex release
|
||||
maintenance flows from the sibling `../codex-flows` repository:
|
||||
|
||||
```bash
|
||||
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`.
|
||||
|
||||
## Related Runtime Packages
|
||||
|
||||
These published packages define the current patch.moi integration baseline:
|
||||
|
|
|
|||
|
|
@ -34,7 +34,32 @@ git status --short --branch
|
|||
If `git status` shows local changes or untracked files, resolve them before an
|
||||
automated rebase.
|
||||
|
||||
## 2. Point Patch at a workspace backend
|
||||
## 2. Install the Codex release capabilities
|
||||
|
||||
The Codex release maintenance capabilities are installed from the neighboring
|
||||
`../codex-flows` pack into `.codex/flows`:
|
||||
|
||||
```bash
|
||||
codex-flows pack add ../codex-flows \
|
||||
--include openai-codex-bindings \
|
||||
--include peezy-codex-fork \
|
||||
--apply
|
||||
codex-flows pack doctor --json
|
||||
```
|
||||
|
||||
The current local install pins `openai-codex-bindings` and `peezy-codex-fork`
|
||||
in `.codex/pack-lock.json`. `@peezy.tech/flow-runtime@0.4.0` discovers
|
||||
installed `.codex/flows/*` before source-owned `flows/*`, so the installed
|
||||
Codex capabilities are visible to patch.moi while the harness remains a
|
||||
source-owned 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_ENABLE_CODE_MODE=1`. Do not fabricate a full
|
||||
`openai/codex` release lifecycle just to exercise the flow.
|
||||
|
||||
## 3. Point Patch at a workspace backend
|
||||
|
||||
```bash
|
||||
PATCH_WORKSPACE_BACKEND_URL=http://127.0.0.1:3586 \
|
||||
|
|
@ -52,7 +77,7 @@ workspace flow capability. `PATCH_FLOW_BACKEND_URL` and
|
|||
Leave `PATCH_WORKSPACE_BACKEND_URL` unset only when you intentionally want local
|
||||
flow execution from the Patch process working directory.
|
||||
|
||||
## 3. Inspect the stored event
|
||||
## 4. Inspect the stored event
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:3000/flow-events
|
||||
|
|
@ -61,7 +86,7 @@ curl http://127.0.0.1:3000/flow-events
|
|||
When `PATCH_ADMIN_TOKEN` is set, include either `Authorization: Bearer <token>`
|
||||
or `X-Patch-Admin-Token: <token>`.
|
||||
|
||||
## 4. Keep completion workspace-owned, state app-owned
|
||||
## 5. Keep completion workspace-owned, state app-owned
|
||||
|
||||
Patch dispatches the generic event. The installed Codex release flow or
|
||||
workspace owns the work that happens next:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue