Prompt management — UnifiedPromptManager + Langfuse
Status: Active. Lane A ofGlad-Labs/poindexter#450 (the OSS migration umbrella) completed 2026-05-09 — production no longer carries inline prompt constants. The YAML prompt files were subsequently migrated to agentskills.io SKILL.md packs (Glad-Labs/poindexter#528); no prompts/*.yaml files ship anymore.
Where prompts live
The single source of truth for every production prompt is a set ofSKILL.md packs under src/cofounder_agent/skills/<pack>/<skill>/SKILL.md — authoritative at runtime as of poindexter#825 (2026-07). Resolution:
SKILL.mdpack default — what ships with the repo (the in-memory default loaded at boot) and what production serves. Edits land via PR + the prompt contract tests.- Langfuse
productionlabel — consulted FIRST only whenapp_settings.langfuse_prompt_overrides_enabled='true'(defaultfalse). The legacy override layer, kept as an escape hatch for deliberate live prompt experiments. Langfuse itself is otherwise a read-only mirror maintained bySyncPromptCatalogToLangfuseJob— see Langfuse as a read-only mirror. KeyError— if the key isn’t registered. Perfeedback_no_silent_defaults.md, an unknown key is a configuration bug, not a quiet fallback.
The legacyprompts/*.yamlloader (_initialize_prompts) still runs at boot for backward compatibility, but the repo ships zero YAML prompt files — every key now comes from aSKILL.mdpack via_initialize_skills. Skills load after YAML so a migratedSKILL.mdtransparently wins over any leftover YAML entry for the same key.
The cascade
template.format(**kwargs) step at the end, so the call-site contract (kwargs in, formatted string out) is identical regardless of which tier wins.
Available prompt keys
EachSKILL.md pack declares the keys it provides in its frontmatter metadata.prompts list — that frontmatter is the authoritative key inventory (the loader registers exactly those keys). The packs that ship today:
Pack (skills/<pack>/<skill>/SKILL.md) | metadata.category | Surface |
|---|---|---|
content/writer | content | base content-writer persona + narrative-pass seed |
content/two-pass-writer | content | TWO_PASS draft/revise prompts |
content/blog-generation | content | initial draft / SEO+social / iterative refinement |
content/content-qa, content/qa | content_qa | the qa.* review, critique, topic-delivery, consistency, aggregate-rewrite prompts + the DeepEval g-eval criterion |
content/research | research | search-result analysis + topic-candidate ranking |
content/image-generation | image | featured-image, search-query, image-director, and vision alt-text caption prompts |
content/seo-metadata | seo_metadata | seo.* title / description / keywords / excerpt / category / tags |
content/social-media | social | trend research + post creation |
content/podcast, content/video, content/video-director | media | media-script and shot-list prompts |
content/atoms | content | atoms.* system prompts (narrate_bundle, pipeline_architect) |
content/utility | utility | content summarization / JSON conversion helpers |
ops/automation, ops/business, ops/triage, ops/hygiene | ops | task.* business/automation/ops prompts + the retention/memory-hygiene summarizers |
voice/agent | voice | voice.* system prompts — Emma persona + Claude-bridge TTS override ({surface}) |
narrate_bundle and pipeline_architect templates carry the operator persona as {site_name} / {site_url} placeholders, rendered from the run-bound site_config by the calling atom before the text reaches the model.
How operators tune prompts
The one edit path — SKILL.md via PR
- Open the pack that owns the key (e.g.
skills/content/seo-metadata/SKILL.mdforseo.*). - Edit the body in that key’s
## <key>section (in practice: ask an agent to). - Commit / merge. The container picks up the new body on next deploy / restart, and
SyncPromptCatalogToLangfuseJobpushes it into the Langfuse mirror within ~6h. - The prompt contract tests (
tests/unit/services/test_prompt_*.py,test_prompt_manager_skills.py, and per-surface tests liketest_multi_model_qa_prompts.py) pin rendered bodies — update the affected expectation in the same PR.
Reviewing prompts — Langfuse UI (read-only)
Open<langfuse_host>/project/poindexter/prompts to browse every production prompt with full version history — the mirror job keeps it current. Edits made there do not take effect (the runtime serves the SKILL.md default); the mirror will replace a hand-edited version with the current default on its next cycle and page Discord about it.
Escape hatch — live overrides (off by default)
Setlangfuse_prompt_overrides_enabled='true' to restore the legacy Langfuse-first lookup for deliberate live experiments (UI edit takes effect within ~60s, no deploy). While it’s on, remember the trade you’re making: any Langfuse production version shadows its SKILL.md default — the masking-trap behavior — so turn it back off (or delete the experimental versions) when the experiment ends.
The contract tests serve two purposes:
- Drift detection. A prod-side Langfuse edit that strays from the
SKILL.mddefault still lands on the dashboard but ALSO trips the test in CI, forcing a conscious revert-or-update. - Migration safety. When prompts moved (f-string → YAML →
SKILL.md), the tests verified the rendered body stayed byte-for-byte identical, so format-string gotchas ({{/}}doubling, trailing newlines) couldn’t sneak through.
test_prompt_fallback_drift.py). Several resolvers keep an inline _*_FALLBACK copy so the pipeline survives a prompt-registry outage. A parametrized guard drives each resolver with the registry up (SKILL.md path) and down (inline path) and asserts they agree — so a fired fallback can never silently serve stale text. When a fallback DOES fire, the resolver logs at error (per feedback_self_heal_not_suppress: self-heal by serving the byte-identical inline copy, but surface the registry outage loudly rather than swallow it).
How callers fetch prompts
str.format(**kwargs). Literal braces in the prompt body must be doubled ({{ / }}); the SKILL.md bodies preserve the doubling.
SKILL.md prompt format
A prompt-bearing pack is a single SKILL.md: YAML frontmatter declares the keys, the body holds one ## <key> section per prompt with the template in a fenced block.
prompt_manager._initialize_skills) lives inside the package (<pkg>/skills/) so a package-relative path resolves identically on the host (src/cofounder_agent/skills) and in the worker container (/app/skills). Operator action skills (the repo-root skills/poindexter/ pack that wraps the CLI/MCP) are a different layer — they carry no metadata.prompts and are not loaded here.
Parsing + import-time validation
One parser readsSKILL.md everywhere: services/skill_frontmatter.py (parse_frontmatter + extract_section), shared by both the runtime loader (_initialize_skills) and the importer (poindexter skills import). It anchors the closing --- to the start of a line, so a --- inside a quoted frontmatter value — or a thematic break in the body — is never mistaken for the delimiter. (The two paths previously used different parsers; a pack could import clean and then fail to load.)
poindexter skills import fails loud (per feedback_no_silent_defaults.md) when a declared key has no resolvable ## <key> section — it runs the same extract_section the loader uses, so a pack that imports clean is guaranteed to resolve every key it advertises. The check runs before anything is written to disk or recorded in skill_catalog.
Langfuse as a read-only mirror
SyncPromptCatalogToLangfuseJob (services/jobs/sync_prompt_catalog_to_langfuse.py, every 6h, gated by langfuse_prompt_mirror_enabled) keeps Langfuse showing every production prompt so the operator reviews the whole catalog in one UI. Per cycle:
- Missing key → created with the
productionlabel and aconfig.source='skill_sync'provenance marker. - Default changed → new version pushed (the label moves with it), so the mirror always matches what production serves.
- Hand-edited version (production version without the sync marker) → replaced with the current default + a warn
prompt_catalog_driftfinding (“Langfuse is a mirror — edit the SKILL.md”). The edited body stays in Langfuse version history. - Orphaned name (key renamed/deleted in the repo) → warn
prompt_catalog_driftfinding; deletion stays a human action.
History — why the mirror is one-way
The pre-2026-07 design was Langfuse-first resolution populated by a manual bulk import (scripts/import_prompts_to_langfuse.py, now retired; its imported_by versions are treated as sync-owned and upgraded in place). That produced the masking trap: the import left a production copy of every key, so later SKILL.md edits shipped green through CI while production silently served stale snapshots — a 2026-07-03 audit found 12 of 39 imported prompts masking newer defaults, including a same-day critic fix, plus 8 orphans under renamed/deleted keys. The fix is structural: SKILL.md is authoritative, the mirror is write-only from the repo’s perspective, and the override layer is an explicit opt-in (poindexter#824, #825).
Why Langfuse + SKILL.md, not just one
SKILL.mdalone: no single place to review the live catalog with version history — you’d read 20+ pack files. Prompt bodies also wouldn’t be linkable from traces.- Langfuse alone: prompts wouldn’t ship with the product (the consumer stack has no Langfuse at all), couldn’t be edited atomically with the code that formats them, and would bypass PR review + the contract tests.
- Both, with
SKILL.mdauthoritative: the pack default is what ships, what CI tests, and what production serves; Langfuse mirrors it for review and keeps the override escape hatch for live experiments.
Related docs
Glad-Labs/poindexter#450— OSS migration umbrella (Lane A)Glad-Labs/poindexter#528— YAML →SKILL.mdpack migrationGlad-Labs/poindexter#47— the original UnifiedPromptManager migrationdocs/architecture/business-os-endgame.md— the agentskills.io pack model and why prompts live as skillsdocs/reference/app-settings.md—langfuse_host,langfuse_public_key,langfuse_secret_key,langfuse_tracing_enableddocs/architecture/cost-tier-routing.md— model selection via per-step*_modelpins (not prompt selection)