Skip to content

fix(ralplan): stop infinite consensus loops with reviewer ratchet + per-lane review budget - #3356

Merged
Yeachan-Heo merged 3 commits into
devfrom
feat/ralplan-infinite-loop-convergence
Jul 28, 2026
Merged

fix(ralplan): stop infinite consensus loops with reviewer ratchet + per-lane review budget#3356
Yeachan-Heo merged 3 commits into
devfrom
feat/ralplan-infinite-loop-convergence

Conversation

@Yeachan-Heo

@Yeachan-Heo Yeachan-Heo commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes infinite-ralplan symptoms observed after enabling heavy models for the Critic/Architect roles: endless ITERATE/REJECT loops driven by review goalpost-moving (one fresh HIGH per pass on previously-approved ground), escalating revision sizes (20KB → 99KB), and lane passes escaping the #3165 opener-only cap (8 architect passes in one run).

Approach — behavior-first, per approved spec and consensus plan:

Reviewer behavior contracts (primary surface)

  • critic.md — 5-rule re-review ratchet from pass 2: delta-only review, novelty justification for new blockers on reviewed ground, verdict monotonicity, carryover-vs-fresh severity discipline, and a counter-review duty that flags Architect over-engineering/scope expansion as a review defect. Enrichment lane preserved, justification-gated from pass 2.
  • architect.md — ratchet rules 1–4 + rule-5 awareness; the unconditional "Never approve CRITICAL or HIGH" line is scoped: carryover CRITICAL/HIGH stay unapprovable; fresh HIGHs minted on approved ground need a why-not-visible-earlier justification, else demote to non-blocking caveats.
  • ralplan SKILL.md — mandatory pass-2+ re-review context bundle (receipts/paths + run-level pass ordinal that never resets per opener), sequential Architect→Critic cadence from pass 2 (pass-1 parallel fan-out unchanged), per-lane budget operator contract, --lane-verdict assignment instruction.

Runtime backstop (minimal, reuses #3165 cap machinery)

  • Per-lane review budget: default 1 pass/lane/opener iteration, gjc.ralplan.maxReviewPassesPerLane (integer 1..10, registered in settings schema + regenerated config.schema.json).
  • Lane-specific PLANNING-STUCK (exit 3, marker); post-interview/adr/final always allowed so stuck plans escalate visibly, never silently approve.
  • Fail-closed on-disk lane-artifact floor (only ENOENT masks as empty); crash-gap dedupe repair (identical retry repairs missing/short ledger rows exactly once; final-stage retries verify/recreate pending-approval.md).
  • HUD: arch/crit pass-count chips + last verdict via optional --lane-verdict token (closed vocabularies, run-scoped, zero artifact parsing; legacy verdict cleared on run-id change; stale-run repairs cannot leak verdicts into the active run).

No-regression boundaries (held)

Verdict vocabularies, receipt-only role contracts, full-depth pass-1 review, Critic enrichment lane, and all #3165 semantics unchanged — the existing opener-cap test block is byte-identical.

Accepted risks (documented per consensus, intentionally not implemented)

  1. No cross-process admission lock/CAS (parity with existing ralplan: planner/critic loop has no round cap or convergence forcing — silent unbounded planning in headless runs #3165 check-then-persist posture)
  2. Lane-count HUD chips may be replaced by later generic state writes
  3. No transactional replay of display-only verdict metadata on crash-gap dedupe

Provenance

The approved planning artifacts are session-local files under .gjc/ (gitignored by design) and are therefore not linkable from this PR; exact untruncated paths in the authoring worktree (research-enhance-infinite-ralplan-4ad850b0):

  • Deep-interview spec (10 rounds, final ambiguity 5%, threshold 5%): .gjc/_session-019fa2eb-abd6-7000-9ad3-d563f3cc5787/specs/deep-interview-ralplan-infinite-loop-convergence.md
  • Ralplan consensus plan rev 5 (Architect CLEAR/APPROVE, Critic OKAY, reconciled-clean post-interview): .gjc/_session-019fa2eb-abd6-7000-9ad3-d563f3cc5787/plans/ralplan/019fa2eb-abd6-7000-9ad3-d563f3cc5787/pending-approval.md

The plan's full ADR and Intent Reconciliation sections are embedded in that artifact and summarized in the first commit message on this branch.

Verification (focused, per plan)

bun test packages/coding-agent/test/gjc-runtime/ralplan-runtime.test.ts \
  packages/coding-agent/test/ralplan-decision-artifacts.test.ts \
  packages/coding-agent/test/gjc-runtime/state-runtime.test.ts \
  packages/coding-agent/test/workflow-hud-summary.test.ts \
  scripts/generate-json-schemas.test.ts
# 135 pass, 0 fail, 741 expect() calls
bun scripts/generate-json-schemas.ts --check   # green

Red-team highlights: pathological 20-stage replay refuses exactly at the degenerate passes (a2/a6/rev6/c6/a8) while final still succeeds; healthy replays produce zero warnings/refusals; justified blockers still block; injected EIO fails closed; cross-run verdict-leak, short-row double-count, and final pending-approval crash-gap attacks all fixed and retested.

…er-lane review budget

Behavior-first fix for heavy-model ralplan runs looping indefinitely
(evidence: 8 architect passes, revisions 20KB->99KB, one fresh HIGH per
pass on previously-approved ground).

Reviewer behavior contracts (primary surface):
- critic.md: 5-rule re-review ratchet from pass 2 (delta-only review,
  novelty justification, verdict monotonicity, severity discipline,
  counter-review of Architect over-engineering); enrichment lane
  justification-gated from pass 2
- architect.md: ratchet rules 1-4 + rule-5 awareness; "Never approve
  CRITICAL/HIGH" scoped to carryover findings (fresh HIGHs on approved
  ground need why-not-visible-earlier justification)
- ralplan SKILL.md: mandatory pass-2+ re-review context bundle with
  run-level pass ordinals, sequential Architect->Critic cadence from
  pass 2 (pass-1 parallel fan-out unchanged), per-lane budget operator
  contract, --lane-verdict assignment instruction

Runtime backstop (minimal, reuses #3165 cap machinery):
- Per-lane review budget: default 1 architect + 1 critic pass per opener
  iteration, gjc.ralplan.maxReviewPassesPerLane (1..10, schema-registered)
- Lane-specific PLANNING-STUCK (exit 3), post-interview/adr/final always
  allowed; fail-closed on-disk lane artifact floor (ENOENT-only masking)
- Crash-gap dedupe repair: identical retry repairs missing/short ledger
  rows exactly once; final-stage retries verify/recreate pending-approval.md
- HUD: arch/crit pass-count chips + last verdict via optional
  --lane-verdict token (closed vocabularies, run-scoped, no artifact
  parsing); legacy verdict cleared on run-id change; cross-run repair
  cannot leak verdicts into the active run

Accepted risks (documented, per consensus): no cross-process admission
lock (#3165 parity), no every-writer HUD chip preservation, no
transactional verdict replay. #3165 opener-cap tests byte-identical.

Verification: 135 tests / 741 expectations green across ralplan-runtime,
ralplan-decision-artifacts, state-runtime, workflow-hud-summary, and
generate-json-schemas suites; schema --check green.

Spec: deep-interview (10 rounds, final ambiguity 5%)
Plan: ralplan consensus rev 5 (Architect CLEAR/APPROVE, Critic OKAY)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4c054a053e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +509 to +513
"gjc.ralplan.maxReviewPassesPerLane": {
type: "number",
default: 1,
validate: (value: number) => Number.isInteger(value) && value >= 1 && value <= 10,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record the workflow change in the package changelog

This adds the user-visible gjc.ralplan.maxReviewPassesPerLane setting alongside new lane-budget and --lane-verdict behavior, but the commit leaves packages/coding-agent/CHANGELOG.md unchanged, so the next release notes will omit the new configuration and failure semantics. Add an entry under the coding-agent changelog's ## [Unreleased] section.

AGENTS.md reference: AGENTS.md:L144-L146

Useful? React with 👍 / 👎.

Yeachan-Heo and others added 2 commits July 28, 2026 10:49
…g entry

- Move native-dependent WI-6 HUD tests (artifact/state ordering, lane-verdict
  precedence) from state-runtime.test.ts to ralplan-runtime.test.ts so the
  gjc-state-gates runtime CI shard stays native-free
- Add Unreleased changelog entry for gjc.ralplan.maxReviewPassesPerLane,
  per-lane review budget / lane PLANNING-STUCK, --lane-verdict HUD carriage,
  and the reviewer re-review ratchet contracts
root-check still failed with 5 Biome errors after 516a550 fixed the
native-free state gate. These are formatting/import-only corrections with no
behavior change:

- ralplan-runtime.ts, workflow-hud.ts: formatter output
- ralplan-runtime.test.ts: organizeImports, formatter output, and path.join
  instead of string concatenation for the index.jsonl fixture path
  (lint/style/useTemplate)
- state-runtime.test.ts: drop the stray double blank lines

Verified locally:
- bun run check:tools: clean (was 5 errors)
- ralplan-runtime + state-runtime + workflow-hud-summary +
  ralplan-decision-artifacts + generate-json-schemas: 135 pass / 0 fail
- all four gjc-state-gates groups with pi_natives removed: pass, confirming
  the native-free contract restored by 516a550 still holds
@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Red-check repair pushed: 59c8b9709fc009270a9935936b1c7fb7ddeb55a5

Repaired from exact head 4c054a053efeb75e30e34c3eb32b53ff17b73170. Both failures were caused by this PR, not flake — no rerun-only response, no retry or timeout masking.

1. root-check — Biome, 5 errors

Reproduced locally verbatim, then fixed. Formatting/import-only, no behavior change:

  • ralplan-runtime.ts, workflow-hud.ts — formatter output
  • ralplan-runtime.test.tsorganizeImports, formatter output, and path.join(truncatedDir, "index.jsonl") instead of string concatenation (lint/style/useTemplate, line 1590)
  • state-runtime.test.ts — stray double blank lines

2. gjc-state-gates/runtimepi_natives not built

This was PR-caused, not a workflow precondition gap. scripts/ci-gjc-state-gates.ts is a deliberately native-free lane; its own comments state that suites which transitively load @gajae-code/natives belong in the heavier "Affected path validation" job. This PR added import { runNativeRalplanCommand } from ".../ralplan-runtime" to state-runtime.test.ts, so the whole native-free suite failed at import time.

Proven by controlled experiment with pi_natives physically removed:

Condition Result
PR head state-runtime.test.ts fails at import (Failed to load pi_natives native addon)
origin/dev version of the same file 31 pass / 0 fail

516a55092 landed the same structural relocation while I was repairing, so I dropped my duplicate structural commit and rebased only the still-failing Biome fixes on top.

Verification on 59c8b9709

  • bun run check:tools — clean (was 5 errors)
  • ralplan-runtime + state-runtime + workflow-hud-summary + ralplan-decision-artifacts + generate-json-schemas135 pass / 0 fail
  • All four gjc-state-gates groups (static/runtime/integrity/read) with pi_natives removed — pass, confirming the native-free contract restored by 516a55092 still holds

Dev CI run 30321545956 started on exact head 59c8b9709fc009270a9935936b1c7fb7ddeb55a5.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Terminal verdict — PR #3356 repair verified at 59c8b9709fc009270a9935936b1c7fb7ddeb55a5

Pushed head verification

Check Result
git/refs/heads/feat/ralplan-infinite-loop-convergence 59c8b9709fc009270a9935936b1c7fb7ddeb55a5
Local repair branch head 59c8b9709fc009270a9935936b1c7fb7ddeb55a5 (identical)
PR head / mergeable state 59c8b970 · open · mergeable: true · clean
Failing check-runs none
Commit scope 4 files, +75 / −51, formatting-only

1. root-check formatting repair

Biome reported 5 errors at 4c054a05; each was reproduced locally before being fixed, and every fix is formatter-authored with no behavior change:

  • ralplan-runtime.ts — formatter output (+11/−18)
  • workflow-hud.ts — formatter output (+5/−1)
  • ralplan-runtime.test.tsorganizeImports, formatter output, and path.join(truncatedDir, "index.jsonl") replacing string concatenation (lint/style/useTemplate, line 1590) (+59/−30)
  • state-runtime.test.ts — stray double blank lines (+0/−2)

bun run check:tools is clean at this head (was 5 errors).

2. Native-free state-runtime boundary

scripts/ci-gjc-state-gates.ts runs its runtime group without building @gajae-code/natives; the script's own comments state that suites which transitively load the addon belong in the heavier "Affected path validation" job. 4c054a05 added import { runNativeRalplanCommand } from ".../ralplan-runtime" to state-runtime.test.ts, so the entire native-free suite failed at import time. This was PR-caused, not a workflow precondition gap.

Controlled experiment with pi_natives physically removed:

Condition Result
4c054a05 state-runtime.test.ts fails at import — Failed to load pi_natives native addon
origin/dev version of the same file 31 pass / 0 fail

516a55092 (owner-side) landed the relocation of the two cross-runtime lane-verdict cases into ralplan-runtime.test.ts, which owns the ralplan helpers and runs in a native-capable job. My duplicate structural commit was dropped; only the still-failing Biome fixes were rebased onto it. The boundary is re-verified at 59c8b970: all four gjc-state-gates groups (static/runtime/integrity/read) pass with pi_natives removed.

Verification summary at 59c8b970

  • bun run check:tools — clean
  • ralplan-runtime + state-runtime + workflow-hud-summary + ralplan-decision-artifacts + generate-json-schemas135 pass / 0 fail
  • All four gjc-state-gates groups with pi_natives removed — pass
  • No rerun-only response, no retry loops, no timeout inflation, no continue-on-error

Merge hold

Not merging. Current dev head e9ecc93ad0dd7d20a0d2bd4bd7e9dc435a1d3dfd (run 30321208841) is still in_progress. Per the never-merge-while-dev-is-red constraint, #3356 waits until that exact dev head is terminal green.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 59c8b9709f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (laneVerdict && (await applyLaneVerdictUpdate(cwd, resolved.sessionId, laneVerdict, resolved.runId))) {
appliedLaneVerdict = laneVerdict;
}
return buildDeduplicatedResult(resolved, repairedArtifact, sha256, cwd, repositoryBinding, appliedLaneVerdict);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Sync the HUD after repairing a crash-gap write

When a lane artifact survived a crash before its ledger append, an identical retry can repair the ledger and persist --lane-verdict, but this early return skips writeSessionActivityMarker and syncRalplanHud below. The command therefore reports success while the active TUI summary still shows the preceding stage and omits the repaired pass/verdict until an unrelated later mutation rebuilds it; sync the repaired write before returning.

Useful? React with 👍 / 👎.

Comment on lines 1390 to +1393
const summary = summarizeRalplanIndex(rows);
iterationFromIndex = summary.iteration;
stages = formatRalplanStagePresence(summary.currentStages);
architectPasses = summary.currentStages.filter(stage => stage === "architect").length;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Deduplicate repaired ledger rows before building HUD counts

When crash-gap repair encounters a short parseable row such as {"stage":"architect","stage_n":2}, it intentionally appends a complete row with the same stage identity. The budget gate deduplicates those rows, but this HUD path summarizes the raw ledger, so the next ordinary write renders two Architect passes for the single repaired pass (and may show values such as 3/2 after the genuine second pass). Apply the same stage-identity deduplication before deriving the summary and lane counters.

Useful? React with 👍 / 👎.

@Yeachan-Heo
Yeachan-Heo merged commit 4831d03 into dev Jul 28, 2026
87 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant