Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/council.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,17 @@ and command authority to every seat. Both refuse while a turn is in flight, and
kills anything: seats move on their next turn. Only the bare word is a command, so
`/write a test for this` never changes the posture.

**`/retry` sends the last brief again, to the seats that owe an answer.** A turn where four
seats replied and one failed, was cut with `x`, or fell over leaves you retyping the brief and
the mentions to finish it. Type `/retry` instead: the brief comes back into the composer
addressed to exactly the seats that did not answer — `@codex @agy <brief>` — and **enter is
still what sends it**, so the footer prices the re-send before you pay for it and you can edit
the draft first. A seat that answered is never re-sent to, and neither is one that sat the turn
out, so the bill never grows past the turn you are finishing. A seat that finished and streamed
nothing counts as having answered: that is a measured zero, not a missing reply. It refuses
while a turn is in flight, when nothing has been dispatched yet, and when every seat answered.
The text goes out unchanged — `/retry` re-sends, it does not re-brief.

**A slash the room does not know is refused, not sent.** A draft that opens with `/` and
names no room command used to go to the vendors as a brief, so a mistyped verb cost every
seated seat a turn. Now nothing spawns, the draft stays in the composer, and the notice names
Expand Down
70 changes: 70 additions & 0 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -10271,6 +10271,76 @@ the honest shape rather than only a saving: the two take one argument in one voc
only in direction, so a reader who finds either has found both. "times" paid for the width — the row
is a list of controls, and `/trace <file>` is unambiguous without the verb.

#### Amendment, 2026-08-17: `/retry` sends the last brief again, to the seats that owe an answer

**The gap.** §9.37's 2026-08-17 amendment gave the operator a way to stop ONE seat of an ordinary
turn, and it argued from the room's most probable live failure: five seats on an `@all` turn, four
answers, one vendor that fails or stalls. The room can now end that turn. It has no way to finish
the brief. The only act left is to retype the brief and retype the mentions — arithmetic at the
keyboard, on the one line where getting it wrong bills seats that already answered. That is the
same complaint `-@` and `/unseat` were built for, one turn later.

**`/retry` puts the last dispatched brief back in the composer, addressed to the seats that did not
answer it.** The brief comes from the columns' own per-turn record (`Column.Prompt`), unchanged. The
mentions are the seats that owe an answer, so the draft reads `@codex @agy <brief>` — a draft the
operator could have typed, in the grammar that already exists.

**It arms; it does not dispatch, and enter is still what spends the money.** The verb writes a
draft, `setDraft` re-derives the route from it, and the footer prices that route through the same
`State.SeatsIn` intersection dispatch gates on (§9.21). So the operator reads the bill before paying
it, and can edit the draft — drop a seat off the front, fix a word — because it is an ordinary
draft. A verb that spawned on the spot would spend up to five quotas on a keystroke that named none
of them, and the room would have no surface left on which to say which ones.

**What counts as an ANSWER is defined against the four endings**, and it is narrower than "the
column looks empty":

- **`PhaseDone` answered**, including the seat whose body reads `[Turn completed with 0 text chunks
streamed]`. That is a measured zero, not a missing reply (§4a.1), and re-sending on it would be
the room overruling a vendor's honest empty answer — and billing a seat that did the work.
- **`PhaseFailed` and `PhaseCancelled` did not.** Cancelled covers `ctrl+c` and the per-seat
give-up together, deliberately: a seat the operator cut is the case this verb exists for, and
after the turn the room holds one cancelled phase for both.
- **A seat that SAT THE TURN OUT is not a candidate at all.** `dispatch` never calls `startTurn` on
it, so its `Column.TurnN` still names the last turn it took and the scan skips it. That is
load-bearing rather than incidental: without it, a `/retry` after an `@codex` turn would widen the
bill to four seats the operator deliberately did not address.

**Bare-only, on `/read` and `/write`'s rule.** The verb takes no argument, and "/retry the failing
test" is a sentence someone types. A verb that swallowed that argument would run a re-send and
discard the brief — §9.17's vanishing-brief failure. The bare draft is the command; anything longer
is refused with the space escape named, which costs nothing.

**Three refusals, three sentences, and only the first keeps the draft.** *A turn in flight*: the
phases this verb reads are not settled, so any list it produced would be a claim about a turn that
has not ended — and the operator still wants the verb one turn later, which is `postureCommand`'s
own reason for holding the draft. *No brief on record*: turn 0 and the degenerate turn whose brief
sanitized away to nothing are one sentence, because they are one fact. *Every seat answered*: there
is nothing to re-send, and saying so beats a composer the operator has to clear by hand. The verb
sits in `roomVerbs` like every other, so §9.31's walked refusal teaches it for free — no second copy
of the vocabulary was added, and the notice fits the reference width with one cell to spare.

**`--brief` stays unfiled, and this verb is careful not to file it.** It re-sends the brief
UNCHANGED: no re-briefing, no edit, no automatic second attempt, and it never reads `Model.brief`.
§9.17's sweep left `--brief` out on the ruling that first-turn context is a different feature from
re-briefing; that question is still open and still to be decided on its own.

**After a race it re-sends the race's brief as an ORDINARY turn.** `Column.Prompt` holds the brief
the racers were given and never the `/arena` draft that wrapped it, and this verb invents no grammar
to put the wrapper back. The composer shows exactly what will be sent before enter, which is where
the operator reads that the worktrees are not part of it; `/arena <brief>` races again.

**The help panel does not name it**, on `/adopt` and `/arena drop`'s precedent: the room-controls
row is at its budget (§9.20), and the verb is taught by the slash refusal and by this block.

Verified offline only. `roomcmd_test.go` pins the four endings producing the right seat list, the
measured zero counting as an answer, the three refusals, the bare-word rule, the verb's appearance
in the walked refusal table, and — at the dispatch level, with `countSpawns` — that the re-send
spawns one process per seat owing an answer and none for a seat that already replied. The
`slash-refusal` goldens and their `--ascii` twins carry the new word. No test here spawns a vendor.
A live `/retry` on the Windows reference box is not owed as a separate payment: nothing here is a
claim about vendor behaviour, and every process the verb can cause is an ordinary turn's spawn.

<a id="s9-32"></a>

### 9.32 the room remembered where it was and forgot who was in it
Expand Down
136 changes: 136 additions & 0 deletions internal/council/roomcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ func roomVerbs() []roomVerb {
{verb: "/cd", run: (*Model).cdCommand},
{verb: "/flow"},
{verb: "/read", bare: true, run: func(m *Model, _ string) bool { return m.postureCommand(false) }},
// Bare-only, for /read and /write's reason rather than for symmetry: it
// takes no argument, and "/retry the failing test" is a sentence someone
// types. A verb that swallowed that argument would run a re-send and
// discard the brief the user meant — §9.17's vanishing-brief failure. The
// bare form is the command; anything after it is refused with the space
// escape named, which costs nothing.
{verb: "/retry", bare: true, run: (*Model).retryCommand},
{verb: "/seat", run: (*Model).seatCommand},
{verb: "/trace", run: (*Model).traceCommand},
{verb: "/unseat", run: (*Model).unseatCommand},
Expand Down Expand Up @@ -880,6 +887,135 @@ func (m *Model) applyPosture(write bool) {
}
}

// retryCommand puts the last dispatched brief back in the composer, addressed
// to the seats that did not answer it.
//
// IT DOES NOT DISPATCH, and that is the shape of the control rather than a
// caution. The seats a re-send would bill are read off a turn that is already
// over, so the operator can see the bill before paying it: setDraft re-derives
// the route from the draft this builds, and the footer prices it through the
// same State.SeatsIn arithmetic dispatch itself gates on (§9.21). Enter sends
// it, the way enter sends any other brief. A verb that spawned on the spot
// would spend up to five quotas on a keystroke that named none of them, and the
// room would have no surface on which to say which ones.
//
// THE DRAFT IS A MENTION LIST AND A BRIEF, which is why this adds no grammar.
// "@codex @agy <brief>" is what a user types by hand, so ParseRoute reads it,
// the footer renders it, and dispatch intersects it with the roster exactly as
// it does for typed text. The draft is also editable, because it is an ordinary
// draft — the operator can drop a seat off the front before pressing enter.
//
// WHAT COUNTS AS AN ANSWER is defined against the four endings §9.37's
// 2026-08-17 amendment names, and it is narrower than "the column looks empty":
//
// - PhaseDone ANSWERED. That includes the seat whose body reads "[Turn
// completed with 0 text chunks streamed]": it is a measured zero, not a
// missing reply (§4a.1), and re-sending on it would be the room overruling a
// vendor's honest empty answer.
// - PhaseFailed and PhaseCancelled did not answer. Cancelled covers ctrl+c and
// the per-seat give-up (`x`) together, deliberately: a seat the operator cut
// is the case a re-send exists for, and after the turn the room holds one
// cancelled phase for both.
// - A seat that SAT THE TURN OUT is not a candidate at all. dispatch never
// calls startTurn on it, so its Column.TurnN still names the last turn it
// took and the scan below skips it. That is load-bearing: without it, a
// /retry after an "@codex" turn would widen the bill to four seats the
// operator deliberately did not address.
//
// It re-sends the brief UNCHANGED and does nothing else. No re-briefing, no
// edit, no automatic second attempt. That is what keeps §9.17's `--brief`
// question unfiled: `--brief` is first-turn context by definition, re-briefing
// is a separate feature to be decided on its own, and this verb never reads
// Model.brief.
//
// AFTER A RACE it re-sends the race's brief as an ORDINARY turn. Column.Prompt
// holds the brief the racers were given and never the "/arena" draft that
// wrapped it, and this verb invents no grammar to put the wrapper back. The
// composer shows exactly what will be sent before enter, which is where the
// operator reads that the worktrees are not part of it; "/arena <brief>" races
// again.
//
// THREE REFUSALS, THREE SENTENCES, and only the first keeps the draft. A turn in
// flight: the phases this verb reads are not settled, so any list it produced
// would be a claim about a turn that has not ended — and the operator still
// wants the verb one turn later, which is postureCommand's own reason for
// holding the draft. No brief on record: turn 0 and the degenerate turn whose
// brief sanitized away to nothing are one sentence, because they are one fact.
// Every seat answered: there is nothing to re-send, and saying so is better than
// a composer the operator has to clear by hand.
func (m *Model) retryCommand(_ string) bool {
if m.turn != nil {
m.st.Notice = "a turn is in flight — /retry re-sends between turns"
return true
}

brief, seats := m.lastTurnUnanswered()
if brief == "" {
m.st.Notice = "no brief to re-send — /retry sends the last one again to the seats that did not answer it"
m.setDraft("")
return true
}
if len(seats) == 0 {
m.st.Notice = "every seat answered turn " + itoa(m.st.Turn) + " — /retry has nothing to re-send"
m.setDraft("")
return true
}

var draft strings.Builder
names := make([]string, 0, len(seats))
for _, v := range seats {
draft.WriteString("@" + string(v) + " ")
names = append(names, string(v))
}
draft.WriteString(brief)
m.setDraft(draft.String())
// ", " joins the names, because Route.label joins them that way and the
// footer is about to print the same set one line below this notice. Two
// spellings of one list would read as two lists.
m.st.Notice = strings.Join(names, ", ") + " did not answer turn " + itoa(m.st.Turn) +
" — enter re-sends that brief to them, and to no other seat"
return true
}

// lastTurnUnanswered reads the previous turn off the columns: the brief it
// carried, and the seats it left without an answer.
//
// THE COLUMNS ARE THE RECORD HERE, not Column.History, and that is not a
// shortcut. startTurn files a turn into History when the NEXT one is dispatched,
// so between turns the finished turn is still the live one on the column;
// reading History would answer about the turn before last.
//
// The brief is taken from the first column that took the turn, and any of them
// would do — dispatch sanitizes ONE echo for the whole turn and hands that same
// string to every seat it starts. It is read WITHOUT the seating filter, so a
// seat unseated since the turn can still supply the text it was asked. The seat
// list applies the filter, because a mention of an unseated seat prices nothing:
// State.SeatsIn intersects the route with the roster, and dispatch drops the
// same seat for the same reason.
//
// An empty brief with seats behind it is possible and is left to the caller: a
// draft of nothing but control characters passes dispatch's own empty check and
// sanitizes to "", so the turn is on record with no text to re-send.
func (m *Model) lastTurnUnanswered() (brief string, seats []model.VendorID) {
last := m.st.Turn
if last == 0 {
return "", nil
}
for _, c := range m.st.Columns {
if c.TurnN != last {
continue
}
if brief == "" {
brief = c.Prompt
}
if c.Phase == PhaseDone || !m.st.seats(c) {
continue
}
seats = append(seats, c.Vendor)
}
return brief, seats
}

// plural is the one-word difference between "1 turn" and "2 turns".
func plural(n int, word string) string {
if n == 1 {
Expand Down
Loading