feat(spawn): split AFK vs manual model/effort defaults + generic provider-options bag (Claude Code ultracode) #19
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#19
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Summary
Let the operator configure different model/effort for AFK runs vs manual instances, and add a generic, provider-owned spawn-options bag whose first (and currently only) entry is Claude Code's AFK-only ultracode flag.
model/effort stay first-class and typed (ADR-0008 / D14 unchanged). The new options bag is the extensible seam that makes "the interface works for all providers" real: when Codex (#2) / Gemini land, each declares its own spawn options and applies them behind the same seam — zero refactor of the typed core.
Design was fully grilled with the operator on 2026-07-07; the decisions below are settled — do not re-litigate them, record them in an ADR (see Deliverables).
Decisions (settled)
Product shape
spawn_model_default/spawn_effort_defaultand the repo columnsmodel_default/effort_defaultkeep their current meaning as the base (used by manual, and by AFK as a fallback). Add optional AFK-override slots at both scopes; empty = inherit base. Manual resolution is unchanged (request → repo.base → global.base); AFK resolution becomesrepo.afk ?? global.afk ?? repo.base ?? global.base. Non-breaking: existing values keep working, AFK behaviour changes only where an override is set.POST /repos/{id}/afk/startstays bodyless). Confirmed against Claude Code docs/changelog/--help: ultracode is a prompt trigger keyword (renamed from "workflow" in v2.1.186), not a CLI flag, env var, or settings.json field — which is exactly why application is prompt injection, not argv.Mechanism
Hybrid, not a full generalization. model/effort remain typed & first-class across the interface, run row, repo columns, settings, API, and UI (ADR-0008/D14 intact). Beside them sits ONE generic bag: the provider declares its options schema, lab stores/validates/renders the bag generically, and the provider applies it.
ultracodeis claude-code's first entry.Application is provider-owned and is NOT argv. Replace the positional
SpawnArgv(session, model, effort, prompt)withSpawnArgv(SpawnSpec{SessionName, Model, Effort, Options, InitialPrompt})so future options never churn the interface signature. claude-code appliesOptions["ultracode"]by prepending a provider-owned ultracode directive line to a non-emptyInitialPrompt; a manual instance passes an empty prompt → natural no-op (matches decision 3).internal/afkstays provider-agnostic — ultracode must not leak into the afk package (unlikeincogni, which is a lab-domain repo flag). Provider-owned prepend text, tunable freely, e.g.:Validation mirrors model/effort. The bag validates against the resolving repo's provider schema — unknown option key or bad value → 400, same discipline as an unknown model/effort today. Empty AFK model/effort is explicitly allowed (means inherit). A global bag may span providers once >1 exists; at spawn it is filtered to the repo's provider.
Architecture
spawn_model_default_afk,spawn_effort_default_afk(empty = inherit),spawn_options_afk(JSON, e.g.{"ultracode":"true"}).repos, new nullable columns):afk_model_default,afk_effort_default,afk_options(JSON).runs.optionshistory column and no run-row options persistence (re-adoption re-adopts a live tmux session, it never re-spawns, so nothing needs the bag after launch); bool option type only (enum/string reserved).internal/instance):ResolveModelEffortbecomes run-kind-aware (AFK consults the afk-override layer first, then falls back to base). NewResolveSpawnOptions(ctx, prov, repo, kind)— manual → empty map; AFK →repo.afk_options ?? global.spawn_options_afk, filtered + validated against the provider. Both feed a newLaunchSpec.Options→SpawnSpec.Options.GET /api/v1/providersgrows anoptionsfield (the declared schema). Global Settings and Repo Settings render an "AFK defaults" section from it: model/effort selects with an explicit "same as default" (inherit) entry + a bool→checkbox for ultracode. Future provider options render automatically.StartInstanceForm(manual) is unchanged.Implementation notes (seams, not prescriptions)
internal/provider/provider.go: addSpawnOptions() []OptionSpec({Key, Label, Type:"bool", Default};Values/enum reserved). ReplaceSpawnArgvpositional args with theSpawnSpecstruct. Update theprovidertestfake and the compatSpawnArgvsnapshot test accordingly.internal/provider/claudecode: declare theultracodeoption; implement the prompt-prepend insideSpawnArgv(guarded on non-emptyInitialPrompt); leave model/effort flag-building as-is. This is our own wording, not a new pinned Claude coupling — just re-snapshot the argv builder.internal/instance(start.go,credential.go): kind-awareResolveModelEffort, newResolveSpawnOptions,LaunchSpec.Options, thread intoSpawnSpec.internal/afk/launch.go: pass the run kind into the resolver and request the AFK options bag; the afk package gains no ultracode knowledge.internal/store: new settings keys (settings.gomin-set/typed handling), newreposcolumns +Update(Opt[*string]for the two strings, JSON forafk_options), scan/insert plumbing.SeedDefaultSettingsunchanged (AFK keys default to empty = inherit).internal/httpapi:providers.go(optionsin the response),settings.go(validate the 3 new keys — empty allowed for AFK model/effort; JSON bag against the provider schema; a 400 still writes nothing),repos.go(3 new optional fields).migrations: next free number, add the 3reposcolumns (sqlite + postgres mirrors).web:Settings.tsx+RepoSettings.tsxAFK-defaults section (schema-driven, inherit option, ultracode checkbox);api.tstypes (Provider.options,Repo.afk_*, AFK fields onSpawnDefaults);StartInstanceForm.tsxunchanged.Deliverables
docs/adr/<next-free-number>-spawn-defaults-provider-options.mdin the house style (statement title, Status, Considered options, Consequences) recording the decisions above — the hybrid typed-core + generic-options-bag choice, B2 base+AFK-override layering, ultracode as AFK-only provider-owned prompt injection, and theSpawnSpecseam. Take the next number free on main at implementation time — do NOT hardcode 0017 (other in-flight issues may claim it). The ADR must be part of this issue's commits/PR, not a follow-up.CONTEXT.md: add vocabulary if warranted (e.g. Spawn options — the provider-owned bag — and the AFK-vs-manual default split) in the established Language/Relationships style, honouring the avoid-list.ResolveSpawnOptions, provider seam (SpawnSpec+SpawnOptions), claude-code ultracode application, settings + repo storage, API, migration.SpawnSpeccompat snapshot (ultracode prepend present for AFK, no-op for manual/empty prompt), UI inherit + checkbox behaviour.Out of scope (explicitly deferred)
Per-run AFK override form · manual-scoped provider options · enum/string option types (bool only) ·
runs.optionshistory column / persisting resolved options · applying ultracode to manual instances · Codex/Gemini option declarations (#2 consumes this seam on its own).Relates to #2 (OpenAI Codex provider — the first external consumer of the generic options bag). Evolves ADR-0008 (provider seam) and brief decisions D12d (spawn model/effort layering) and D14 (provider-owned catalogs).