AFK seed prompt override layers repo over global over the built-in template, replacing it wholesale with no silent edits¶
AFK's seed prompt (SeedPrompt, internal/afk/decide.go:214) is the one part
of the launch path ADR-0021's layering never reached: model, effort, and the
options bag all resolve through base ?? AFK-override before a run spawns, but
the eight-step instruction text stayed a single hardcoded template, so an
operator who wanted different framing or house conventions baked into every
run had no lever short of a code change and a redeploy.
Issue #52 grills this to consensus, reusing ADR-0021's layering apparatus rather than inventing a new one. But a prompt is prose, not a knob: replacement is whole-text, not field-by-field, and two open questions — does an override get the incogni sentence spliced in, does the frontend need to know the built-in words — are answered by making lab's own edits WYSIWYG and the built-in text an API-served value.
This ADR extends ADR-0021 (the layering pattern, and its discipline of serving a source of truth through the API instead of duplicating it in the frontend) and settles issue #52. The decisions, pinned (settled with the operator 2026-07-08 — do not re-litigate):
- Layering extends ADR-0021 one rung deeper.
repos.afk_prompt→ global settingafk_prompt→ the built-inSeedPrompttemplate; empty or NULL at a layer means inherit the next. Both AFK kinds resolve through the samelaunch()call, soafk_manualandafk_autonever diverge for the same repo. No per-run override —POST /repos/{id}/afk/startstays bodyless, the same stance ADR-0021 used forultracode. Naming:afk_promptnames both the repo column (fitting the existingafk_-prefixed family) and the settings key, which follows theafk_budget_minutesgrammar — an AFK-only key, prefixed — not the_afk-suffix grammar ofspawn_model_default_afk, an override of a base default that also exists; there is no base prompt to suffix. - Full replacement, tokens interpolated everywhere. A non-empty override
replaces the entire built-in text, not a clause at a time.
<N>(reusinggitx.NToken) and<BRANCH>(the rendered claim branch) interpolate at every occurrence viastrings.ReplaceAll— looser than a branch pattern's exactly-one<N>, since prose may reference either more than once. Both tokens are optional (labctl issue viewresolves the run's own claimed issue with no argument), and an unknown token passes through as literal text rather than erroring. - WYSIWYG — lab never edits what the operator typed. The incogni step-5
sentence is not auto-appended to overrides; the textarea always shows
exactly the run's prompt. Safe because the sentence was redundant defense,
not the enforcement — incogni is mechanically enforced elsewhere (the
pre-push hook,
agentapi'ssanitizeBody, the provider'sSeedOpts.Incogni) regardless of prompt text. Theultracodeprovider prepend (ADR-0021) is a different, provider-owned layer applied after prompt source resolves, and still fires on an overridden prompt. - The built-in default is API-served, never frontend-hardcoded.
GET /api/v1/settingsgains read-onlyafk_prompt_default(built-in template, tokens un-interpolated); a repoGETgains read-onlyafk_prompt_effective(global override if set, else built-in, incogni-aware). Both Settings pages show the effective text as the empty textarea's placeholder, with a Customize button that copies it in for editing; clearing the textarea returns to inherit. The AFK strip is unchanged — Settings is the only display surface. - Validation is shape-only; the transcript is the audit trail.
Whitespace-only input normalizes to
NULL(inherit). A 16 KiB cap, since the prompt travels as spawn argv. No content validation beyond that — instead a static Settings hint: a run is detected done only by an open PR on its branch, so a prompt that never opens one burns its budget, counts a failure, and three failures auto-pause the repo's AFK. No per-run snapshot: the seed prompt is already the run's first transcript message (ADR-0016 read-through).
Status¶
Accepted. Extends ADR-0021's layering (base, then an AFK-specific layer) to the seed prompt, and reuses its discipline of surfacing a source of truth through the API rather than duplicating it in the frontend. Settled via issue
52. One migration (0005, sqlite + postgres) adds nullable repos.afk_prompt;¶
the settings key afk_prompt is intentionally unseeded — absent means
inherit — matching the AFK-override precedent ADR-0021 set for
spawn_model_default_afk. Non-breaking: an operator who sets no override sees
today's SeedPrompt text, byte-for-byte (TestSeedPrompt still pins it).
Considered options¶
- Keep splicing the incogni sentence into overrides. Rejected: the
sentence never did the enforcing — the pre-push hook,
sanitizeBody, andSeedOpts.Incognido — so silently editing the operator's text buys no safety and breaks WYSIWYG. - A per-run prompt field on
POST /repos/{id}/afk/start. Rejected on the same ground ADR-0021 used forultracode: AFK is unattended and defaults-only, so the endpoint stays bodyless. - Persist a per-run prompt snapshot on the
runsrow. Rejected: read-through (ADR-0016) already puts the seed prompt in the transcript as the first message, so a snapshot would duplicate an existing record.
Consequences¶
internal/afk/decide.go:SeedPromptgains an override param — empty keeps today's template, non-empty replaces it wholesale with<N>/<BRANCH>interpolated byReplaceAll; incogni untouched by the override path.TestSeedPromptextends accordingly.internal/instancegains a resolver alongsideResolveSpawnOptions—repo.AFKPrompt ?? global afk_prompt ?? ""— feedinginternal/afk/launch.go'sSeedPromptcall for both AFK kinds.internal/store: migration 0005 (sqlite + postgres) addsrepos.afk_prompt; settings gains theafk_promptkey, unseeded.internal/httpapi: settingsPATCHvalidates trim + 16 KiB,GETaddsafk_prompt_default; reposPATCH/GETaddafk_prompt/afk_prompt_effective.web/src/routes/Settings.tsxandRepoSettings.tsx: a textarea in the existing "AFK defaults" card, placeholder-as-effective-text, a Customize copy-to-edit affordance.