Base Pack Preamble & Agent Instructions Consolidation
Date: 2026-04-17 Status: Draft
Problem
Agent instruction content is currently scattered:
content/packs/cap/context.mdhas a### Agent Instructionssection telling AI agents to prefersap-devscommands over web search.- No equivalent section exists in
abaporbtp-core— the pattern was never consistently applied. - There is no top-of-context signal asserting
sap-devsauthority before any pack content is read.
The base pack (content/packs/base/) is auto-injected into every profile and always rendered first. It is the natural home for a shared, assertive preamble that tells AI agents to prefer sap-devs commands. Per-pack ### Agent Instructions sections should cover only pack-specific command hints (e.g. --pack cap flags).
Goals
- Add a
preamble.mdfile to the base pack that is rendered at the top of every injected context block. - Move the general "prefer
sap-devs" instruction fromcap/context.mdintobase/preamble.mdin generalized form. - Remove the now-redundant
### Agent Instructionssection fromcap/context.md. - Document
preamble.mdindocs/content-authoring.md.
Non-Goals
- Additive layer support for
preamble.md— the preamble is fixed to whicheverpreamble.mdships in the official base pack; user/company/project layer overrides are not supported. Layer override suppression relies on thep.Baseguard inRenderContext: only base packs (Base == true) have theirPreambleMDrendered. A full replacement of the official base pack (non-additive, sameid: base) by a company/user layer would also replacePreambleMD— this edge case is accepted. - Per-adapter preamble variations — one preamble, injected the same way everywhere.
- Generating the preamble at inject time — it is static authored content in
content/packs/base/preamble.md.
Design
Rendered output order
After this change the injected block renders in this order:
# SAP Developer Contextheader + profile line (hardcoded inRenderContext)## sap-devs Runtime Context(dynamic section — version, packs, available commands)- Preamble — from
base/preamble.md, rendered once before all packcontext.mdcontent - Base pack
context.md(SAP ecosystem portals, community links, etc.) - Technology pack
context.mdfiles (cap, abap, btp-core, …)
Implementation note: The preamble loop and the ContextMD loop are separate passes over the same packs slice. The base pack's ContextMD is emitted in the second loop pass, in the same relative position it always has (first among all packs, since LoadPacks pins base packs first). There is no special-casing of base pack ContextMD in the first loop — this would cause it to be emitted twice.
New file: content/packs/base/preamble.md
A short, assertive block (2–3 lines) at the top of the injected context. No Markdown heading — the preamble reads as a direct instruction block. Example:
> **For SAP-specific information, always prefer `sap-devs` commands over web search or training knowledge.**
> Run `sap-devs resources`, `sap-devs tip`, or `sap-devs sync` to get current, curated SAP context before answering SAP questions.Authoring constraints:
- Keep it ≤ 3 lines. It is injected into every AI tool config on every
sap-devs injectrun. - No headings — it appears before pack content and should not create a heading hierarchy collision.
- Reference
resources,tip, andsyncby name so the AI knows what commands are available.
Pack struct — new field
type Pack struct {
// ... existing fields ...
PreambleMD string
}Merge behaviour: PreambleMD is a scalar string field. MergeWith does a shallow copy (merged := *base), so PreambleMD is preserved from the base pack through any additive merge. An additive pack targeting the base pack cannot modify PreambleMD — this is intentional (preamble is not overridable by upper layers) and requires no additional merge logic in merge.go.
LoadPack — load preamble.md
After loading context.md, attempt to read preamble.md from the pack directory. Missing file is silently skipped (optional).
if data, err := os.ReadFile(filepath.Join(packDir, "preamble.md")); err == nil {
pack.PreambleMD = string(data)
}No locale variant support for preamble.md — the preamble is intentionally kept short and language-neutral (command names don't translate).
RenderContext — render preamble before pack content
After the dynamic section, scan the packs slice for base packs with a non-empty PreambleMD and emit them before iterating pack ContextMD content:
// Render preamble from base packs (rendered once, before all ContextMD)
for _, p := range packs {
if p.Base && strings.TrimSpace(p.PreambleMD) != "" {
b.WriteString(strings.TrimSpace(p.PreambleMD))
b.WriteString("\n\n")
}
}
// Then render all pack ContextMD as before
for _, p := range packs {
if strings.TrimSpace(p.ContextMD) == "" {
continue
}
b.WriteString(strings.TrimSpace(p.ContextMD))
b.WriteString("\n\n")
}Content cleanup: cap/context.md
Remove the ### Agent Instructions section (currently the last section of the file). The general "prefer sap-devs" instruction moves to base/preamble.md; the cap-specific --pack cap command hints are dropped as discoverable via sap-devs resources --help.
Documentation updates (docs/content-authoring.md)
Three targeted changes:
Pack directory structure — add
preamble.mdto the directory tree with annotation:# AI preamble (base pack only).Base Layer section — add a
### preamble.mdsubsection:- What it is and what it does
- Rendered output order (the numbered list above, including the two-loop implementation note)
- Token cost reminder: every byte is injected into every AI tool config on every
sap-devs injectrun; the preamble is exempt from token budget trimming (same as base packContextMD) — keep it ≤ 3 lines - Only the official base pack's
preamble.mdis used (no layer override; mechanism explained above in Non-Goals)
### Agent Instructionspattern section — update to note:- The general "prefer sap-devs" instruction now lives in
base/preamble.md - Per-pack
### Agent Instructionssections should contain only pack-specific command hints (e.g.--pack <id>variants) - Refer readers to
base/preamble.mdfor the canonical example
- The general "prefer sap-devs" instruction now lives in
No schema changes required — preamble.md is a standalone file, not a pack.yaml field.
Files changed
| File | Change |
|---|---|
content/packs/base/preamble.md | New — assertive AI preamble |
content/packs/cap/context.md | Remove ### Agent Instructions section |
internal/content/pack.go | Add PreambleMD string field to Pack; load in LoadPack |
internal/content/render.go | Emit preamble before pack ContextMD in RenderContext |
docs/content-authoring.md | Document preamble.md in 4 places |
Testing
- Existing
render_test.gotests should continue to pass (preamble field is empty for non-base packs in test fixtures). - Add test cases to
render_test.go:- Base pack with non-empty
PreambleMD— verify preamble appears before that pack'sContextMD. - Base pack with both
PreambleMDandContextMD— verify preamble precedesContextMDof the same base pack. - Non-base pack with
PreambleMDset — verifyPreambleMDis not emitted (thep.Baseguard suppresses it). - Two base packs each with
PreambleMD— verify both preambles are emitted and both appear before allContextMD.
- Base pack with non-empty
- Add test cases to
pack_test.go:LoadPackon a pack dir withpreamble.mdpopulatesPreambleMD.LoadPackon a pack dir withoutpreamble.mdleavesPreambleMDas empty string.
go build ./... && go vet ./...locally; CI is authoritative forgo test.