CB-582 + CB-604: the ask nudge, and kind: is now validated

The kind: entry documented the unvalidated-typo gotcha as live. CB-604 fixed it,
so the entry now carries the refusal message instead, and points at CB-606 for
the three fields that still have the same shape.

New entry for CB-582: a worker paused on bridge_ask nudges the lead's pane, and
bridge_status and REST both show the open question. The gotcha is the part that
matters — the ~55s window is closed, not removed, so 'do not brief a worker to
ask me' still stands.
Dai Ha
2026-08-16 18:52:19 +02:00
parent 073f01088f
commit c37ced6e6e
+56 -8
@@ -1166,6 +1166,43 @@ durable artefact; the PR is.** If you orchestrate over REST, poll on a timer.
---
## Get told when a worker is waiting on your answer
**What.** A worker on an async (`wait:false`) delegation that pauses mid-turn in `bridge_ask` now
nudges the lead's pane by itself, naming the exact call that resumes it:
```
Worker term_a asked a question (ticket task-3) — answer it with
bridge_send(turnId="term_a#1", content=...) to resume its turn:
which config file?
```
`bridge_status{sessionId}` shows the same open question, and so does REST
`GET /sessions/{id}/status` (as `question`, `turnId`, `ticket`).
**On.** Automatic, on the same terms as the ticket nudge above — it is a **third source in the same
per-lead schedule**, not a new push path, so CB-590's one-schedule-per-lead guarantee still holds and
it spends from its own `push_reminders` budget.
**Why.** `bridge_ask` opens a reverse-rendezvous window of about **55 seconds**. A lead polling on its
normal cadence of minutes never saw it, so the worker timed out and carried on without an answer —
the ask was, in practice, unusable on the delegation mode the charter tells leads to prefer. Two
smaller holes closed with it: REST `GET /tasks/{ticket}` dropped `turnId` on an `ASKING` phase, so a
REST caller could read the question and had no way to answer it, and `bridge_status` said nothing
about an open question at all.
**Gotcha.** **This closes the window; it does not remove it.** The worker still gets ~55 seconds, and
a nudge only helps a lead that is injectable right now — a lead mid-turn for a minute still misses it.
So the standing advice is unchanged: **do not brief a worker to "ask me."** Decide the question before
you delegate, or give the worker an explicit default to use.
Why the window is not simply widened: `DEFAULT_ASK_TIMEOUT_MS = 55_000` sits just under the *worker's
own* MCP client cap of about 60 seconds, so the daemon can return a clean typed timeout before the
client severs the call. Raising the server constant buys nothing — the worker's client kills the call
regardless.
---
## Members cannot use the operator's admin forge token
**What.** Every member launch overwrites `GITEA_ACCESS_TOKEN` with a non-blank blocked sentinel, and
@@ -1671,14 +1708,25 @@ adapter.
existing *Pin an opencode endpoint* entry). The partition-by-kind design itself reads as the
natural consequence: profiles fully own their backend, so routing is a lookup, not a branch.
**Gotcha.** `kind:` is lower-cased but **never validated against the two known values.** A typo —
`kind: opencod`, say — is silently accepted, normalized, and (because it doesn't equal
`"opencode"`) routed into the **claude-code** adapter bucket. If `argv:` was also left unset, the
launch command defaults to `List.of(k)` — literally the misspelled string itself — rather than
`claude`, because the argv-defaulting logic only special-cases the exact string
`"claude-code"`. There is no config-load check anywhere that would catch this before spawn.
Separately, the composite constructor does refuse two adapters claiming the same profile name
("worker profile '…' is claimed by two peer adapters"), so that failure mode is caught loud.
**Validated at config load since CB-604 (2026-08-16).** An unrecognized `kind:` now refuses to
start, naming the profile, the bad value, the accepted set, and what would otherwise happen:
```
refusing to start: profile(s) [gemini=opencod] set an unrecognized kind — accepted values are
claude-code, opencode (case-insensitive); an unrecognized kind would otherwise fall back to the
claude-code adapter and try to launch a program named after the typo.
```
Before that fix, `kind: opencod` was silently accepted, normalized, and — because it did not equal
`"opencode"` — routed into the **claude-code** adapter bucket. With `argv:` also unset, the launch
command defaulted to `List.of(kind)`, literally the misspelled string, because the argv default only
special-cases the exact string `"claude-code"`. Nothing caught it before the spawn failed.
**Gotcha.** The composite constructor separately refuses two adapters claiming the same profile name
("worker profile '…' is claimed by two peer adapters"), so that failure mode has always been loud.
Checking for the same shape elsewhere found three more unvalidated fields — `auth.mode`, the
per-profile `placement:`, and the top-level `placement:` policy, which is validated but only lazily
at first spawn. That is CB-606; until it lands, a typo in those three is still accepted silently.
---