Skip to content

Suggest the room: a department offers, the founder convenes - #250

Merged
TruongGiang2000 merged 9 commits into
mainfrom
feat/suggest-room
Oct 9, 2026
Merged

TruongGiang2000 merged 9 commits into
mainfrom
feat/suggest-room

Conversation

@TruongGiang2000

Copy link
Copy Markdown
Collaborator

Step 3 of "one founder runs the whole company", after #247 (a department answers as itself) and #248 (it keeps a notebook).

What

Since #247, a department answering in Ask is told to say "this one needs Marketing too", but the founder had no way to act on that. Now the department can offer to bring the others in, and one press convenes that room.

  • Chat tool suggest_room. It is offered only on a turn that answers as a known department. coerceRoomOffer keeps 2–4 known keys, removes duplicates and caps the list, and clips question/why. An invalid offer is dropped without failing the turn. The offer rides the done frame as room_offer and costs nothing until the founder presses it.
  • Card. It shows "Bring Finance + Sales in →" plus the department's reason. One press convenes the room through sendChat(convenesRoom:requestedAgents:), which sets VirtualCompanyRequest.agents. The card is disabled while a turn is in flight and cannot be used twice.
  • The room honours the pick (applyRequestedAgents). With 2–4 valid departments the routing is forced to multi_agent with exactly those, so the room never takes the escape hatch. chief_of_staff, the red team and Product can't be requested. The router's real_question is kept, and so is devils_advocate if the router named it. The routing panel is made consistent with the forced room: picked departments leave "not in the room", each gets a ✓ reason, and there is no "Missing:" line. Without agents, behaviour is unchanged.
  • SSE contract (the authority): adds the agents field and the rule that a founder-picked room never escapes.

Spec: docs/superpowers/specs/2026-10-09-suggest-room-design.md · Plan: docs/superpowers/plans/2026-10-09-suggest-room.md

Verified

  • jest 1086/1086. Covers the coercion, the tool being offered only on department turns, room_offer on the done frame, validateRunPayload, and applyRequestedAgents, including a run whose router returns single_agent but which still convenes the requested room.
  • Swift RoomOfferWireTests (decode, and agents omitted when nil) and RoomInviteTests (the offer lands on the reply; one press convenes exactly finance+sales on the offer's question, and a second press does nothing). The existing RoomOfferTests, CompanyStoreChatTests, CompanyStoreVirtualCompanyTests and ApprovalParityTests pass.
  • A fresh whole-branch review found no Critical issues. Of the two Important findings, the routing panel is fixed (test red → green). The other, a press that could burn the offer, is unreachable: both busy flags are set and cleared together on the main actor with no await between them.
  • Not yet tried in the running app. The machine was in use. Steps: ask "Ask Finance: should we discount the Pro plan by 30% this month to close more sales?", check that the card appears, press it, and check that the room convenes with Finance and Sales.

Deferred minors

  • JS prototype keys (toString) get past in ROOM_AGENT_FOR. A model would have to emit one.
  • An offer is not required to include the answering department.
  • The press posts the question as a founder bubble plus a generic chat reply, as Plan does.
  • The Swift key-map test is a Swift-side copy.
  • ChatTailAction was not updated (harmless).
  • One escape-path test is a regression pin rather than a guard.

🤖 Generated with Claude Code

TruongGiang2000 and others added 9 commits October 9, 2026 13:02
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A department in Ask was told to say 'this one needs Marketing too' and the
founder had no way to act on it. suggest_room is offered only on a turn
that answers as a known department; coerceRoomOffer keeps 2-4 known keys
(deduped, capped), clips question/why, and drops a bad offer rather than
failing the turn. It resolves beside remember_fact and rides the done
frame as room_offer. An offer costs nothing until the founder presses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pressing 'Bring Finance + Sales in' must give that room; otherwise the
router could still answer single_agent and the escape hatch would discard
it, so the button would visibly do nothing. applyRequestedAgents runs after
intake: 2-4 routable departments (not chief_of_staff, the red team or
product) force multi_agent with exactly those, keeping devils_advocate if
the router named it and the router's real_question. Fewer than two, or no
field, returns the routing object untouched. validateRunPayload 400s a
non-string-array. The SSE contract (the authority) records both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ChatDoneAction.roomOffer decodes the done frame's room_offer; the room
request gains an optional agents list that is not encoded when nil, so
every existing run is byte-identical on the wire.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A department's room_offer lands on its reply under the Noted chip as
'Bring Finance + Sales in ->' with its reason. acceptRoomOffer maps the
native keys to room agent ids (RoomInvite, pinned key-for-key against
ROOM_AGENT_FOR) and convenes through sendChat(convenesRoom:requestedAgents:)
-> startVirtualCompanyRun -> VirtualCompanyRequest.agents. It sets its own
used flag before the await so it convenes once, and is disabled while a
turn is in flight. Named RoomInvite because RoomOffer is already the
composer's convene control.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review finding: applyRequestedAgents overrode only decision and agents, so
the panel showed '✗ Sales — not a pricing question' under NOT IN THE ROOM
while Sales sat in the room, gave Sales no ✓ line, and kept a 'Missing:'
ask on a room that was running. Picked agents now leave excluded, get a
reason ('Asked for by the founder.') when the router gave none, and
missing_info is cleared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the app no card ever appeared. Traced layer by layer: the app sent
dept_key=fin, the bundled sidecar had the tool, claude's init listed
mcp__codepet__suggest_room with the MCP server connected — and the model
never called it. Replaying the app's exact request: 3/3 offers without a
linked folder, 0/3 with one (Read/Glob/Grep on). The role framing told it
to SAY 'this one needs Marketing too', so with the folder attached it did
exactly that, in prose.

The role block now says to give its own answer AND call suggest_room when
another department's interest pulls the other way. Same request, folder
linked: 4/4 offers (fin+mkt+sales). A one-department question (runway
from burn and cash): 0/3 offers — it does not over-trigger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the app the reply said 'press the room button above'; the card draws
below the reply, the same slip the draft cards had on build 10. Both the
tool description and the role block now say it appears below.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@TruongGiang2000

Copy link
Copy Markdown
Collaborator Author

Checked in the running app at 71fd6dc, real account with a project folder linked:

  • "Ask Finance: Marketing wants to spend $5,000 on paid ads… Sales wants that money for a demo contractor. Which should we fund?" → Crash · Finance answered, and the card Bring Finance + Marketing + Sales in → appeared under the reply with its reason.
  • Pressing it convened the room with Finance, Marketing and Sales, all three answering.

What testing found, and fixed:

  1. No card at first. Traced layer by layer:

    • the app sent dept_key=fin;
    • the bundled sidecar had the tool;
    • claude's init listed mcp__codepet__suggest_room with the MCP server connected;
    • the model simply never called it.

    Replaying the app's exact request gave 3/3 offers without the linked folder and 0/3 with it. The role framing only told the model to say another department was needed. The role block now tells it to answer and call suggest_room. Same request, folder linked: 4/4. A one-department question (runway from burn and cash): 0/3, so it doesn't over-trigger. (2dc8429)

  2. Copy. The reply said "press the room button above", but the card sits below. The tool description and the role block now both say below. (71fd6dc)

jest 1089/1089.

@TruongGiang2000
TruongGiang2000 merged commit ada4b3c into main Oct 9, 2026
2 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