Skip to content

fix(guard): switching to Build no longer hijacks the session back to Goal#429

Merged
devinoldenburg merged 1 commit into
mainfrom
fix/issue-428-build-switch-reviews
Jun 21, 2026
Merged

fix(guard): switching to Build no longer hijacks the session back to Goal#429
devinoldenburg merged 1 commit into
mainfrom
fix/issue-428-build-switch-reviews

Conversation

@devinoldenburg

Copy link
Copy Markdown
Owner

Closes #428

When using the goal agent and then switching to Build, it temporarily works. However, once the build response finishes, it automatically switches back to the goal agent, attempting to run reviews and also switching the main agent.

Root cause

When a user switches a goal session off the goal agent (to Build/Plan, or via an action that cycles the agent), chat.params / chat.message faithfully set state.active = false. But the idle handler's "is this still a Goal session?" check ignored that deactivation:

// plugins/goal-guard/guard.js — resolveIdleSession()
const goalSession =
  state.active ||
  Boolean(state.contract) ||              // ← stale contract re-activates
  goalSessionActiveForAgent("goal", state); // ← TAUTOLOGY: always returns true
  • Boolean(state.contract) — the contract persists from the prior goal, so a stale contract re-activated the session.
  • goalSessionActiveForAgent("goal", state) is tautologicalgoalSessionActiveForAgent returns true unconditionally when its first argument is the primary "goal" agent (see agents.js), so once a session had ever been a goal, the idle path treated it as a goal forever, no matter how many times the user switched away.

The result: on the Build turn's idle, canProgrammaticReview evaluated to true, the guard launched a programmatic review cycle, and launchReviewBatch — which pins the batch to PRIMARY_AGENTswitched the user's main agent back to goal, exactly the symptom in the bug report.

The fix

Two changes, both small and surgical:

1. Honor explicit deactivation in the idle path (root cause)

plugins/goal-guard/guard.jsresolveIdleSession() now requires state.active for a session to be eligible for programmatic review or auto-continue:

const goalSession = state.active;

A stale contract (or any other persisted goal state) can no longer re-activate a session the user explicitly paused. Switching back to the goal agent (or running /goal) re-activates it as before, and reviews then resume with all of the contract, evidence, and review-cycle state intact.

2. Defense-in-depth in launchReviewBatch

plugins/goal-guard/review-runner.jslaunchReviewBatch() now refuses to run on a state.active === false session, surfacing as a SessionBusyError so the outer retry loop defers cleanly. This guarantees that even a future regression in the idle-path eligibility check can never reach the call that pins the batch to PRIMARY_AGENT and switches the user's main agent back to goal. state is the live store record, so a fresh agent switch is reflected here even if it happened after the caller's eligibility check.

Why this is the smallest correct change

  • The other two terms in the old goalSession expression were either always-true (the tautology) or ignored deactivation (the contract). Removing them is strictly more correctstate.active was already the source of truth everywhere else (auto-continue, the state.dirty && state.active and state.active && !completionAllowed log branches, sidebar scoping, etc.).
  • The worker-handoff path (goal-implementer, goal-explorer, …) is preserved: those agents keep state.active = true via goalSessionActiveForAgent in chat.params/chat.message, so programmatic reviews still run after a worker handoff — covered by the existing programmatic review runs when a goal worker subagent was the last agent on the session test.
  • Re-activation on switching back to goal is unchanged and explicitly tested.

Regression coverage

Three new tests, all named REGRESSION [issue #428]:

Test File Asserts
a goal session switched to Build is NOT re-activated on idle (no reviews, no agent switch) tests/plugin.test.mjs The exact reported flow — /goal → switch to Build → prompt Build → idle → 0 reviewer subtasks launched, no agent: "goal" prompt sent, goal state preserved.
companion: switching back to Goal re-activates and programmatic review resumes tests/plugin.test.mjs Goal → Build (idle inert) → back to Goal → idle runs ≥5 reviewers. The fix does not over-fire.
a deactivated session never launches a reviewer batch (defense-in-depth in launchReviewBatch) tests/programmatic-review.test.mjs The defense-in-depth guard refuses to launch on a Build session; re-activation recovers cleanly.

Verification

  • npm test676/676 pass (673 baseline + 3 new). Includes the existing programmatic review runs when a goal worker subagent was the last agent on the session parity test, which still passes.
  • npm run lint — no new diagnostics (64 warnings / 60 infos are pre-existing on main, verified via git stash).
  • node scripts/validate-opencode-config.mjsOpenCode Goal Mode package validation passed.

Files changed

 plugins/goal-guard/guard.js         |  23 ++++++--    (root-cause fix + comment)
 plugins/goal-guard/review-runner.js |  17 ++++++      (defense-in-depth)
 tests/plugin.test.mjs               | 111 ++++++++++++++++++++++++++++   (2 regression tests)
 tests/programmatic-review.test.mjs  |  21 ++++++      (1 defense-in-depth test)
 README.md                           |  12 +++-        (troubleshooting note clarified)
 CHANGELOG.md                        |  33 +++++++++++  (Unreleased fix entry)
 6 files changed, 210 insertions(+), 7 deletions(-)

Docs

  • README.md troubleshooting — the "Goal Mode stopped after switching agents" entry now explicitly lists everything that halts when paused (sidebar, completion rewrites, auto-continue, and programmatic reviews), with a direct callout that the guard will never switch the main agent back to goal on its own.
  • CHANGELOG.md — new ## Unreleased### Fix: switching to Build no longer hijacks the session back to Goal (issue #428) entry following the established format (root cause + fix narrative for release readers).

Closes #428.

…Goal (#428)

When a user started /goal, interrupted, switched to Build, and prompted it,
the guard would — after Build's response finished — automatically switch the
main agent back to  and start running review subagents. Root cause: the
idle handler's "is this still a goal session?" check OR'd in two terms that
ignored the explicit deactivation set by chat.params/chat.message:

  - Boolean(state.contract) — a stale contract re-activated the session; and
  - goalSessionActiveForAgent("goal", state) — which is TAUTOLOGICAL: it
    returns true unconditionally for the primary "goal" agent, so once a
    session had ever been a goal it was treated as one forever.

The reviewer batch (pinned to PRIMARY_AGENT in launchReviewBatch) then switched
the user's main agent back to Goal — the reported "automatically switching
to Build" symptom.

Fix:
  - resolveIdleSession now requires state.active for a session to be eligible
    for programmatic review / auto-continue. A stale contract or other persisted
    goal state can no longer re-activate a session the user explicitly paused.
    Switching back to goal (or running /goal) re-activates it and reviews resume
    with all state intact.
  - launchReviewBatch now refuses to run on a state.active===false session
    (defense-in-depth so a future regression in the eligibility check can never
    switch the user's main agent back to Goal).

Adds 3 regression tests (the exact reported flow + a re-activation parity test
+ the launchReviewBatch guard). Updates the README troubleshooting note and
adds a CHANGELOG entry.

All 676 tests pass (673 baseline + 3 new); no new lint diagnostics.
@devinoldenburg devinoldenburg merged commit 46fd184 into main Jun 21, 2026
11 checks passed
@devinoldenburg devinoldenburg deleted the fix/issue-428-build-switch-reviews branch June 21, 2026 15:17
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.

Automatically switching to Build

2 participants