fix(engine): break Plan Review REVISE replan loop (feedback + bounded cap) (#2078)

## Problem
A task whose Plan Review step returns verdict `REVISE` can loop forever:
plan → plan-review REVISE → `needs-replan` → re-plan → near-identical
plan → REVISE → repeat. The triage **pre-execution** Plan Review gate
(`runPlanReviewBeforeExecution`) sets `status: "needs-replan"` on REVISE
with **no cap and no escape to `awaiting-approval`** — unlike the
executor graph path, which already has `PLAN_REVIEW_REPLAN_HARD_CAP`.
Under `planApprovalMode: require-all` there is also no human exit,
because the task never reaches `awaiting-approval`.

Separately, replan feedback (`triage.ts`) was derived only from
`task.log` comment actions + the latest user comment; it never consulted
the plan-review verdict stored in `task.workflowStepResults`.

## Fix
1. **Thread plan-review feedback into replan** — when re-planning with
no comment-derived feedback, seed `buildSpecificationPrompt` from the
most recent `plan-review` REVISE `output` in `workflowStepResults`
(existing user/AI-comment precedence preserved).
2. **Bounded cap** — new `planReviewReplanCount` counter (`types.ts`,
`store.ts` column + updateTask, `db.ts` migration 146,
`manual-retry-reset.ts`). After `PLAN_REVIEW_GATE_REPLAN_CAP = 3`
consecutive REVISE replans the task escalates to `awaiting-approval`
(`awaitingApprovalReason: "plan-review-replan-cap"`) instead of
replanning. Counter resets on APPROVE.

## Tests
Adds `triage-replan-feedback-from-plan-review.test.ts` and
`triage-plan-review-replan-cap.test.ts`. Merge gate green locally
(`verify:fast`, `test:gate` 337+63, `lint`); changeset included.

Made with Claude (see `Co-Authored-By` trailer).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Prevented Plan Review “REVISE” from looping indefinitely by enforcing
a bounded replan cap.
* After repeated Plan Review replans, tasks now escalate to an
approval-hold state with a dedicated reason.
* Improved replan feedback by seeding from the latest Plan Review output
when no explicit feedback is available; the counter clears when Plan
Review approves.
  * Manual retries now reset the Plan Review replan cap counter.
* **Documentation**
  * Added release notes describing the Plan Review replan safeguards.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Victor Canô
2026-07-14 12:15:09 -03:00
committed by GitHub
parent 03966ecb79
commit bc348345a4
16 changed files with 549 additions and 18 deletions

View File

@@ -86,6 +86,7 @@ CREATE TABLE IF NOT EXISTS project.tasks (
-- FNXC:SqliteFinalRemoval 2026-06-25: retry/stuck counters missed in initial snapshot
stuck_kill_count integer DEFAULT 0,
post_review_fix_count integer DEFAULT 0,
plan_review_replan_count integer DEFAULT 0,
verification_failure_count integer DEFAULT 0,
branch_conflict_recovery_count integer DEFAULT 0,
reviewer_context_retry_count integer DEFAULT 0,

View File

@@ -167,6 +167,10 @@ export const EXPECTED_PROJECT_COLUMNS: ReadonlyArray<{ schema?: string; table: s
// FNXC:WorkflowLifecycle 2026-07-12: FN-7863 execute self-requeue streak (merge port).
{ table: "tasks", column: "execute_requeue_loop_count", type: "integer" },
{ table: "tasks", column: "execute_requeue_loop_signature", type: "text" },
// FNXC:PlanReviewReplan 2026-07-13: bounded triage Plan Review REVISE replan counter.
// Additive column not present in the baseline snapshot, so existing embedded-PG
// databases must self-heal it via ALTER TABLE ADD COLUMN IF NOT EXISTS on boot.
{ table: "tasks", column: "plan_review_replan_count", type: "integer" },
// distributed_task_id_state
{ table: "distributed_task_id_state", column: "prefix", type: "text" },
{ table: "distributed_task_id_state", column: "next_sequence", type: "integer" },

View File

@@ -122,6 +122,7 @@ export const tasks = projectSchema.table("tasks", {
*/
stuckKillCount: integer("stuck_kill_count").default(0),
postReviewFixCount: integer("post_review_fix_count").default(0),
planReviewReplanCount: integer("plan_review_replan_count").default(0),
verificationFailureCount: integer("verification_failure_count").default(0),
branchConflictRecoveryCount: integer("branch_conflict_recovery_count").default(0),
reviewerContextRetryCount: integer("reviewer_context_retry_count").default(0),