feat(model-groups): surface effective modalities and fail early on spawn (issue #26) - #27
feat(model-groups): surface effective modalities and fail early on spawn (issue #26)#27grzegorznowak wants to merge 31 commits into
Conversation
Implement issue agenticoding#26: spawn and main session become modality-aware. - Add pure derivation module model-groups/modalities.ts (common/supported/effective sets from live ModelRegistry input+reasoning). - Persist per-group modalityOverride with v2 schema-version guard: lossless normalization, opaque-key preservation, v1 in-memory migration, future-version write refusal. - Inject effective modalities per group into the main-session system prompt via before_agent_start. - Add optional spawn requiredModalities checked at the router gate; fail early before child session when the routed model/group cannot satisfy a requirement. - TUI: modality display, empty-common + stale-override warnings, editor for automatic/supported subsets. - Add focused AC1-AC7 test coverage incl. new model-groups-modalities test. Validators: typecheck, npm test 618/618, e2e 16/16, snapshots 11/11, compat:floor, package-host all pass. test:compat:current is skipped (pre-existing host-skew, unrelated; tracked separately).
On the failing
|
…se review gaps A_R: normalizeGroups now runs validateOverride for every accepted source version, so a malformed hand-added modalityOverride in a legacy (missing/0/1) config surfaces as a schema-invalid load issue + backup + empty recovery instead of a raw TypeError from cloneDef. Minimal stabilization; no v1 valid-override migration feature. B (coverage, 619->628): - crud: A1 regression (legacy malformed override), store-level derivation of empty-common + stale flags via summarizeBootValidation counts, CRUD gate rejects unsupported override on create and combined member-change update (0 writes, byte-for-byte unchanged), v2 load rejects non-array/duplicate/ out-of-vocabulary override - tui: modality editor commits override + Automatic path through updateGroup, error-retention on updateGroup failure - integration: session_start boot notification counts for empty-common and stale overrides - router: plain inherited route honors requiredModalities with empty no-op - spawn: tool schema validated via Value.Check, inherited requiredModalities forwarded and succeeding when satisfied PR agenticoding#27 review1 gaps A1/A2 closed (A2 wording corrected in PR description).
# Conflicts: # tests/unit/spawn.test.ts
Code-review findings (implementation pass) — in scope for this PRReviewed against base 1. HIGH — v1 config with a valid hand-added
|
Code-review findings — all addressed (triage + patch)Thanks for the thorough pass. Every in-scope item is resolved. Note: the repo restructured during the #23 merge (
Validation after patch: typecheck PASS · unit 652/652 (+1 new test) · e2e 16/16 · snapshots 11/11 · compat:current 0.84.2 PASS · package-host PASS · Pre-existing HIGH store findings (fsync/locking/symlink/backup) remain tracked in #28, out of scope here as you filed them. |
… TUI editor - documented the locked v1 valid-override pass-through as intentional (no migration) - documented saveModelGroups as low-level CRUD-only cap enforcement (no signature change) - registered spawn-tool test: requiredModalities rejection throws SpawnRouteError before any child session (zero factory calls, both session maps empty) - happy-path registered-spawn test now asserts liveChildSessions cleared - corrected integration fixture comment: claude unresolved -> empty common, override stale - TUI modality editor commit test selects rows by rendered label, not row numbers
…bel, stale editor, prose single-source - store: strip runtime-derived keys (name/scope/sourcePath/modalities/validation) at the persistence boundary; opaque user keys + modalityOverride preserved - tui: draft projection persists only models/override; MODALITIES editor choices union supported + stale override members; empty effective labels unambiguous - index: empty effective renders '(no common modalities)' instead of '(none)' - prose vocab single-sourced from MODEL_GROUP_MODALITIES (spawn + prompt section) - router: documented absent == empty requiredModalities semantics
Resolve the pluggability gap with a typed, compile-time constraint registry + generic envelopes, absorbed into PR agenticoding#27 (unmerged-v2, no migration; version stays 2). - constraints/: pure generic kernel (engine/registry/resolution/presentation) with injectable registries; production registers only modalities. - Modality constraint descriptor owns extraction, aggregation, reconciliation, requirements, diagnostics, presentation, editor; model-groups/modalities.ts becomes thin compatibility façades (parity). - Persisted envelope: ModelGroupDef.constraints (canonical) + modalityOverride (conflict-safe deprecated alias; equal coalesces, unequal rejects; unknown slots round-trip opaquely). - Spawn envelope: constraints (descriptor-generated TypeBox) + requiredModalities alias, normalized at one boundary pre-route. - Router: iterates registry descriptors; modality violations keep the exact missing-modality SpawnRouteError arrays; injected scalar routes to additive constraint-unsatisfied. - Prompt/boot-summary/TUI iterate descriptor presentation metadata; notification text and (no common modalities) fallback byte-identical. - AC6 proof: synthetic testMinContext descriptor traverses the full seam via router + registered spawn (0 factory calls, both maps empty); production registry stays [modalities], no production testMinContext. Refactor-only: no materialized cost/context/param dimension. Full battery green: typecheck, unit 669/669, e2e 16/16, snapshots 11/11, compat:current 0.84.2, package-host, git diff --check.
Debt review — all 7 items triaged + resolved (Option C landed)Reviewed the operator's code-debt pass against the current tree (head #1 (HIGH, pluggability gap) — resolved via Option C. The modality vertical slice (vocab, derivation, persistence, validation, boot counts, TUI editor, spawn schema, route gate) is now backed by a typed, compile-time constraint registry with generic persisted override and spawn requirement envelopes ( #2 (four divergent group semantics) — deferred with the auth dimension (deferred debt #2). The engine now receives an explicit #3 (TUI → config projected pollution) — verified fixed by the earlier B29 derived-key strip (persisted config carries only authored/persisted + the new generic envelope; runtime-derived keys and the old #4 (CRUD-only cap) — documented. Generic envelope normalization runs against the registry before mutation; low-level #5 ("name (none)" collision + stale-override editor blind spot) — fixed: the empty-effective prompt label is now unambiguous #6 (requiredModalities machinery) — reconciled: schema is descriptor-generated from the registry; prose stays modality-specific by design (refactor-only); empty == absent documented; the router rejects unknown requirement keys before session creation. #7 (unbounded uncached derivation) — still deferred (perf), tracked separately. Not resolved by this refactor; a cache/version boundary is a separate follow-up. All prior review items and B-cycle fixes remain green. Full battery at |
…ses (clean v2 surface) Since v2 never shipped, the alias layer had no audience. Make the generic 'constraints' envelope the single public surface: - ModelGroupDef: constraints only; modalityOverride removed. - Spawn tool: constraints only; requiredModalities removed from schema, SpawnParameters, and the normalizer. - Router: requirements come only via constraints. - Store/TUI/modalities: read/write constraints.modalities only. - v1 files: a modalityOverride key is preserved opaquely (not interpreted) and dropped on the first v2 mutation; a constraints key in legacy config is still rejected. - Prompt guidance now instructs passing requirements as constraints. - Tests migrated to the constraints shape; alias-conflict/coalesce tests replaced with canonical round-trips + B1 legacy-opaque tests. Full battery green: typecheck, unit 669/669, e2e 16/16, snapshots 11/11, compat:current 0.84.2, package-host, git diff --check.
Replace the trailing per-group dim footer ('name: modalities ...') with
colored inline chips appended to each group's select label, drawn from the
effective modality set in vocab order (accent=text, success=image,
thinkingHigh=reasoning). Add a single compact legend line above the list.
Respects B29 token discipline (no console.*, no bare ANSI, escaped labels).
…th thinking effort Per operator ADJ-001 tweaks: - Move modality markers out of the select label into the description column, aligned with the thinking-effort segment (same inline position). - Use single colored letters per modality instead of full words: T=text, I=image, R=reasoning. - Recolor text modality from accent (teal) to syntaxKeyword (blue) so it no longer collides with the selected-row highlight, which also wraps in accent. - Legend now reads 'modalities: T text · I image · R reasoning' with colored letters. Each description segment re-asserts dim after inner colored spans so the surrounding dim survives the fg reset (fg resets to terminal default). - Update unit test to scope single-letter assertions to the review row.
… modalities UI ADJ-002 (approved) + option A: - Replace the power-set enumeration in the MODALITIES screen with an Automatic row (reset; [✓] while default) + one toggle row per media modality (text/image). Enter on a toggle flips that modality in/out, seeding from the current effective set on the first toggle so an automatic group transitions to an explicit override smoothly. - New ConstraintEditorRow kind 'toggle' in constraints/presentation.ts; multi-select rows now enumerate automatic + one toggle per choice instead of the 2^n subset list. Activation builds the next override (vocab-ordered) from effective plus/minus the pressed modality. - Reasoning is a distinct pre-existing capability (per-member thinkingLevel + routing gate), so it is excluded from the modality presentation: the MODALITIES screen rows, the list legend/chips, and the EDITOR summary lines all show only text/image. Kernel/schema/router unchanged; any persisted reasoning override value is preserved (never clobbered). - Add generic multi-select editor-shape test; re-baseline the TUI modal editor commit test to toggle individual modalities.
ADJ-002 follow-up: - Enter on a modality toggle (or the Automatic reset) now persists the change and STAYS on the MODALITIES screen so the user can keep toggling, instead of bouncing back to the group editor. Esc still closes back to the editor. - Space key now toggles the selected modality as well as Enter (both dispatch to activate on the MODALITIES screen). - Add a dim hint line: 'up/down navigate • Enter/Space toggle • Esc close'. - Refactor updateDraft into persistDraft (persist + refresh + re-resolve, no navigation) + updateDraft (navigates on success). The MODALITIES toggle uses persistDraft and re-binds the edit draft to the refreshed group. Preserves original stay-put-on-error semantics (afterSuccess only runs on success). - Tests: rework commit test to assert stay-on-screen + Esc exit; add a Space toggle test.
Make text a structurally guaranteed base capability (matching the invariant that image/reasoning both imply text). reconcile now always keeps text in the effective set whenever the group supports it, regardless of override. This eliminates the empty-group asymmetry: deselecting every modality now yields a text-only group (effective=[text]) rather than effective=[] that let text spawns through while denying image/reasoning. MODALITIES screen becomes thin: a fixed non-selectable 'text [always]' line plus the Automatic reset and toggles for additive capabilities only (image; reasoning stays per-model via thinkingLevel, text is the base). Tests: update unresolved-member and override-effective expectations to include always-present text; add a backstop-invariant test; adjust TUI text-row assertion to '[always]'.
Modality-specific usability follow-up only (from #planner UX review); general TUI debt left untouched and tracked separately. - MODALITIES screen title now names the group: 'Modalities — <name>' instead of the bare 'MODALITIES'. - Fix hint wording: 'Esc close' -> 'Esc back', 'Enter/Space toggle' -> 'Enter/Space apply' (the Automatic row resets rather than toggles). - Static text row: align with selectable rows and reword '[always]' -> 'required base' so it describes the invariant without claiming an empty/all-unavailable group is usable. - EDITOR modality line: drop 'Common:' jargon -> 'Supported by every model:'. - Descriptor automatic label: 'Automatic (common: X)' -> 'Automatic (X)' (the 'common:' vocabulary was redundant; text is now the always-present base via the kernel invariant). - Update TUI + constraints test expectations for the new wording/labels.
…odels in editor
(#code-review design) Give the group editor two compact static section bars so
the modalities config reads as its own stand-alone block distinct from the
model membership:
Model Group: review
Location: project / global
Name: review
-- Capabilities --
Supported by every model: text
Modalities: Automatic (text, image)
-- Models --
provider/model (available, thinking X)
+ Add model...
- 'Capabilities' bar groups the group-level modality policy ('Supported by
every model' + the Modalities action).
- 'Models' bar heads the member list/Add row.
- Bars and the 'Supported by every model' line are static/non-selectable, so
all logical row indices and Enter targets are unchanged (only visual bars
added; zero keyboard rework).
- Automatic/Override now capitalized in the editor summary line.
- Theme discipline kept: dim rules, accent labels, no bare ANSI, no chips in
the editor (full names are more legible here; LIST chips/legend unchanged),
MODALITIES screen + constraint kernel untouched.
- Update editor test assertions (Override/Automatic casing, add Capabilities/
Models coverage).
…ete (ADJ-005) Share the modality letter/color lexicon in model-groups/modality.ts and have the #-mention suggestion tooltip prepend each group's effective media-modality letters (T=text I=image, reasoning excluded) via a lazy ctx.ui.theme colorizer, keeping the trigger/value/label and per-model route details intact.
…-005) Pad the effective media-modality letter column to a shared width across the visible suggestion rows so per-model route details start at the same column, instead of variable-offset text that can cut into the capability column.
… (ADJ-005) Image presence implies text, so the #-mentions show a single capability letter (OpenRouter-style): text-only groups keep T, image-bearing groups show only I. Shared modalityLetterRun gains hideTextWhenOtherMedia; the model list rows keep the full T/I lexicon.
When a routed group carries an explicit modality override that excludes image, the spawned child prompt now includes a Model Group capability ceiling notice telling it to report a capability mismatch rather than silently work around it (e.g. reading an image). The router exposes the override ceiling on the route regardless of whether the caller declared a requirement. Advisory only: no enforcement or model-input stripping yet (operator will test-drive before extending scope).
When the operator explicitly names a specific model group (e.g. #quick-review) and the task requires a capability that group lacks, the parent must NOT substitute a different group, inherit the parent model, or work around the missing capability. It stops and reports the mismatch, asking whether to pick a different group or drop the capability. Aligns the system-prompt model-groups guidance (index.ts modelGroupsPromptSection) with the spawn tool prompt guidelines (spawn/index.ts SPAWN_PROMPT_GUIDELINES).
… picker chips (D2) D1: when a #group spawn declares a modality requirement, resolveSpawnModelRoute now narrows the selection pool to members whose individual modality fact satisfies it, and round-robins across that capable set via an in-memory session cursor (state.spawnRouteCursors) instead of uniform random over all usable members. Unauthenticated models and non-capable members are excluded; overridden group ceilings still govern and wholly-incapable groups still reject missing-modality. D2: the Add-model picker (WIZARD_MODEL) now renders a per-model colored T/I capability chip by reusing the shared modality lexicon and getModalitiesModelFact, so capability is visible while adding.
…the union Automatic (no-override) groups now expose the union of their members' individual modality facts as the effective capability set, instead of the intersection. This fixes a real routing gate bug: an Automatic mixed group [text, text+image] with an image spawn selected the image-capable member via D1 pre-selection, but the group gate then saw effective=common=[text] and rejected a missingFromGroup=image. An explicit persisted override remains an authoritative subtractive ceiling (text always present; filtered to each member's supported list), preserving ADJ-005 Policy B and the named-group 'stops + reports' contract (e.g. quick-review [text, reasoning] still rejects image spawns). common stays the factual every-member editor field; only the reconcile default changes. Battery: unit 688, snapshots 11, e2e 16, compat 16, package-host, audit-ci.
…ited modalities guidance Model rows in the editor now render the same colored T/I chips as the Add-model picker (D2), derived per-member from getModalitiesModelFact; unresolved members render without a chip. The modalities hand-edit screen gains two short dim guidance lines explaining Automatic (uses every capability its members support) vs Override (limited to exactly the listed capabilities) — the D3 union-default vs subtractive-ceiling semantics.
The modalities hand-edit screen drops the Automatic selector row and is now built around disabling capabilities from the available union set: T text [required] → I image [off] - Text is a non-toggleable required base row (always [required], matching the operator decision that text is present whenever any model is added). - The remaining union media capabilities are [on]/[off] toggles. - Toggling OFF a capability writes a subtractive override from the current effective (preserving hidden reasoning ceilings, e.g. image off on [text,image,reasoning] stores [text,reasoning]). - Toggling the last capability back ON collapses to Automatic only when the ordered candidate equals the full union — so no override (and no spawn ceiling) is stored for a non-limit; entering Automatic remains fully reachable even without a selector row. - A single dynamic status line replaces the two static guidance lines: 'Automatic — using every capability its members support.' vs 'Override — media limited to <media>.' - Zero-toggle screens (text-only) show 'No optional media capabilities available.' + 'Esc back'; Enter/Space are inert. Generic constraintEditorRows() and its constraint-layer test are unchanged; only the TUI screen specializes. Tests updated for the new interaction (override commit, re-enable-to-Automatic collapse, failure notify, dynamic status) plus a new text-only inert-screen test. Battery: unit 690, snapshots 11, e2e 16, compat, package-host.
The [on]/[off] state of a capability row read like a fixed fact beside the [required] text base row. Editable rows now append a dim hint so the image capability is clearly interactive: T text [required] → I image [on] — Enter/Space toggles The required text row and text-only screens are unchanged (no hint). Tests extended: editable row shows the hint, required row does not, text-only screen never shows it.
…t highlight Per planner-design + operator calls: - The per-row '— Enter/Space toggles' hint is removed (was noise on the typical T+I vocabulary). - Single editable row screens drop '↑↓ navigate' — the footer is just 'Enter/Space toggle • Esc back' (arrow-nav returns only if a second visible media row ever appears). - The selected capability row is now accent-highlighted: accent arrow + accent label while the modality letter keeps its own color. Previously the selectableLine accent wrap was neutralized by inner color resets, so the row appeared unhighlighted. - Text-only screens keep 'No optional media capabilities available.' + 'Esc back' with Enter/Space inert. Tests: hint assertions replaced by compact-footer assertions; accent-token test extended to MODALITIES asserting the highlight markup; width-bounded test comment corrected (the assertion after the name row is MODALITIES, not MODEL_EDIT). Battery: unit 690, snapshots 11, e2e 16, compat, package-host.
…ing#27) - V2-A-01: fully-stale non-empty modality override falls back to derived union; explicit [] override stays text-only; subtractive ceiling kept - V2-B-02: TUI cloneDef preserves opaque top-level group keys on edit - V1-01: trim trailing whitespace in modalities diagnostic - V2-B-01/V1-03: low-level save is shape-only, CRUD rejects union-cap invalid overrides; boundary locked by test - V1-04: malformed legacy v1 modalityOverride loads opaquely, drops on v2 - V3-A-02: generic descriptor selection + descriptor-owned ceiling advisories (no image literal in spawn) - test-only synthetic min-context and max-budget descriptors prove the capability-generic seam; production registry stays exactly {modalities} Battery: typecheck, npm test 695/695, e2e 16/16, snapshots 11/11, compat 166/166, package-host, audit-ci, git diff --check
…agenticoding#27) - N-27-1: cursor round-robin engages only when declared requirements genuinely narrow the pool (capable < usable); routed empty {required:[]} returns to uniform RNG (regression fixed + routed test) - F2-3: reset invariant now populates and asserts spawnRouteCursors cleared - F2-2: spawn tool constraints schema built from the registry passed at registration (injected/test descriptors accepted at tool-schema level); whole-path test validates through real registered schema, not direct execute - D-7: removed dead descriptor fields (persistence.clone, editor.format, editor.allowAutomatic); single-sourced MODEL_GROUP_MODALITY_PROSE; corrected reasoning-doc default - readability: split modalities.ts nested ternary into explicit four cases F2-1 (downgrade data-loss) intentionally left open per operator. Battery: typecheck, npm test 697/697, e2e 16/16, snapshots 11/11, compat 167/167, package-host, audit-ci, git diff --check.
Split the single-line field declarations + nested-ternary message builder into a documented class with one field per line, an explicit details interface, and a branch-based describeRouteError helper. Public surface (reason/group/missing*/constraintUnsatisfied/message strings) is unchanged and asserted by existing router + spawn tests. Battery: typecheck, unit 697/697, e2e 16/16, snapshots 11/11, compat 167/167, package-host, audit-ci, git diff --check.
…re/tui/types Structure-only readability pass (no logic change, no new behavior): - router: expand effectiveGroupMap loop, resolveSpawnModelRoute setup/pool/ cursor/route construction, and modality-error extraction into named locals - store: split backups, loadScope, mergeLoaded, normalizeGroups, saveModelGroups, CRUD (create/update/rename/delete/move), validateModelGroups into readable multi-line form; preserved error-wrapping order and phases - tui: bind MODEL_EDIT lookup + decompose renderEditorComponent nested chain - types: expand ModelGroupsPersistenceError ctor Independent review caught and I fixed one real delta introduced mid-refactor: moveGroup source-removal failure rewrap reported newScope path; restored to oldScope sourcePath. F2-1 downgrade guard deliberately NOT added (store.ts semantics unchanged). Battery: typecheck, unit 697/697, e2e 16/16, snapshots 11/11, compat 167/167, package-host, audit-ci, git diff --check.
Extract cohesive helpers (decodeDeclared, inheritedRoute, usableMembers, selectMember, buildRoutedRoute, attachCeilings, collectViolations, raiseRouteFailure) so the routing function drops from ~43 decision points to ~7. Stictly behavior-preserving: cursor round-robin semantics, capable-pool fallback, RNG clamp, error taxonomy/messages, and empty-requirements guard are unchanged and lock-blocked by the existing router + spawn suites. Battery: typecheck, unit 697/697, e2e 16/16, snapshots 11/11, compat 167/167, package-host, audit-ci, git diff --check.
README: add a plain-language Capabilities feature bullet and section covering the T/I modality chips in pickers/editor rows, narrowable group sets, and fail-early spawn constraint checks. Model Groups bullet unchanged. CHANGELOG [Unreleased]: add capability-aware spawn routing + pluggable capability kernel entries under Added, and per-member capability chips under Changed. Modalities scoped to user-facing text/image chips.
Implements issue #26 — "Let the spawn system read model modalities to understand the tasks it's good/bad at".
Related to
agenticoding/pi-agenticodingissue #26 (spawn reading model modalities). No issue is auto-closed by this PR.What this PR does
The spawn system and the main session now understand what each Model Group is capable of, and fail early when a delegated task asks for a capability the selected model/group cannot deliver.
Capabilities are pluggable, not hard-coded to modalities.
model-groups/constraints/kernel generalizes how model capabilities are derived and checked. One constraint descriptor owns the whole concern for a single capability: how to read the fact off aModel, how to combine it across the group's members, how to reconcile a user override, how to check a spawn requirement, and how to present it in the UI/prompt.modalities). Future capabilities (e.g. a minimum context-window) can be added as another descriptor plus tests, with no edits to the layer that loads config, routes spawns, or renders the UI. This is demonstrated by a synthetic test-only descriptor that runs through the whole path without ever appearing in production code.Persisted overrides use a single versioned, forwards-compatible shape.
constraintsmap on the group (e.g.constraints.modalities).2; since this branch has not shipped v2 yet, no migration path is needed.modalityOverridekey is preserved as opaque data and dropped on the first v2 write).Spawn fail-early.
constraints(a keyed object, schema-generated from the descriptors), normalized at one boundary before routing.missing-modalitySpawnRouteError; unknown requirement keys are rejected; the check holds before any child session is created.Main-session awareness and TUI.
before_agent_startinjects each group's capability summary (e.g.websearch (text, image)) into the orchestrator system prompt, reusing the existing hook.Scope note: intentionally refactor-only
This PR deliberately does not add a cost, minimum-context, or parameter-count feature. It lays the pluggable foundation and exercises it with a synthetic test descriptor so the extension point is proven rather than aspirational. Two constraints of the derivation are handled separately: deciding whether group capability means "all registered members" vs "only authenticated/usable members" is a separate issue, and a parameter-count source does not exist in the host
Modeltype today (model-name inference is not an option).Review feedback addressed
Review cycle 1 (contract + first implementation review)
modalityOverridepreviously crashed load; they now validate cleanly and recover with a.bakbackup.Code-review implementation pass
requiredModalitiesrejection throws before any child session (zero factory calls, both session maps empty).liveChildSessionsis cleared.Pluggability review
Follow-on capability routing (D1 + D2, on this branch)
D1 — capability-aware model pre-selection in spawn. When a
#groupspawn declares a non-empty modality requirement (e.g.constraints.modalities.required: ["image"]),resolveSpawnModelRoutenow narrows the auth-usable pool to members whose per-model modality fact satisfies it, then round-robins across that capable set via an in-memory per-session cursor (state.spawnRouteCursors) instead of picking uniform-random among all usable members. The earlier failure where an image-capable member existed in the group but a text-only sibling happened to be selected (then rejected as missing-modality) is eliminated. Watch-outs honored: unauthenticated capable models are excluded, overridden group ceilings still govern, and a wholly-incapable group still rejects withmissing-modality. Round-robin is threaded only from the spawn path — main-session group switches keep the prior behavior.D2 — capability chips in the Add-model picker. The
WIZARD_MODELstep now renders a per-member coloredT Ichip (shared modality.ts lexicon +getModalitiesModelFact) next to each model, so capability is visible while adding. The editor member-list rows are unchanged (out of scope).D3 — Automatic groups default their capability set to the union. Without an explicit override, a group's
effectivemodalities are now the union of its members' individual modality facts (was: the intersection of what every member shares). This fixes a real routing-gate bug: an Automatic mixed group ([text, text+image]) with an image spawn selected the image-capable member via D1 pre-selection, but the group gate then readeffective=common=[text]and rejectedmissingFromGroup=image. With the union default the gate passes and the image-capable member runs. Guarantees kept:quick-review-style[text, reasoning]still rejects image spawns even when the routed member itself supports image (regression-tested).commonremains the factual every-member editor field (Supported by every model: …); only the reconcile default changes (one line inreconcile).D4 — Per-model capability chips in the editor + limited/unlimited guidance. Two TUI refinements.
Modelssection renders the same colored per-member chip as the Add-model picker (D2), derived live from the model registry viagetModalitiesModelFact(unresolved members render without a chip). Example:Automatic: the group uses every capability its members support.Override: the group is limited to exactly the listed capabilities.D5 — Modalities editor rebuilt around disabling capabilities (toggle-union UX). The hand-edit screen drops the "Automatic (…)" selector row and is now built around disabling capabilities from the available union set:
T text [required]is a non-toggleable required base row (text is guaranteed whenever any model is present).Union media capabilities (
image) are[on]/[off]toggles;reasoningstays out of the media screen (per-modelthinkingLevel, per the earlier "don't collude reasoning with modalities" decision).Toggling off writes a subtractive override from the current effective — hidden
reasoningceilings are preserved (image off on[text,image,reasoning]stores[text,reasoning]).Toggling the last capability back on collapses to Automatic only when the candidate equals the full union, removing the stored override — so no spawn ceiling attaches to a non-limit, and Automatic stays reachable without a selector row.
One dynamic status line replaces the two static guidance lines:
Automatic — using every capability its members support./Override — media limited to <media>.Text-only groups show
No optional media capabilities available.+Esc back; Enter/Space are inert (no-op).The generic
constraintEditorRows()constraint-layer surface is unchanged; only the TUI screen specializes.D6 — Modalities editor streamlined to a single toggle + selected-row highlight. Amendments to the D5 toggle-union screen after operator feedback:
— Enter/Space toggleshint is removed (it was noise for the dominant T+I vocabulary).↑↓ navigate; the footer is justEnter/Space toggle • Esc back(arrow navigation returns automatically if a second visible media row is ever added).→+ accent label, modality letter keeps its own color) — previously theselectableLineaccent wrap was neutralized by inner color resets, so the being-edited item appeared unhighlighted.No optional media capabilities available.+Esc back; Enter/Space are inert.Render (T+I):
Validation (full battery, all PASS)
npm run typechecknpm test(690/690)npm run test:e2e(16/16)npm run test:snapshots:check(11/11)npm run test:compat:current(0.84.2)npm run test:package-hostgit diff --checkclean; no test-only descriptor key leaks into product files; no stray console outputRefers to #26.