feat(spawn): AFK vs manual model/effort defaults + generic provider-options bag (ADR-0021) #20
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab!20
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/spawn-defaults-provider-options"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements #19 — split AFK-run spawn defaults from the manual pre-fill, and add a generic provider-owned spawn-options bag (first entry: claude-code's AFK-only
ultracode). model/effort stay typed and first-class; the bag is the extensible seam #2 (Codex) applies behind. Design was fully grilled with the operator on 2026-07-07 and recorded in ADR-0021.What changed
Provider seam —
SpawnArgv(SpawnSpec{SessionName, Model, Effort, Options, InitialPrompt})replaces the positional signature, so a new option never churns the interface.OptionSpec+SpawnOptions()declare a schema;Filter/ValidateSpawnOptionshelpers. claude-code declaresultracodeand applies it by prepending a provider-owned directive to a non-emptyInitialPrompt— a manual/empty-prompt spawn is a natural no-op. Not a pinned Claude coupling (it's our wording); the argv builder is just re-snapshot tested.Resolver —
ResolveModelEffortis run-kind-aware (manual = request → repo.base → global.base; AFK = repo.afk ?? global.afk ?? repo.base ?? global.base, empty = inherit). NewResolveSpawnOptions(manual → empty; AFK →repo.afk_options ?? global.spawn_options_afk, filtered + validated to the provider). Both feedLaunchSpec.Options→SpawnSpec.Options. The dialog-capture--settingsflag stays among the flags, before the trailing prompt.afk — passes the opaque bag through;
ultracodenever leaks intointernal/afk.Storage — global settings
spawn_model_default_afk/spawn_effort_default_afk(empty = inherit) /spawn_options_afk(JSON); nullable repo columnsafk_model_default/afk_effort_default/afk_options(JSON). Migration 0004 (sqlite + postgres).nilmap = NULL = inherit; a present map (even{}) = explicit. Non-breaking: an operator who sets no override keeps today's behaviour. Deferred: noruns.optionspersistence (re-adoption never re-spawns); bool option type only.API —
GET /providersexposesoptions; settings PATCH validates the 3 AFK keys (empty model/effort allowed; bag keys/values against the provider schema → 400, all-or-nothing); repo PATCH plumbs the 3 fields (validated at spawn, mirroringmodel_default).Web — schema-driven "AFK defaults" section in global + repo settings (shared
CatalogSelectwith an inherit entry + anultracodecheckbox; future options render automatically).StartInstanceFormunchanged. Also fixes a latentCatalogSelectselection-drop when the provider catalog resolves after the form mounts.Docs — ADR-0021 + CONTEXT.md vocabulary (Spawn options; AFK default vs manual pre-fill).
Tests
Kind-aware resolver layering (base / AFK override / inherit), options resolve/filter/validate,
LaunchSpec→SpawnSpecthreading, settings validation 400s + AFK roundtrip, repo afk-column roundtrip, ultracode compat snapshots (prepend for AFK, no-op for manual/empty prompt), and web inherit + checkbox behaviour.Validation
go build+go test ./...— greenbuild+ 375 tests +lint— greennix flake check(lab, labctl, web, golangci-lint, nixos-module) — all passedEvolves ADR-0008 and brief decisions D12d/D14. Relates to #2.
Landing audit — verdict: PASS
Verification signal relied on: local
nix flake check(full build). The repo'sci / flake-checkaction has no runner and stays pending forever, so it cannot vouch — validated locally instead. All check derivations realized and all checks passed:lab,labctl,web(build + tests),golangci-lint,nixos-moduleeval. Head SHA63ddb15unchanged, so these are the exact outputs for this tree.Checked
nix flake check— green (Go build +go test ./..., web build + tests + lint, golangci-lint, nixos-module eval).feat(spawn): …— valid Conventional Commits.main, merge-base =main, zero conflicts.Non-blocking follow-ups (not merge blockers)
web/src/routes/RepoSettings.tsx(~L283): the ultracode checkbox is 2-state over a 3-state inherit model. A repo inheriting a globalultracode:"true"renders unchecked while effectively running ultracode; unchecking stores explicitfalsewith no UI path back to inherit. By-design per the ADR (null=inherit / present=explicit) and the hint text, but can mislead an operator about whether an AFK fleet runs ultracode. Worth a UI follow-up.internal/provider/claudecode/claudecode.go(~L283): whitespace-onlyInitialPromptisn't treated as empty, so it'd receive the directive. Unreachable in current flows (manual passes"", AFK passes substantive text).Note: the body says "Implements #19", which is not a Forgejo closing keyword — the merge will not auto-close #19; it will be closed manually on merge.