ADR 0004: Per-distro Prompt Dispatch
Status
Accepted (merged in PR #203, 2026-04-25).
Context
SysKnife gained Ubuntu/Debian action support in Phase 2b. The action catalogue split into two mostly non-overlapping sets:
- Fedora-family:
AddLayeredPackage,RemoveLayeredPackage,rpm-ostreedeployment controls,flatpak,toolbox,firewalld, … - Debian-family:
AptInstall,AptRemove,AptUpdate,snap,distrobox,ufw,netplan, …
The original single-distro prompt listed all action names in one block. After adding Debian actions this caused two problems:
-
Prompt confusion: the model saw both
AddLayeredPackageandAptInstallfor every intent and had to infer which to use from context. In practice, GPT-4o and Claude sometimes proposed the wrong family —AptInstallon Silverblue, orAddLayeredPackageon Ubuntu — especially for compound intents that did not name the package manager explicitly. -
Context window waste: every planning call carried ~40% action names that could never legally be executed on the current host.
The E2E story suite (recorded at the time as 65/65 passing on Ubuntu 24.04 with gpt-4.1 — see the correction note at the end of this ADR) validated that per-distro isolation fixes (1) and reduces prompt size meaningfully.
Decision
build_system_prompt dispatches to one of three pure render functions
based on distro_hint.family:
distro_hint.family == "fedora" → render_fedora_prompt
distro_hint.family == "debian" → render_debian_prompt
_ → render_generic_prompt
Each render function concatenates:
- Shared
constblocks: role, the single-planning-rule, cross-distro action catalogue, and the six worked examples (A–F). - Per-distro
constblocks: distro header (with detected version), distro-specific action catalogue, risk overrides, selection rules, and action parameter reference.
The FEDORA_ONLY_ACTIONS and DEBIAN_ONLY_ACTIONS string-slice constants
back safety-fence unit tests that assert no cross-contamination at the type
level.
The DistroHint is provided by sysknife-core's detect_distro() function
which reads /etc/os-release at startup. If detection fails, distro_hint is
None and render_generic_prompt is used.
Consequences
- The model cannot propose a Fedora-specific action on a Debian host (or vice versa) because the action name simply does not appear in its context window.
- Adding a new distro family requires: a new
render_*function, a newDISTRO_FAMILY_*constant, a dispatch arm inbuild_system_prompt, and a*_ONLY_ACTIONSslice for the safety fence. This is intentionally mechanical — each step has a test to confirm it. - Prompt size decreases by roughly 20–25% for Fedora hosts and 15–20% for Debian hosts compared to the pre-dispatch single-prompt.
- The generic fallback deliberately omits all distro-specific actions. A host
with an unrecognised
/etc/os-releasegets cross-distro-only planning until the user adds a manualdistro_hintoverride in~/.config/sysknife/config.toml.
References
- PR #203 — "refactor(prompt): per-distro dispatch"
crates/sysknife-brain/src/prompt.rs— implementationdocs/architecture.md— prompt construction overviewdocs/research/prompt-composition-patterns.md— survey of dynamic prompt patterns that informed this design (dynamic middleware, template composition, conditional blocks)
Correction (2026-08-05)
The "65/65" figure quoted above was never backed by a run: no story set of that
size has existed, and the Ubuntu family contains 50 stories. Later runs were
observed at 46/50 on 22.04 and 45/50 on 24.04 with openai/gpt-oss-120b, but
those numbers come from run logs that were never committed, so they are not
artifact-backed either and are recorded here only as history. Figures fit to
publish live in tests/evidence/story-runs/, written by the run itself. The decision this ADR records — structural per-distro
prompt isolation — is unaffected, and was independently confirmed later: the two
Fedora actions that still reached Ubuntu plans came from the tool schema, which
this ADR did not cover, not from the prompt. Published figures now derive from
tests/evidence/ and are enforced by scripts/check_evidence_claims.py.
Follow-up (2026-08-13)
The "50 stories" above was the family size at the time. It is 79 now: GetHostState
— the action this ADR's fence required somebody to introduce, because naming
Fedora's GetSystemState on an apt host is what the fence forbids — had no story
at all, and neither did 28 other Debian-only actions. Every Debian-only action now
has one, and the family runs 79/79 on all three LTSes.
Two of the new stories say something about this decision that the fence itself does not:
- The four
Ubuntu*Flatpakactions build byte-identical argv to their un-prefixed twins, whichaction_family.rsdeliberately leaves unfenced. So the Debian prompt names only the prefixed set while the tool schema offers both, and the choice has no effect on the host. Measured: the model picks the un-prefixed name 3 times in 4. Those stories accept either and log which, because asserting a distinction the host cannot observe is not a gate. ConfigureUnattendedUpgradesis described in the tool schema and named in none of the Debian prose blocks. Story 126 is therefore a measurement of whether the schema description alone is enough to steer. It is.