# Audit — ContextEntry role/layer placement (Vikunja #312)

**Date:** 2026-07-26 · **Scope:** every `ContextEntry` producer on the orchestrator stage path.
**Output:** table + recommendations. No code changes.

## How placement actually resolves

`PromptRenderer.render` (core/inference/.../PromptRenderer.kt):

1. **Any** entry with `layer == L0` **or** `role == SYSTEM` → folded into the single leading
   system message, ordered by `(layer.ordinal, entry.ordinal)`. Role is irrelevant at L0.
2. Everything else renders inline as its own message, ordered by `entry.ordinal`
   (layer priority is a tiebreak only when all ordinals are 0 — router chat).
3. `sourceType == "steeringNote"` **and** SYSTEM-folded → additionally re-emitted as a trailing
   user "Reminder" turn.
4. `sourceType ∈ repairMandateSourceTypes` (currently `{"retryFeedback"}`) → pulled out of the
   inline flow and emitted as the **final** message, role user.

So there are three destinations, not four: **leading system fold**, **inline transcript**,
**trailing user slot**. `EntryRole.SYSTEM` is not "high salience" — it is "buried at the top".

## Inventory

| Entry (sourceType) | Producer | Current layer/role → renders as | Recommended | Rationale |
|---|---|---|---|---|
| `retryFeedback` | ContextFeedback.kt:34 | L1/USER → **trailing** | keep | Reference implementation (#293). |
| `recoveryTicket` | ContextFeedback.kt:118 | L1/SYSTEM → **system fold** | **→ USER + trailing** | ⚠️ Highest-value defect. The recovery stage exists *only* because of this ticket, and its mandate ends up above the whole transcript. Same shape as `retryFeedback`; got the opposite treatment. |
| `remainingDelta` | ContextFeedback.kt:186 | L1/SYSTEM → **system fold** | **→ USER + trailing** | ⚠️ Loop-state, recomputed every turn a write lands. It is the stage's completion signal and it is buried, *and* mutating it invalidates the cached system prefix each turn — the one cache anti-pattern #312 asked to flag. Trailing is both more salient and cache-safe. |
| `groundingFeedback` | ContextFeedback.kt:89 | L1/SYSTEM → **system fold** | **→ USER + trailing** | Gate verdict on a returned plan; "emit a corrected plan" is a repair mandate by any reading. |
| `rejectionFeedback` | SessionOrchestratorContext.kt:117 | L2/SYSTEM → **system fold** | **→ USER + trailing** | Literally operator voice ("the operator declined"). Escalation-grade; the user channel is where it belongs. |
| `steeringNote` (locked) | Context.kt:80 | L0/SYSTEM → system fold **+ trailing anchor** | keep | Already double-anchored. |
| `steeringNote` (unlocked) | Context.kt:94, ToolExec.kt:528 | L2/USER → **inline** | keep, note inconsistency | The renderer's anchor only catches the SYSTEM variant, so the two steering paths get different salience. Cosmetic, not load-bearing. |
| `artifactRepair` | Artifacts.kt:132 | L2/USER → inline, but **sole entry** in its pack | keep | Isolated tools-less pack; already the last (only) message. |
| `criticFeedback` / `neededArtifact` | Context.kt:444 | L1/USER → **inline** | keep | Stage *input*, not a mid-loop correction. Trailing slot should not carry inputs. |
| `unconfirmedFix` | Concepts.kt:107 | L1/USER → inline | keep | Advisory, not a mandate. |
| `toolResult` (incl. failures) | Execution.kt:351/564, ToolExec.kt:215… | L2/TOOL → inline tool turns | **keep** | Routine self-correctable — "target not found", READ_BEFORE_WRITE, patch misses. Lifting these floods the user channel and destroys the effect. Escalation happens on *repetition*, and that path already exists (`STAGE_LOOP_BREAK_GATE` → `recoveryTicket`); #309's loop-breaker is the right place, not the per-failure site. |
| `assistantToolCall` | ToolExec.kt:205… | L2/ASSISTANT → inline | keep | Correct. |
| `initialIntent` | Context.kt:162 | L1/USER, but **L0 ⇒ system fold**… no: L1/USER → inline | keep | Doc comment at Context.kt:148 claims "pinned L0 SYSTEM"; code says L1/USER. Comment is stale — **fix the comment**, not the code (L1/USER is right). |
| `clarificationAnswer` | Context.kt:205 | L0/SYSTEM → system fold | keep, flag cache | Mutable mid-run (grows per clarification) inside the cached prefix. Low frequency; acceptable. |
| `schemaInstruction`, `systemPrompt`, `operatingGuidance`, `projectProfile`, `agentInstructions`, `operatorProfile`, `claimedTask`, `promotedConcept`, `verifiedBaseline`, `successfulPlanShape` | various | L0/SYSTEM → system fold | keep | Stable per stage. Correct and cache-friendly. |
| `agentPrompt` | Execution.kt:120/147 | L1/USER → inline | keep | The stage task. |
| `repoMap`, `docsCatalog`, `relevantFiles`, `decisionJournal` | Context.kt:289/376, ContextFeedback.kt:245, Execution.kt:189 | L3/USER → inline | keep | Reference material; #290 already moved these out of the system fold. |

## Recommendations, in order

1. **Re-role the four escalation entries** (`recoveryTicket`, `remainingDelta`,
   `groundingFeedback`, `rejectionFeedback`) to `EntryRole.USER` and add their sourceTypes to
   `PromptRenderer.repairMandateSourceTypes`. One-line change each plus the set.

2. **Guard the scarcity invariant first.** `repairMandateSourceTypes` currently joins all matches
   with `\n\n`. With one member that is fine; with five, a recovery stage on a retry with an unmet
   delta emits three stacked "mandates" and the channel stops being authoritative. Before (1) lands,
   the trailing slot needs either a priority order emitting the single highest-precedence entry, or
   one consolidated block under a single header. Suggested precedence:
   `recoveryTicket > retryFeedback > groundingFeedback > rejectionFeedback > remainingDelta`.
   `remainingDelta` is the exception worth appending unconditionally — it is the completion signal,
   not a competing mandate.

3. **Cache note.** All four moves are *out of* the system prefix and are therefore cache-positive.
   `remainingDelta` is the biggest win: per-turn mutation currently sits inside the cached prefix.
   No move *into* system is recommended anywhere.

4. **Do not touch tool-role failures.** Escalation belongs at the repeat detector (#309), not at
   each failure site.

Everything above stays event-derived (invariant #9); the trailing block is synthetic and should keep
its `## `-headed, non-conversational framing so it never reads as a real operator turn.

## Note on sequencing

The sprint deferred #312 behind #307 (manifest event) "so the audit has ground truth". Not needed —
placement is statically determined by `PromptRenderer` + the producer's role/layer, both read
directly. #307 remains useful for verifying the *result* of recommendation (1) on a live run.
