fix: review fires for all tasks — commit then review committed diff, merge on pass

Previous flow reviewed uncommitted changes, so when a task agent
self-committed its work the review was silently skipped (no uncommitted
changes → review loop body never entered). This caused reviews to fire
inconsistently across tasks.

New review-gated flow (when autoReview is on):
1. Execute task
2. Ensure committed — commit session fallback when the agent didn't
   self-commit (handles both self-commit and no-commit agents)
3. Review the COMPLETE task diff (baseRef..HEAD) captured before
   execution, so the reviewer sees all commits not just the latest
4. On fail → re-execute with feedback → commit → re-review (same
   baseRef, so reviewer sees complete state including fixes)
5. On pass → merge worktree (all changes already committed)

Commit is now mandated when autoReview is on (autoCommit forced true,
not asked at startup). autoCommit only asked when autoReview is off.

Add captureGitHead + getCommitRangeDiff helpers to utils.ts. Switch
review prompt from buildReviewPromptUncommitted to buildReviewPrompt
(reviewing committed changes, not uncommitted).
This commit is contained in:
2026-07-22 18:29:29 -04:00
parent 0ef540ed47
commit 74c9ead7af
5 changed files with 200 additions and 98 deletions

View File

@@ -807,3 +807,76 @@ export function getLatestCommitDiff(
return null;
}
}
/**
* Capture the current HEAD commit SHA. Returns the full 40-char SHA, or
* undefined when not a git repo / git unavailable. Used to snapshot the
* worktree HEAD before a task runs so the review can diff the complete task
* output (baseRef..HEAD) — including any commits the task agent makes.
*/
export function captureGitHead(projectDir: string): string | undefined {
const { execSync } = require("node:child_process");
try {
const sha = execSync("git rev-parse HEAD", {
cwd: projectDir,
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
}).trim();
// Guard against injection — only accept hex SHAs.
return /^[0-9a-f]{7,40}$/i.test(sha) ? sha : undefined;
} catch {
return undefined;
}
}
/**
* Get the diff from `baseRef` to HEAD — the complete set of committed changes
* made since the base reference. Used by the review-gated loop so the reviewer
* sees the full task diff (all commits, not just the latest) across execution
* attempts and re-execution fixes. `baseRef` must be a validated hex SHA from
* captureGitHead(). Returns the short HEAD hash, HEAD subject, and range diff,
* or null when git is unavailable / baseRef is invalid / no changes exist.
*/
export function getCommitRangeDiff(
projectDir: string,
baseRef: string,
): { hash: string; subject: string; diff: string } | null {
const { execSync } = require("node:child_process");
// Only pass validated hex SHAs to the shell.
if (!/^[0-9a-f]{7,40}$/i.test(baseRef)) return null;
try {
execSync("git rev-parse --git-dir", {
cwd: projectDir,
stdio: "pipe",
});
} catch {
return null;
}
try {
const hash = execSync("git rev-parse --short HEAD", {
cwd: projectDir,
encoding: "utf-8",
}).trim();
const subject = execSync("git log -1 --format=%s", {
cwd: projectDir,
encoding: "utf-8",
}).trim();
// Diff from baseRef to HEAD — shows all committed changes made since
// the snapshot. Includes stat overview + full patch.
const diff = execSync(`git diff ${baseRef} HEAD --stat --patch`, {
cwd: projectDir,
encoding: "utf-8",
maxBuffer: 1024 * 1024,
}).trim();
if (!diff) return null; // no changes since baseRef
return { hash, subject, diff };
} catch {
return null;
}
}