Repository navigation
planning: refuse a recommendation change that leaves the alternatives unreviewed #5801
Description
Problem
On the /planning:interview page, a recommendation can be revised while the alternatives beside it stay as they were. The alternatives then contrast with a recommendation that no longer exists, and the user is asked to choose between a new recommendation and options written for the old one.
Observed in a live interview on 2026-10-02 (planning surface, round.sh apply). The recommendation on Q25 was revised twice through reply ops carrying rec, in answer to the user's questions. Its three alternatives still described the original recommendation ("Arena only, swarm deferred", "Both as opt-in modes the user passes, with no plan flag"). The user caught it on the page: "update the decisions, like the options 1 through 6, because they're stale. You need to make sure that you always do that."
Cause
op_reply(plugins/planning/surface/round.py:409-439) replacesrecommendationwhenrecis given, but it has noalternativesfield and never checks the existing ones.op_revise(round.py:442-500) changes the recommendation and the alternatives independently. Arecwith noalternativespasses silently.- R-I (
round.py:255-259, surface.md R-I) checks only that a question has at least two alternatives. It does not check that they were reviewed against the current recommendation, or that none of them repeats it. context/surface.md(ops table,replyandreviserows) never tells the author to restate the alternatives when the recommendation changes.
Expected
A recommendation change cannot leave the alternatives unreviewed.
Suggested fix
replyandreviserefuse arecunless the same op also carriesalternatives, or an explicitkeepAlternatives: truethat states they were checked against the new recommendation. Mirrorrequire_affects, which already forces the author to state the blast radius of a rec change.- Add
alternativesto thereplyop so an answer and a full revision can land in one op. - Refuse an alternative whose text equals the new recommendation (protocol rule: a recommendation never repeats among its alternatives).
- In
context/surface.md, state the rule beside thereplyandreviserows. - Optional, on the page: when
contentRevbumps for a recommendation change, mark the alternatives "rewritten" or "unchanged" next to the "Recommendation revised" banner.
Acceptance criteria
round.sh applywith areplyorreviseop that hasrecbut neitheralternativesnorkeepAlternativesexits non-zero, names the question, and writes nothing.- The same op with
alternativesapplies, and the history line records both changes. - A
surface/test covers the refusal, thekeepAlternativespath and the duplicate-of-recommendation refusal. surface.mddocuments the rule.
Related: #5769 (stale recommendations when a new constraint arrives mid-interview) and #5795 (page visual audit follow-ups). Neither covers alternatives that go stale relative to their own recommendation.
Activity
kyle-sexton commentedon Oct 4, 2026
This was generated by AI during triage.
Decision defaulted:
replygainsalternatives, so an answer and a full revision land in one op, as this issue specifies — veto before mergeBasis: verified, plugins/planning/surface/round.py:790-791 (
op_replycurrently refuses with "reply cannot change alternatives; use revise --rec with --alt in one op", added by #5732 the day before this issue). The vetoable alternative: keepreplyrecommendation-only and requirekeepAlternativeson it, so a full revision still goes throughrevise.Routing: decision-defaulted,
agent-ready,work-class: scoped(C3, bug fix),priority: medium(assessed default).Verified against current code. Part of this is already built:
replywithrecand forrevise. Basis: verified, plugins/planning/surface/round.py:230-240 (check_rec_alts), called at round.py:787-792 and round.py:858-864.replyrow (surface.md:68) still says to usereviseinstead.recwith no alternatives and no explicit keep passes on both ops. The only check is collision, so alternatives written for the old recommendation survive unreviewed. Basis: verified, round.py:851-864.Where I looked:
round.py(op_reply,op_revise,check_rec_alts,require_affects, the module docstring),context/surface.md(ops table, R-I),docs/out-of-scope/(nothing matches), and closed #5710/#5732. #5732 fixed the collision half of #5710. This issue is the unreviewed-alternatives half.Cluster: none. #5769, #5815 and #5910 touch recommendation changes, but none decides this.
Agent Brief
Type: Bug
Summary: A recommendation change on an interview question must either carry the revised alternatives or explicitly state that the existing ones were checked against it.
Current behavior:
A
replyorreviseop that setsrecis refused only when an existing alternative repeats or contains the new recommendation. When there is no collision, the op applies and keeps alternatives written for the old recommendation, without any sign that they were reviewed.replycannot carry alternatives at all, and refuses with a pointer torevise.Desired behavior:
replyorrevisethat setsrecis refused unless the same op also carriesalternatives, or carrieskeepAlternatives: true, which states that the current alternatives were checked against the new recommendation. The CLI exposes an equivalent flag for each. The refusal exits non-zero, names the question, says what to pass, and writes nothing.replyacceptsalternatives(same shape and validation asrevise: at least two, keys, the collision check), so an answer and a full revision land in one op.keepAlternatives, it runs against the existing ones.replyandreviserows of the ops table, the R-I row, and the CLI help.Key interfaces:
replyandreviseops inround.sh apply(JSON) and their CLI forms: newkeepAlternativesboolean, andalternativesonreply.applyrefusal contract: non-zero exit, question id in the message, no write. Mirror howrequire_affectsforcesaffectson a recommendation change.Acceptance criteria:
round.sh applywith areplyorreviseop that hasrecbut neitheralternativesnorkeepAlternativesexits non-zero, names the question, and leaves the data files unchanged.alternativesapplies, and the history line records both changes. The same op withkeepAlternatives: trueapplies and records that the alternatives were kept.replywithrecandalternativesapplies in one op. The collision refusal still fires when a passed alternative repeats the new recommendation.keepAlternativespath,replywithalternatives, and the duplicate-of-recommendation refusal on the new alternatives.Out of scope:
kyle-sexton commentedon Oct 4, 2026
This was generated by AI during work-loop.
Ratify autonomous dispatch of this C3 bug-fix item?
Classification:
work-class: scoped(C3), bug-fix-shaped (confirmed at triage this cycle). Intended dispatch: the work-loop worker lane, through/work-items:work, to a draft PR the lane never merges. Once ratified, the lane dispatches the interview-page items #5815, #5908, #5910 and #5911 one at a time, because they edit overlapping page code. First drain is not complete and no item-level ratification exists, so it is queued here. Ratify through/work-items:attend-queueand it dispatches on a later cycle.Lane: work-loop, instance melo-worker-1, cycle 1.
kyle-sexton commentedon Oct 11, 2026
Delivered on branch
feat/planning-interview-page-batchin draft PR #7182, one commit for this issue (withCloses #5801).A
replyorrevisethat changes the recommendation now needs the revisedalternativesorkeepAlternatives: true(CLI--keep-alternatives); with neither it is refused and nothing is written.replytakesalternativeswithrec, and the history line recordsrewrittenorkept.Verified: New
TestRecReviewsAlternativesintest_round.py; the Python suites passed (401 tests). The fullsurface.test.shwas last run in full before the final page commits; the later commits were checked with the ui_page phases. Its one standing failure (AC34 "user name found") is an environment artifact that also fails on main.Left for a human: the PR stays a draft until Kyle flips it to ready, which starts the CI and review lanes.
The
needs-humanlabel was left for Kyle to clear.Generated by Claude Code
Merge origin/main into feat/planning-interview-page-batch