Skip to content

planning: refuse a recommendation change that leaves the alternatives unreviewed #5801

Open

Description

@kyle-sexton

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) replaces recommendation when rec is given, but it has no alternatives field and never checks the existing ones.
  • op_revise (round.py:442-500) changes the recommendation and the alternatives independently. A rec with no alternatives passes 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, reply and revise rows) never tells the author to restate the alternatives when the recommendation changes.

Expected

A recommendation change cannot leave the alternatives unreviewed.

Suggested fix

  1. reply and revise refuse a rec unless the same op also carries alternatives, or an explicit keepAlternatives: true that states they were checked against the new recommendation. Mirror require_affects, which already forces the author to state the blast radius of a rec change.
  2. Add alternatives to the reply op so an answer and a full revision can land in one op.
  3. Refuse an alternative whose text equals the new recommendation (protocol rule: a recommendation never repeats among its alternatives).
  4. In context/surface.md, state the rule beside the reply and revise rows.
  5. Optional, on the page: when contentRev bumps for a recommendation change, mark the alternatives "rewritten" or "unchanged" next to the "Recommendation revised" banner.

Acceptance criteria

  • round.sh apply with a reply or revise op that has rec but neither alternatives nor keepAlternatives exits non-zero, names the question, and writes nothing.
  • The same op with alternatives applies, and the history line records both changes.
  • A surface/ test covers the refusal, the keepAlternatives path and the duplicate-of-recommendation refusal.
  • surface.md documents 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

  1. added
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 2, 2026
  2. kyle-sexton commented on Oct 4, 2026

    @kyle-sexton
    ContributorAuthor

    This was generated by AI during triage.

    Decision defaulted: reply gains alternatives, so an answer and a full revision land in one op, as this issue specifies — veto before merge
    Basis: verified, plugins/planning/surface/round.py:790-791 (op_reply currently 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: keep reply recommendation-only and require keepAlternatives on it, so a full revision still goes through revise.

    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:

    • Step 3 (refuse an alternative that repeats or contains the new recommendation) exists for reply with rec and for revise. Basis: verified, plugins/planning/surface/round.py:230-240 (check_rec_alts), called at round.py:787-792 and round.py:858-864.
    • Step 4 is half done. R-I in the page protocol already says to revise the alternatives in the same op whenever a recommendation changes. Basis: verified, plugins/planning/skills/interview/context/surface.md:208. The reply row (surface.md:68) still says to use revise instead.
    • Still missing, as the issue says: a rec with 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 reply or revise op that sets rec is 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. reply cannot carry alternatives at all, and refuses with a pointer to revise.

    Desired behavior:

    • A reply or revise that sets rec is refused unless the same op also carries alternatives, or carries keepAlternatives: 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.
    • reply accepts alternatives (same shape and validation as revise: at least two, keys, the collision check), so an answer and a full revision land in one op.
    • When alternatives are passed, the collision check runs against the new alternatives. With keepAlternatives, it runs against the existing ones.
    • The history line for an applied op records that the recommendation changed and whether the alternatives were rewritten or kept.
    • The page protocol states the rule beside the reply and revise rows of the ops table, the R-I row, and the CLI help.

    Key interfaces:

    • reply and revise ops in round.sh apply (JSON) and their CLI forms: new keepAlternatives boolean, and alternatives on reply.
    • The apply refusal contract: non-zero exit, question id in the message, no write. Mirror how require_affects forces affects on a recommendation change.

    Acceptance criteria:

    • round.sh apply with a reply or revise op that has rec but neither alternatives nor keepAlternatives exits non-zero, names the question, and leaves the data files unchanged.
    • The same op with alternatives applies, and the history line records both changes. The same op with keepAlternatives: true applies and records that the alternatives were kept.
    • A reply with rec and alternatives applies in one op. The collision refusal still fires when a passed alternative repeats the new recommendation.
    • Surface tests cover the refusal, the keepAlternatives path, reply with alternatives, and the duplicate-of-recommendation refusal on the new alternatives.
    • The page protocol's ops table and R-I row, and the CLI help, state the rule.

    Out of scope:

  3. added
    priority: mediumReal value, no hard deadline; normal backlog flow.
    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.
    work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.
    status: readyTriaged, unblocked, and fully specified; eligible to pick up.
    and removed
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 4, 2026
  4. kyle-sexton commented on Oct 4, 2026

    @kyle-sexton
    ContributorAuthor

    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-queue and it dispatches on a later cycle.

    Lane: work-loop, instance melo-worker-1, cycle 1.

  5. added
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    and removed
    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.
    on Oct 4, 2026
  6. kyle-sexton commented on Oct 11, 2026

    @kyle-sexton
    ContributorAuthor

    Delivered on branch feat/planning-interview-page-batch in draft PR #7182, one commit for this issue (with Closes #5801).

    A reply or revise that changes the recommendation now needs the revised alternatives or keepAlternatives: true (CLI --keep-alternatives); with neither it is refused and nothing is written. reply takes alternatives with rec, and the history line records rewritten or kept.

    Verified: New TestRecReviewsAlternatives in test_round.py; the Python suites passed (401 tests). The full surface.test.sh was 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-human label was left for Kyle to clear.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.priority: mediumReal value, no hard deadline; normal backlog flow.status: readyTriaged, unblocked, and fully specified; eligible to pick up.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions