Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ 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.
`--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 both working tree changes and committed branch changes from the registered `baseCommit` 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/<role>-report.md` into `.cewp/runs/<run-id>/reports/` after execution and appends `.cewp-worker-output/<role>-events.jsonl` when present. `.cewp-worker-output/` is runtime output and should not be committed.

Expand Down
85 changes: 84 additions & 1 deletion bin/cewp.js
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,48 @@ function getGitStatusShort(worktreePath) {
.filter(Boolean);
}

function getGitHeadCommit(repoRoot) {
const result = getGitOutput(["rev-parse", "HEAD"], repoRoot);

if (result.status !== 0) {
throw new Error(`Failed to read git HEAD for ${repoRoot}: ${(result.stderr || result.stdout || "").trim()}`);
}

return result.stdout.trim();
}

function getCommittedChangedFiles(worktreePath, baseCommit) {
const result = getGitOutput(["diff", "--name-only", `${baseCommit}...HEAD`], worktreePath);

if (result.status !== 0) {
throw new Error(`Failed to inspect committed changes for ${worktreePath}: ${(result.stderr || result.stdout || "").trim()}`);
}

return result.stdout
.split(/\r?\n/)
.map((line) => line.trim().replace(/\\/g, "/"))
.filter(Boolean);
}

function uniqueFileList(files) {
const seen = new Set();
const output = [];

for (const file of files) {
const normalized = String(file || "").replace(/\\/g, "/");
const key = process.platform === "win32" ? normalized.toLowerCase() : normalized;

if (!normalized || seen.has(key)) {
continue;
}

seen.add(key);
output.push(normalized);
}

return output;
}

function getGitBranchName(worktreePath) {
const result = getGitOutput(["branch", "--show-current"], worktreePath);

Expand Down Expand Up @@ -559,6 +601,7 @@ function buildWorktreePlans(runId, runRoot) {
const runJson = readJsonIfExists(path.join(runRoot, "run.json"));
const repoRoot = (runJson && runJson.repoRoot) || process.cwd();
const taskEntries = readTasks(runRoot);
const baseCommit = getGitHeadCommit(repoRoot);

const plans = taskEntries.map(({ task }) => {
if (!task.id) {
Expand All @@ -574,6 +617,7 @@ function buildWorktreePlans(runId, runRoot) {
branch,
targetWorktree,
resolvedPath,
baseCommit,
targetExists: fs.existsSync(resolvedPath),
branchExists: branchExists(repoRoot, branch),
};
Expand Down Expand Up @@ -2162,6 +2206,10 @@ function getDispatchExecPreview(options) {
failures.push(`${taskId} path is not a git worktree: ${worktree.path}`);
}

if (worktree && !worktree.baseCommit) {
warnings.push(`${taskId} worktree registry missing baseCommit; committed diff post-check will fail safely.`);
}

if (!fs.existsSync(promptPath)) {
if (options.ignoreMissingDispatchPrompts) {
warnings.push(`${taskId} dispatch prompt missing; pipeline will create it before worker execution.`);
Expand Down Expand Up @@ -2256,6 +2304,7 @@ function getDispatchExecPreview(options) {

console.log("Post-checks:");
console.log(" git status --short");
console.log(" git diff --name-only <baseCommit>...HEAD");
console.log(" allowedFiles / forbiddenFiles");
console.log(" report exists");
console.log(" adapter output exists");
Expand Down Expand Up @@ -2537,7 +2586,17 @@ function runDispatchExecActual(options = {}) {
localEventsPath: workerOutput.eventsPath,
});
const statusLines = getGitStatusShort(preview.cwd);
const changedFiles = statusLines.map(parseChangedFile);
const statusChangedFiles = statusLines.map(parseChangedFile);
let committedChangedFiles = [];
let committedDiffError;
if (preview.worktree.baseCommit) {
try {
committedChangedFiles = getCommittedChangedFiles(preview.cwd, preview.worktree.baseCommit);
} catch (error) {
committedDiffError = error;
}
}
const changedFiles = uniqueFileList([...statusChangedFiles, ...committedChangedFiles]);
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"));
Expand All @@ -2559,6 +2618,14 @@ function runDispatchExecActual(options = {}) {
failuresAfterExec.push(`codex exec exited with code ${exitCode}.`);
}

if (!preview.worktree.baseCommit) {
failuresAfterExec.push("worktree registry missing baseCommit; committed branch scope check could not run.");
}

if (committedDiffError) {
failuresAfterExec.push(committedDiffError.message);
}

failuresAfterExec.push(...outsideAllowedWarnings);
failuresAfterExec.push(...forbiddenWarnings);

Expand Down Expand Up @@ -2609,6 +2676,19 @@ function runDispatchExecActual(options = {}) {
}
}
console.log("");
console.log("Committed changes:");
if (!preview.worktree.baseCommit) {
console.log(" skipped: worktrees.json entry missing baseCommit");
} else if (committedDiffError) {
console.log(` failed: ${committedDiffError.message}`);
} else if (committedChangedFiles.length === 0) {
console.log(" none");
} else {
for (const filePath of committedChangedFiles) {
console.log(` ${filePath}${isWorkerRuntimeOutputPath(filePath) ? " (runtime output)" : ""}`);
}
}
console.log("");
console.log("Runtime output:");
if (runtimeOutputFiles.length === 0) {
console.log(" none tracked by git status");
Expand Down Expand Up @@ -3311,6 +3391,7 @@ function writeWorktreesRegistry(runRoot, runId, created) {
branch: entry.branch,
path: entry.resolvedPath,
status: "created",
baseCommit: entry.baseCommit,
})),
});
}
Expand Down Expand Up @@ -4210,6 +4291,7 @@ function runWorktreesCreate(options = {}) {
taskId: plan.task.id,
branch: plan.branch,
path: plan.resolvedPath,
baseCommit: plan.baseCommit,
})),
});

Expand All @@ -4218,6 +4300,7 @@ function runWorktreesCreate(options = {}) {
console.log(` ${plan.task.id}: created`);
console.log(` branch: ${plan.branch}`);
console.log(` path: ${plan.resolvedPath}`);
console.log(` baseCommit: ${plan.baseCommit}`);
}
console.log("");
console.log("Next:");
Expand Down
2 changes: 1 addition & 1 deletion docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ With explicit user approval, the adapter can run one worker at a time:
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/<run-id>/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.
This guarded execution mode runs `codex exec` only for worker roles, captures adapter output under `.cewp/runs/<run-id>/adapter-output/`, and performs post-checks for working tree changes, committed branch changes from the registered `baseCommit`, 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/<role>-report.md` into `.cewp/runs/<run-id>/reports/` and appends `.cewp-worker-output/<role>-events.jsonl` when present. `.cewp-worker-output/` is runtime output and should not be committed.

Expand Down
Loading