Route downstream releases to codex-flows fork
All checks were successful
check / check (push) Successful in 36s
All checks were successful
check / check (push) Successful in 36s
This commit is contained in:
parent
c14470a8f4
commit
3313b54a10
16 changed files with 802 additions and 19 deletions
395
.codex/flows/peezy-codex-flows-fork/exec/release-fork.ts
Normal file
395
.codex/flows/peezy-codex-flows-fork/exec/release-fork.ts
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
import { existsSync } from "node:fs";
|
||||
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
type FlowContext = {
|
||||
flow: {
|
||||
config?: Record<string, unknown>;
|
||||
event: {
|
||||
id: string;
|
||||
payload?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type CommandResult = {
|
||||
code: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
const context = JSON.parse(await Bun.stdin.text()) as FlowContext;
|
||||
const config = context.flow.config ?? {};
|
||||
const payload = context.flow.event.payload ?? {};
|
||||
|
||||
function finish(value: Record<string, unknown>): never {
|
||||
process.stdout.write(`FLOW_RESULT ${JSON.stringify(value)}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
const sourcePackage = stringValue(payload.packageName);
|
||||
const sourceVersion = stringValue(payload.version);
|
||||
const packageName = stringConfig("package_name", "@peezy.tech/codex-flows");
|
||||
const codexPackageName = stringConfig("codex_package_name", "@peezy.tech/codex");
|
||||
|
||||
if (!sourcePackage || !sourceVersion) {
|
||||
finish({ status: "failed", message: "downstream.release requires packageName and version." });
|
||||
}
|
||||
if (sourcePackage !== packageName && sourcePackage !== codexPackageName) {
|
||||
finish({ status: "skipped", message: `Ignoring downstream release for ${sourcePackage}.` });
|
||||
}
|
||||
|
||||
const repoRoot = path.resolve(envConfig(stringConfig("codex_flows_repo_env", "")) || stringConfig("codex_flows_repo", process.cwd()));
|
||||
const sourceBranch = stringConfig("source_branch", "main");
|
||||
const forkBranch = stringConfig("fork_branch", "fork");
|
||||
const worktreeDir = path.resolve(repoRoot, stringConfig("worktree_dir", ".codex/flow-artifacts/codex-flows-fork-worktree"));
|
||||
const artifactDir = path.resolve(repoRoot, stringConfig("artifact_dir", ".codex/flow-artifacts/codex-flows-fork-release"));
|
||||
const fetchEnabled = enabled("fetch", true);
|
||||
const commitEnabled = enabled("commit", true);
|
||||
const pushEnabled = enabled("push", false);
|
||||
const publishEnabled = enabled("publish", false);
|
||||
const linkLocalPackage = enabled("link_local_package", false);
|
||||
|
||||
await requireCleanRepo(repoRoot);
|
||||
if (fetchEnabled) {
|
||||
await runChecked("fetch source branch", ["git", "fetch", "origin", sourceBranch, "--prune"], repoRoot);
|
||||
}
|
||||
|
||||
const baseVersion = sourcePackage === packageName
|
||||
? sourceVersion
|
||||
: await readPackageVersion(path.join(repoRoot, "packages/codex-client/package.json"));
|
||||
const codexVersion = sourcePackage === codexPackageName
|
||||
? sourceVersion
|
||||
: envConfig(stringConfig("codex_version_env", "")) || await npmPackageVersion(codexPackageName);
|
||||
const forkVersion = forkPackageVersion(baseVersion, codexVersion);
|
||||
const baseSha = (await runChecked("resolve source branch", ["git", "rev-parse", "--verify", `${sourceBranch}^{commit}`], repoRoot)).stdout.trim();
|
||||
|
||||
await prepareWorktree(repoRoot, worktreeDir, forkBranch, sourceBranch);
|
||||
await applyForkOverlay({
|
||||
worktreeDir,
|
||||
packageName,
|
||||
codexPackageName,
|
||||
codexVersion,
|
||||
forkVersion,
|
||||
});
|
||||
|
||||
await runChecked("install fork package dependency", ["bun", "install"], worktreeDir);
|
||||
await runChecked("fork release check", ["bun", "run", "--filter", packageName, "release:check"], worktreeDir);
|
||||
|
||||
await rm(artifactDir, { recursive: true, force: true });
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
const pack = await runChecked(
|
||||
"pack fork release",
|
||||
["npm", "pack", "--pack-destination", artifactDir],
|
||||
path.join(worktreeDir, "packages/codex-client"),
|
||||
);
|
||||
const tarball = pack.stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
|
||||
const tarballPath = tarball ? path.join(artifactDir, tarball) : undefined;
|
||||
|
||||
if (linkLocalPackage) {
|
||||
await runChecked("link fork release package", ["bun", "pm", "link"], path.join(worktreeDir, "packages/codex-client"));
|
||||
}
|
||||
|
||||
const status = await runChecked("read fork diff", ["git", "status", "--porcelain"], worktreeDir);
|
||||
let commitSha = "";
|
||||
if (commitEnabled && status.stdout.trim()) {
|
||||
await runChecked("stage fork release changes", [
|
||||
"git",
|
||||
"add",
|
||||
"--",
|
||||
"bun.lock",
|
||||
"packages/codex-client/package.json",
|
||||
"packages/codex-client/src/mode.ts",
|
||||
"packages/codex-client/src/app-server/stdio-transport.ts",
|
||||
"packages/codex-client/test/stdio-transport.test.ts",
|
||||
], worktreeDir);
|
||||
await runChecked("commit fork release", [
|
||||
"git",
|
||||
"commit",
|
||||
"-m",
|
||||
`release: codex-flows fork ${forkVersion}`,
|
||||
], worktreeDir);
|
||||
commitSha = (await runChecked("read fork commit", ["git", "rev-parse", "HEAD"], worktreeDir)).stdout.trim();
|
||||
} else {
|
||||
commitSha = (await runChecked("read fork head", ["git", "rev-parse", "HEAD"], worktreeDir)).stdout.trim();
|
||||
}
|
||||
|
||||
let pushed = false;
|
||||
if (pushEnabled) {
|
||||
await runChecked("push fork branch", ["git", "push", "origin", `HEAD:refs/heads/${forkBranch}`, "--force-with-lease"], worktreeDir);
|
||||
pushed = true;
|
||||
}
|
||||
|
||||
let published = false;
|
||||
if (publishEnabled && tarballPath) {
|
||||
await runChecked("publish fork package", [
|
||||
"npm",
|
||||
"publish",
|
||||
tarballPath,
|
||||
"--access",
|
||||
"public",
|
||||
"--tag",
|
||||
stringConfig("fork_dist_tag", "fork"),
|
||||
], worktreeDir);
|
||||
published = true;
|
||||
}
|
||||
|
||||
finish({
|
||||
status: "changed",
|
||||
message: `Prepared ${packageName} fork ${forkVersion} from ${sourcePackage}@${sourceVersion}.`,
|
||||
artifacts: {
|
||||
sourcePackage,
|
||||
sourceVersion,
|
||||
packageName,
|
||||
baseVersion,
|
||||
codexPackageName,
|
||||
codexVersion,
|
||||
forkVersion,
|
||||
sourceBranch,
|
||||
forkBranch,
|
||||
baseSha,
|
||||
commitSha,
|
||||
worktreeDir,
|
||||
tarballPath,
|
||||
linked: linkLocalPackage,
|
||||
pushed,
|
||||
published,
|
||||
candidateRefs: [{
|
||||
kind: "branch",
|
||||
repo: "peezy-tech/codex-flows",
|
||||
ref: `refs/heads/${forkBranch}`,
|
||||
sha: commitSha,
|
||||
pushed,
|
||||
}],
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
finish({
|
||||
status: "failed",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
|
||||
async function requireCleanRepo(repoRoot: string): Promise<void> {
|
||||
const status = await runChecked("read repository status", ["git", "status", "--porcelain"], repoRoot);
|
||||
const relevant = status.stdout
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => line.trim())
|
||||
.filter((line) => !line.includes(".codex/flow-artifacts/"));
|
||||
if (relevant.length > 0) {
|
||||
finish({
|
||||
status: "blocked",
|
||||
message: "codex-flows checkout has local changes before fork release preparation.",
|
||||
artifacts: { status: relevant.join("\n") },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareWorktree(
|
||||
repoRoot: string,
|
||||
worktreeDir: string,
|
||||
forkBranch: string,
|
||||
sourceBranch: string,
|
||||
): Promise<void> {
|
||||
if (existsSync(worktreeDir)) {
|
||||
await run("remove old fork worktree", ["git", "worktree", "remove", "--force", worktreeDir], repoRoot);
|
||||
await rm(worktreeDir, { recursive: true, force: true });
|
||||
}
|
||||
await run("prune worktrees", ["git", "worktree", "prune"], repoRoot);
|
||||
await runChecked("create fork worktree", ["git", "worktree", "add", "--force", "-B", forkBranch, worktreeDir, sourceBranch], repoRoot);
|
||||
}
|
||||
|
||||
async function applyForkOverlay(input: {
|
||||
worktreeDir: string;
|
||||
packageName: string;
|
||||
codexPackageName: string;
|
||||
codexVersion: string;
|
||||
forkVersion: string;
|
||||
}): Promise<void> {
|
||||
const packageJsonPath = path.join(input.worktreeDir, "packages/codex-client/package.json");
|
||||
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8")) as Record<string, unknown>;
|
||||
packageJson.version = input.forkVersion;
|
||||
packageJson.dependencies = sortRecord({
|
||||
...(recordValue(packageJson.dependencies)),
|
||||
[input.codexPackageName]: input.codexVersion,
|
||||
});
|
||||
await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}\n`, "utf8");
|
||||
|
||||
const modePath = path.join(input.worktreeDir, "packages/codex-client/src/mode.ts");
|
||||
let modeText = await readFile(modePath, "utf8");
|
||||
if (!modeText.includes("CODEX_FLOWS_FORK_DEFAULT_CODE_MODE")) {
|
||||
modeText = modeText.replace(
|
||||
`export const DEFAULT_CODE_MODE_CODEX_PACKAGE = "${input.codexPackageName}";\n`,
|
||||
`export const DEFAULT_CODE_MODE_CODEX_PACKAGE = "${input.codexPackageName}";\nexport const CODEX_FLOWS_FORK_DEFAULT_CODE_MODE = true;\n`,
|
||||
);
|
||||
}
|
||||
modeText = modeText.replace(
|
||||
"return booleanEnv(env.CODEX_FLOWS_ENABLE_CODE_MODE) || codexFlowsMode(env) === CODEX_FLOWS_CODE_MODE;",
|
||||
[
|
||||
"if (booleanEnv(env.CODEX_FLOWS_DISABLE_CODE_MODE)) {",
|
||||
"\t\treturn false;",
|
||||
"\t}",
|
||||
"\treturn CODEX_FLOWS_FORK_DEFAULT_CODE_MODE || booleanEnv(env.CODEX_FLOWS_ENABLE_CODE_MODE) || codexFlowsMode(env) === CODEX_FLOWS_CODE_MODE;",
|
||||
].join("\n\t"),
|
||||
);
|
||||
await writeFile(modePath, modeText, "utf8");
|
||||
|
||||
const transportPath = path.join(input.worktreeDir, "packages/codex-client/src/app-server/stdio-transport.ts");
|
||||
let transportText = await readFile(transportPath, "utf8");
|
||||
transportText = transportText.replace(
|
||||
"return { command: DEFAULT_CODEX_COMMAND, args };",
|
||||
[
|
||||
"return {",
|
||||
"\t\tcommand: env.CODEX_APP_SERVER_BUNX_COMMAND?.trim() || \"bunx\",",
|
||||
"\t\targs: [DEFAULT_CODE_MODE_CODEX_PACKAGE, ...args],",
|
||||
"\t};",
|
||||
].join("\n"),
|
||||
);
|
||||
await writeFile(transportPath, transportText, "utf8");
|
||||
|
||||
const testPath = path.join(input.worktreeDir, "packages/codex-client/test/stdio-transport.test.ts");
|
||||
let testText = await readFile(testPath, "utf8");
|
||||
testText = testText.replace(
|
||||
[
|
||||
"expect(resolveCodexStdioCommand({}, {})).toEqual({",
|
||||
"\t\tcommand: \"codex\",",
|
||||
"\t\targs: [\"app-server\", \"--listen\", \"stdio://\", \"--enable\", \"apps\", \"--enable\", \"hooks\"],",
|
||||
"\t});",
|
||||
].join("\n\t"),
|
||||
[
|
||||
"expect(resolveCodexStdioCommand({}, {})).toEqual({",
|
||||
"\t\tcommand: \"bunx\",",
|
||||
"\t\targs: [",
|
||||
"\t\t\tDEFAULT_CODEX_NPM_PACKAGE,",
|
||||
"\t\t\t\"app-server\",",
|
||||
"\t\t\t\"--listen\",",
|
||||
"\t\t\t\"stdio://\",",
|
||||
"\t\t\t\"--enable\",",
|
||||
"\t\t\t\"apps\",",
|
||||
"\t\t\t\"--enable\",",
|
||||
"\t\t\t\"hooks\",",
|
||||
"\t\t],",
|
||||
"\t});",
|
||||
].join("\n\t"),
|
||||
);
|
||||
testText = testText.replace(
|
||||
[
|
||||
"expect(resolveCodexStdioCommand({}, { CODEX_FLOWS_ENABLE_CODE_MODE: \"1\" })).toEqual({",
|
||||
"\t\tcommand: \"codex\",",
|
||||
"\t\targs: [\"app-server\", \"--listen\", \"stdio://\", \"--enable\", \"apps\", \"--enable\", \"hooks\"],",
|
||||
"\t});",
|
||||
].join("\n\t"),
|
||||
[
|
||||
"expect(resolveCodexStdioCommand({}, { CODEX_FLOWS_ENABLE_CODE_MODE: \"1\" })).toEqual({",
|
||||
"\t\tcommand: \"bunx\",",
|
||||
"\t\targs: [",
|
||||
"\t\t\tDEFAULT_CODEX_NPM_PACKAGE,",
|
||||
"\t\t\t\"app-server\",",
|
||||
"\t\t\t\"--listen\",",
|
||||
"\t\t\t\"stdio://\",",
|
||||
"\t\t\t\"--enable\",",
|
||||
"\t\t\t\"apps\",",
|
||||
"\t\t\t\"--enable\",",
|
||||
"\t\t\t\"hooks\",",
|
||||
"\t\t],",
|
||||
"\t});",
|
||||
].join("\n\t"),
|
||||
);
|
||||
await writeFile(testPath, testText, "utf8");
|
||||
}
|
||||
|
||||
function forkPackageVersion(baseVersion: string, codexVersion: string): string {
|
||||
const prefix = sanitizePrerelease(stringConfig("fork_version_prefix", "peezy"));
|
||||
const codex = sanitizePrerelease(codexVersion);
|
||||
return baseVersion.includes("-")
|
||||
? `${baseVersion}.${prefix}.${codex}`
|
||||
: `${baseVersion}-${prefix}.${codex}`;
|
||||
}
|
||||
|
||||
function sanitizePrerelease(value: string): string {
|
||||
return value
|
||||
.replace(/^v/, "")
|
||||
.replace(/[^0-9A-Za-z]+/g, ".")
|
||||
.split(".")
|
||||
.filter(Boolean)
|
||||
.join(".") || "0";
|
||||
}
|
||||
|
||||
async function readPackageVersion(packageJsonPath: string): Promise<string> {
|
||||
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8")) as { version?: string };
|
||||
if (!packageJson.version) {
|
||||
throw new Error(`Could not read package version from ${packageJsonPath}`);
|
||||
}
|
||||
return packageJson.version;
|
||||
}
|
||||
|
||||
async function npmPackageVersion(packageName: string): Promise<string> {
|
||||
const result = await runChecked("read latest Codex fork package version", ["npm", "view", packageName, "version", "--json"], process.cwd());
|
||||
return JSON.parse(result.stdout) as string;
|
||||
}
|
||||
|
||||
async function runChecked(label: string, command: string[], cwd: string): Promise<CommandResult> {
|
||||
const result = await run(label, command, cwd);
|
||||
if (result.code !== 0) {
|
||||
throw new Error(`${label} failed with exit ${result.code}:\n${result.stderr || result.stdout}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function run(label: string, command: string[], cwd: string): Promise<CommandResult> {
|
||||
process.stderr.write(`+ ${label}: ${command.join(" ")}\n`);
|
||||
const proc = Bun.spawn(command, {
|
||||
cwd,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout, stderr, code] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
if (stdout) process.stderr.write(stdout);
|
||||
if (stderr) process.stderr.write(stderr);
|
||||
return { code, stdout, stderr };
|
||||
}
|
||||
|
||||
function enabled(name: string, fallback: boolean): boolean {
|
||||
const envName = `CODEX_FLOW_${name.toUpperCase()}`;
|
||||
const envValue = process.env[envName];
|
||||
if (envValue !== undefined) {
|
||||
return booleanValue(envValue);
|
||||
}
|
||||
const value = config[name];
|
||||
if (typeof value === "boolean") return value;
|
||||
if (typeof value === "string") return booleanValue(value);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function stringConfig(name: string, fallback: string): string {
|
||||
const value = config[name];
|
||||
return typeof value === "string" && value.trim() ? value : fallback;
|
||||
}
|
||||
|
||||
function envConfig(name: string): string | undefined {
|
||||
return name ? process.env[name]?.trim() || undefined : undefined;
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value : undefined;
|
||||
}
|
||||
|
||||
function recordValue(value: unknown): Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: {};
|
||||
}
|
||||
|
||||
function sortRecord(value: Record<string, unknown>): Record<string, unknown> {
|
||||
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)));
|
||||
}
|
||||
|
||||
function booleanValue(value: string): boolean {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
||||
}
|
||||
33
.codex/flows/peezy-codex-flows-fork/flow.toml
Normal file
33
.codex/flows/peezy-codex-flows-fork/flow.toml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
name = "peezy-codex-flows-fork"
|
||||
version = 1
|
||||
description = "Build a local fork release of codex-flows when Peezy Codex or codex-flows releases."
|
||||
|
||||
[config]
|
||||
package_name = "@peezy.tech/codex-flows"
|
||||
codex_package_name = "@peezy.tech/codex"
|
||||
codex_version_env = "PEEZY_CODEX_VERSION"
|
||||
codex_flows_repo_env = "PEEZY_CODEX_FLOWS_REPO"
|
||||
codex_flows_repo = "/home/peezy/meta-workspace/codex-flows"
|
||||
source_branch = "main"
|
||||
fork_branch = "fork"
|
||||
fork_dist_tag = "fork"
|
||||
fork_version_prefix = "peezy"
|
||||
worktree_dir = ".codex/flow-artifacts/codex-flows-fork-worktree"
|
||||
artifact_dir = ".codex/flow-artifacts/codex-flows-fork-release"
|
||||
commit = true
|
||||
push = false
|
||||
publish = false
|
||||
link_local_package = false
|
||||
|
||||
[guidance]
|
||||
skills = ["jojo-development-flow", "bun-flow-author"]
|
||||
|
||||
[[steps]]
|
||||
name = "release-fork"
|
||||
runner = "bun"
|
||||
script = "exec/release-fork.ts"
|
||||
timeout_ms = 1200000
|
||||
|
||||
[steps.trigger]
|
||||
type = "downstream.release"
|
||||
schema = "schemas/downstream-release.schema.json"
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"type": "object",
|
||||
"required": ["packageName", "version"],
|
||||
"properties": {
|
||||
"packageName": {
|
||||
"type": "string",
|
||||
"enum": ["@peezy.tech/codex", "@peezy.tech/codex-flows"]
|
||||
},
|
||||
"version": { "type": "string" },
|
||||
"tag": { "type": "string" },
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"enum": ["peezy-tech/codex", "peezy-tech/codex-flows"]
|
||||
},
|
||||
"provider": { "type": "string" },
|
||||
"sourceId": { "type": "string" },
|
||||
"entryId": { "type": "string" },
|
||||
"publishedAt": { "type": "string" },
|
||||
"url": { "type": "string" }
|
||||
}
|
||||
}
|
||||
|
|
@ -7,12 +7,25 @@
|
|||
"source": {
|
||||
"input": "../codex-flows",
|
||||
"type": "local",
|
||||
"commit": "c3905c73feff72add4f88ba8ec5c11cb1921c386"
|
||||
"commit": "58c91cfa12706213a072c6c67ae3910b6b95f120"
|
||||
},
|
||||
"sourcePath": "flows/openai-codex-bindings",
|
||||
"destinationPath": ".codex/flows/openai-codex-bindings",
|
||||
"contentHash": "sha256:6bfd8118be031c145813dcacbf47f6939b2460aeeb0d8959075e6018297d0403",
|
||||
"installedAt": "2026-05-18T19:14:31.421Z"
|
||||
"installedAt": "2026-05-18T19:29:51.103Z"
|
||||
},
|
||||
{
|
||||
"name": "peezy-codex-flows-fork",
|
||||
"kind": "flow",
|
||||
"source": {
|
||||
"input": "../codex-flows",
|
||||
"type": "local",
|
||||
"commit": "58c91cfa12706213a072c6c67ae3910b6b95f120"
|
||||
},
|
||||
"sourcePath": "flows/peezy-codex-flows-fork",
|
||||
"destinationPath": ".codex/flows/peezy-codex-flows-fork",
|
||||
"contentHash": "sha256:149c21269fe4de463f9bdf8e8086f8aae9601c8f319d62c08749c8f65db4a465",
|
||||
"installedAt": "2026-05-18T19:29:51.103Z"
|
||||
},
|
||||
{
|
||||
"name": "peezy-codex-fork",
|
||||
|
|
@ -20,12 +33,12 @@
|
|||
"source": {
|
||||
"input": "../codex-flows",
|
||||
"type": "local",
|
||||
"commit": "c3905c73feff72add4f88ba8ec5c11cb1921c386"
|
||||
"commit": "58c91cfa12706213a072c6c67ae3910b6b95f120"
|
||||
},
|
||||
"sourcePath": "flows/peezy-codex-fork",
|
||||
"destinationPath": ".codex/flows/peezy-codex-fork",
|
||||
"contentHash": "sha256:613f0733ebea22b191ed6e706ad09e05a2dc97aad16cdbe14db01065a1b06582",
|
||||
"installedAt": "2026-05-18T19:14:31.421Z"
|
||||
"installedAt": "2026-05-18T19:29:51.103Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,54 @@
|
|||
}
|
||||
},
|
||||
"pollIntervalSeconds": 300
|
||||
},
|
||||
{
|
||||
"id": "npm-peezy-codex-releases",
|
||||
"provider": "npm",
|
||||
"url": "https://registry.npmjs.org/@peezy.tech%2Fcodex",
|
||||
"event": "release",
|
||||
"repo": {
|
||||
"owner": "@peezy.tech",
|
||||
"name": "codex",
|
||||
"fullName": "@peezy.tech/codex",
|
||||
"webUrl": "https://www.npmjs.com/package/@peezy.tech/codex"
|
||||
},
|
||||
"target": {
|
||||
"mode": "workspace_flow",
|
||||
"eventType": "downstream.release",
|
||||
"workspaceUrlEnv": "PATCH_WORKSPACE_BACKEND_URL",
|
||||
"workspaceSecretEnv": "PATCH_WORKSPACE_BACKEND_SECRET",
|
||||
"payload": {
|
||||
"provider": "npm",
|
||||
"packageName": "@peezy.tech/codex",
|
||||
"repo": "peezy-tech/codex"
|
||||
}
|
||||
},
|
||||
"pollIntervalSeconds": 300
|
||||
},
|
||||
{
|
||||
"id": "npm-peezy-codex-flows-releases",
|
||||
"provider": "npm",
|
||||
"url": "https://registry.npmjs.org/@peezy.tech%2Fcodex-flows",
|
||||
"event": "release",
|
||||
"repo": {
|
||||
"owner": "@peezy.tech",
|
||||
"name": "codex-flows",
|
||||
"fullName": "@peezy.tech/codex-flows",
|
||||
"webUrl": "https://www.npmjs.com/package/@peezy.tech/codex-flows"
|
||||
},
|
||||
"target": {
|
||||
"mode": "workspace_flow",
|
||||
"eventType": "downstream.release",
|
||||
"workspaceUrlEnv": "PATCH_WORKSPACE_BACKEND_URL",
|
||||
"workspaceSecretEnv": "PATCH_WORKSPACE_BACKEND_SECRET",
|
||||
"payload": {
|
||||
"provider": "npm",
|
||||
"packageName": "@peezy.tech/codex-flows",
|
||||
"repo": "peezy-tech/codex-flows"
|
||||
}
|
||||
},
|
||||
"pollIntervalSeconds": 300
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { discoverFlows, matchingSteps, type FlowEvent as RuntimeFlowEvent } from
|
|||
import {
|
||||
dispatchWorkspaceEventDetailed,
|
||||
maintenanceAttemptForWorkspaceDispatch,
|
||||
patchDownstreamReleaseEvent,
|
||||
patchUpstreamBranchUpdateEvent,
|
||||
patchUpstreamReleaseEvent,
|
||||
replayWorkspaceEventDetailed,
|
||||
|
|
@ -55,6 +56,7 @@ Usage:
|
|||
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 downstream-release --package PACKAGE --version VERSION [--repo OWNER/NAME] [--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]
|
||||
|
|
@ -234,7 +236,7 @@ async function handleAttempts(context: CliContext): Promise<number> {
|
|||
async function handleRun(positionals: string[], context: CliContext): Promise<number> {
|
||||
const target = positionals[0];
|
||||
if (!target) {
|
||||
throw new UsageError("run requires harness, codex-release, or event");
|
||||
throw new UsageError("run requires harness, codex-release, codex-main, downstream-release, or event");
|
||||
}
|
||||
if (target === "harness") {
|
||||
const eventFile = flagValue(context.parsed, "event") ??
|
||||
|
|
@ -267,6 +269,25 @@ async function handleRun(positionals: string[], context: CliContext): Promise<nu
|
|||
}
|
||||
return await runEvent(event, context);
|
||||
}
|
||||
if (target === "downstream-release") {
|
||||
const packageName = flagValue(context.parsed, "package") ?? flagValue(context.parsed, "package-name");
|
||||
const version = flagValue(context.parsed, "version") ?? flagValue(context.parsed, "tag");
|
||||
if (!packageName) {
|
||||
throw new UsageError("run downstream-release requires --package");
|
||||
}
|
||||
if (!version) {
|
||||
throw new UsageError("run downstream-release requires --version");
|
||||
}
|
||||
const event = patchDownstreamReleaseEvent({
|
||||
packageName,
|
||||
version,
|
||||
repo: flagValue(context.parsed, "repo"),
|
||||
});
|
||||
if (!flagBool(context.parsed, "dry-run") && !flagBool(context.parsed, "record-only")) {
|
||||
assertCodexDispatchAllowed(context, "downstream-release");
|
||||
}
|
||||
return await runEvent(event, context);
|
||||
}
|
||||
if (target === "event") {
|
||||
const eventFile = flagValue(context.parsed, "file");
|
||||
if (!eventFile) {
|
||||
|
|
|
|||
|
|
@ -96,6 +96,38 @@ export function parseFeedEntries(xml: string): FeedEntry[] {
|
|||
}).filter((entry) => entry.id);
|
||||
}
|
||||
|
||||
export function parseNpmPackageEntries(text: string): FeedEntry[] {
|
||||
const parsed = JSON.parse(text) as {
|
||||
name?: string;
|
||||
time?: Record<string, string>;
|
||||
"dist-tags"?: Record<string, string>;
|
||||
};
|
||||
const packageName = parsed.name ?? "unknown-package";
|
||||
const time = parsed.time ?? {};
|
||||
return Object.entries(time)
|
||||
.filter(([version]) => version !== "created" && version !== "modified")
|
||||
.map(([version, publishedAt]) => ({
|
||||
id: `npm:${packageName}:${version}`,
|
||||
title: version,
|
||||
url: `https://www.npmjs.com/package/${encodeURIComponent(packageName)}/v/${encodeURIComponent(version)}`,
|
||||
author: "npm",
|
||||
publishedAt,
|
||||
raw: JSON.stringify({
|
||||
packageName,
|
||||
version,
|
||||
distTags: distTagsForVersion(parsed["dist-tags"] ?? {}, version),
|
||||
}),
|
||||
}))
|
||||
.sort((left, right) => new Date(right.publishedAt).getTime() - new Date(left.publishedAt).getTime());
|
||||
}
|
||||
|
||||
function distTagsForVersion(distTags: Record<string, string>, version: string): string[] {
|
||||
return Object.entries(distTags)
|
||||
.filter(([, tagVersion]) => tagVersion === version)
|
||||
.map(([tag]) => tag)
|
||||
.sort();
|
||||
}
|
||||
|
||||
function shaFromEntry(entry: FeedEntry): string | undefined {
|
||||
const value = entry.url ?? entry.id;
|
||||
return value.match(/[0-9a-f]{40}/i)?.[0];
|
||||
|
|
@ -174,13 +206,14 @@ export async function pollFeedSource(input: {
|
|||
fetchImpl?: FetchLike;
|
||||
}): Promise<{ signals: FeedSignal[]; jobs: number; flowDispatches: number; primed: boolean }> {
|
||||
const response = await (input.fetchImpl ?? fetch)(input.source.url, {
|
||||
headers: { accept: "application/atom+xml, application/rss+xml, application/xml, text/xml;q=0.9" },
|
||||
headers: { accept: "application/json, application/atom+xml, application/rss+xml, application/xml, text/xml;q=0.9" },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Feed ${input.source.id} returned ${response.status}`);
|
||||
}
|
||||
|
||||
const entries = parseFeedEntries(await response.text());
|
||||
const body = await response.text();
|
||||
const entries = input.source.provider === "npm" ? parseNpmPackageEntries(body) : parseFeedEntries(body);
|
||||
const newestId = entries[0]?.id;
|
||||
const previous = input.state[input.source.id];
|
||||
const primed = !previous?.lastSeenId;
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ function tagFromSignal(signal: FeedSignal): string | undefined {
|
|||
}
|
||||
|
||||
function flowPayloadFromSignal(signal: FeedSignal): Record<string, unknown> {
|
||||
const tag = tagFromSignal(signal);
|
||||
return {
|
||||
provider: signal.provider,
|
||||
event: signal.event,
|
||||
|
|
@ -68,7 +69,8 @@ function flowPayloadFromSignal(signal: FeedSignal): Record<string, unknown> {
|
|||
repoName: signal.repo.name,
|
||||
ref: signal.ref,
|
||||
sha: signal.sha,
|
||||
tag: tagFromSignal(signal),
|
||||
tag,
|
||||
...(signal.provider === "npm" && tag ? { packageName: signal.repo.fullName, version: tag } : {}),
|
||||
raw: signal.raw,
|
||||
};
|
||||
}
|
||||
|
|
@ -130,6 +132,26 @@ export function patchUpstreamBranchUpdateEvent(input: {
|
|||
};
|
||||
}
|
||||
|
||||
export function patchDownstreamReleaseEvent(input: {
|
||||
packageName: string;
|
||||
version: string;
|
||||
repo?: string;
|
||||
receivedAt?: string;
|
||||
}): FlowEvent<Record<string, unknown>> {
|
||||
return {
|
||||
id: `${serviceSource}:downstream.release:${input.packageName}:${input.version}`,
|
||||
type: "downstream.release",
|
||||
source: serviceSource,
|
||||
receivedAt: input.receivedAt ?? new Date().toISOString(),
|
||||
payload: {
|
||||
packageName: input.packageName,
|
||||
version: input.version,
|
||||
tag: input.version,
|
||||
...(input.repo ? { repo: input.repo } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function dispatchFlowEvent(
|
||||
event: FlowEvent,
|
||||
target: Partial<FeedWorkspaceFlowTarget> = {},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export type FeedProvider = "codeberg" | "github" | "jojo";
|
||||
export type FeedProvider = "codeberg" | "github" | "jojo" | "npm";
|
||||
|
||||
export type FeedEventName = "push" | "release";
|
||||
|
||||
|
|
|
|||
|
|
@ -116,6 +116,38 @@ describe("patch.moi CLI", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("dry-runs downstream release matching for codex-flows fork releases", async () => {
|
||||
const dryRun = await invoke([
|
||||
"run",
|
||||
"downstream-release",
|
||||
"--package",
|
||||
"@peezy.tech/codex-flows",
|
||||
"--version",
|
||||
"0.4.0",
|
||||
"--repo",
|
||||
"peezy-tech/codex-flows",
|
||||
"--workspace-root",
|
||||
workspaceRoot,
|
||||
"--dry-run",
|
||||
"--json",
|
||||
]);
|
||||
|
||||
expect(dryRun.code).toBe(0);
|
||||
expect(JSON.parse(dryRun.stdout)).toMatchObject({
|
||||
event: {
|
||||
type: "downstream.release",
|
||||
payload: {
|
||||
packageName: "@peezy.tech/codex-flows",
|
||||
version: "0.4.0",
|
||||
repo: "peezy-tech/codex-flows",
|
||||
},
|
||||
},
|
||||
matches: [
|
||||
{ flow: "peezy-codex-flows-fork", step: "release-fork", runner: "bun" },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("syncs a maintenance attempt from workspace run state", async () => {
|
||||
const dataDir = await mkdtemp(join(tmpdir(), "patch-cli-"));
|
||||
const store = new EventStore(dataDir);
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { loadSources, parseFeedEntries, pollFeedsOnce, signalFromEntry } from "../src/feed";
|
||||
import { dispatchWorkspaceEvent, patchUpstreamReleaseEvent } from "../src/flow";
|
||||
import { loadSources, parseFeedEntries, parseNpmPackageEntries, pollFeedsOnce, signalFromEntry } from "../src/feed";
|
||||
import { dispatchWorkspaceEvent, patchDownstreamReleaseEvent, patchUpstreamReleaseEvent } from "../src/flow";
|
||||
import type { FeedSourceConfig } from "../src/types";
|
||||
|
||||
const atom = `<?xml version="1.0"?>
|
||||
|
|
@ -35,6 +35,19 @@ const rss = `<?xml version="1.0"?>
|
|||
</item>
|
||||
</channel></rss>`;
|
||||
|
||||
const npmPackage = JSON.stringify({
|
||||
name: "@peezy.tech/codex-flows",
|
||||
"dist-tags": {
|
||||
latest: "0.4.0",
|
||||
},
|
||||
time: {
|
||||
created: "2026-05-10T00:00:00.000Z",
|
||||
modified: "2026-05-17T00:00:00.000Z",
|
||||
"0.3.6": "2026-05-15T00:00:00.000Z",
|
||||
"0.4.0": "2026-05-17T00:00:00.000Z",
|
||||
},
|
||||
});
|
||||
|
||||
const source: FeedSourceConfig = {
|
||||
id: "github-openai-codex-main",
|
||||
provider: "github",
|
||||
|
|
@ -69,6 +82,15 @@ describe("feed watcher", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("parses npm package releases newest first", () => {
|
||||
expect(parseNpmPackageEntries(npmPackage).map((entry) => entry.title)).toEqual(["0.4.0", "0.3.6"]);
|
||||
expect(parseNpmPackageEntries(npmPackage)[0]).toMatchObject({
|
||||
id: "npm:@peezy.tech/codex-flows:0.4.0",
|
||||
author: "npm",
|
||||
publishedAt: "2026-05-17T00:00:00.000Z",
|
||||
});
|
||||
});
|
||||
|
||||
test("normalizes commit feed entries into push signals", () => {
|
||||
const signal = signalFromEntry(source, parseFeedEntries(atom)[0]);
|
||||
expect(signal).toMatchObject({
|
||||
|
|
@ -88,6 +110,8 @@ describe("feed watcher", () => {
|
|||
"codeberg-forgejo-releases",
|
||||
"github-openai-codex-main",
|
||||
"github-openai-codex-releases",
|
||||
"npm-peezy-codex-releases",
|
||||
"npm-peezy-codex-flows-releases",
|
||||
]);
|
||||
expect(sources.find((item) => item.id === "github-openai-codex-main")?.target).toMatchObject({
|
||||
mode: "workspace_flow",
|
||||
|
|
@ -274,6 +298,67 @@ describe("feed watcher", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("later npm release polls dispatch downstream release events", async () => {
|
||||
const dataDir = await mkdtemp(join(tmpdir(), "patch-feed-"));
|
||||
const sourcesPath = join(dataDir, "sources.json");
|
||||
const releaseSource: FeedSourceConfig = {
|
||||
id: "npm-peezy-codex-flows-releases",
|
||||
provider: "npm",
|
||||
url: "https://registry.npmjs.org/@peezy.tech%2Fcodex-flows",
|
||||
event: "release",
|
||||
repo: {
|
||||
owner: "@peezy.tech",
|
||||
name: "codex-flows",
|
||||
fullName: "@peezy.tech/codex-flows",
|
||||
webUrl: "https://www.npmjs.com/package/@peezy.tech/codex-flows",
|
||||
},
|
||||
target: {
|
||||
mode: "workspace_flow",
|
||||
eventType: "downstream.release",
|
||||
workspaceUrlEnv: "WORKSPACE_URL",
|
||||
payload: {
|
||||
packageName: "@peezy.tech/codex-flows",
|
||||
repo: "peezy-tech/codex-flows",
|
||||
},
|
||||
},
|
||||
};
|
||||
await writeFile(sourcesPath, JSON.stringify({ sources: [releaseSource] }), "utf8");
|
||||
await writeFile(join(dataDir, "feed-state.json"), JSON.stringify({
|
||||
"npm-peezy-codex-flows-releases": {
|
||||
lastSeenId: "npm:@peezy.tech/codex-flows:0.3.6",
|
||||
lastCheckedAt: "2026-05-15T00:00:00.000Z",
|
||||
},
|
||||
}), "utf8");
|
||||
|
||||
let dispatchedBody = "";
|
||||
await pollFeedsOnce({
|
||||
dataDir,
|
||||
sourcesPath,
|
||||
discord: { enabled: false, notifyEvents: new Set(["release"]) },
|
||||
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(npmPackage, { 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("downstream.release");
|
||||
expect(flowEvent.payload.packageName).toBe("@peezy.tech/codex-flows");
|
||||
expect(flowEvent.payload.version).toBe("0.4.0");
|
||||
expect(flowEvent.payload.tag).toBe("0.4.0");
|
||||
expect(flowEvent.payload.repo).toBe("peezy-tech/codex-flows");
|
||||
expect(JSON.parse(dispatchedBody).id).toBe(flowEvent.id);
|
||||
});
|
||||
|
||||
test("workspace dispatch uses default workspace backend env names", async () => {
|
||||
let dispatchedUrl = "";
|
||||
let dispatchedSignature = "";
|
||||
|
|
@ -519,6 +604,26 @@ describe("feed watcher", () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("Patch downstream release helper creates deterministic product events", () => {
|
||||
expect(patchDownstreamReleaseEvent({
|
||||
packageName: "@peezy.tech/codex-flows",
|
||||
version: "0.4.0",
|
||||
repo: "peezy-tech/codex-flows",
|
||||
receivedAt: "2026-05-17T00:00:00.000Z",
|
||||
})).toEqual({
|
||||
id: "patch:downstream.release:@peezy.tech/codex-flows:0.4.0",
|
||||
type: "downstream.release",
|
||||
source: "patch",
|
||||
receivedAt: "2026-05-17T00:00:00.000Z",
|
||||
payload: {
|
||||
packageName: "@peezy.tech/codex-flows",
|
||||
version: "0.4.0",
|
||||
tag: "0.4.0",
|
||||
repo: "peezy-tech/codex-flows",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function headerValue(headers: HeadersInit | undefined, name: string): string {
|
||||
|
|
|
|||
|
|
@ -100,6 +100,20 @@ 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 Codex fork release-cycle flow.
|
||||
|
||||
When a Peezy downstream package release needs to refresh the codex-flows fork
|
||||
release candidate, dry-run the downstream release event:
|
||||
|
||||
```bash
|
||||
bun run patch.moi -- run downstream-release \
|
||||
--package @peezy.tech/codex \
|
||||
--version 0.130.0 \
|
||||
--repo peezy-tech/codex \
|
||||
--dry-run
|
||||
```
|
||||
|
||||
That event should match the `peezy-codex-flows-fork/release-fork` Bun step. The
|
||||
same flow also accepts `@peezy.tech/codex-flows` releases.
|
||||
|
||||
Dry-run the upstream main branch update path separately:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -98,6 +98,16 @@ bun run patch.moi -- run codex-main \
|
|||
--dry-run
|
||||
```
|
||||
|
||||
Verify a downstream Peezy package release without executing fork packaging:
|
||||
|
||||
```bash
|
||||
bun run patch.moi -- run downstream-release \
|
||||
--package @peezy.tech/codex-flows \
|
||||
--version 0.4.0 \
|
||||
--repo peezy-tech/codex-flows \
|
||||
--dry-run
|
||||
```
|
||||
|
||||
Dispatching the Codex release task requires an explicit execution surface. Use
|
||||
Actions/local mode when no workspace backend is running:
|
||||
|
||||
|
|
@ -124,6 +134,7 @@ Read patch.moi-owned state:
|
|||
```bash
|
||||
bun run patch.moi -- status
|
||||
bun run patch.moi -- events --type upstream.release
|
||||
bun run patch.moi -- events --type downstream.release
|
||||
bun run patch.moi -- dispatches --status failed
|
||||
bun run patch.moi -- attempts --status needs_intervention
|
||||
```
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ tags as the maintained project source of truth.
|
|||
```ts
|
||||
type FeedSourceConfig = {
|
||||
id: string;
|
||||
provider: "github";
|
||||
provider: "codeberg" | "github" | "jojo" | "npm";
|
||||
url: string;
|
||||
event: "push" | "release";
|
||||
repo: {
|
||||
|
|
@ -51,7 +51,39 @@ 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`.
|
||||
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.
|
||||
For upstream release maintenance, use a stable event type such as
|
||||
`upstream.release`. For upstream main movement, use `upstream.branch_update`.
|
||||
For downstream package releases, use `downstream.release` with `packageName` and
|
||||
`version` in the payload.
|
||||
|
||||
Include only routing hints in `payload`; avoid copying branch topology into the
|
||||
feed source when it can be read from the repository.
|
||||
|
||||
## npm release sources
|
||||
|
||||
npm package sources read the registry package document and treat each published
|
||||
version as a release entry. patch.moi uses this for the local downstream release
|
||||
broadcasts from `@peezy.tech/codex` and `@peezy.tech/codex-flows`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "npm-peezy-codex-flows-releases",
|
||||
"provider": "npm",
|
||||
"url": "https://registry.npmjs.org/@peezy.tech%2Fcodex-flows",
|
||||
"event": "release",
|
||||
"repo": {
|
||||
"owner": "@peezy.tech",
|
||||
"name": "codex-flows",
|
||||
"fullName": "@peezy.tech/codex-flows",
|
||||
"webUrl": "https://www.npmjs.com/package/@peezy.tech/codex-flows"
|
||||
},
|
||||
"target": {
|
||||
"mode": "workspace_flow",
|
||||
"eventType": "downstream.release",
|
||||
"payload": {
|
||||
"packageName": "@peezy.tech/codex-flows",
|
||||
"repo": "peezy-tech/codex-flows"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -62,7 +62,9 @@ codex-flows pack doctor --json
|
|||
|
||||
`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
|
||||
`upstream.branch_update` events. `peezy-codex-flows-fork` matches
|
||||
`downstream.release` events for `@peezy.tech/codex` and
|
||||
`@peezy.tech/codex-flows`. 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`.
|
||||
|
||||
|
|
|
|||
|
|
@ -50,12 +50,13 @@ The Codex release maintenance capabilities are installed from the neighboring
|
|||
codex-flows pack add ../codex-flows \
|
||||
--include openai-codex-bindings \
|
||||
--include peezy-codex-fork \
|
||||
--include peezy-codex-flows-fork \
|
||||
--apply
|
||||
codex-flows pack doctor --json
|
||||
```
|
||||
|
||||
The current local install pins `openai-codex-bindings` and `peezy-codex-fork`
|
||||
in `.codex/pack-lock.json`. The codex-flows runtime discovers installed
|
||||
The current local install pins `openai-codex-bindings`, `peezy-codex-fork`, and
|
||||
`peezy-codex-flows-fork` in `.codex/pack-lock.json`. The codex-flows runtime 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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue