Skip to content

Give rooms a goal and send its lessons to the room's rounds (#174) - #258

Merged
memtomem merged 4 commits into
mainfrom
feat/174-room-goal-lessons
Nov 5, 2026
Merged

memtomem merged 4 commits into
mainfrom
feat/174-room-goal-lessons

Conversation

@memtomem

@memtomem memtomem commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Follow-up 3 for #174, after #245, #250 and #253. A room can now have a goal, and its rounds get the lessons promoted for that goal. Until now room promote --scope GOAL made a lesson that reached only board items of that goal, and never a room round.

This was the last follow-up listed on #174. This PR does not close the issue; whether anything is left on it is your call.

What changes

A goal per room

  • room create --goal GOAL, or the new Goal field in the TUI create dialog, sets the goal once for the room's life. There is no way to change it: to work toward another goal, close the room and create a new one.
  • It is stored in a new nullable rooms.goal column, added through the existing migration tuple. Rooms created before this change read NULL, meaning no goal.
  • rooms.normalize_goal validates it on the text as given:
    • control characters are refused, checked before any whitespace check, so "\ng" cannot slip through;
    • blank and whitespace-only goals are refused;
    • text that is not valid UTF-8 is refused;
    • at most 200 bytes.
  • The goal is stored verbatim, never stripped. room promote --scope and its retry key (source_key) keep the scope as typed. Stripping only the room side would make a lesson promoted with --scope " g " unreachable from any room, and normalizing the promote side would change the identity of lessons that already exist.

Lessons

  • Board.project_lessons(project_id, goal=) adds the active lessons whose scope equals the goal exactly, in the same oldest-first order.
  • goal=None runs the same query as before, so run, start and rooms without a goal see no goal lessons.
  • The 20-lesson / 4000-byte budget is unchanged: the newest lessons win, whatever their scope.
  • room round (foreground and --detach) and the TUI round dialog resolve lessons with the room's goal. The detached worker still gets exactly the listed (id, text) pairs over --lessons-fd.
  • The previews mark each goal lesson (goal G). The replace hint adds --scope GOAL when a goal lesson is listed, because board decide --supersedes refuses a replacement in another scope.

Where the goal shows

  • room create and room list print it; room show returns it through get_room.
  • The TUI room pane and the TUI Compare view show it.
  • room compare shows it as a text line; room export puts it in a fenced Markdown block; the JSON form has room.goal (null without one). The field is additive, so schema_version stays 1.

Promotion

  • --scope still defaults to the whole project.
  • When you promote for the whole project from a room that has a goal, it prints the command that would limit the lesson to that goal:
    • the command uses --scope=GOAL, so a goal starting with - still parses, and adds --repo when you gave one;
    • for a goal the terminal cannot show unchanged, it names the goal escaped and prints no command.
  • The help text and the TUI label now say that a scoped lesson also reaches rooms created with that goal.
  • The TUI goal field and the TUI promote scope field are a new tui_render.ExactInput. Textual 8.2.8's Input keeps only the first line of a paste and runs each class's _on_paste along the MRO, so ExactInput calls prevent_default() and inserts the whole paste. As a result:
    • a multi-line goal is refused instead of being cut to g;
    • a pasted scope stays equal to the goal it was copied from.

Not in this PR

  • start --goal / run --goal. start --compare creates a room, but start falls back to a single-agent run, which has no goal. Adding it would mean either a new goal reach for plain runs or a flag that works on only one branch of start. start --compare rooms stay goal-less.
  • Control characters in a promoted scope → Refuse control characters in a promoted lesson's scope #257. Both surfaces now store the scope as typed, so the TUI matches the CLI; refusing control characters is left to that issue.

Tests

  • New tests/test_room_goal.py covers:
    • the validation matrix, including leading and trailing controls that a strip-first check would miss, multibyte text at exactly 200 and 201 bytes, and a lone surrogate;
    • a populated pre-goal store gaining the column, and a concurrent first open racing on that column;
    • board scope and project filtering;
    • the CLI preview and the injected pairs;
    • the real launcher's --lessons-fd payload for a goal lesson;
    • padded goals matched end to end through the real coordinator;
    • run still excluding goal lessons;
    • the promote hint: quoted, starting with a dash, with --repo, and with an escaped goal;
    • compare, export and markdown with a hostile goal;
    • TUI paste events into the goal and scope fields, and a TUI round sending goal lessons.
  • Mutation checks: 23 mutations of the new code. 20 make a test fail. The 3 that survive are no-ops: ExactInput subclasses Input, PlainStatic already escapes, and the create error message carries no goal text.
  • Two existing tests changed because they pin the interface: the pre-PR3 golden JSON test drops room.goal as an additive key, and two fakes accept goal=.
  • uv run pytest -q: clean on macOS.

Review

  • Codex design review: 2 rounds.
    • R1 found the verbatim-scope mismatch.
    • R2 found the paste truncation in the TUI field.
  • Codex implementation review: 3 rounds.
    • R1: the promote scope paste (Major), plus two Minors.
    • R2: two Minors.
    • R3, run with the diff only and no framing: SHIP, with one Minor (the --repo hint), fixed in the last commit.

🤖 Generated with Claude Code

memtomem and others added 4 commits November 5, 2026 23:15
`room create --goal GOAL` (and the TUI create dialog's Goal field) fixes a
goal for the room's life, stored verbatim in the new nullable rooms.goal
column. Every round of that room also gets the project's lessons promoted
with --scope GOAL, matched exactly as typed, because promote keeps its scope
and source_key verbatim. Board.project_lessons(project_id, goal=) adds those
lessons; goal=None is the unchanged query, so run, start and goal-less rooms
see no goal lessons.

The CLI and TUI previews mark goal lessons "(goal G)". room list/show, the
TUI room pane, room compare/export and the TUI Compare view show the goal
(room.goal in JSON, additive). A whole-project promote from a goal room
prints the --scope= command that would limit the lesson to the goal. The TUI
goal field is an ExactInput, so a pasted value reaches the validator whole
instead of being cut to its first line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The promote dialog's scope field becomes an ExactInput: a plain Input keeps
only a paste's first line, so a scope copied from a room goal such as one
with a Unicode line separator was stored shortened and never reached that
room. A test now hands a goal lesson to a detached worker without skipping
it, and the guides note that the promote hint prints no command for a goal
the terminal cannot show unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…174)

The detached goal test now runs the real launcher and reads the
--lessons-fd payload the worker gets, resolved from the board through the
room's goal. The TUI round dialog's replace hint adds "--scope GOAL" when a
goal lesson is listed, as the CLI preview does: board decide --supersedes
refuses a replacement in another scope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A promote run with --repo printed a hint command without it, so copying
the command from another directory looked for the room in the wrong
project. The hint now adds --repo when one was given, as the other printed
commands do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@memtomem
memtomem merged commit cba1d3e into main Nov 5, 2026
11 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