Using LDD — the bundle entry-point for Gradient Descent for Agents
The Metaphor
The conductor before the orchestra. Twelve instruments sit ready — violins (reproducibility-first), brass (root-cause-by-layer), percussion (loss-backprop-lens), a vocalist who sings counter-arguments (dialectical-reasoning), a second vocalist who sings the per-step dialectic for reasoning chains (dialectical-cot). The conductor plays none of them. She picks which and when each enters, watches the composition (the loss curve), and cues the next instrument from the score (the trace). using-ldd is the conductor. Every other skill is an instrument on one of the four axes of the gradient descent.
Overview
Loss-Driven Development (LDD) is Gradient Descent for Agents — a twelve-skill bundle that treats code changes, output revisions, skill edits, and reasoning steps as SGD steps on four distinct parameter spaces, and forbids overfitting to the current test. This entry-skill exists so you (the agent) know when to reach for which LDD skill without the user having to name each one by hand. It also owns the thinking-levels auto-dispatch (v0.10.1), the step-size controller that picks rigor (L0…L4) per task before any gradient descends.
Core principle: if the user prefixes their message with LDD: or mentions any of the trigger phrases below, LDD discipline is explicitly requested. Match their intent to the right sub-skill and announce which one you are applying. The four-loop structure is:
- Inner (
θ= code,∂L/∂code) —reproducibility-first,root-cause-by-layer,loss-backprop-lens,e2e-driven-iteration,loop-driven-engineering - Refinement (
y= deliverable,∂L/∂output) —iterative-refinement - Outer (
m= method,∂L/∂method) —method-evolution,drift-detection - CoT (
t= reasoning chain,∂L/∂thought) —dialectical-cot - Cross-cutting —
dialectical-reasoning,docs-as-definition-of-done,define-metric - Opt-in —
architect-mode(5-phase greenfield discipline; reached via L3/L4 preset or explicit flag)
Trigger phrases (user → skill mapping)
When the user's message contains any of these patterns, invoke the paired skill.
| User signal | Invoke |
|---|---|
LDD: prefix, "apply LDD", "use LDD" |
Enter full LDD mode — default entry = loop-driven-engineering unless a more specific sub-skill matches below |
| "failing test", "CI is red", "flaky", "one-off failure", "intermittent" | First reproducibility-first, then root-cause-by-layer if the failure is real |
| "bug", "error", "exception", "unexpected behavior", "why is this breaking" | root-cause-by-layer |
| "I've tried this 3 times", "keeps failing the same way", five fix-commits in one area | loss-backprop-lens (local-minimum trap) |
| "this fix works but I'm not sure it generalizes", "will this break sibling tests" | loss-backprop-lens (generalization check) |
| "I'm mid-debug", "is the fix done yet", before closing any fix-loop | e2e-driven-iteration |
| "should we ship this", "is this the right approach", design trade-off | dialectical-reasoning |
| "this doc/diff/output is okay but could be better", "polish this" | iterative-refinement |
| "same thing keeps happening across tasks", "the skill itself might be wrong" | method-evolution |
| "is this codebase healthy", release-candidate review, weekly check | drift-detection |
| "about to commit", "ready to merge", "declaring this done" | docs-as-definition-of-done |
| "design X", "architect Y", "greenfield", "from scratch", "how should I structure", "propose an architecture", "decompose this problem", "what's the right shape for X" | architect-mode (opt-in — three paths: inline LDD[level=L3]: / /ldd-architect command / these trigger phrases — plus the auto-dispatch scorer landing at L3/L4; see skill for the 5-phase protocol and the hand-off back to reactive) |
Precedence when multiple rows match (v0.13.1)
A single user message may hit multiple rows — e.g. "bug: I've tried this 3 times" matches both the generic "bug" row (→ root-cause-by-layer) and the specific "I've tried this 3 times" row (→ loss-backprop-lens). Two rules resolve the conflict, applied in order:
- Specificity wins. Longer / more literal triggers beat shorter / more generic ones.
"I've tried this 3 times"is more specific than"bug"—loss-backprop-lensfires first because the agent has already diagnosed + fixed three times and the meta-signal is "the local-fix strategy is stuck," not "find the bug." - Table order is the tiebreaker. On equal specificity, the upper row wins. The table is ordered from narrowest-scope (reproducibility check — must run before any gradient use) to broadest-scope (architect-mode — whole-design discipline).
When the primary skill completes but the other matches still apply (e.g. loss-backprop-lens diagnoses a local-minimum trap → the agent now needs a fresh structural pass), the agent MAY chain-invoke the secondary skill in the same iteration, and must announce both invocations explicitly per §"Announcing skill invocation." Chains of ≥ 3 skills in one iteration are a red flag (the iteration is doing too much; split it).
Mutual exclusion on the same error: once root-cause-by-layer has closed with a named structural/conceptual origin, loss-backprop-lens must NOT re-invoke on the same error signal within that task — the structural diagnosis is the gradient; re-running the meta-lens on it produces oscillation. Inversely, once loss-backprop-lens has declared a local-minimum trap, a fourth root-cause-by-layer invocation on the unchanged error must NOT fire — escalate to architectural rethink per loop-driven-engineering § Escalation.
Auto-dispatch: thinking-levels
Every non-trivial task enters the LDD bundle through a level scorer that picks one of five thinking levels (L0..L4) and emits a mandatory dispatch-header line. The user does not need to configure anything; the scorer runs on the task text. The user also does not need to know the scorer exists — an override is a single inline token away (see §Override syntax below).
Default is L2, not L0. Zero-config users get a deliberate baseline — one rank above reflexive pattern-matching — because "lieber ein klein wenig schlau als zu dumm" (asymmetric loss: a low-side miss ships a silent symptom-patch; a high-side miss wastes tokens). The scorer is designed to bias upward on boundaries.
The 5 levels
The Name column is the canonical human-readable label. Every display of a
level (dispatch header, trace log meta line, statusline) uses the combined
L<n>/<name> form — e.g. L3/structural. The old separate mode=reactive / architect axis is derived from the level (L0–L2 ⇒ reactive, L3/L4 ⇒
architect) and is no longer displayed or user-facing (v0.11.0).
| Level | Name | Preset — k_max / reproduce_runs |
Creativity applies? | Skill floor (minimum set invoked) |
|---|---|---|---|---|
| L0 | reflex |
2 / 1 | no | e2e-driven-iteration |
| L1 | diagnostic |
3 / 2 | no | + reproducibility-first, root-cause-by-layer |
| L2 | deliberate (default baseline) |
5 / 2 | no | + dialectical-reasoning, loss-backprop-lens, docs-as-definition-of-done |
| L3 | structural |
5 / 2 | yes — defaults to standard |
+ architect-mode (standard), drift-detection, iterative-refinement |
| L4 | method |
8 / 3 | yes — defaults to inventive (ack-gated) |
+ method-evolution, dialectical-cot, define-metric |
Skill floor is a floor, not a ceiling. A task at L2 that benefits from drift-detection may still invoke it; a task at L3 is not allowed to skip architect-mode.
The architect-mode skill's 5-phase protocol is active automatically at L3/L4 — there is no separate mode=architect axis to set. Score the task; if it lands at L3 or L4, architect-mode is active by preset.
The 9-signal scorer
Deterministic, pure function of the task text plus (optional) history of recently-touched files. No LLM call. Reference implementation: ../../scripts/level_scorer.py.
| Signal | Weight | Detect via |
|---|---|---|
Greenfield ("from scratch", "new service", "new module", "no existing code", "design a new") |
+3 | literal phrase match |
≥ 3 new components (≥ 2 matches of the components pattern, or ≥ 3 distinct "new <noun>" phrases) |
+2 | pattern / noun count |
Cross-layer ("across", "between … and", "integrate", "wire", "bridge", "hook into") |
+2 | literal phrase match |
Ambiguous requirements ("somehow", "after my last change", "I'm not sure", "when … doesn't") |
+2 | literal phrase match |
Explicit bug-fix ("fix", "failing", "broken", "off-by-one", "typo") |
−5 | literal phrase match |
Single-file known-solution (exactly one file path AND (line ref OR fix verb OR rename/move/delete/remove)) |
−3 | path + bounded-work signal |
| Layer-crossings (≥ 2 named layer / subsystem terms from the LDD/AWP vocabulary — validator, critique, delegation loop, runner, manager, orchestration, etc.) | +2 | vocabulary count |
Contract / R-rule hit (R\d+, "schema", "contract", "API surface", "invariant", "confidence (field|threshold)", "critique gate", "deliverable_presence") |
+2 | literal phrase match |
Unknown-file-territory (paths not seen in .ldd/trace.log history of the last 20 runs; when log is empty or absent, contributes 0) |
+1 | trace-log lookup |
The three bolded signals are new in the thinking-levels design; the other six are inherited from the pre-thinking-levels architect-mode scorer.
Score-to-level buckets (Phase-1-tuned, upward-biased)
| Summed score | Level |
|---|---|
score ≤ −7 (explicit-bugfix AND single-file both fire, nothing else) |
L0 |
−6 ≤ score ≤ −2 |
L1 |
−1 ≤ score ≤ 3 |
L2 (zero-signal baseline lands here) |
4 ≤ score ≤ 7 |
L3 |
score ≥ 8 |
L4 |
Creativity-clamp rule (L4 ↔ L3 interaction). When the score buckets L4 BUT the creativity inferrer returns standard, the level clamps to L3. Reason: L4's preset mandates creativity=inventive (ack-gated); running L4 with standard would mix two loss functions into one gradient (violates ../architect-mode/SKILL.md §"Cannot switch mid-task"). The dispatch header MUST show [clamped from L4] when this fires (the creativity is already echoed inline in the main body — no parenthetical duplicate). The clamp is one-directional: it cannot promote L3 → L4 when creativity would be inventive.
Creativity inference (applied at L3 / L4)
The creativity inferrer is unchanged from the pre-thinking-levels design; it now runs at L3 and L4 only.
| Level | Triggering signals | Notes |
|---|---|---|
conservative |
"regulated", "compliance", "HIPAA", "PCI", "SOC2", "migration of production", "existing stack only", "no new tech", "on-call", "tight deadline", "team of N" |
Any one hit → conservative |
standard (default) |
none of the other levels' signals dominate | Picked when L3/L4 fires but no conservative/inventive cue is present |
inventive |
"novel", "research", "prototype", "no known pattern", "invent", "experimental", "paradigm" |
Scorer proposes inventive; ack-gate must pass before the inventive loss function activates (see §Inventive ack below) |
Conservative beats inventive on a tie.
Mandatory trace-header echo
On every non-trivial task, the agent MUST emit exactly one line in the trace header before doing any work. The format is single-line (v0.11.0) — no more second mode: architect, creativity: … line:
Dispatched: L<n>/<name> (signals: <sig1>=<±N>, <sig2>=<±N>)
Dispatched: L<n>/<name> · creativity=<value> (signals: <sig1>=<±N>, <sig2>=<±N>)
Dispatched: L<n>/<name> · creativity=<value> (signals: ...) [clamped from L4]
Dispatched: L<n>/<name> · creativity=<value> (user-explicit; scorer proposed L<m>)
Dispatched: L<n>/<name> (user-bump from L<m>, fragment: "<fragment>")
Dispatched: L<n>/<name> · creativity=<value> (user-override-down from L<m>). User accepts loss risk.
Rules for the single line:
- The level is always rendered with its canonical name:
L0/reflex,L1/diagnostic,L2/deliberate,L3/structural,L4/method. - The
· creativity=<value>segment is emitted only at L3 and L4. It is omitted at L0/L1/L2 (where creativity does not apply). - The auto-dispatch case is implicit — no
auto-levelkeyword. If no dispatch-source phrase (user-explicit/user-bump/user-override-down) appears in the parenthetical, the line is an auto-dispatch. - The bracketed clamp reason is the bare
[clamped from L4]— the old(creativity=standard)parenthetical is dropped because the creativity is already inlined before the signals. - Signal pairs are the top-2 by absolute weight, stable tie-break by signal name. The agent can invoke the scorer via
python scripts/level_scorer.py "<task>"(CLI) orfrom level_scorer import score_task(library).
Worked example
User: "design a webhook replay service that stores every inbound webhook and lets partners replay arbitrary subsets; ~500/min, 6-8 week timeline, team of 2"
Scorer run (deterministic):
- greenfield (
"design … service"): +3 - components≥3 (intake + store + replay + CLI): +2
- cross-layer (
"… and …"spans ingestion + persistence + delivery): +2 - ambiguous (no stack chosen, no retention named): +2
- layer-crossings (no LDD/AWP vocabulary hit): 0
- contract-rule-hit (no R-rule / schema named): 0
- others: 0
- sum: +9 → L4 bucket.
Creativity inference: no inventive cues, no conservative cues → standard.
Creativity-clamp rule fires: L4 + standard → clamp to L3.
Trace header (single line — the old mode: architect, creativity: … second line is gone in v0.11.0; the creativity is echoed inline):
Dispatched: L3/structural · creativity=standard (signals: greenfield=+3, components>=3=+2) [clamped from L4]
Relation to the trigger-phrase table above
The trigger-phrase table (§Trigger phrases) still fires specific skills when literal phrases match (e.g. "failing test" → reproducibility-first). Auto-dispatch is parallel to that, not in competition: the trigger-phrase table picks which skills to invoke inside the level's skill floor. The scorer picks the level. Both run; they refine each other.
The "LDD:" buzzword
Users who want guaranteed LDD activation (no reliance on auto-triggering) can prefix their message with LDD:. When you see this prefix:
- Load
loop-driven-engineeringas the orchestrating skill - Before any code, announce in one sentence which sub-skill you are reaching for and why
- Apply that sub-skill's discipline literally, not "in spirit"
- Report which rubric items you satisfied at the end of the task
Example:
User:
LDD: the checkout test is failing and I need to ship in an hourYou: "Invokingreproducibility-firstfirst to check whether this is a real gradient or noise. [runs check] Confirmed reproducible — invokingroot-cause-by-layernext to diagnose at layer 4/5 before editing."
Inline hyperparameter overrides: LDD[k=N]:
Users can override hyperparameters for a single task by writing flags in square brackets after LDD and before the colon:
LDD[k=3]: quick exploratory fix
LDD[k=10, reproduce=4]: deep dive on this flaky test
LDD[max-refinement=1]: one polish pass on this doc, then ship
LDD[no-reproduce]: I've already confirmed reproducibility — go straight to root-cause
LDD[level=L3]: explicit level L3, overrides the auto-level scorer
Accepted flags (full reference in ../../docs/ldd/hyperparameters.md):
k=<N>/kmax=<N>— inner-loopk_max(range 1–20)reproduce=<N>—reproducibility-firstBranch A rerun count (0–10; 0 is allowed but warned)no-reproduce— shortcut forreproduce=0max-refinement=<N>— refinement-loop hard cap (1–10)level=<L0..L4>— explicit thinking-level, overrides the auto-level scorer. When the explicit level is below the scorer's proposal, the dispatch header emitsuser-override-downwith the "User accepts loss risk" warning.levelis not a persisted hyperparameter (cannot be set in.ldd/config.yamlor via/ldd-set); it is per-task only, parsed from the inline flag.creativity=<conservative|standard|inventive>— sub-parameter selecting the loss function for this task (three discrete objectives, not a continuous freedom dial). Only valid at L3/L4. At L0/L1/L2 the flag is ignored and a trace warning is emitted:ignored (level=L<n> does not accept creativity).inventivetriggers the one-line acknowledgment flow (see §Inventive ack below). Cannot be set project-level (per-task only).
Deprecated (v0.11.0, removed in v0.12.0): mode=architect and mode=reactive are removed as user-facing overrides — mode is a pure function of level (L0–L2 ⇒ reactive, L3/L4 ⇒ architect). For one release, the parser silently rewrites the deprecated forms and emits a note in the trace header:
LDD[mode=architect]: <task> → LDD[level=L3]: <task> (deprecated)
LDD[mode=reactive]: <task> → LDD[level=L2]: <task> (deprecated)
Trace header note on a deprecated invocation: deprecated: mode= is derived from level; use level= instead.
Multiple flags are comma-separated. Inline flags beat everything else (session /ldd-set, .ldd/config.yaml, bundle defaults).
Relative bumps — when the user knows they want "more"
Shortcuts for "bump one or two levels above auto-dispatch". These are category 3 in the §Precedence below — lower than LDD[level=Lx], higher than natural language.
LDD+: <task> → auto-level + 1
LDD++: <task> → auto-level + 2
LDD=max: <task> → L4 directly (equivalent to LDD[level=L4])
Clamp rule: the bumped level is capped at L4. Bumping beyond L4 is a no-op. A bump of LDD+ on a task that already auto-dispatches to L4 emits a trace note (bump ignored — already at L4).
Natural-language bumps — for users who don't know the syntax
The user does not need to know any LDD syntax. These phrases in the task text are recognized as "be more careful than the scorer said":
| Phrase fragments (case-insensitive) | Effect |
|---|---|
"take your time", "think hard", "think carefully", "careful", "denk gründlich", "denke gründlich", "sorgfältig", "durchdacht" |
+1 level |
"really think", "think really hard", "very careful", "ultra-careful", "maximum rigor", "think thoroughly", "sehr sorgfältig" |
+2 levels |
"full LDD", "use everything", "maximum deliberation", "volle Kanne" |
clamp to L4 |
All +1 phrases dedup semantically — "take your time and think hard" contributes +1 total, not +2. Both express the same "be careful" intent. A +2 bump requires an explicit strong phrase from the second row.
The natural-language path is the lowest-priority override (category 4); any explicit LDD-syntax flag above beats it.
Dispatch-header echo for overrides
When ANY override fires (explicit flag, relative bump, or natural-language bump), the dispatch header surfaces it so the user can see what actually ran. The format is single-line (v0.11.0) — creativity is inlined at L3/L4 only:
Dispatched: L3/structural · creativity=standard (user-explicit; scorer proposed L2)
Dispatched: L2/deliberate (user-bump from L0, fragment: "LDD++")
Dispatched: L2/deliberate (user-bump from L0, fragment: "take your time")
Dispatched: L4/method · creativity=inventive (user-bump from L0, fragment: "LDD=max")
Dispatched: L0/reflex (user-override-down from L3). User accepts loss risk.
If the user expresses a budget in prose ("budget of 3 iterations", "give me only one refinement pass"), parse the intent and apply — echo in the trace as (parsed from prose). When ambiguous, ask one clarifying question rather than guessing.
Inventive ack — user consent to switch the loss function
creativity=inventive uses a different loss function than standard or conservative (see ../architect-mode/SKILL.md §"The neural-code-network framing"). The agent is never allowed to activate inventive on its own — it can only propose, and the user consents.
Three paths to consent, in order of precedence:
Explicit inline flag.
LDD[creativity=inventive]:or the/ldd-architect inventivecommand. Consent is carried in the flag; no further ack needed.Literal ack token. When the scorer proposes inventive (via cues like
"novel","prototype","research"), the agent asks the user for explicit consent. The canonical ack is the wordacknowledged. But any of these natural-language affirmatives also count (bilingual):Positive (→ inventive activates) Negative (→ silent downgrade to standard)"acknowledged","ack","yes","ja","go","go ahead","proceed","los","okay mach","okay machen","passt","mach""no","nein","stop","cancel","abbruch","halt", or silenceAmbiguous replies (
"hmm","maybe","let me think") do NOT activate inventive — they require the literalacknowledgedor a positive token.Implicit ack from the original task prompt. When the user's initial message already contains ≥ 2 inventive cues (
"novel","research","prototype","no known pattern","invent","experimental","paradigm") AND is ≥ 100 characters long, the agent treats the prompt itself as consent — the user has already verbalized inventive intent in a substantive task description. The dispatch header MUST surface this explicitly:Dispatched: L4/method · creativity=inventive (signals: …) [implicit ack from ≥2 inventive cues in prompt]If the prompt is shorter than 100 characters or contains only 1 inventive cue, fall back to path 2 (explicit ack).
Neither path 2 nor path 3 allows the AGENT to select inventive on its own. Both still require user-originated consent — path 2 via a reply, path 3 via the task text itself. The moving-target-loss protection (the user is the only authority that can set the loss function) is preserved.
Precedence — level selection (highest wins)
1. LDD[level=Lx]: explicit level ← highest
2. LDD=max: / "volle Kanne" literal max (→ L4)
3. LDD++: / LDD+: relative bump
4. Natural-language phrases "take your time" etc.
5. Auto-scorer output 9-signal scorer bucket ← lowest
LDD[level=L0]: on a task the scorer would bucket L3 is an explicit downward override — honored, but user-override-down warning is emitted. No silent demotions.
Precedence — other hyperparameters (k, reproduce, max-refinement, creativity)
inline LDD[...] flags ← highest priority
↓
/ldd-set session overrides
↓
.ldd/config.yaml in project
↓
bundle defaults ← lowest
Use /loss-driven-development:ldd-config to see the full stack with per-key provenance. Only the knobs listed in docs/ldd/hyperparameters.md are exposed — level itself is a DERIVED value (the auto-scorer's output or an explicit override), not a persisted hyperparameter. Requests to tune other parameters (learning rates, loss weights, skill-enable flags) are moving-target-loss risks and are refused per docs/ldd/hyperparameters.md §"What is NOT exposed (by design)".
The LDD trace — mandatory visible output
For every non-trivial LDD task, emit a visible trace block inline in your reply so the user can see what discipline is running, how the loss is moving, and which skill fired. The user wants to audit this in real-time; the block is part of the deliverable, not an internal monologue.
Emit a trace line AFTER EVERY ITERATION (not per skill invocation — within one iteration multiple skills may fire; they share ONE trace emission at iteration close). Two formats exist — pick by the emission trigger, not by taste:
- Compact inline format (default per-iteration, v0.14.0+) — 2 lines for
i1/i2, 3 lines fromi3onward. Used for live iteration progress in normal task flow. Spec: see### Compact inline formatbelow. - Full trace block — 15–25 lines with header, per-iteration section, mini chart, close block. Used for task close, explicit
/ldd-tracerequest, and post-hoc reconstruction. Spec: see### Full trace block formatbelow.
Re-emit the compact line at the end of each message when the task spans multiple messages. At loop close, switch to the full block (terminal status + layer fix + docs-sync verdict + full sparkline + mini chart). Consecutive compact emissions still grow the sparkline suffix monotonically from i3 onward — iteration k's line differs from k−1's by one new sparkline glyph and a fresh Δ-arrow.
Post-hoc reconstruction exception: when the user hands you a COMPLETED task's iteration data (losses, skill names, actions already known) and asks you to render the trace, emit ONE final full block with all iterations — the per-iteration compact rule does not apply because no real iterations are happening. The tests/fixtures/using-ldd-trace-visualization/ fixture exercises this exception (all three scenarios are post-hoc).
Budget — why compact is the default. The full block's token cost multiplies by iteration count. A 5-iteration task previously emitted ~100–125 lines of trace just for the per-iteration channel; the compact format delivers the same load-bearing signal (who fired, which loop, what loss, where it's moving, what was done) in ~10–15 lines total over the same 5 iterations — about 1/8 the token cost. The full block still lands once at close, so the audit-after-the-fact is unchanged.
Compact inline format (default per-iteration, v0.14.0+)
Line layout — two-liner for the first two iterations, three-liner once a sparkline becomes meaningful:
# i1 / i2 (no trajectory history yet):
LDD i<k>/<loop> · loss=<norm> (<raw>)[ Δ<±value> <arrow>] · <skill-name>
→ <one-line concrete action the iteration produced>
# i3+ (sparkline suffix appears once ≥ 3 data points exist):
LDD i<k>/<loop> · loss=<norm> (<raw>) Δ<±value> <arrow> · ▂▅▃ <trend>
*<skill-name>*
→ <one-line concrete action>
Worked example — three inner-loop iterations on a contract-violation bug:
LDD i1/inner · loss=0.500 (4/8) · reproducibility-first
→ 5/5 reruns reproducible, no flake
LDD i2/inner · loss=0.375 (3/8) Δ−0.125 ↓ · root-cause-by-layer
→ L4 (contract): filter ignores None; L5 (concept): implicit total-function assumption
LDD i3/inner · loss=0.000 (0/8) Δ−0.375 ↓ · ▇▅· ↓
*e2e-driven-iteration*
→ contract made explicit, 8/8 green
Rules:
- Line 1 is the control line — iteration-counter / loop / normalized-loss / raw-violations / (from i2 onward) step-Δ with arrow / (from i3 onward) sparkline-suffix + net-trend arrow / skill name.
- Line 2 is the action line — indented
→prefix, one concrete sentence about what the iteration actually changed. Never restate the loss number here; never write "fixed the bug" — name the specific change. - Line 3 (optional, i3+) — when the sparkline suffix requires a separate visual break, or when more than one skill fired and you want to name them explicitly via
*skill*list. Can be merged into line 2 if space allows. - Sparkline-suffix rule: built from the same
▁▂▃▄▅▆▇█block glyphs as the full block, auto-scaled to the max observed loss in the current task. Zero values render as·. The final glyph is the current iteration's loss; the sparkline carries the full history. - Trend-arrow semantics are identical to the full block:
↓if(last − first) < −0.005,↑if> +0.005,→otherwise. Computed net-direction, never local. - Step-Δ arrow (per-iteration, separate from the net-trend arrow) appears from i2 onward inline in line 1 —
Δ−0.125 ↓means this iteration dropped the loss by 0.125 vs. the previous one. Threshold is the same0.005boundary. - Loop labels —
inner,refine,outer,cot,design(L3/L4 architect-mode usesdesignwith ap<k>prefix instead ofi<k>). - Close the loop with the FULL block, not a compact line. The reader wants to see the mini chart once, at the end, with the Close section.
Compact is a per-iteration convenience, not a replacement for the full block's audit surface. drift-detection and method-evolution skills still read the .ldd/trace.log persisted record — compact only shortens the user-visible display, not the on-disk format.
Full trace block format
╭─ LDD trace ─────────────────────────────────────────╮
│ Store : <tier scope — see bootstrap-userspace>
│ Task : <one-line description of what the user asked>
│ Loop : inner | refinement | outer
│ Loss-type : <see "Loss-types" below — primary is normalized [0,1]>
│ Budget : k=<current>/K_MAX=<max> (inner loop)
│
│ Iteration 1:
│ *Invoking <skill-name-1>*
│ <1-line result — branch chosen, layer named, verdict>
│ *Invoking <skill-name-2>*
│ <1-line result>
│ loss_1 = 0.000 (0/<max> violations)
│
│ Iteration 2 (if applicable):
│ ...
│ loss_2 = 0.125 (1/8 violations)
│ Δloss = −0.125 (regression — revert before next edit)
│
│ Close:
│ Fix at layer: <4: structural-name, 5: conceptual-name>
│ Docs synced : yes | N/A | no (BLOCKED)
│ Terminal : complete | partial | failed | aborted
╰─────────────────────────────────────────────────────╯
Keep it compact. The goal is one screenful the user can eyeball. If the trace grows beyond ~25 lines (many iterations), collapse older iterations to one summary line each.
Loss-types — how to display the loss number
Four display modes, chosen per task by the nature of the measurement. Pick one, name it on the Loss-type header line, use it consistently for the whole trace block.
| Loss-type | When it applies | Display format | Example |
|---|---|---|---|
normalized [0,1] (violations / rubric_max) |
Binary rubric items (the default for most LDD skills): count violations, divide by rubric max | Primary: float in [0, 1] with 3 decimals. Secondary: raw (N/max violations) in parens |
loss_0 = 0.375 (3/8 violations) |
rate (already in [0,1]) |
Ratio signals already bounded: flake rate, passing-test fraction, coverage | Single float, secondary raw optional | loss_0 = 0.333 (3/9 runs failed) |
absolute (continuous, no natural max) |
Unbounded signals: latency / throughput / queue depth | Absolute value with unit, NO normalization attempt | loss_0 = 45.0 ms (p99 regression) |
vector (multi-dim Pareto) (v0.13.x Fix 1) |
Multi-objective tasks where forcing a scalar would hide Pareto dominance (latency AND memory AND correctness; three different SLOs you must all hit) | One value per dim, name:value pairs in loss_vec=. Scalar loss= stays alongside as a mean-view fallback for non-vector consumers. Δ across iterations is rendered as a Pareto-dominance arrow ⇓/⇔/⇑ instead of scalar ↓/→/↑ |
loss_vec=lat:0.5,mem:0.3,corr:0.2 (Pareto ⇓ vs prev) |
Scalar loss= and loss_vec= may coexist on the same iteration. Readers that understand vector mode use loss_vec; pre-v0.13 readers fall back to the scalar.
Normalization rule (primary Loss-type):
loss_normalized = violations / rubric_max
Δloss = loss_{k-1} − loss_k # positive = progress, negative = regression
Normalization makes Δloss comparable across skills (drift-detection with 6 rubric items vs. architect-mode with 10 items become apples-to-apples). The raw (N/max) in parens keeps it actionable — the user still sees exactly which items are still open.
Anti-pattern: never display a normalized float without the raw denominator in parens. "loss_0 = 0.375" alone implies a measurement precision that isn't there — it hides the fact that it's 3/8. Show both; the normalized form is for comparison, the raw form is for action.
Anti-pattern: never compute a normalized float from a count that has no natural max (commit counts, latency, token usage). Those stay absolute with units — trying to normalize them invents a denominator and produces fake precision.
Epoch marker — Δloss across rubric/scope shifts is suppressed
The monotonicity assumption Δloss = loss_{k-1} − loss_k relies on a stable measurement frame. Mid-task rubric changes, bedrohungsmodell shifts, or explicit scope expansions break that assumption: iteration k+1's loss is not comparable to iteration k's, and pretending otherwise produces a fake gradient.
v0.13.x Fix 1 introduces a first-class epoch field — an integer that increments at every deliberate frame change. Writers call the epoch subcommand to bump the counter AND persist a reason in the same step:
./.ldd/ldd_trace epoch --reason "PSD2 SCA compliance added to rubric mid-task"
# → epoch 0 → 1
Subsequent iterations pass --epoch 1 (the writer-side discipline; the renderer reads both the epoch on the current iteration and the previous to decide whether to render a Δ). The visual contract:
- Scalar sparkline:
┊at every epoch boundary.▅▃▅ ┊ ▃▁·means two epochs, a trend arrow is still emitted but covers the full range honestly. - Value sequence: vertical bar
│between epochs:0.500 → 0.250 │ 0.400 → 0.000. - Per-iteration Δ-column: the first iteration in a new epoch shows
Δ n/a (epoch boundary)instead of a scalar delta. The reader is told the comparison is invalid; nothing is fabricated.
Anti-abuse guard: epoch bumps are monitored by drift-detection. More than one bump per 5 iterations in the same task is a surface signal for moving-target-loss — an agent gaming the convergence display by resetting the frame whenever Δ goes the wrong way. The bump reason is written alongside the counter; drift-scan reads both.
Loss visualization — sparkline, mini chart, mode+info line, trend arrow
The numeric loss per iteration gives the user the value. To make the trajectory auditable at a glance AND the work done per iteration reviewable at a glance, the trace block carries four parallel channels. Mandatory thresholds:
| Channel | When mandatory | What it is |
|---|---|---|
| Trajectory sparkline | ≥ 2 iterations | Single-line Unicode-block series (▁▂▃▄▅▆▇█), one char per iteration, auto-scaled to max(loss_observed). Zero values render as ·. Sits on a Trajectory: line inside the trace block. |
| Trend arrow | ≥ 2 iterations | Single glyph at the end of the sparkline line: ↓ net descent, ↑ net regression, → flat. Reflects first-vs-last loss delta, NOT local or majority direction. |
| Mini ASCII loss-curve chart | ≥ 3 iterations | Multi-line chart: y-axis auto-scaled to smallest 0.25-step multiple ≥ max(loss), values snap round-half-up to the nearest gridline; x-axis labels are the iteration labels (i1, r2, o1, …) with label first-char aligned to the data marker column. Data marker: ●. |
| Per-iteration mode + info line | every iteration | The iteration-label line names the loop AND the mode (e.g. (inner, reactive), Phase p1 (architect, inventive), (refine), (outer)) so the reader can tell at a glance which discipline was active. An indented continuation line carries *<skill-name>* + a one-line description of what concrete change the iteration produced — so the user can follow the skill's work step-by-step without scrolling elsewhere. |
The sparkline gives micro-dynamics (8-level resolution — separates a converged tail where losses differ by 0.05). The mini chart gives macro-trajectory (tail convergence collapses to the baseline row, which is visually honest — the loss IS flat below the snap step). The mode+info line gives audit surface — which mode, which skill, which action, per iteration. Consistency constraint: the sparkline's last bar, the chart's last marker, and the final iteration's loss= value must all reflect the same number.
Phase-indicator grammar (per iteration label, v0.11.0):
- Inner loop, default discipline →
Iteration i<k> (inner, reactive) - Design phase (L3/L4, architect-mode protocol) →
Phase p<k> (design, <creativity>)where<creativity>is one ofstandard/conservative/inventive. The wordPhase(notIteration) signals the 5-phase protocol is running. The parenthetical carries the creativity (already displayed once in the header) but no separatemode=word — the level is the mode. - Refine loop →
Iteration r<k> (refine)— no mode/creativity (refine is always y-axis work on a deliverable) - Outer loop →
Iteration o<k> (outer)— no mode/creativity (outer is always θ-axis work on a skill/rubric)
A session that fires the design-phase protocol and then hands off to reactive inner iterations renders both in the same trace: Phase p1..p5 followed by Iteration i1..i<k>.
Delta column (≥ 2 iterations): every iteration after iter 1 appends Δ <±value> <arrow> to its loss line, where arrow is ↓ (progress), ↑ (regression), or → (plateau, |Δ| < 0.0005). This is the per-step arrow — distinct from the end-to-end trend arrow on the sparkline line.
Rendering recipe (deterministic — copy verbatim):
sparkline char : ▁▂▃▄▅▆▇█ indexed by round(v / max(v) * 7); v == 0 → ·
trend arrow : ↓ if (last − first) < −0.005 · ↑ if > +0.005 · → otherwise
chart y-axis : ylim = ceil(max(v) / 0.25) * 0.25 ; rows at 0, 0.25, 0.50, … , ylim
chart data snap : row = floor(v / 0.25 + 0.5) * 0.25 (round-half-up)
chart x-axis : "└─" + "─".join(labels) + "→ iter" (label first-char = col start)
mode indicator : (<loop>, <mode>[, <creativity>]) as specified above
info line : " *<skill-name>* → <one-line description of change produced>"
Example — inner (reactive) → refine → outer, 6 iterations:
│ Store : local (.ldd/trace.log)
│ Trajectory : █▆▃▂·· 0.500 → 0.375 → 0.125 → 0.100 → 0.000 → 0.000 ↓
│
│ Loss curve (auto-scaled, linear):
│ 0.50 ┤ ● ●
│ 0.25 ┤ ●
│ 0.00 ┤ ● ● ●
│ └─i1─i2─i3─r1─r2─o1→ iter
│ Phase prefixes: i=inner · r=refine · o=outer
│
│ Iteration i1 (inner, reactive) loss=0.500 (4/8)
│ *reproducibility-first* + *root-cause-by-layer* → guard empty list, filter None values
│ Iteration i2 (inner, reactive) loss=0.375 (3/8) Δ −0.125 ↓
│ *e2e-driven-iteration* → isinstance-based filter for non-numeric types
│ Iteration i3 (inner, reactive) loss=0.125 (1/8) Δ −0.250 ↓
│ *loss-backprop-lens* → sibling-signature generalization check 3/3 green
│ Iteration r1 (refine) loss=0.100 (1/10) Δ −0.025 ↓
│ *iterative-refinement* → docstring sections + ValueError on all-invalid
│ Iteration r2 (refine) loss=0.000 (0/10) Δ −0.100 ↓
│ *iterative-refinement* → runtime invariants via assert
│ Iteratio
…(truncated)