From 608d6fe2b89c7ce62e0e311c7ef8b0a90914a5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuncay=20=C3=96lmez?= Date: Fri, 29 May 2026 14:33:29 +0300 Subject: [PATCH 1/6] feat: add codex exec adapter dry-run preview --- README.md | 9 ++ bin/cewp.js | 248 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/install.md | 8 ++ 3 files changed, 265 insertions(+) diff --git a/README.md b/README.md index e3fa40b..d96217d 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,15 @@ cewp run dispatch start --run 20260528-232250 --dry-run `dispatch start` is dry-run only in this slice. It prints manual execution steps for workers and reviewer, does not start agents, and does not run `codex exec`, merge, push, or publish. +Preview a future `codex exec` adapter command: + +```bash +cewp run dispatch exec worker-a --adapter codex-exec --dry-run +cewp run dispatch exec worker-a --run 20260528-232250 --adapter codex-exec --dry-run +``` + +`dispatch exec` currently renders a safe command preview only. It does not run `codex exec`, start agents, write adapter output, merge, push, or publish. + Collect reviewer context into one local packet: ```bash diff --git a/bin/cewp.js b/bin/cewp.js index 1bfb194..8022613 100644 --- a/bin/cewp.js +++ b/bin/cewp.js @@ -36,6 +36,7 @@ Usage: cewp run dispatch check cewp run dispatch prompts cewp run dispatch start --dry-run + cewp run dispatch exec --adapter codex-exec --dry-run cewp run collect cewp run finalize [--dry-run] cewp run cleanup [--yes] @@ -66,6 +67,7 @@ Examples: cewp run dispatch check --run 20260528-232250 cewp run dispatch prompts --run 20260528-232250 cewp run dispatch start --run 20260528-232250 --dry-run + cewp run dispatch exec worker-a --run 20260528-232250 --adapter codex-exec --dry-run cewp run collect --run 20260528-232250 cewp run finalize --run 20260528-232250 --dry-run cewp run cleanup --run 20260528-232250 @@ -86,6 +88,7 @@ function parseArgs(argv) { help: false, dryRun: false, yes: false, + adapter: undefined, workers: undefined, reviewer: false, }; @@ -116,6 +119,11 @@ function parseArgs(argv) { continue; } + if (args.command === "run" && args.subcommand === "dispatch" && args.action === "exec" && index === 3) { + args.role = arg; + continue; + } + if (arg === "--help" || arg === "-h") { args.help = true; continue; @@ -185,6 +193,16 @@ function parseArgs(argv) { continue; } + if (args.command === "run" && arg === "--adapter") { + const value = argv[index + 1]; + if (!value || value.startsWith("--")) { + throw new Error("--adapter requires an adapter name."); + } + args.adapter = value; + index += 1; + continue; + } + throw new Error(`Unknown argument: ${arg}`); } @@ -1937,6 +1955,231 @@ function runDispatchStart(options = {}) { } } +function getWorkerTaskForRole(taskEntries, role) { + const matches = taskEntries + .map((entry) => entry.task) + .filter((task) => task.assignedRole === role); + + if (matches.length === 0) { + throw new Error(`Cannot create codex-exec preview: no task assigned to ${role}.`); + } + + if (matches.length > 1) { + throw new Error(`Cannot create codex-exec preview: multiple tasks assigned to ${role}; this slice supports one task per worker role.`); + } + + return matches[0]; +} + +function getDispatchPromptPathForTask(runRoot, role, taskId) { + return path.join(runRoot, "dispatch-prompts", safeDispatchPromptFileName(role, taskId)); +} + +function printCodexExecPreview({ cwd, promptPath, outputPath, sandbox }) { + console.log("PowerShell preview:"); + console.log(` $prompt = Get-Content -Raw ${quote(promptPath)}`); + console.log(` codex exec --cd ${quote(cwd)} --sandbox ${sandbox} --output-last-message ${quote(outputPath)} $prompt`); +} + +function runDispatchExecDryRun(options = {}) { + const supportedRoles = ["worker-a", "worker-b", "reviewer"]; + + if (!options.dryRun) { + throw new Error("dispatch exec currently supports --dry-run only."); + } + + if (!options.adapter) { + throw new Error("dispatch exec requires --adapter codex-exec."); + } + + if (options.adapter !== "codex-exec") { + throw new Error(`Unsupported dispatch adapter: ${options.adapter}. Supported adapter: codex-exec.`); + } + + if (!supportedRoles.includes(options.role)) { + throw new Error(`Unsupported dispatch exec role: ${options.role || "(missing)"}. Supported roles: worker-a, worker-b, reviewer.`); + } + + const { runId, runRoot } = findRun(options); + const runJson = readJsonIfExists(path.join(runRoot, "run.json")); + const boardJson = readJsonIfExists(path.join(runRoot, "board.json")); + const taskEntries = readTasks(runRoot); + const worktreesRegistry = readWorktreesRegistry(runRoot); + const dispatchPromptsRoot = path.join(runRoot, "dispatch-prompts"); + const adapterOutputRoot = path.join(runRoot, "adapter-output"); + const outputLastMessagePath = path.join(adapterOutputRoot, `${options.role}-last-message.md`); + const failures = []; + const warnings = []; + let preview; + + if (!runJson) { + failures.push("run.json missing."); + } + + if (!boardJson) { + failures.push("board.json missing."); + } + + if (taskEntries.length === 0) { + failures.push("tasks not found. Ask the Manager to create tasks first."); + } + + if (!worktreesRegistry) { + failures.push("worktrees.json missing. Run cewp run worktrees create first."); + } + + if (!fs.existsSync(dispatchPromptsRoot)) { + failures.push(`dispatch-prompts directory missing. Run: cewp run dispatch prompts --run ${runId}`); + } + + if (options.role === "reviewer") { + const promptPath = path.join(dispatchPromptsRoot, "reviewer-prompt.md"); + const reviewPacketPath = path.join(runRoot, "review-packets", "review-packet.md"); + const reportPath = path.join(runRoot, "reviews", "reviewer-report.md"); + const eventPath = path.join(runRoot, "events", "reviewer.jsonl"); + const workdir = (runJson && runJson.repoRoot) || process.cwd(); + + if (!fs.existsSync(promptPath)) { + failures.push(`reviewer dispatch prompt missing: ${relativeRunPath(runRoot, promptPath)}`); + } + + if (!fs.existsSync(reviewPacketPath)) { + warnings.push(`review packet missing: ${relativeRunPath(runRoot, reviewPacketPath)}`); + } + + if (!fs.existsSync(workdir)) { + failures.push(`reviewer working directory missing: ${workdir}`); + } + + preview = { + role: options.role, + cwd: workdir, + promptPath, + reportPath, + eventPath, + outputLastMessagePath, + sandbox: "read-only", + reviewPacketPath, + }; + } else if (taskEntries.length > 0) { + const task = getWorkerTaskForRole(taskEntries, options.role); + const taskId = task.id || "unknown-task"; + const worktree = getDispatchWorktree(worktreesRegistry, task.id); + const promptPath = getDispatchPromptPathForTask(runRoot, options.role, taskId); + const reportPath = path.join(runRoot, "reports", `${options.role}-report.md`); + const eventPath = path.join(runRoot, "events", `${options.role}.jsonl`); + + if (!task.id) { + failures.push("task file missing required id."); + } + + if (!worktree) { + failures.push(`${taskId} matching worktree missing in worktrees.json.`); + } else if (!worktree.path) { + failures.push(`${taskId} worktree path missing.`); + } else if (!fs.existsSync(worktree.path)) { + failures.push(`${taskId} worktree path does not exist: ${worktree.path}`); + } else if (!isGitWorktreePath(worktree.path)) { + failures.push(`${taskId} path is not a git worktree: ${worktree.path}`); + } + + if (!fs.existsSync(promptPath)) { + failures.push(`${taskId} dispatch prompt missing: ${relativeRunPath(runRoot, promptPath)}`); + } + + preview = { + role: options.role, + task, + worktree, + cwd: worktree && worktree.path, + promptPath, + reportPath, + eventPath, + outputLastMessagePath, + sandbox: "workspace-write", + }; + } + + console.log("CEWP Coordinator Mode codex-exec adapter dry-run"); + console.log(`Run ID: ${runId}`); + console.log(`Role: ${options.role}`); + console.log(`Adapter: ${options.adapter}`); + console.log("Mode: dry-run"); + console.log(""); + console.log(`Readiness: ${failures.length > 0 ? "FAIL" : warnings.length > 0 ? "WARN" : "PASS"}`); + console.log(""); + + if (failures.length > 0) { + console.log("Failures:"); + for (const failure of failures) { + console.log(` - ${failure}`); + } + console.log(""); + } + + if (warnings.length > 0) { + console.log("Warnings:"); + for (const warning of warnings) { + console.log(` - ${warning}`); + } + console.log(""); + } + + if (preview) { + if (preview.task) { + console.log("Task:"); + console.log(` ${preview.task.id || "unknown-task"} / ${preview.role}`); + console.log(` Title: ${preview.task.title || "(untitled)"}`); + console.log(` Branch: ${(preview.worktree && preview.worktree.branch) || preview.task.branch || "unknown"}`); + console.log(""); + console.log("Worktree:"); + console.log(` ${preview.cwd || "missing"}`); + console.log(""); + } else { + console.log("Reviewer:"); + console.log(` Workdir: ${preview.cwd}`); + console.log(" Sandbox: read-only"); + console.log(" Note: reviewer report writing will require a future workspace-write execution mode."); + console.log(` Review packet: ${relativeRunPath(runRoot, preview.reviewPacketPath)}`); + console.log(""); + } + + console.log("Prompt:"); + console.log(` ${relativeRunPath(runRoot, preview.promptPath)}`); + console.log(""); + console.log("Expected outputs:"); + console.log(` Report: ${relativeRunPath(runRoot, preview.reportPath)}`); + console.log(` Event log: ${relativeRunPath(runRoot, preview.eventPath)}`); + console.log(` Last message: ${relativeRunPath(runRoot, preview.outputLastMessagePath)}`); + console.log(""); + console.log("Recommended execution strategy:"); + console.log(" Pass prompt content to codex exec from the dispatch prompt file."); + console.log(" Do not inline the full prompt in a shell command."); + console.log(""); + printCodexExecPreview({ + cwd: preview.cwd || "", + promptPath: preview.promptPath, + outputPath: preview.outputLastMessagePath, + sandbox: preview.sandbox, + }); + console.log(""); + } + + console.log("Post-checks:"); + console.log(" git status --short"); + console.log(" allowedFiles / forbiddenFiles"); + console.log(" report exists"); + console.log(" adapter output exists"); + console.log(" no merge/push/publish"); + console.log(""); + console.log("No processes were started."); + console.log("No files were changed."); + + if (failures.length > 0) { + process.exitCode = 1; + } +} + function getWorktreePreflightErrors(plans) { const errors = []; const seenPaths = new Map(); @@ -2975,6 +3218,11 @@ function runCommand(options) { return; } + if (options.subcommand === "dispatch" && options.action === "exec") { + runDispatchExecDryRun(options); + return; + } + throw new Error(`Unsupported run command: ${options.subcommand}`); } diff --git a/docs/install.md b/docs/install.md index f949c91..372ce4e 100644 --- a/docs/install.md +++ b/docs/install.md @@ -142,6 +142,14 @@ cewp run dispatch start --dry-run In this slice dispatch start is dry-run only. It does not spawn processes, automate terminal input, run `codex exec`, merge, push, or publish. +`cewp run dispatch exec --adapter codex-exec --dry-run` previews a future `codex exec` adapter command: + +```bash +cewp run dispatch exec worker-a --adapter codex-exec --dry-run +``` + +This is a local Coordinator Mode runtime helper. It renders the command, prompt path, output path, and post-check plan only; it does not spawn processes, run `codex exec`, merge, push, or publish. + `cewp run collect` creates a reviewer packet from local run state: ```bash From b75ea3e2587a55ca00dfdade711b1f6059da7259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuncay=20=C3=96lmez?= Date: Fri, 29 May 2026 14:44:49 +0300 Subject: [PATCH 2/6] feat: add guarded codex exec worker execution --- README.md | 9 ++ bin/cewp.js | 331 ++++++++++++++++++++++++++++++++++++++---------- docs/install.md | 8 ++ 3 files changed, 283 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index d96217d..f7ad6f4 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,15 @@ cewp run dispatch exec worker-a --run 20260528-232250 --adapter codex-exec --dry `dispatch exec` currently renders a safe command preview only. It does not run `codex exec`, start agents, write adapter output, merge, push, or publish. +Run a single worker through the guarded `codex-exec` adapter: + +```bash +cewp run dispatch exec worker-a --adapter codex-exec --yes +cewp run dispatch exec worker-a --adapter codex-exec --yes --timeout 120 +``` + +`--yes` is required for real execution and is currently limited to one worker role at a time. Reviewer execution is still manual/dry-run. After `codex exec` exits, CEWP checks changed files against `allowedFiles` and `forbiddenFiles`, verifies the worker report and adapter output, and still does not merge, push, or publish. + Collect reviewer context into one local packet: ```bash diff --git a/bin/cewp.js b/bin/cewp.js index 8022613..6e21e2b 100644 --- a/bin/cewp.js +++ b/bin/cewp.js @@ -37,6 +37,7 @@ Usage: cewp run dispatch prompts cewp run dispatch start --dry-run cewp run dispatch exec --adapter codex-exec --dry-run + cewp run dispatch exec --adapter codex-exec --yes [--timeout ] cewp run collect cewp run finalize [--dry-run] cewp run cleanup [--yes] @@ -68,6 +69,7 @@ Examples: cewp run dispatch prompts --run 20260528-232250 cewp run dispatch start --run 20260528-232250 --dry-run cewp run dispatch exec worker-a --run 20260528-232250 --adapter codex-exec --dry-run + cewp run dispatch exec worker-a --run 20260528-232250 --adapter codex-exec --yes --timeout 120 cewp run collect --run 20260528-232250 cewp run finalize --run 20260528-232250 --dry-run cewp run cleanup --run 20260528-232250 @@ -89,6 +91,7 @@ function parseArgs(argv) { dryRun: false, yes: false, adapter: undefined, + timeoutSeconds: 120, workers: undefined, reviewer: false, }; @@ -203,6 +206,19 @@ function parseArgs(argv) { continue; } + if (args.command === "run" && arg === "--timeout") { + const value = argv[index + 1]; + if (!value || value.startsWith("--") || !/^\d+$/.test(value)) { + throw new Error("--timeout requires a positive number of seconds."); + } + args.timeoutSeconds = Number.parseInt(value, 10); + if (args.timeoutSeconds <= 0) { + throw new Error("--timeout requires a positive number of seconds."); + } + index += 1; + continue; + } + throw new Error(`Unknown argument: ${arg}`); } @@ -1981,12 +1997,9 @@ function printCodexExecPreview({ cwd, promptPath, outputPath, sandbox }) { console.log(` codex exec --cd ${quote(cwd)} --sandbox ${sandbox} --output-last-message ${quote(outputPath)} $prompt`); } -function runDispatchExecDryRun(options = {}) { +function getDispatchExecPreview(options) { const supportedRoles = ["worker-a", "worker-b", "reviewer"]; - - if (!options.dryRun) { - throw new Error("dispatch exec currently supports --dry-run only."); - } + const shouldPrint = options.printPreview !== false; if (!options.adapter) { throw new Error("dispatch exec requires --adapter codex-exec."); @@ -2100,86 +2113,274 @@ function runDispatchExecDryRun(options = {}) { }; } - console.log("CEWP Coordinator Mode codex-exec adapter dry-run"); - console.log(`Run ID: ${runId}`); - console.log(`Role: ${options.role}`); - console.log(`Adapter: ${options.adapter}`); - console.log("Mode: dry-run"); - console.log(""); - console.log(`Readiness: ${failures.length > 0 ? "FAIL" : warnings.length > 0 ? "WARN" : "PASS"}`); - console.log(""); + if (shouldPrint) { + console.log("CEWP Coordinator Mode codex-exec adapter dry-run"); + console.log(`Run ID: ${runId}`); + console.log(`Role: ${options.role}`); + console.log(`Adapter: ${options.adapter}`); + console.log(`Mode: ${options.dryRun ? "dry-run" : "execution"}`); + console.log(""); + console.log(`Readiness: ${failures.length > 0 ? "FAIL" : warnings.length > 0 ? "WARN" : "PASS"}`); + console.log(""); - if (failures.length > 0) { - console.log("Failures:"); - for (const failure of failures) { - console.log(` - ${failure}`); + if (failures.length > 0) { + console.log("Failures:"); + for (const failure of failures) { + console.log(` - ${failure}`); + } + console.log(""); } - console.log(""); - } - if (warnings.length > 0) { - console.log("Warnings:"); - for (const warning of warnings) { - console.log(` - ${warning}`); + if (warnings.length > 0) { + console.log("Warnings:"); + for (const warning of warnings) { + console.log(` - ${warning}`); + } + console.log(""); } - console.log(""); - } - if (preview) { - if (preview.task) { - console.log("Task:"); - console.log(` ${preview.task.id || "unknown-task"} / ${preview.role}`); - console.log(` Title: ${preview.task.title || "(untitled)"}`); - console.log(` Branch: ${(preview.worktree && preview.worktree.branch) || preview.task.branch || "unknown"}`); + if (preview) { + if (preview.task) { + console.log("Task:"); + console.log(` ${preview.task.id || "unknown-task"} / ${preview.role}`); + console.log(` Title: ${preview.task.title || "(untitled)"}`); + console.log(` Branch: ${(preview.worktree && preview.worktree.branch) || preview.task.branch || "unknown"}`); + console.log(""); + console.log("Worktree:"); + console.log(` ${preview.cwd || "missing"}`); + console.log(""); + } else { + console.log("Reviewer:"); + console.log(` Workdir: ${preview.cwd}`); + console.log(" Sandbox: read-only"); + console.log(" Note: reviewer report writing will require a future workspace-write execution mode."); + console.log(` Review packet: ${relativeRunPath(runRoot, preview.reviewPacketPath)}`); + console.log(""); + } + + console.log("Prompt:"); + console.log(` ${relativeRunPath(runRoot, preview.promptPath)}`); console.log(""); - console.log("Worktree:"); - console.log(` ${preview.cwd || "missing"}`); + console.log("Expected outputs:"); + console.log(` Report: ${relativeRunPath(runRoot, preview.reportPath)}`); + console.log(` Event log: ${relativeRunPath(runRoot, preview.eventPath)}`); + console.log(` Last message: ${relativeRunPath(runRoot, preview.outputLastMessagePath)}`); console.log(""); - } else { - console.log("Reviewer:"); - console.log(` Workdir: ${preview.cwd}`); - console.log(" Sandbox: read-only"); - console.log(" Note: reviewer report writing will require a future workspace-write execution mode."); - console.log(` Review packet: ${relativeRunPath(runRoot, preview.reviewPacketPath)}`); + console.log("Recommended execution strategy:"); + console.log(" Pass prompt content to codex exec from the dispatch prompt file."); + console.log(" Do not inline the full prompt in a shell command."); + console.log(""); + printCodexExecPreview({ + cwd: preview.cwd || "", + promptPath: preview.promptPath, + outputPath: preview.outputLastMessagePath, + sandbox: preview.sandbox, + }); console.log(""); } - console.log("Prompt:"); - console.log(` ${relativeRunPath(runRoot, preview.promptPath)}`); - console.log(""); - console.log("Expected outputs:"); - console.log(` Report: ${relativeRunPath(runRoot, preview.reportPath)}`); - console.log(` Event log: ${relativeRunPath(runRoot, preview.eventPath)}`); - console.log(` Last message: ${relativeRunPath(runRoot, preview.outputLastMessagePath)}`); + console.log("Post-checks:"); + console.log(" git status --short"); + console.log(" allowedFiles / forbiddenFiles"); + console.log(" report exists"); + console.log(" adapter output exists"); + console.log(" no merge/push/publish"); console.log(""); - console.log("Recommended execution strategy:"); - console.log(" Pass prompt content to codex exec from the dispatch prompt file."); - console.log(" Do not inline the full prompt in a shell command."); + console.log("No processes were started."); + console.log("No files were changed."); + } + + return { + runId, + runRoot, + runJson, + taskEntries, + failures, + warnings, + preview, + }; +} + +function writeAdapterLog(filePath, value) { + fs.writeFileSync(filePath, value || ""); +} + +function runCodexExec({ worktreePath, promptPath, outputLastMessagePath, timeoutSeconds }) { + const prompt = fs.readFileSync(promptPath, "utf8"); + return childProcess.spawnSync("codex", [ + "exec", + "--cd", + worktreePath, + "--sandbox", + "workspace-write", + "--output-last-message", + outputLastMessagePath, + prompt, + ], { + cwd: worktreePath, + encoding: "utf8", + shell: false, + timeout: timeoutSeconds * 1000, + windowsHide: true, + }); +} + +function runDispatchExecActual(options = {}) { + if (!options.adapter) { + throw new Error("dispatch exec requires --adapter codex-exec."); + } + + if (options.adapter !== "codex-exec") { + throw new Error(`Unsupported dispatch adapter: ${options.adapter}. Supported adapter: codex-exec.`); + } + + if (options.role === "reviewer") { + throw new Error("reviewer execution is not implemented yet. Use --dry-run/manual review."); + } + + const result = getDispatchExecPreview({ ...options, dryRun: false, printPreview: false }); + const { runId, runRoot, failures, warnings, preview } = result; + + if (failures.length > 0) { + console.log("CEWP Coordinator Mode codex-exec execution"); + console.log(`Run ID: ${runId}`); + console.log(`Role: ${options.role}`); + console.log("Adapter: codex-exec"); console.log(""); - printCodexExecPreview({ - cwd: preview.cwd || "", - promptPath: preview.promptPath, - outputPath: preview.outputLastMessagePath, - sandbox: preview.sandbox, - }); + console.log("Preflight: FAIL"); + console.log("Failures:"); + for (const failure of failures) { + console.log(` - ${failure}`); + } console.log(""); + console.log("No processes were started."); + console.log("No merge/push/publish was performed."); + process.exitCode = 1; + return; } - console.log("Post-checks:"); - console.log(" git status --short"); - console.log(" allowedFiles / forbiddenFiles"); - console.log(" report exists"); - console.log(" adapter output exists"); - console.log(" no merge/push/publish"); + const adapterOutputRoot = path.join(runRoot, "adapter-output"); + fs.mkdirSync(adapterOutputRoot, { recursive: true }); + console.log(""); - console.log("No processes were started."); - console.log("No files were changed."); + console.log("Executing codex exec..."); + const execResult = runCodexExec({ + worktreePath: preview.cwd, + promptPath: preview.promptPath, + outputLastMessagePath: preview.outputLastMessagePath, + timeoutSeconds: options.timeoutSeconds, + }); - if (failures.length > 0) { + const stdoutPath = path.join(adapterOutputRoot, `${options.role}-stdout.log`); + const stderrPath = path.join(adapterOutputRoot, `${options.role}-stderr.log`); + writeAdapterLog(stdoutPath, execResult.stdout); + writeAdapterLog(stderrPath, execResult.stderr); + + const statusLines = getGitStatusShort(preview.cwd); + const changedFiles = statusLines.map(parseChangedFile); + const scopeWarnings = findScopeWarnings(preview.task.id || "unknown-task", changedFiles, preview.task); + const forbiddenWarnings = scopeWarnings.filter((warning) => warning.includes("changed forbidden file")); + const outsideAllowedWarnings = scopeWarnings.filter((warning) => warning.includes("outside allowedFiles")); + const reportExists = fs.existsSync(preview.reportPath); + const lastMessageExists = fs.existsSync(preview.outputLastMessagePath); + const timedOut = Boolean(execResult.error && execResult.error.code === "ETIMEDOUT"); + const exitCode = typeof execResult.status === "number" ? execResult.status : 1; + const failuresAfterExec = []; + + if (timedOut) { + failuresAfterExec.push(`codex exec timed out after ${options.timeoutSeconds}s.`); + } + + if (exitCode !== 0) { + failuresAfterExec.push(`codex exec exited with code ${exitCode}.`); + } + + failuresAfterExec.push(...outsideAllowedWarnings); + failuresAfterExec.push(...forbiddenWarnings); + + if (!reportExists) { + failuresAfterExec.push(`report missing: ${relativeRunPath(runRoot, preview.reportPath)}`); + } + + if (!lastMessageExists) { + failuresAfterExec.push(`adapter output missing: ${relativeRunPath(runRoot, preview.outputLastMessagePath)}`); + } + + const status = failuresAfterExec.length > 0 ? "FAIL" : "PASS"; + appendRunEvent(runRoot, "cli", { + event: status === "PASS" ? "dispatch_exec_completed" : "dispatch_exec_failed", + runId, + adapter: "codex-exec", + role: options.role, + exitCode, + status, + taskId: preview.task.id, + }); + + console.log(""); + console.log("CEWP Coordinator Mode codex-exec execution"); + console.log(`Run ID: ${runId}`); + console.log(`Role: ${options.role}`); + console.log("Adapter: codex-exec"); + console.log(""); + console.log(`Preflight: ${warnings.length > 0 ? "WARN" : "PASS"}`); + console.log(`Execution: ${exitCode === 0 && !timedOut ? "PASS" : "FAIL"}`); + console.log(`Exit code: ${exitCode}`); + console.log(`Timeout: ${options.timeoutSeconds}s`); + console.log(""); + console.log("Changed files:"); + if (changedFiles.length === 0) { + console.log(" none"); + } else { + for (const filePath of changedFiles) { + console.log(` ${filePath}`); + } + } + console.log(""); + console.log(`Scope: ${outsideAllowedWarnings.length === 0 ? "OK" : "FAIL"}`); + console.log(`Forbidden files: ${forbiddenWarnings.length === 0 ? "OK" : "FAIL"}`); + console.log(`Report: ${reportExists ? `found ${relativeRunPath(runRoot, preview.reportPath)}` : `missing ${relativeRunPath(runRoot, preview.reportPath)}`}`); + console.log(`Last message: ${lastMessageExists ? relativeRunPath(runRoot, preview.outputLastMessagePath) : "missing"}`); + console.log(`Stdout log: ${relativeRunPath(runRoot, stdoutPath)}`); + console.log(`Stderr log: ${relativeRunPath(runRoot, stderrPath)}`); + console.log(""); + console.log(`Status: ${status}`); + + if (failuresAfterExec.length > 0) { + console.log("Reasons:"); + for (const failure of failuresAfterExec) { + console.log(` - ${failure}`); + } + } + + console.log(""); + console.log("No merge/push/publish was performed."); + + if (status === "FAIL") { process.exitCode = 1; } } +function runDispatchExec(options = {}) { + if (options.dryRun && options.yes) { + throw new Error("Use either --dry-run or --yes, not both."); + } + + if (!options.dryRun && !options.yes) { + throw new Error("dispatch exec requires --dry-run or --yes."); + } + + if (options.dryRun) { + const result = getDispatchExecPreview(options); + if (result.failures.length > 0) { + process.exitCode = 1; + } + return; + } + + runDispatchExecActual(options); +} + function getWorktreePreflightErrors(plans) { const errors = []; const seenPaths = new Map(); @@ -3219,7 +3420,7 @@ function runCommand(options) { } if (options.subcommand === "dispatch" && options.action === "exec") { - runDispatchExecDryRun(options); + runDispatchExec(options); return; } diff --git a/docs/install.md b/docs/install.md index 372ce4e..da1c5db 100644 --- a/docs/install.md +++ b/docs/install.md @@ -150,6 +150,14 @@ cewp run dispatch exec worker-a --adapter codex-exec --dry-run This is a local Coordinator Mode runtime helper. It renders the command, prompt path, output path, and post-check plan only; it does not spawn processes, run `codex exec`, merge, push, or publish. +With explicit user approval, the adapter can run one worker at a time: + +```bash +cewp run dispatch exec worker-a --adapter codex-exec --yes --timeout 120 +``` + +This guarded execution mode runs `codex exec` only for worker roles, captures adapter output under `.cewp/runs//adapter-output/`, and performs post-checks for changed files, forbidden files, report existence, and exit code. Reviewer execution is not implemented yet, and merge/push/publish remain separate user-approved steps. + `cewp run collect` creates a reviewer packet from local run state: ```bash From 36f865c1e2f54acbd138381daaf620eff09eaea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuncay=20=C3=96lmez?= Date: Fri, 29 May 2026 18:46:02 +0300 Subject: [PATCH 3/6] fix: collect codex exec worker output from worktrees --- README.md | 2 + bin/cewp.js | 106 ++++++++++++++++++++++++++++++++++++++++++++++-- docs/install.md | 2 + 3 files changed, 106 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f7ad6f4..1454d33 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,8 @@ cewp run dispatch exec worker-a --adapter codex-exec --yes --timeout 120 `--yes` is required for real execution and is currently limited to one worker role at a time. Reviewer execution is still manual/dry-run. After `codex exec` exits, CEWP checks changed files against `allowedFiles` and `forbiddenFiles`, verifies the worker report and adapter output, and still does not merge, push, or publish. +For sandbox compatibility, worker reports are written inside the assigned worktree under `.cewp-worker-output/`. The CLI copies `.cewp-worker-output/-report.md` into `.cewp/runs//reports/` after execution and appends `.cewp-worker-output/-events.jsonl` when present. `.cewp-worker-output/` is runtime output and should not be committed. + Collect reviewer context into one local packet: ```bash diff --git a/bin/cewp.js b/bin/cewp.js index 6e21e2b..cdc65fb 100644 --- a/bin/cewp.js +++ b/bin/cewp.js @@ -1599,6 +1599,8 @@ function createWorkerDispatchPrompt({ runId, runRoot, runJson, task, worktree }) const assignedRole = task.assignedRole || "unassigned"; const reportPath = path.join(runRoot, "reports", `${assignedRole}-report.md`); const eventPath = path.join(runRoot, "events", `${assignedRole}.jsonl`); + const workerOutputReport = `.cewp-worker-output/${assignedRole}-report.md`; + const workerOutputEvents = `.cewp-worker-output/${assignedRole}-events.jsonl`; return `# CEWP Dispatch Prompt - Worker @@ -1638,9 +1640,16 @@ ${markdownArray(task.verification)} - Do not automate terminal input. ## Required Outputs +Write inside your assigned worktree: +- ${workerOutputReport} +- ${workerOutputEvents} + +Do not write directly to: - ${relativeRunPath(runRoot, reportPath)} - ${relativeRunPath(runRoot, eventPath)} +The CLI will copy worker output into the run directory after execution. + ## Report Template \`\`\`md # Worker Report @@ -2162,6 +2171,11 @@ function getDispatchExecPreview(options) { console.log(` ${relativeRunPath(runRoot, preview.promptPath)}`); console.log(""); console.log("Expected outputs:"); + if (preview.task && preview.cwd) { + const workerOutput = getWorkerOutputPaths(preview.cwd, preview.role); + console.log(` Worker report source: ${path.relative(preview.cwd, workerOutput.reportPath).replace(/\\/g, "/")}`); + console.log(` Worker events source: ${path.relative(preview.cwd, workerOutput.eventsPath).replace(/\\/g, "/")}`); + } console.log(` Report: ${relativeRunPath(runRoot, preview.reportPath)}`); console.log(` Event log: ${relativeRunPath(runRoot, preview.eventPath)}`); console.log(` Last message: ${relativeRunPath(runRoot, preview.outputLastMessagePath)}`); @@ -2205,6 +2219,43 @@ function writeAdapterLog(filePath, value) { fs.writeFileSync(filePath, value || ""); } +function getWorkerOutputPaths(worktreePath, role) { + const outputRoot = path.join(worktreePath, ".cewp-worker-output"); + return { + outputRoot, + reportPath: path.join(outputRoot, `${role}-report.md`), + eventsPath: path.join(outputRoot, `${role}-events.jsonl`), + }; +} + +function copyWorkerOutputToRun({ runRoot, role, localReportPath, localEventsPath }) { + const reportPath = path.join(runRoot, "reports", `${role}-report.md`); + const eventPath = path.join(runRoot, "events", `${role}.jsonl`); + const copied = { + report: false, + events: false, + reportPath, + eventPath, + }; + + if (fs.existsSync(localReportPath)) { + fs.mkdirSync(path.dirname(reportPath), { recursive: true }); + fs.copyFileSync(localReportPath, reportPath); + copied.report = true; + } + + if (fs.existsSync(localEventsPath)) { + const eventContent = fs.readFileSync(localEventsPath, "utf8"); + if (eventContent.trim().length > 0) { + fs.mkdirSync(path.dirname(eventPath), { recursive: true }); + fs.appendFileSync(eventPath, eventContent.endsWith("\n") ? eventContent : `${eventContent}\n`); + copied.events = true; + } + } + + return copied; +} + function runCodexExec({ worktreePath, promptPath, outputLastMessagePath, timeoutSeconds }) { const prompt = fs.readFileSync(promptPath, "utf8"); return childProcess.spawnSync("codex", [ @@ -2276,16 +2327,27 @@ function runDispatchExecActual(options = {}) { writeAdapterLog(stdoutPath, execResult.stdout); writeAdapterLog(stderrPath, execResult.stderr); + const workerOutput = getWorkerOutputPaths(preview.cwd, options.role); + const copiedOutput = copyWorkerOutputToRun({ + runRoot, + role: options.role, + localReportPath: workerOutput.reportPath, + localEventsPath: workerOutput.eventsPath, + }); const statusLines = getGitStatusShort(preview.cwd); const changedFiles = statusLines.map(parseChangedFile); + const runtimeOutputFiles = changedFiles.filter(isWorkerRuntimeOutputPath); const scopeWarnings = findScopeWarnings(preview.task.id || "unknown-task", changedFiles, preview.task); const forbiddenWarnings = scopeWarnings.filter((warning) => warning.includes("changed forbidden file")); const outsideAllowedWarnings = scopeWarnings.filter((warning) => warning.includes("outside allowedFiles")); const reportExists = fs.existsSync(preview.reportPath); + const localReportExists = fs.existsSync(workerOutput.reportPath); + const localEventsExist = fs.existsSync(workerOutput.eventsPath); const lastMessageExists = fs.existsSync(preview.outputLastMessagePath); const timedOut = Boolean(execResult.error && execResult.error.code === "ETIMEDOUT"); const exitCode = typeof execResult.status === "number" ? execResult.status : 1; const failuresAfterExec = []; + const warningsAfterExec = []; if (timedOut) { failuresAfterExec.push(`codex exec timed out after ${options.timeoutSeconds}s.`); @@ -2298,8 +2360,14 @@ function runDispatchExecActual(options = {}) { failuresAfterExec.push(...outsideAllowedWarnings); failuresAfterExec.push(...forbiddenWarnings); - if (!reportExists) { - failuresAfterExec.push(`report missing: ${relativeRunPath(runRoot, preview.reportPath)}`); + if (!localReportExists) { + failuresAfterExec.push(`worker output report missing: ${path.relative(preview.cwd, workerOutput.reportPath).replace(/\\/g, "/")}`); + } else if (!reportExists) { + failuresAfterExec.push(`report copy missing: ${relativeRunPath(runRoot, preview.reportPath)}`); + } + + if (!localEventsExist) { + warningsAfterExec.push(`worker output events missing: ${path.relative(preview.cwd, workerOutput.eventsPath).replace(/\\/g, "/")}`); } if (!lastMessageExists) { @@ -2315,6 +2383,8 @@ function runDispatchExecActual(options = {}) { exitCode, status, taskId: preview.task.id, + copiedReport: copiedOutput.report, + copiedEvents: copiedOutput.events, }); console.log(""); @@ -2333,13 +2403,25 @@ function runDispatchExecActual(options = {}) { console.log(" none"); } else { for (const filePath of changedFiles) { + console.log(` ${filePath}${isWorkerRuntimeOutputPath(filePath) ? " (runtime output)" : ""}`); + } + } + console.log(""); + console.log("Runtime output:"); + if (runtimeOutputFiles.length === 0) { + console.log(" none tracked by git status"); + } else { + for (const filePath of runtimeOutputFiles) { console.log(` ${filePath}`); } } + console.log(`Worker report source: ${localReportExists ? path.relative(preview.cwd, workerOutput.reportPath).replace(/\\/g, "/") : "missing"}`); + console.log(`Worker events source: ${localEventsExist ? path.relative(preview.cwd, workerOutput.eventsPath).replace(/\\/g, "/") : "missing"}`); console.log(""); console.log(`Scope: ${outsideAllowedWarnings.length === 0 ? "OK" : "FAIL"}`); console.log(`Forbidden files: ${forbiddenWarnings.length === 0 ? "OK" : "FAIL"}`); - console.log(`Report: ${reportExists ? `found ${relativeRunPath(runRoot, preview.reportPath)}` : `missing ${relativeRunPath(runRoot, preview.reportPath)}`}`); + console.log(`Report: ${reportExists ? `copied to ${relativeRunPath(runRoot, preview.reportPath)}` : `missing ${relativeRunPath(runRoot, preview.reportPath)}`}`); + console.log(`Worker events: ${copiedOutput.events ? `appended to ${relativeRunPath(runRoot, preview.eventPath)}` : "not provided"}`); console.log(`Last message: ${lastMessageExists ? relativeRunPath(runRoot, preview.outputLastMessagePath) : "missing"}`); console.log(`Stdout log: ${relativeRunPath(runRoot, stdoutPath)}`); console.log(`Stderr log: ${relativeRunPath(runRoot, stderrPath)}`); @@ -2353,6 +2435,13 @@ function runDispatchExecActual(options = {}) { } } + if (warningsAfterExec.length > 0) { + console.log("Warnings:"); + for (const warning of warningsAfterExec) { + console.log(` - ${warning}`); + } + } + console.log(""); console.log("No merge/push/publish was performed."); @@ -2486,13 +2575,22 @@ function pathMatchesPattern(filePath, pattern) { return normalizedFile === normalizedPattern; } +function isWorkerRuntimeOutputPath(filePath) { + const normalizedFile = filePath.replace(/\\/g, "/"); + return normalizedFile === ".cewp-worker-output" || normalizedFile.startsWith(".cewp-worker-output/"); +} + function findScopeWarnings(taskId, changedFiles, task) { const warnings = []; const allowedFiles = Array.isArray(task.allowedFiles) ? task.allowedFiles : []; const forbiddenFiles = Array.isArray(task.forbiddenFiles) ? task.forbiddenFiles : []; for (const filePath of changedFiles) { - if (allowedFiles.length > 0 && !allowedFiles.some((pattern) => pathMatchesPattern(filePath, pattern))) { + if ( + allowedFiles.length > 0 && + !isWorkerRuntimeOutputPath(filePath) && + !allowedFiles.some((pattern) => pathMatchesPattern(filePath, pattern)) + ) { warnings.push(`${taskId} changed file outside allowedFiles: ${filePath}`); } diff --git a/docs/install.md b/docs/install.md index da1c5db..fa770a5 100644 --- a/docs/install.md +++ b/docs/install.md @@ -158,6 +158,8 @@ cewp run dispatch exec worker-a --adapter codex-exec --yes --timeout 120 This guarded execution mode runs `codex exec` only for worker roles, captures adapter output under `.cewp/runs//adapter-output/`, and performs post-checks for changed files, forbidden files, report existence, and exit code. Reviewer execution is not implemented yet, and merge/push/publish remain separate user-approved steps. +Worker reports are written inside the assigned worktree under `.cewp-worker-output/` so `codex exec` stays within its sandbox. After execution, the CLI copies `.cewp-worker-output/-report.md` into `.cewp/runs//reports/` and appends `.cewp-worker-output/-events.jsonl` when present. `.cewp-worker-output/` is runtime output and should not be committed. + `cewp run collect` creates a reviewer packet from local run state: ```bash From fbf4a1cffeee686c546260eb49d33effb3497edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuncay=20=C3=96lmez?= Date: Fri, 29 May 2026 18:55:06 +0300 Subject: [PATCH 4/6] feat: add guarded codex exec reviewer execution --- README.md | 8 +++ bin/cewp.js | 169 ++++++++++++++++++++++++++++++++++++++++++++---- docs/install.md | 8 +++ 3 files changed, 174 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 1454d33..cb3a6d9 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,14 @@ cewp run dispatch exec worker-a --adapter codex-exec --yes --timeout 120 For sandbox compatibility, worker reports are written inside the assigned worktree under `.cewp-worker-output/`. The CLI copies `.cewp-worker-output/-report.md` into `.cewp/runs//reports/` after execution and appends `.cewp-worker-output/-events.jsonl` when present. `.cewp-worker-output/` is runtime output and should not be committed. +Reviewer execution is also supported after `cewp run collect` creates a review packet: + +```bash +cewp run dispatch exec reviewer --adapter codex-exec --yes --timeout 120 +``` + +The reviewer runs inside `.cewp/runs//`, must write `reviews/reviewer-report.md`, and the report must contain `Decision: PASS | REQUEST_CHANGES | BLOCK`. It still does not merge, push, or publish. + Collect reviewer context into one local packet: ```bash diff --git a/bin/cewp.js b/bin/cewp.js index cdc65fb..737da08 100644 --- a/bin/cewp.js +++ b/bin/cewp.js @@ -70,6 +70,7 @@ Examples: cewp run dispatch start --run 20260528-232250 --dry-run cewp run dispatch exec worker-a --run 20260528-232250 --adapter codex-exec --dry-run cewp run dispatch exec worker-a --run 20260528-232250 --adapter codex-exec --yes --timeout 120 + cewp run dispatch exec reviewer --run 20260528-232250 --adapter codex-exec --yes --timeout 120 cewp run collect --run 20260528-232250 cewp run finalize --run 20260528-232250 --dry-run cewp run cleanup --run 20260528-232250 @@ -2059,14 +2060,19 @@ function getDispatchExecPreview(options) { const reviewPacketPath = path.join(runRoot, "review-packets", "review-packet.md"); const reportPath = path.join(runRoot, "reviews", "reviewer-report.md"); const eventPath = path.join(runRoot, "events", "reviewer.jsonl"); - const workdir = (runJson && runJson.repoRoot) || process.cwd(); + const workdir = runRoot; + const reportFiles = listFiles(path.join(runRoot, "reports"), ".md"); if (!fs.existsSync(promptPath)) { failures.push(`reviewer dispatch prompt missing: ${relativeRunPath(runRoot, promptPath)}`); } if (!fs.existsSync(reviewPacketPath)) { - warnings.push(`review packet missing: ${relativeRunPath(runRoot, reviewPacketPath)}`); + failures.push(`review packet missing: ${relativeRunPath(runRoot, reviewPacketPath)}. Run cewp run collect first.`); + } + + if (reportFiles.length === 0) { + failures.push("worker reports missing. Run worker execution before reviewer execution."); } if (!fs.existsSync(workdir)) { @@ -2080,8 +2086,9 @@ function getDispatchExecPreview(options) { reportPath, eventPath, outputLastMessagePath, - sandbox: "read-only", + sandbox: "workspace-write", reviewPacketPath, + reportFiles, }; } else if (taskEntries.length > 0) { const task = getWorkerTaskForRole(taskEntries, options.role); @@ -2161,9 +2168,9 @@ function getDispatchExecPreview(options) { } else { console.log("Reviewer:"); console.log(` Workdir: ${preview.cwd}`); - console.log(" Sandbox: read-only"); - console.log(" Note: reviewer report writing will require a future workspace-write execution mode."); + console.log(" Sandbox: workspace-write"); console.log(` Review packet: ${relativeRunPath(runRoot, preview.reviewPacketPath)}`); + console.log(` Worker reports: ${preview.reportFiles.length}`); console.log(""); } @@ -2256,14 +2263,14 @@ function copyWorkerOutputToRun({ runRoot, role, localReportPath, localEventsPath return copied; } -function runCodexExec({ worktreePath, promptPath, outputLastMessagePath, timeoutSeconds }) { +function runCodexExec({ worktreePath, promptPath, outputLastMessagePath, timeoutSeconds, sandbox = "workspace-write" }) { const prompt = fs.readFileSync(promptPath, "utf8"); return childProcess.spawnSync("codex", [ "exec", "--cd", worktreePath, "--sandbox", - "workspace-write", + sandbox, "--output-last-message", outputLastMessagePath, prompt, @@ -2276,6 +2283,145 @@ function runCodexExec({ worktreePath, promptPath, outputLastMessagePath, timeout }); } +function getRepoStatusForReviewer(repoRoot) { + if (!repoRoot || !fs.existsSync(repoRoot) || !isGitWorktreePath(repoRoot)) { + return []; + } + + return getGitStatusShort(repoRoot); +} + +function runDispatchReviewerExecActual(options, preflight) { + const { runId, runRoot, runJson, failures, warnings, preview } = preflight; + + if (failures.length > 0) { + console.log("CEWP Coordinator Mode codex-exec reviewer execution"); + console.log(`Run ID: ${runId}`); + console.log("Role: reviewer"); + console.log("Adapter: codex-exec"); + console.log(""); + console.log("Preflight: FAIL"); + console.log("Failures:"); + for (const failure of failures) { + console.log(` - ${failure}`); + } + console.log(""); + console.log("No processes were started."); + console.log("No merge/push/publish was performed."); + process.exitCode = 1; + return; + } + + const repoRoot = (runJson && runJson.repoRoot) || process.cwd(); + const repoStatusBefore = getRepoStatusForReviewer(repoRoot); + const adapterOutputRoot = path.join(runRoot, "adapter-output"); + fs.mkdirSync(adapterOutputRoot, { recursive: true }); + + console.log(""); + console.log("Executing codex exec reviewer..."); + const execResult = runCodexExec({ + worktreePath: preview.cwd, + promptPath: preview.promptPath, + outputLastMessagePath: preview.outputLastMessagePath, + timeoutSeconds: options.timeoutSeconds, + sandbox: "workspace-write", + }); + + const stdoutPath = path.join(adapterOutputRoot, "reviewer-stdout.log"); + const stderrPath = path.join(adapterOutputRoot, "reviewer-stderr.log"); + writeAdapterLog(stdoutPath, execResult.stdout); + writeAdapterLog(stderrPath, execResult.stderr); + + const reportExists = fs.existsSync(preview.reportPath); + const lastMessageExists = fs.existsSync(preview.outputLastMessagePath); + const eventExists = fs.existsSync(preview.eventPath); + const decision = reportExists ? findReviewerDecisionStrict(preview.reportPath) : undefined; + const timedOut = Boolean(execResult.error && execResult.error.code === "ETIMEDOUT"); + const exitCode = typeof execResult.status === "number" ? execResult.status : 1; + const repoStatusAfter = getRepoStatusForReviewer(repoRoot); + const repoChanged = repoStatusBefore.join("\n") !== repoStatusAfter.join("\n"); + const failuresAfterExec = []; + const warningsAfterExec = []; + + if (timedOut) { + failuresAfterExec.push(`codex exec timed out after ${options.timeoutSeconds}s.`); + } + + if (exitCode !== 0) { + failuresAfterExec.push(`codex exec exited with code ${exitCode}.`); + } + + if (!reportExists) { + failuresAfterExec.push(`reviewer report missing: ${relativeRunPath(runRoot, preview.reportPath)}`); + } else if (!decision) { + failuresAfterExec.push("reviewer report does not contain Decision: PASS | REQUEST_CHANGES | BLOCK"); + } + + if (!lastMessageExists) { + failuresAfterExec.push(`adapter output missing: ${relativeRunPath(runRoot, preview.outputLastMessagePath)}`); + } + + if (repoChanged) { + failuresAfterExec.push("public repo git status changed during reviewer execution."); + } + + if (!eventExists) { + warningsAfterExec.push(`reviewer event log missing: ${relativeRunPath(runRoot, preview.eventPath)}`); + } + + const status = failuresAfterExec.length > 0 ? "FAIL" : "PASS"; + appendRunEvent(runRoot, "cli", { + event: status === "PASS" ? "dispatch_exec_completed" : "dispatch_exec_failed", + runId, + adapter: "codex-exec", + role: "reviewer", + exitCode, + status, + decision: decision || "not_found", + }); + + console.log(""); + console.log("CEWP Coordinator Mode codex-exec reviewer execution"); + console.log(`Run ID: ${runId}`); + console.log("Role: reviewer"); + console.log("Adapter: codex-exec"); + console.log(""); + console.log(`Preflight: ${warnings.length > 0 ? "WARN" : "PASS"}`); + console.log(`Execution: ${exitCode === 0 && !timedOut ? "PASS" : "FAIL"}`); + console.log(`Exit code: ${exitCode}`); + console.log(`Timeout: ${options.timeoutSeconds}s`); + console.log(`Decision: ${decision || "not found"}`); + console.log(`Report: ${reportExists ? `found ${relativeRunPath(runRoot, preview.reportPath)}` : `missing ${relativeRunPath(runRoot, preview.reportPath)}`}`); + console.log(`Event log: ${eventExists ? relativeRunPath(runRoot, preview.eventPath) : "not provided"}`); + console.log(`Last message: ${lastMessageExists ? relativeRunPath(runRoot, preview.outputLastMessagePath) : "missing"}`); + console.log(`Stdout log: ${relativeRunPath(runRoot, stdoutPath)}`); + console.log(`Stderr log: ${relativeRunPath(runRoot, stderrPath)}`); + console.log(`Public repo changed: ${repoChanged ? "yes" : "no"}`); + console.log(""); + console.log(`Status: ${status}`); + + if (failuresAfterExec.length > 0) { + console.log("Reasons:"); + for (const failure of failuresAfterExec) { + console.log(` - ${failure}`); + } + } + + if (warningsAfterExec.length > 0) { + console.log("Warnings:"); + for (const warning of warningsAfterExec) { + console.log(` - ${warning}`); + } + } + + console.log(""); + console.log("No merge/push/publish was performed."); + + if (status === "FAIL") { + process.exitCode = 1; + } +} + function runDispatchExecActual(options = {}) { if (!options.adapter) { throw new Error("dispatch exec requires --adapter codex-exec."); @@ -2285,13 +2431,14 @@ function runDispatchExecActual(options = {}) { throw new Error(`Unsupported dispatch adapter: ${options.adapter}. Supported adapter: codex-exec.`); } - if (options.role === "reviewer") { - throw new Error("reviewer execution is not implemented yet. Use --dry-run/manual review."); - } - const result = getDispatchExecPreview({ ...options, dryRun: false, printPreview: false }); const { runId, runRoot, failures, warnings, preview } = result; + if (options.role === "reviewer") { + runDispatchReviewerExecActual(options, result); + return; + } + if (failures.length > 0) { console.log("CEWP Coordinator Mode codex-exec execution"); console.log(`Run ID: ${runId}`); diff --git a/docs/install.md b/docs/install.md index fa770a5..6e81ee7 100644 --- a/docs/install.md +++ b/docs/install.md @@ -160,6 +160,14 @@ This guarded execution mode runs `codex exec` only for worker roles, captures ad Worker reports are written inside the assigned worktree under `.cewp-worker-output/` so `codex exec` stays within its sandbox. After execution, the CLI copies `.cewp-worker-output/-report.md` into `.cewp/runs//reports/` and appends `.cewp-worker-output/-events.jsonl` when present. `.cewp-worker-output/` is runtime output and should not be committed. +Reviewer execution is guarded too: + +```bash +cewp run dispatch exec reviewer --adapter codex-exec --yes --timeout 120 +``` + +Run `cewp run collect` first. The reviewer runs inside `.cewp/runs//`, writes `reviews/reviewer-report.md`, and must include `Decision: PASS | REQUEST_CHANGES | BLOCK`. It does not merge, push, or publish. + `cewp run collect` creates a reviewer packet from local run state: ```bash From 5b13681ff26a6fb6767fd6f76657e1a86b0e5d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuncay=20=C3=96lmez?= Date: Fri, 29 May 2026 19:03:32 +0300 Subject: [PATCH 5/6] feat: add sequential codex exec workers command --- README.md | 8 ++++ bin/cewp.js | 116 ++++++++++++++++++++++++++++++++++++++++++++++-- docs/install.md | 8 ++++ 3 files changed, 128 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cb3a6d9..45ac275 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,14 @@ cewp run dispatch exec reviewer --adapter codex-exec --yes --timeout 120 The reviewer runs inside `.cewp/runs//`, must write `reviews/reviewer-report.md`, and the report must contain `Decision: PASS | REQUEST_CHANGES | BLOCK`. It still does not merge, push, or publish. +Run both workers sequentially: + +```bash +cewp run dispatch exec workers --adapter codex-exec --yes --timeout 120 +``` + +`workers` runs `worker-a` then `worker-b`. It is not parallel, does not run the reviewer, and stops before `worker-b` if `worker-a` fails. + Collect reviewer context into one local packet: ```bash diff --git a/bin/cewp.js b/bin/cewp.js index 737da08..bdd6b10 100644 --- a/bin/cewp.js +++ b/bin/cewp.js @@ -38,6 +38,7 @@ Usage: cewp run dispatch start --dry-run cewp run dispatch exec --adapter codex-exec --dry-run cewp run dispatch exec --adapter codex-exec --yes [--timeout ] + cewp run dispatch exec workers --adapter codex-exec --yes [--timeout ] cewp run collect cewp run finalize [--dry-run] cewp run cleanup [--yes] @@ -70,6 +71,7 @@ Examples: cewp run dispatch start --run 20260528-232250 --dry-run cewp run dispatch exec worker-a --run 20260528-232250 --adapter codex-exec --dry-run cewp run dispatch exec worker-a --run 20260528-232250 --adapter codex-exec --yes --timeout 120 + cewp run dispatch exec workers --run 20260528-232250 --adapter codex-exec --yes --timeout 120 cewp run dispatch exec reviewer --run 20260528-232250 --adapter codex-exec --yes --timeout 120 cewp run collect --run 20260528-232250 cewp run finalize --run 20260528-232250 --dry-run @@ -2309,7 +2311,7 @@ function runDispatchReviewerExecActual(options, preflight) { console.log("No processes were started."); console.log("No merge/push/publish was performed."); process.exitCode = 1; - return; + return "FAIL"; } const repoRoot = (runJson && runJson.repoRoot) || process.cwd(); @@ -2420,6 +2422,8 @@ function runDispatchReviewerExecActual(options, preflight) { if (status === "FAIL") { process.exitCode = 1; } + + return status; } function runDispatchExecActual(options = {}) { @@ -2435,8 +2439,7 @@ function runDispatchExecActual(options = {}) { const { runId, runRoot, failures, warnings, preview } = result; if (options.role === "reviewer") { - runDispatchReviewerExecActual(options, result); - return; + return runDispatchReviewerExecActual(options, result); } if (failures.length > 0) { @@ -2454,7 +2457,7 @@ function runDispatchExecActual(options = {}) { console.log("No processes were started."); console.log("No merge/push/publish was performed."); process.exitCode = 1; - return; + return "FAIL"; } const adapterOutputRoot = path.join(runRoot, "adapter-output"); @@ -2595,6 +2598,101 @@ function runDispatchExecActual(options = {}) { if (status === "FAIL") { process.exitCode = 1; } + + return status; +} + +function runDispatchExecWorkersDryRun(options = {}) { + const roles = ["worker-a", "worker-b"]; + let hasFailure = false; + + console.log("CEWP Coordinator Mode codex-exec workers dry-run"); + console.log("Mode: sequential preview"); + console.log(""); + console.log("Worker order:"); + console.log(" 1. worker-a"); + console.log(" 2. worker-b"); + console.log(""); + + for (const role of roles) { + const result = getDispatchExecPreview({ ...options, role, dryRun: true }); + if (result.failures.length > 0) { + hasFailure = true; + } + console.log(""); + } + + console.log("No processes were started."); + console.log("No files were changed."); + + if (hasFailure) { + process.exitCode = 1; + } +} + +function runDispatchExecWorkersActual(options = {}) { + if (!options.adapter) { + throw new Error("dispatch exec requires --adapter codex-exec."); + } + + if (options.adapter !== "codex-exec") { + throw new Error(`Unsupported dispatch adapter: ${options.adapter}. Supported adapter: codex-exec.`); + } + + const { runId } = findRun(options); + const results = []; + + console.log("CEWP Coordinator Mode codex-exec workers execution"); + console.log(`Run ID: ${runId}`); + console.log("Adapter: codex-exec"); + console.log("Mode: sequential"); + console.log(""); + console.log("Worker order:"); + console.log(" 1. worker-a"); + console.log(" 2. worker-b"); + console.log(""); + + for (const role of ["worker-a", "worker-b"]) { + const previousExitCode = process.exitCode; + process.exitCode = undefined; + const status = runDispatchExecActual({ ...options, role, yes: true, dryRun: false }); + results.push({ role, status: status || "FAIL" }); + + if (status !== "PASS") { + process.exitCode = 1; + if (role === "worker-a") { + results.push({ role: "worker-b", status: "SKIPPED" }); + } + if (previousExitCode) { + process.exitCode = previousExitCode; + } + break; + } + } + + const overall = results.some((result) => result.status === "FAIL") ? "FAIL" : "PASS"; + + console.log(""); + console.log("CEWP Coordinator Mode codex-exec workers summary"); + for (const result of results) { + console.log(`${result.role}: ${result.status}`); + } + console.log(""); + console.log(`Overall: ${overall}`); + + if (overall === "PASS") { + console.log(""); + console.log("Next:"); + console.log(` cewp run collect --run ${runId}`); + console.log(` cewp run dispatch exec reviewer --run ${runId} --adapter codex-exec --yes`); + } else { + const failed = results.find((result) => result.status === "FAIL"); + console.log(`Reason: ${failed ? `${failed.role} failed post-check` : "worker execution failed"}`); + process.exitCode = 1; + } + + console.log(""); + console.log("No reviewer execution, merge, push, or publish was performed."); } function runDispatchExec(options = {}) { @@ -2606,6 +2704,16 @@ function runDispatchExec(options = {}) { throw new Error("dispatch exec requires --dry-run or --yes."); } + if (options.role === "workers") { + if (options.dryRun) { + runDispatchExecWorkersDryRun(options); + return; + } + + runDispatchExecWorkersActual(options); + return; + } + if (options.dryRun) { const result = getDispatchExecPreview(options); if (result.failures.length > 0) { diff --git a/docs/install.md b/docs/install.md index 6e81ee7..763e4ee 100644 --- a/docs/install.md +++ b/docs/install.md @@ -168,6 +168,14 @@ cewp run dispatch exec reviewer --adapter codex-exec --yes --timeout 120 Run `cewp run collect` first. The reviewer runs inside `.cewp/runs//`, writes `reviews/reviewer-report.md`, and must include `Decision: PASS | REQUEST_CHANGES | BLOCK`. It does not merge, push, or publish. +To run both workers sequentially: + +```bash +cewp run dispatch exec workers --adapter codex-exec --yes --timeout 120 +``` + +This runs `worker-a` then `worker-b`, never in parallel. If `worker-a` fails, `worker-b` is skipped. Reviewer execution remains a separate command. + `cewp run collect` creates a reviewer packet from local run state: ```bash From 0a86b8ee6e54cdea7a1bd325ec964d9fdff65f89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuncay=20=C3=96lmez?= Date: Fri, 29 May 2026 19:18:20 +0300 Subject: [PATCH 6/6] feat: add codex exec dispatch pipeline --- README.md | 8 ++ bin/cewp.js | 275 +++++++++++++++++++++++++++++++++++++++++++++--- docs/install.md | 8 ++ 3 files changed, 278 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 45ac275..e87e309 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,14 @@ cewp run dispatch exec workers --adapter codex-exec --yes --timeout 120 `workers` runs `worker-a` then `worker-b`. It is not parallel, does not run the reviewer, and stops before `worker-b` if `worker-a` fails. +Run the guarded sequential dispatch pipeline: + +```bash +cewp run dispatch pipeline --adapter codex-exec --yes --timeout 120 +``` + +`pipeline` runs dispatch check, refreshes dispatch prompts, executes workers sequentially, collects a review packet, and executes the reviewer. It does not finalize, clean up, merge, push, or publish; finalize remains a separate user command. + Collect reviewer context into one local packet: ```bash diff --git a/bin/cewp.js b/bin/cewp.js index bdd6b10..4b1676a 100644 --- a/bin/cewp.js +++ b/bin/cewp.js @@ -39,6 +39,7 @@ Usage: cewp run dispatch exec --adapter codex-exec --dry-run cewp run dispatch exec --adapter codex-exec --yes [--timeout ] cewp run dispatch exec workers --adapter codex-exec --yes [--timeout ] + cewp run dispatch pipeline --adapter codex-exec --yes [--timeout ] cewp run collect cewp run finalize [--dry-run] cewp run cleanup [--yes] @@ -73,6 +74,8 @@ Examples: cewp run dispatch exec worker-a --run 20260528-232250 --adapter codex-exec --yes --timeout 120 cewp run dispatch exec workers --run 20260528-232250 --adapter codex-exec --yes --timeout 120 cewp run dispatch exec reviewer --run 20260528-232250 --adapter codex-exec --yes --timeout 120 + cewp run dispatch pipeline --run 20260528-232250 --adapter codex-exec --yes --timeout 120 + cewp run dispatch pipeline --run 20260528-232250 --adapter codex-exec --dry-run cewp run collect --run 20260528-232250 cewp run finalize --run 20260528-232250 --dry-run cewp run cleanup --run 20260528-232250 @@ -1335,6 +1338,13 @@ function addDispatchCheck(checks, level, message) { checks.push({ level, message }); } +function shouldIgnorePromptMissingForPipeline(options, promptPath, role, runRoot) { + return Boolean( + options.ignoreMissingDispatchPrompts && + promptPath === getPromptPath(runRoot, role) + ); +} + function getTaskReadinessStatus(levels) { if (levels.includes("fail")) { return "FAIL"; @@ -1389,18 +1399,20 @@ function runDispatchCheck(options = {}) { for (const role of supportedWorkers) { const promptPath = getPromptPath(runRoot, role); + const ignorePromptMissing = shouldIgnorePromptMissingForPipeline(options, promptPath, role, runRoot); addDispatchCheck( checks, - fs.existsSync(promptPath) ? "ok" : "fail", - `${role} prompt ${fs.existsSync(promptPath) ? "found" : `missing: ${relativeRunPath(runRoot, promptPath)}`}`, + fs.existsSync(promptPath) || ignorePromptMissing ? "ok" : "fail", + `${role} prompt ${fs.existsSync(promptPath) ? "found" : ignorePromptMissing ? "will be generated by dispatch prompts" : `missing: ${relativeRunPath(runRoot, promptPath)}`}`, ); } const reviewerPromptPath = getPromptPath(runRoot, "reviewer"); + const ignoreReviewerPromptMissing = shouldIgnorePromptMissingForPipeline(options, reviewerPromptPath, "reviewer", runRoot); addDispatchCheck( checks, - fs.existsSync(reviewerPromptPath) ? "ok" : "fail", - `reviewer prompt ${fs.existsSync(reviewerPromptPath) ? "found" : `missing: ${relativeRunPath(runRoot, reviewerPromptPath)}`}`, + fs.existsSync(reviewerPromptPath) || ignoreReviewerPromptMissing ? "ok" : "fail", + `reviewer prompt ${fs.existsSync(reviewerPromptPath) ? "found" : ignoreReviewerPromptMissing ? "will be generated by dispatch prompts" : `missing: ${relativeRunPath(runRoot, reviewerPromptPath)}`}`, ); const reviewPacketPath = path.join(runRoot, "review-packets", "review-packet.md"); @@ -1501,8 +1513,8 @@ function runDispatchCheck(options = {}) { } const promptPath = assignedRole ? getPromptPath(runRoot, assignedRole) : undefined; - if (promptPath && fs.existsSync(promptPath)) { - addDispatchCheck(checks, "ok", `${taskId} prompt file found`); + if (promptPath && (fs.existsSync(promptPath) || shouldIgnorePromptMissingForPipeline(options, promptPath, assignedRole, runRoot))) { + addDispatchCheck(checks, "ok", `${taskId} prompt file ${fs.existsSync(promptPath) ? "found" : "will be generated by dispatch prompts"}`); addTaskLevel("ok"); } else { addDispatchCheck(checks, "fail", `${taskId} prompt file missing`); @@ -1540,7 +1552,7 @@ function runDispatchCheck(options = {}) { } const reviewerLevels = []; - reviewerLevels.push(fs.existsSync(reviewerPromptPath) ? "ok" : "fail"); + reviewerLevels.push(fs.existsSync(reviewerPromptPath) || ignoreReviewerPromptMissing ? "ok" : "fail"); reviewerLevels.push(fs.existsSync(reviewPacketPath) ? "ok" : "warn"); reviewerLevels.push(reviewerReportPath ? "ok" : "fail"); reviewerLevels.push(reviewerEventPath ? "ok" : "fail"); @@ -1588,6 +1600,8 @@ function runDispatchCheck(options = {}) { if (status === "FAIL") { process.exitCode = 1; } + + return status; } function markdownArray(value) { @@ -1818,6 +1832,12 @@ function runDispatchPrompts(options = {}) { console.log(` Review with: cewp run dispatch check --run ${runId}`); console.log(" Paste each dispatch prompt into the matching Codex session manually."); console.log(" This command did not start agents."); + + return { + runId, + created, + warnings, + }; } function runDispatchStart(options = {}) { @@ -2054,7 +2074,11 @@ function getDispatchExecPreview(options) { } if (!fs.existsSync(dispatchPromptsRoot)) { - failures.push(`dispatch-prompts directory missing. Run: cewp run dispatch prompts --run ${runId}`); + if (options.ignoreMissingDispatchPrompts) { + warnings.push(`dispatch-prompts directory missing; pipeline will run dispatch prompts before execution.`); + } else { + failures.push(`dispatch-prompts directory missing. Run: cewp run dispatch prompts --run ${runId}`); + } } if (options.role === "reviewer") { @@ -2066,15 +2090,27 @@ function getDispatchExecPreview(options) { const reportFiles = listFiles(path.join(runRoot, "reports"), ".md"); if (!fs.existsSync(promptPath)) { - failures.push(`reviewer dispatch prompt missing: ${relativeRunPath(runRoot, promptPath)}`); + if (options.ignoreMissingDispatchPrompts) { + warnings.push(`reviewer dispatch prompt missing; pipeline will create it before reviewer execution.`); + } else { + failures.push(`reviewer dispatch prompt missing: ${relativeRunPath(runRoot, promptPath)}`); + } } if (!fs.existsSync(reviewPacketPath)) { - failures.push(`review packet missing: ${relativeRunPath(runRoot, reviewPacketPath)}. Run cewp run collect first.`); + if (options.ignoreMissingReviewInputs) { + warnings.push(`review packet missing; pipeline will run collect before reviewer execution.`); + } else { + failures.push(`review packet missing: ${relativeRunPath(runRoot, reviewPacketPath)}. Run cewp run collect first.`); + } } if (reportFiles.length === 0) { - failures.push("worker reports missing. Run worker execution before reviewer execution."); + if (options.ignoreMissingReviewInputs) { + warnings.push("worker reports missing; pipeline will run workers before reviewer execution."); + } else { + failures.push("worker reports missing. Run worker execution before reviewer execution."); + } } if (!fs.existsSync(workdir)) { @@ -2115,7 +2151,11 @@ function getDispatchExecPreview(options) { } if (!fs.existsSync(promptPath)) { - failures.push(`${taskId} dispatch prompt missing: ${relativeRunPath(runRoot, promptPath)}`); + if (options.ignoreMissingDispatchPrompts) { + warnings.push(`${taskId} dispatch prompt missing; pipeline will create it before worker execution.`); + } else { + failures.push(`${taskId} dispatch prompt missing: ${relativeRunPath(runRoot, promptPath)}`); + } } preview = { @@ -2628,6 +2668,8 @@ function runDispatchExecWorkersDryRun(options = {}) { if (hasFailure) { process.exitCode = 1; } + + return hasFailure ? "FAIL" : "PASS"; } function runDispatchExecWorkersActual(options = {}) { @@ -2641,6 +2683,7 @@ function runDispatchExecWorkersActual(options = {}) { const { runId } = findRun(options); const results = []; + let overall = "PASS"; console.log("CEWP Coordinator Mode codex-exec workers execution"); console.log(`Run ID: ${runId}`); @@ -2666,11 +2709,12 @@ function runDispatchExecWorkersActual(options = {}) { if (previousExitCode) { process.exitCode = previousExitCode; } + overall = "FAIL"; break; } } - const overall = results.some((result) => result.status === "FAIL") ? "FAIL" : "PASS"; + overall = results.some((result) => result.status === "FAIL") ? "FAIL" : overall; console.log(""); console.log("CEWP Coordinator Mode codex-exec workers summary"); @@ -2693,6 +2737,11 @@ function runDispatchExecWorkersActual(options = {}) { console.log(""); console.log("No reviewer execution, merge, push, or publish was performed."); + + return { + overall, + results, + }; } function runDispatchExec(options = {}) { @@ -2725,6 +2774,195 @@ function runDispatchExec(options = {}) { runDispatchExecActual(options); } +function validateCodexExecAdapter(options) { + if (!options.adapter) { + throw new Error("dispatch pipeline requires --adapter codex-exec."); + } + + if (options.adapter !== "codex-exec") { + throw new Error(`Unsupported dispatch adapter: ${options.adapter}. Supported adapter: codex-exec.`); + } +} + +function runDispatchPipelineDryRun(options = {}) { + validateCodexExecAdapter(options); + + const { runId, runRoot } = findRun(options); + const dispatchPromptsRoot = path.join(runRoot, "dispatch-prompts"); + const reviewPacketPath = path.join(runRoot, "review-packets", "review-packet.md"); + const promptCheckOptions = fs.existsSync(dispatchPromptsRoot) + ? options + : { ...options, ignoreMissingDispatchPrompts: true }; + + console.log("CEWP Coordinator Mode dispatch pipeline"); + console.log(`Run ID: ${runId}`); + console.log("Adapter: codex-exec"); + console.log("Mode: dry-run sequential preview"); + console.log(""); + + const previousExitCode = process.exitCode; + process.exitCode = undefined; + const checkStatus = runDispatchCheck(promptCheckOptions); + const checkFailed = process.exitCode === 1 || checkStatus === "FAIL"; + process.exitCode = previousExitCode; + console.log(""); + + console.log("Pipeline preview:"); + console.log(` Step 1 dispatch check: ${checkStatus || "UNKNOWN"}`); + console.log(` Step 2 dispatch prompts: ${fs.existsSync(dispatchPromptsRoot) ? "would refresh existing prompt bundles" : "would create prompt bundles"}`); + console.log(" Step 3 workers: sequential preview follows"); + const previewOptions = { + ...options, + ignoreMissingDispatchPrompts: true, + ignoreMissingReviewInputs: true, + }; + const workersPreviewStatus = runDispatchExecWorkersDryRun({ ...previewOptions, dryRun: true, role: "workers" }); + console.log(` Step 4 collect: would write ${relativeRunPath(runRoot, reviewPacketPath)}`); + console.log(" Step 5 reviewer: preview follows"); + const reviewerPreview = getDispatchExecPreview({ ...previewOptions, role: "reviewer", dryRun: true }); + console.log(""); + console.log("Overall dry-run:"); + console.log(checkFailed || workersPreviewStatus === "FAIL" || reviewerPreview.failures.length > 0 ? " FAIL" : " PASS"); + console.log(""); + console.log("No processes were started."); + console.log("No files were changed."); + + if (checkFailed || workersPreviewStatus === "FAIL" || reviewerPreview.failures.length > 0) { + process.exitCode = 1; + } +} + +function runDispatchPipelineActual(options = {}) { + validateCodexExecAdapter(options); + + const { runId, runRoot } = findRun(options); + const steps = []; + let decision = "not found"; + let packetPath; + const dispatchPromptsRoot = path.join(runRoot, "dispatch-prompts"); + const promptCheckOptions = fs.existsSync(dispatchPromptsRoot) + ? options + : { ...options, ignoreMissingDispatchPrompts: true }; + + console.log("CEWP Coordinator Mode dispatch pipeline"); + console.log(`Run ID: ${runId}`); + console.log("Adapter: codex-exec"); + console.log("Mode: sequential"); + console.log(""); + + const previousExitCode = process.exitCode; + process.exitCode = undefined; + const checkStatus = runDispatchCheck(promptCheckOptions); + const checkFailed = process.exitCode === 1 || checkStatus === "FAIL"; + process.exitCode = previousExitCode; + steps.push({ name: "dispatch check", status: checkFailed ? "FAIL" : checkStatus }); + if (checkFailed) { + printDispatchPipelineSummary({ runId, steps, decision, overall: "FAIL", reason: "dispatch check failed" }); + process.exitCode = 1; + return; + } + + let promptsResult; + try { + promptsResult = runDispatchPrompts(options); + steps.push({ name: "dispatch prompts", status: "PASS" }); + } catch (error) { + steps.push({ name: "dispatch prompts", status: "FAIL" }); + printDispatchPipelineSummary({ runId, steps, decision, overall: "FAIL", reason: error.message }); + process.exitCode = 1; + return; + } + + const workersResult = runDispatchExecWorkersActual(options); + steps.push({ name: "workers", status: workersResult.overall, details: workersResult.results }); + if (workersResult.overall !== "PASS") { + printDispatchPipelineSummary({ runId, steps, decision, overall: "FAIL", reason: "workers failed" }); + process.exitCode = 1; + return; + } + + let collectResult; + try { + collectResult = runCollect(options); + packetPath = collectResult.packetPath; + steps.push({ name: "collect", status: "PASS" }); + } catch (error) { + steps.push({ name: "collect", status: "FAIL" }); + printDispatchPipelineSummary({ runId, steps, decision, overall: "FAIL", reason: error.message }); + process.exitCode = 1; + return; + } + + const reviewerStatus = runDispatchReviewerExecActual(options, getDispatchExecPreview({ ...options, role: "reviewer", printPreview: false })); + steps.push({ name: "reviewer", status: reviewerStatus }); + try { + decision = findReviewerDecisionStrict(path.join(findRun(options).runRoot, "reviews", "reviewer-report.md")) || "not found"; + } catch { + decision = "not found"; + } + + const overall = reviewerStatus === "PASS" ? "PASS" : "FAIL"; + printDispatchPipelineSummary({ runId, steps, decision, overall, packetPath }); + + if (overall !== "PASS") { + process.exitCode = 1; + } +} + +function printDispatchPipelineSummary({ runId, steps, decision, overall, reason, packetPath }) { + console.log(""); + console.log("CEWP Coordinator Mode dispatch pipeline summary"); + steps.forEach((step, index) => { + console.log(`Step ${index + 1}/${steps.length} ${step.name}: ${step.status}`); + if (step.details) { + for (const detail of step.details) { + console.log(` ${detail.role}: ${detail.status}`); + } + } + }); + + if (packetPath) { + console.log(`Review packet: ${packetPath}`); + } + + if (decision && decision !== "not found") { + console.log(`Reviewer decision: ${decision}`); + } + + console.log(""); + console.log(`Overall: ${overall}`); + if (reason) { + console.log(`Reason: ${reason}`); + } + + if (overall === "PASS") { + console.log(""); + console.log("Next:"); + console.log(` cewp run finalize --run ${runId} --dry-run`); + console.log(` cewp run finalize --run ${runId}`); + } + + console.log(""); + console.log("No finalize, cleanup, merge, push, or publish was performed."); +} + +function runDispatchPipeline(options = {}) { + if (options.dryRun && options.yes) { + throw new Error("Use either --dry-run or --yes, not both."); + } + + if (!options.dryRun && !options.yes) { + throw new Error("dispatch pipeline requires --dry-run or --yes."); + } + + if (options.dryRun) { + runDispatchPipelineDryRun(options); + return; + } + + runDispatchPipelineActual(options); +} + function getWorktreePreflightErrors(plans) { const errors = []; const seenPaths = new Map(); @@ -3238,6 +3476,12 @@ function runCollect(options = {}) { console.log(`Run ID: ${runId}`); console.log(`Packet: ${packetPath}`); console.log(`Warnings: ${warnings.length}`); + + return { + runId, + packetPath, + warnings, + }; } function readRequiredJson(filePath, label) { @@ -3777,6 +4021,11 @@ function runCommand(options) { return; } + if (options.subcommand === "dispatch" && options.action === "pipeline") { + runDispatchPipeline(options); + return; + } + throw new Error(`Unsupported run command: ${options.subcommand}`); } diff --git a/docs/install.md b/docs/install.md index 763e4ee..2e6c66c 100644 --- a/docs/install.md +++ b/docs/install.md @@ -176,6 +176,14 @@ cewp run dispatch exec workers --adapter codex-exec --yes --timeout 120 This runs `worker-a` then `worker-b`, never in parallel. If `worker-a` fails, `worker-b` is skipped. Reviewer execution remains a separate command. +The guarded sequential pipeline can run the manual dispatch chain: + +```bash +cewp run dispatch pipeline --adapter codex-exec --yes --timeout 120 +``` + +Pipeline runs check, prompts, sequential workers, collect, and reviewer execution. It does not auto-finalize, clean up, merge, push, or publish. + `cewp run collect` creates a reviewer packet from local run state: ```bash