Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
99d66e9
task: Add open/all source toggle to crew config
santoshkumarradha Sep 16, 2026
c1e4cb5
docs: the change entry for the crew model family toggle
santoshkumarradha Sep 16, 2026
e63b439
crew: round-1 review fixes
santoshkumarradha Sep 16, 2026
304a5a2
crew: round-2 review fix
santoshkumarradha Sep 16, 2026
65477c0
task: Add open/all selector to /crew TUI picker
santoshkumarradha Sep 16, 2026
9189ef7
Merge branch 'task/add-open-all-selector-to-crew-tu-9e8cc0' into feat…
santoshkumarradha Sep 16, 2026
ba9c12d
crew: reconcile the picker onto config's source helpers
santoshkumarradha Sep 16, 2026
ea05bc0
crew: a chosen family counts as an answered crew
santoshkumarradha Sep 16, 2026
e1a037e
Merge remote-tracking branch 'origin/dev' into feat/crew-open-all-toggle
santoshkumarradha Sep 16, 2026
f6c2924
crew: the open table takes the picks we landed on
santoshkumarradha Sep 16, 2026
eff44b5
docs: the change entry carries the open-table move and the family gate
santoshkumarradha Sep 16, 2026
71ce9be
crew: the family reaches the fallback, the settings skin, and the row…
santoshkumarradha Sep 16, 2026
b8ca5f5
crew: round-1 review fixes on the new table
santoshkumarradha Sep 16, 2026
8db5708
crew: round-2 review fixes
santoshkumarradha Sep 16, 2026
ce9759e
crew: round-3 review fixes
santoshkumarradha Sep 16, 2026
956e1f5
crew: round-4 review fixes
santoshkumarradha Sep 16, 2026
37ad27f
crew: fixes from the first Opus round
santoshkumarradha Sep 16, 2026
6ffb958
crew: the fixes from the Opus confirmation round
santoshkumarradha Sep 16, 2026
72b0274
crew: the comments the atomic write left behind
santoshkumarradha Sep 16, 2026
70d0e20
crew: round-four fixes from the Opus review
santoshkumarradha Sep 16, 2026
234300e
Merge branch 'dev' into feat/crew-open-all-toggle
santoshkumarradha Sep 16, 2026
894292f
crew: round-five fixes from the Opus review
santoshkumarradha Sep 16, 2026
64ab7ea
crew: round-six fixes from the Opus review
santoshkumarradha Sep 16, 2026
0700164
crew: round-seven fixes from the Opus review
santoshkumarradha Sep 16, 2026
c24af02
crew: round-eight fixes from the Opus review
santoshkumarradha Sep 16, 2026
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
24 changes: 24 additions & 0 deletions docs/changes/unreleased/1073-crew-model-family.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
kind: added
title: A model family toggle under the crew, so a preset can answer from open weights or from everything
pr: 1073
surface: [chat, engine]
invalidates:
- "The three crew preset words (frugal, balanced, max) always resolved to open-weight models, and `crewModels` in `internal/config/crew.go` was the whole table of what they meant. There is now a second table `crewAllModels`, and `CrewModelsForSource(source, preset)` picks between them on the new `models.crew.source` setting (`open` default, `all` for closed and frontier). A profile that never answers the row still resolves the open table."
- "The careful seat was only ever required to be a different vendor from the worker by convention. It is now asserted by a test over both families: within each preset of `crewModels` and `crewAllModels`, the worker and high ids name different providers."
- "`models.crew` was the only crew setting. The crew row's derivation and `ApplyCrew` now also read `models.crew.source`, so the preset word and the five ids it summarizes can never be drawn from different families; flipping the family reads the crew as `custom` while the tier rows are written and match no preset in the new family; seats nobody pinned follow the family and keep the preset word."
- "The open table's frugal mastermind and its balanced careful and mastermind seats moved onto the roster chosen seat by seat, and the shipped default crew, which is exactly the balanced row, moved with it. Only the default moved; a profile that answered the crew keeps its rows."
- "A profile that applied the old `frugal` or the old `balanced` now matches no preset in the open table, because the roster moved: the crew word, the `/crew` highlight and the seat rung read `custom` until the preset is re-applied, which writes the new roster."
- "`CrewConfigured` counted only the tier rows, so a person who chose a family and never pinned a tier could still be handed a preset by the first-run setup. It now counts `models.crew.source` too: a chosen family is an answered crew."
---

The crew shipped open-weight models for a reason: a default nobody's pricing can
move under them. That reason still holds for the default, so the toggle defaults to
`open` and changes nothing for anyone who does not ask. It is for the person who
has decided the frontier models are worth it on a given machine, and wants the same
three preset words to mean them.

The two families are a table apiece, not a filter over one, because the picks are
argued seat by seat rather than chosen by a rule about vendors or price. The all
table is new and mirrors the open one's three presets; the open table moved onto the
roster this work chose, which is the one edit here that changes a shipped default.
260 changes: 230 additions & 30 deletions internal/config/crew.go

Large diffs are not rendered by default.

229 changes: 226 additions & 3 deletions internal/config/crew_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ func TestCrewPresetsNameTheApprovedModels(t *testing.T) {
ModelTierLow: "deepseek/deepseek-v4-flash-0731",
ModelTierWorker: "deepseek/deepseek-v4-flash-0731",
ModelTierHigh: "z-ai/glm-5.3-flash",
ModelTierMastermind: "z-ai/glm-5.3-flash",
ModelTierMastermind: "z-ai/glm-5.3",
},
CrewBalanced: {
ModelTierReflex: "mistralai/mistral-nemo",
ModelTierLow: "deepseek/deepseek-v4-flash-0731",
ModelTierWorker: "z-ai/glm-5.3-flash",
ModelTierHigh: "qwen/qwen3.8-27b",
ModelTierMastermind: "z-ai/glm-5.3",
ModelTierHigh: "moonshotai/kimi-k3",
ModelTierMastermind: "moonshotai/kimi-k3",
},
CrewMax: {
ModelTierReflex: "mistralai/mistral-nemo",
Expand Down Expand Up @@ -211,6 +211,191 @@ func TestTheCrewRowRefusesAWordThatIsNotAPreset(t *testing.T) {
}
}

// ── the family the three words draw from ────────────────────────────────────

// THE CAREFUL SEAT IS ALWAYS A SECOND VENDOR, IN EVERY PRESET OF EVERY FAMILY.
// A check from the same vendor as the work it checks is a check that shares
// that vendor's blind spots, and the two tables are one law: a preset that
// moved to the all family and put the same vendor on worker and careful would
// break the property the open crew was built on while pretending to keep it.
func TestTheCarefulSeatIsAlwaysASecondVendorInEveryFamily(t *testing.T) {
vendor := func(id string) string {
if at := strings.Index(id, "/"); at > 0 {
return id[:at]
}
return id
}
for _, family := range CrewSources {
for _, preset := range CrewPresets {
models, ok := CrewModelsForSource(family, preset)
if !ok {
t.Fatalf("there is no %s preset in the %s family", preset, family)
}
if vendor(models[ModelTierWorker]) == vendor(models[ModelTierHigh]) {
t.Errorf("%s under %s has %q working and %q checking, and a check from "+
"the same vendor catches what that vendor lets through",
preset, family, models[ModelTierWorker], models[ModelTierHigh])
}
}
}
}

// THE ALL FAMILY IS LOCKED, not derived: this is the page that says which ids
// the three words mean. A wrong id here is a person's bill, and a silent
// rewrite of the locked table would be a decision made by a diff.
func TestTheAllFamilyNamesTheLockedModels(t *testing.T) {
want := map[string]map[string]string{
CrewFrugal: {
ModelTierReflex: "google/gemini-2.5-flash",
ModelTierLow: "deepseek/deepseek-v4-flash-0731",
ModelTierWorker: "openai/gpt-5.6-sol",
ModelTierHigh: "google/gemini-3.8-flash",
ModelTierMastermind: "anthropic/claude-opus-5",
},
CrewBalanced: {
ModelTierReflex: "google/gemini-2.5-flash",
ModelTierLow: "deepseek/deepseek-v4-flash-0731",
ModelTierWorker: "openai/gpt-5.6-sol",
ModelTierHigh: "anthropic/claude-opus-5",
ModelTierMastermind: "anthropic/claude-fable-5.1",
},
CrewMax: {
ModelTierReflex: "google/gemini-2.5-flash",
ModelTierLow: "deepseek/deepseek-v4-flash-0731",
ModelTierWorker: "anthropic/claude-fable-5.1",
ModelTierHigh: "openai/gpt-6-astra",
ModelTierMastermind: "anthropic/claude-fable-5.1",
},
}
for preset, expected := range want {
got, ok := CrewModelsForSource(CrewSourceAll, preset)
if !ok {
t.Fatalf("there is no %s preset in the all family", preset)
}
for _, tier := range ModelTiers {
if got[tier] != expected[tier] {
t.Errorf("%s under all sets %s to %q, want %q", preset, tier, got[tier], expected[tier])
}
if !strings.Contains(got[tier], "/") {
t.Errorf("%s under all sets %s to %q, which is not a provider-qualified id", preset, tier, got[tier])
}
if err := ValidateTierValue(got[tier]); err != nil {
t.Errorf("%s under all sets %s to %q, which its own row would refuse: %v", preset, tier, got[tier], err)
}
}
}
// THE LINES ARE PART OF THE TABLE, and they are the words a person reads while
// deciding to spend frontier money: a line naming a model the preset does not
// pick is the contradiction the chooser exists to prevent.
wantLines := map[string]map[string]string{
CrewSourceOpen: {
CrewFrugal: "deepseek works, glm-5.3 thinks · pennies a day",
CrewBalanced: "glm-flash works, kimi-k3 checks and thinks",
CrewMax: "glm-5.3 works, kimi-k3 thinks and checks",
},
CrewSourceAll: {
CrewFrugal: "gpt-5.6-sol works, gemini-flash checks, opus thinks",
CrewBalanced: "gpt-5.6-sol works, opus checks, fable thinks",
CrewMax: "fable works and thinks, astra checks",
},
}
for family, lines := range wantLines {
for preset, line := range lines {
if got := CrewLineFor(family, preset); got != line {
t.Errorf("the %s family's %s line is %q, want %q", family, preset, got, line)
}
}
}
}

// A SOURCE NOBODY SET IS THE OPEN FAMILY. The all table may exist, but a profile
// that never answered the row resolves the open family: the shipped defaults,
// the open presets under the same words, and a write that lands the open ids.
// This is the default-preservation law for the FAMILY. An unknown word is held
// to the same law, silently, the way a retired choice reads everywhere else.
func TestASourceNobodyAnsweredReadsTheOpenFamily(t *testing.T) {
dir := t.TempDir()
if got := CrewSourceAt(dir); got != DefaultCrewSource {
t.Fatalf("an untouched profile reads the source as %q, want %q", got, DefaultCrewSource)
}
if got := CrewAt(dir); got != DefaultCrew {
t.Fatalf("an untouched profile reads the crew as %q, want %q", got, DefaultCrew)
}
if err := ApplyCrew(dir, CrewMax); err != nil {
t.Fatal(err)
}
want, _ := CrewModels(CrewMax)
for _, tier := range ModelTiers {
if got := TierModelAt(dir, tier); got != want[tier] {
t.Errorf("with no source set, max wrote %s to %q, want the open %q", tier, got, want[tier])
}
}
// A WORD THIS BUILD DOES NOT KNOW READS AS OPEN, silently. Being told a
// preference from an older build is now an error is the one thing a
// retirement must never do, and a family is not a thing to guess either.
if err := writeProfileValue(dir, KeyCrewSource, "wide"); err != nil {
t.Fatal(err)
}
if got := CrewSourceAt(dir); got != CrewSourceOpen {
t.Fatalf("an unknown source word reads as %q, want %q", got, CrewSourceOpen)
}
if got := CrewAt(dir); got != CrewMax {
t.Fatalf("an unknown source word moved the crew to %q", got)
}
}

// FLIPPING TO ALL MAKES THE SAME WORDS MEAN THE ALL FAMILY. Seats nobody pinned
// move at once, because an unwritten seat is the default crew resolved in the
// current family; the five ids already written are theirs, so a flip is a
// meaning change to them rather than a write, and they read custom against the
// new family until one keystroke re-applies them.
func TestFlippingToAllMakesTheWordsMeanTheAllFamily(t *testing.T) {
dir := t.TempDir()
rows := registry(t, dir)
if err := mustRow(t, rows, KeyCrewSource).Apply(CrewSourceAll); err != nil {
t.Fatalf("setting the family to all: %v", err)
}
if got := CrewSourceAt(dir); got != CrewSourceAll {
t.Fatalf("the source row reads %q after all was set", got)
}
// The crew row never writes the source: a stored tier id is not a family claim,
// and the family is not stored in the crew row either.
if err := mustRow(t, registry(t, dir), KeyCrew).Apply(CrewBalanced); err != nil {
t.Fatalf("setting the crew to balanced: %v", err)
}
want, _ := CrewModelsForSource(CrewSourceAll, CrewBalanced)
for _, tier := range ModelTiers {
if got := TierModelAt(dir, tier); got != want[tier] {
t.Errorf("under all, balanced wrote %s to %q, want the all-family %q", tier, got, want[tier])
}
}
if got := CrewAt(dir); got != CrewBalanced {
t.Fatalf("the crew reads %q after balanced was applied under all", got)
}

// And the same five ids are NOT balanced in the open family, which is the
// point of the toggle: the words changed family, and the reading follows.
if err := mustRow(t, registry(t, dir), KeyCrewSource).Apply(CrewSourceOpen); err != nil {
t.Fatal(err)
}
if got := CrewAt(dir); got != CrewCustom {
t.Fatalf("after flipping back to open, the all-family five read %q, want %q", got, CrewCustom)
}
// One keystroke heals it, and heals it open.
if err := mustRow(t, registry(t, dir), KeyCrew).Apply(CrewBalanced); err != nil {
t.Fatal(err)
}
if got := CrewAt(dir); got != CrewBalanced {
t.Fatalf("the crew reads %q after balanced was applied under open again", got)
}
open, _ := CrewModels(CrewBalanced)
for _, tier := range ModelTiers {
if got := TierModelAt(dir, tier); got != open[tier] {
t.Errorf("after re-applying under open, %s reads %q, want the open %q", tier, got, open[tier])
}
}
}

// ── the level on a tier value ───────────────────────────────────────────────

// A LEVEL IS ACCEPTED AND ANY OTHER SUFFIX IS REFUSED IN WORDS.
Expand Down Expand Up @@ -303,3 +488,41 @@ func TestEverySettingsWriteBumpsTheGeneration(t *testing.T) {
t.Fatalf("a refused write moved the generation to %d", got)
}
}

// THE FAMILY ROW REFUSES A WORD THAT IS NOT A FAMILY, the way the crew row
// refuses a word that is not a preset, and a refused write leaves the row absent
// rather than half-written.
func TestSetCrewSourceRefusesAWordThatIsNotAFamily(t *testing.T) {
dir := t.TempDir()
if err := SetCrewSource(dir, "wide"); err == nil {
t.Fatal("SetCrewSource accepted a word that is not a family")
}
if got := CrewSourceAt(dir); got != DefaultCrewSource {
t.Fatalf("a refused write left the row reading %q", got)
}
if CrewConfigured(dir) {
t.Fatal("a refused write marked the crew as answered")
}
}

// AN UNWRITTEN SEAT FOLLOWS THE FAMILY, which is what makes the hint true: a
// profile on the all family with no rows runs the frontier crew without anyone
// picking it, and the crew word already reads the default preset.
func TestAnUnwrittenSeatFollowsTheFamily(t *testing.T) {
dir := t.TempDir()
if err := SetCrewSource(dir, CrewSourceAll); err != nil {
t.Fatal(err)
}
want, ok := CrewModelsForSource(CrewSourceAll, DefaultCrew)
if !ok {
t.Fatal("there is no default preset in the all family")
}
for tier, model := range want {
if got := TierModelAt(dir, tier); got != model {
t.Fatalf("%s on an all profile with no row is %q, want the all default %q", tier, got, model)
}
}
if got := CrewAt(dir); got != CrewBalanced {
t.Fatalf("an all profile with no rows reads the crew as %q, want %q", got, CrewBalanced)
}
}
6 changes: 6 additions & 0 deletions internal/config/derivation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,12 @@ var settingReaders = map[string]string{
// rather than the writer, because a row that could be set and never read
// would be exactly the dial-wired-to-nothing this table exists to catch.
KeyCrew: "CrewAt",
// The source row's own reader is CrewSourceAt, and the picker reaches it by
// that name (internal/tui3's crew.go), so the row names its reader rather
// than the crew derivation one hop away. The crew's writer resolves through
// the same reader, so the row and the five ids it produces cannot disagree
// about which family the preset words mean.
KeyCrewSource: "CrewSourceAt",
// The reflex row names the ROLE that reaches it rather than TierKey, which
// its two neighbours share: internal/reflex resolves [roles.RoleReflex] and
// nothing else lands on this key, so naming the shared spelling a third
Expand Down
10 changes: 7 additions & 3 deletions internal/config/firstrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,14 @@ func APIKeyConfigured(profileDir string) bool {
}

// CrewConfigured is whether a person has ever answered the crew: any of the
// four tier rows is in the profile file. It is any rather than all because
// [ApplyCrew] writes the four together, and a person who pinned one tier by hand
// has an opinion the setup must not paper over with a preset.
// tier rows, or the family row above them, is in the profile file. It is any
// rather than all because [ApplyCrew] writes the tier rows together, and a
// person who pinned one tier by hand, or chose a family, has an opinion the
// setup must not paper over with a preset.
func CrewConfigured(profileDir string) bool {
if _, ok := persistedString(profileDir, KeyCrewSource); ok {
return true
}
for _, tier := range ModelTiers {
if _, ok := persistedString(profileDir, tierKeyFor(tier)); ok {
return true
Expand Down
23 changes: 23 additions & 0 deletions internal/config/firstrun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,26 @@ func TestTheAPIKeyRowMasksReadsTheShellFirstAndWritesTheProfile(t *testing.T) {
t.Fatal("a pinned row must refuse a write")
}
}

// THE FAMILY IS AN OPINION TOO. A person who chose a family and never pinned a
// tier has still answered the crew, so the setup must not paper over it with a
// preset: the law the tier rows already carry, extended to the row above them.
// And a family nobody chose is the default, which is NOT an answer.
func TestTheFamilyRowCountsAsAnAnsweredCrew(t *testing.T) {
dir := t.TempDir()
if CrewConfigured(dir) {
t.Fatal("a profile nobody has touched must read as unanswered")
}
if got := CrewSourceAt(dir); got != DefaultCrewSource {
t.Fatalf("an untouched profile reads the family as %q, want the default", got)
}
if err := SetCrewSource(dir, CrewSourceAll); err != nil {
t.Fatal(err)
}
if !CrewConfigured(dir) {
t.Fatal("a profile whose family was chosen must read as answered")
}
if got := CrewSourceAt(dir); got != CrewSourceAll {
t.Fatalf("the family row reads %q after it was written", got)
}
}
16 changes: 12 additions & 4 deletions internal/config/seats.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,19 @@ func crewRow(profileDir, tier string) (model, from string, source SeatSource, cl
// from the ladder — five extra file reads per row, and a cycle. A surface that
// wants both facts asks for both.
func TierSeatAt(profileDir, tier string) Seat {
return tierSeatUnder(profileDir, CrewSourceAt(profileDir), tier)
}

// tierSeatUnder is [TierSeatAt] with the family already read, so a caller that
// walks every tier ([CrewAt]) reads the profile ONCE for the family instead of
// once per unwritten seat, and the five seats cannot be resolved under one
// family while the preset table is resolved under another.
func tierSeatUnder(profileDir, family, tier string) Seat {
model, from, source, cleared := crewRow(profileDir, tier)
if source == "" {
source = SeatDefault
if !cleared {
model = defaultTierModel(tier)
model = defaultTierModel(family, tier)
}
}
return Seat{Role: tierSeatRole(tier), Model: model, Source: source, From: from}
Expand Down Expand Up @@ -403,7 +411,7 @@ func (s Seat) Report() string { return withNotice(s.Line(), s.Notice()) }

// Describe is one seat in a receipt's voice:
//
// work qwen/qwen3.8-27b (crew frugal)
// work deepseek/deepseek-v4-flash-0731 (crew frugal)
// plan follows the work model (default)
//
// THE EMPTINESS LAW, as the crew row already keeps it: an unfilled plan seat is
Expand Down Expand Up @@ -433,7 +441,7 @@ type Seats struct {
// Sentence is both seats, unlabelled, for a door whose opening lines have a
// label column of their own:
//
// work qwen/qwen3.8-27b (crew frugal) · plan qwen/qwen3.8-27b (crew frugal)
// work deepseek/deepseek-v4-flash-0731 (crew frugal) · plan z-ai/glm-5.3 (crew frugal)
//
// ONE SHAPE AND NOT TWO. It would read a little better to collapse a run whose
// seats came from the same crew into one clause, and it would mean a script
Expand All @@ -445,7 +453,7 @@ func (s Seats) Sentence() string {

// Line is the one line a headless run opens with:
//
// models: work qwen/qwen3.8-27b (crew frugal) · plan qwen/qwen3.8-27b (crew frugal)
// models: work deepseek/deepseek-v4-flash-0731 (crew frugal) · plan z-ai/glm-5.3 (crew frugal)
func (s Seats) Line() string { return modelsLabel + s.Sentence() }

// Notice is the inheritance line for whichever seat was filled by an older row,
Expand Down
Loading
Loading