# v1.0 Retrospective — The observation loop, closed

*Period: 2026-04-20 (same-day continuation of v0.7.0).*
*Sub-releases: v1.0-A / v1.0-B / v1.0-C. None are git tags yet; this retro proposes tagging the three as `v1.0.0`. The real-machine re-verify pass (v1.0-verify) is a companion deliverable, not a sub-release — it lives inline as [`docs/retrospectives/v1.0-verify.md`](./v1.0-verify.md).*
*Tests: 382 → **453** (+71, +18.6%). All green throughout.*

## Scope at a glance

| Sub-release | Theme | Primary deliverable | Tests Δ |
|---|---|---|---|
| v1.0-A | Declarative output-cleaning filter chain | `output_filters: [strip_thinking, strip_stop_markers]` on `ProviderConfig`; stateful filter chain shared between OpenAI-compat and Anthropic-native adapters; doctor reasoning-leak probe extended to detect content-embedded `<think>` / stop markers and emit a `providers.yaml` patch | +49 |
| v1.0-B | Direct `num_ctx` truncation detection | `coderouter doctor --check-model` gains `_probe_num_ctx` — canary `ZEBRA-MOON-847` + ~5K-token padding → echo-back verdict; `_is_ollama_like` 2-signal gate; 5-verdict branch + `extra_body.options.num_ctx: 32768` YAML patch | +10 |
| v1.0-C | Direct output-side truncation detection | `_probe_streaming` — `count 1 to 30` deterministic prompt over SSE; `finish_reason="length"` + short content → NEEDS_TUNING + `options.num_predict: 4096` patch; 2xx-with-0-chunks advisory; Ollama-shape gate shared with v1.0-B | +12 |
| v1.0-verify | 3-scenario real-machine runner | `scripts/verify_v1_0.sh` (bare vs. tuned delta assertion per sub-release); `verify-ollama-bare` / `verify-ollama-tuned` provider pair + `verify-v1-bare` / `verify-v1-tuned` profile pair in `examples/providers.yaml`; [`docs/retrospectives/v1.0-verify.md`](./v1.0-verify.md) evidence doc | ±0 (shell + YAML only) |

The minor was scoped from three threads that converged: plan.md §9.4 symptom #1 (`num_ctx` truncation) was still indirectly detected at v0.7-B close, v0.7-C README symptom #3 (`<think>` leak) had no runtime scrub layer, and the v0.7 retrospective had staked the claim "every transformation ships with a probe" as the habit to adopt before v1.0 output-cleaning lands. v1.0-A installed the transformation (output-cleaning) and its probe-side extension in the same release; v1.0-B closed the last indirect-detection gap from §9.4 (input-side truncation); v1.0-C mirrored v1.0-B onto the output-side so the Ollama 2-knob silent-fail phenotype is observable on both axes. v1.0-verify confirmed all three against a live Ollama instance and is what lets this retro claim PASS beyond the unit-test banking.

## What happened, in order

1. **v1.0-A** landed the largest surface of the minor by LOC. The motivation was concrete — qwen2.5-coder and HF-distilled models intermittently leak `<think>...</think>` blocks and sentinel markers (`<|turn|>`, `<|im_end|>`, `<|channel>thought`, etc.) into the content channel, which v0.5-C's passive `reasoning`-field strip cannot reach because the leakage is not in the reasoning field at all. The fix was a **stateful** filter chain declared per-provider in YAML, applied at the adapter boundary on both non-streaming and streaming paths, on both OpenAI-compat and Anthropic-native adapters. Three design calls shaped the implementation:

   - **Stateful filter, not `re.sub`.** When a chunk boundary splits `<thi` / `nk>`, regex simply doesn't match; the partial `<thi` leaks through verbatim. The `_max_suffix_overlap(buffer, needle)` helper at the core of both `StripThinkingFilter` and `StripStopMarkersFilter` holds back any trailing prefix that could still be part of a needle and releases it only on EOF or when a later chunk disambiguates. This is uglier than regex but wins two properties: (a) streaming and non-streaming share a single code path (non-streaming is "one chunk then EOF"), and (b) half-flushed tags never reach the client. The alternative (regex for non-streaming + ad-hoc scanner for streaming) would have doubled the surface and produced non-obvious inconsistencies.
   - **Per-text-block chain on Anthropic native.** A single Anthropic response can carry multiple `content_block` entries in series — text, tool_use, text, thinking. A per-stream chain would stall block 1's text behind block 0's unfinished `<think>` state, which visibly corrupts output. `dict[int, OutputFilterChain]` keyed on `content_block_start.index` gives each text block an isolated scanner; when `content_block_stop` fires, the index's chain flushes and, if there's non-empty tail text, a synthetic `content_block_delta` is emitted **before** the `content_block_stop` to preserve event order. `_process_stream_event_for_filters` returns a **list** of events for exactly this reason: one input event can expand to two output events.
   - **Fast-fail at config load, not at first request.** `validate_output_filters(names)` is called from two places: `ProviderConfig._check_output_filters_known` at YAML load, and `OutputFilterChain.__init__` when the chain is constructed in tests. A typo'd filter name (`strp_thinking`) fails at `coderouter serve` startup with a list of known names — the same fast-fail pattern as v0.6-A's `default_profile` validator and v0.6-D's `mode_aliases` target validator.

   The doctor-side extension paired with the transformation was the v0.7 retrospective's promised habit in action. The existing `_probe_reasoning_leak` was expanded to grep the response content for `<think>` and each `DEFAULT_STOP_MARKERS` entry, compare against the provider's declared `output_filters`, and emit a `providers.yaml` patch enumerating the missing filters. The probe prompt was upgraded from `"capital of France?"` to `"Think step by step about the capital of France, then answer in one word."` with `max_tokens=128` — tuned models won't emit `<think>` on trivial prompts, so the probe has to actively coerce the leakage path it's trying to observe. Without this, the probe would silently report OK on models that still leak on harder Claude Code workloads. +49 tests across `test_output_filters.py` (pure unit), `test_output_filters_adapters.py` (both adapters × both paths), `test_config.py` (load-time validation), `test_doctor.py` (reasoning-leak patch shape).

2. **v1.0-B** retired the v0.7-B-era indirect detection of symptom #1. In v0.7-B's `tool_calls` probe, an Ollama provider with default `num_ctx = 2048` and a long system prompt would truncate the head of the prompt (including tool declarations), causing the model to emit text instead of a `tool_calls` structure. The probe would then correctly verdict `NEEDS_TUNING` on `capabilities.tools` — but the **wrong remediation**: setting `tools: false` doesn't fix a truncated prompt. v1.0-B's `_probe_num_ctx` fires before `tool_calls`, prepends `"CANARY: ZEBRA-MOON-847"` + 300 repetitions of a 16-token padding sentence (~4800 tokens), and asks the model to echo the canary. Canary-missing → truncation confirmed → `extra_body.options.num_ctx: 32768` patch. Canary-echoed → move on.

   Three mechanics are worth making explicit. **First, the Ollama-shape gate.** `_is_ollama_like(provider)` returns True when either `base_url` contains `:11434` (Ollama's canonical port) or `extra_body.options.num_ctx` is declared (no other `openai_compat` upstream honors that knob, so the operator's declaration is an implicit signal of intent). False-positive surface is confined to self-hosted servers deliberately squatting on `:11434` that also happen to implement `num_ctx` semantics — in which case the probe still works correctly; the patch is benign. **Second, the 5-verdict branch.** The probe must distinguish (a) canary echoed + declared ≥ 8192 → OK (operator already tuned), (b) canary echoed + nothing declared → OK + informational note (upstream's default is fine, unusual but benign), (c) canary missing + nothing declared → NEEDS_TUNING + add patch, (d) canary missing + declared low → NEEDS_TUNING + bump patch, (e) canary missing + declared adequate → NEEDS_TUNING + "model intrinsic limit may be lower than declared" note (Llama 3 8B's 8192 cap surfaces here). Each branch was designed before the first test was written and every branch has a test. **Third, the `extra_body` merge in the probe.** The other four probes bypass `extra_body` on purpose — their job is to see raw upstream behavior with the adapter out of the way. The `num_ctx` probe is the one exception: its entire point is "did the operator's declared `num_ctx` actually take effect?" which requires sending the declared value. Top-level probe fields (`model` / `messages` / `max_tokens` / `temperature`) win over `extra_body` on collision, matching adapter merge semantics.

   The fixture migration was the largest non-production touchpoint. `tests/test_doctor.py`'s `_oa_provider` fixture had been defaulted to `localhost:11434/v1` since v0.7-B. With `_is_ollama_like` shipping, that default would opt every existing test into the `num_ctx` probe path. Solution: flip the default to `localhost:8080/v1` (llama.cpp port), which `_is_ollama_like` correctly rejects. All 36 pre-existing tests then see `num_ctx` probe SKIP with zero mock changes. Five tests were updated to opt into Ollama-shape explicitly via `extra_body={"options": {"num_ctx": ...}}`. +10 tests, all in `test_doctor.py`.

3. **v1.0-C** was v1.0-B's mirror. v1.0-B direct-detects **input-side** truncation (prompt cut at the head). The remaining silent-fail was **output-side** — an Ollama provider whose `options.num_predict` is still the pre-fork default (128 or 256), producing `finish_reason: length` on a short prompt that would otherwise run 200–2000 tokens. Observable to the operator only as "my Claude Code response cuts off mid-sentence." v1.0-C's `_probe_streaming` sends `"Count from 1 to 30, one number per line."` over `stream: true`, consumes the SSE stream to completion with `_http_stream_sse` (a pure-httpx helper that doesn't introduce any new dependency), and branches on `finish_reason` + measured content length:

   - 2xx + `finish_reason="length"` + content < 40 chars → NEEDS_TUNING + `options.num_predict: 4096` patch
   - 2xx + `finish_reason="stop"` → OK (+ informational note if `[DONE]` terminator missing)
   - 2xx + zero chunks (upstream silently ignored `stream: true` and returned JSON, or used non-standard framing) → NEEDS_TUNING **advisory** — no patch emitted, because there's nothing the operator can paste into `providers.yaml` to fix a server-side streaming misconfig. The verdict stays NEEDS_TUNING so the symptom is visible, but the patch field is empty by contract.
   - transport error / 4xx / 5xx → SKIP + diagnostic note
   - non-Ollama-shape → SKIP (same gate as v1.0-B)

   The probe's placement in the chain is the deliberate inverse of v1.0-B's. `num_ctx` was inserted **before** `tool_calls` because truncation would otherwise dominate as a false `tool_calls` failure. `streaming` sits **last**, after `auth → num_ctx → tool_calls → thinking → reasoning-leak`, because output-side truncation is orthogonal to the preceding declaration-layer probes — no risk of a NEEDS_TUNING streaming verdict masking a dominant signal from earlier, and the `auth` short-circuit SKIP tuple simply extends to cover it. The design heuristic, stated for future probes: "if the symptom category is orthogonal to preceding probes, put it last; if it interferes with them, put it first." The two probes written in v1.0 now span both positions.

   The "2xx with 0 chunks" advisory is a small but important piece of the contract. Nginx reverse-proxies without `proxy_buffering off`, LM Studio forks that don't route `stream: true`, and one-off self-hosted setups can all produce this symptom. There's no single YAML knob that fixes it from the client side — the fix is at the upstream's server config. A patch would be misleading, so the probe emits the verdict without a patch body and flags it as "upstream may have ignored `stream: true`". Unit tests lock this branch via pytest-httpx so we get coverage without needing an Nginx reproducer in CI. +12 tests: 2 patch-emitter + 10 probe-behavior.

4. **v1.0-verify** was the real-machine pass. v0.5-verify had established the pattern (narrow experimental control: a bare and a tuned provider that differ only in declaration fields, same model / port / auth), and v1.0-verify reused the bash 3.2 compat runner structure verbatim. What changed: three scenarios (A filter chain, B num_ctx probe, C streaming probe) mapped one-to-one to the three sub-releases, and each scenario PASSes only when the **delta** between bare and tuned matches expectations. Symmetric PASS was the point — an all-OK run on one side would be indistinguishable from "the test didn't actually exercise the code path." Scenarios B and C share a single `coderouter doctor --check-model` invocation per side (the 6-probe chain runs all at once, so grepping for scenario-specific lines in the already-captured stdout is equivalent to running it twice).

   Two quiet-but-meaningful details surfaced in the v1.0-verify implementation. **First**, an Explore-agent audit caught that an initial draft of `verify_v1_0.sh` was grepping for `NEEDS_TUNING` (underscore) and `"output_filter_applied"` (underscore) when the actual rendering is `[NEEDS TUNING]` (bracketed, space-separated) from `format_report()` and `"output-filter-applied"` (hyphens) from `logging.py`. This is the kind of mismatch a unit test can't catch because the verify script is not imported anywhere — the only defense is reading the emitter source and comparing literals. The patch-body literals (`num_ctx: 32768` / `num_predict: 4096`) are grep'd as strings precisely so a future `doctor.py` change that switches to flow-style YAML or quoted values will break verify in a loud, observable way — the lockstep is intentional. **Second**, scenario A's bare side is stochastic (qwen2.5-coder doesn't always emit `<think>` on the sample prompt), so the runner downgrades "symptom could not be induced" from FAIL to advisory. The tuned side's positive assertion (`<think>`-free content AND `output-filter-applied` log line exactly once) is the primary evidence; the bare-side `<think>` presence is nice-to-have. Evidence docs mark this asymmetry explicitly so a future reader doesn't misread a bare-side advisory as a regression.

## The minor's design through-lines

Three patterns crystallized across v1.0's three sub-releases, all of them extensions or consummations of v0.6 / v0.7 threads rather than new inventions:

| Pattern | Instances in v1.0 | Cross-minor roots |
|---|---|---|
| Transformation ships with a probe | v1.0-A output-cleaning chain + doctor reasoning-leak extension; v1.0-B direct `num_ctx` probe + (implicit) tool_calls disambiguation; v1.0-C streaming probe | v0.7-B retrospective named the pattern; v1.0 is the first minor to adopt it end-to-end |
| Stateful boundary scrubber with partial-suffix hold-back | v1.0-A `StripThinkingFilter` / `StripStopMarkersFilter` via `_max_suffix_overlap` | New in v1.0; the shape will recur any time a future filter needs chunk-boundary tolerance |
| Symptom-orthogonality heuristic for probe ordering | v1.0-B `num_ctx` before `tool_calls` (interferes); v1.0-C `streaming` last (orthogonal) | First explicit articulation; stated as a forward-looking rule for any v1.1+ probe |
| Bare/tuned delta assertion for live verify | v1.0-verify 3-scenario runner | v0.5-verify (first instance); v1.0-verify is the second, so the pattern is now a convention |

The first row is the most consequential. **"Every transformation ships with a probe"** was the v0.7 retrospective's ask; v1.0 makes it a track record. v1.0-A's output filter chain landed with the reasoning-leak probe extension in the same release, not as a follow-on. v1.0-B / v1.0-C are probes that verify transformations that already existed (the truncation symptoms were already happening before the probes — the probes just make them visible). A corollary: any v1.1+ transformation that lacks a probe should be interrogated in review. The cost of writing the probe is proportional to the transformation (+3 tests in v1.0-A for the probe extension vs +43 for the transformation surface), so the burden is minor and the legibility payoff is substantial.

The **stateful boundary scrubber** pattern is durable in a narrower sense. Any future adapter-boundary filter (collapse-whitespace, rewrite-role-markers, strip-reasoning-text-when-model-leaks-it-outside-reasoning-field) will want the same `feed(text, eof=False) -> str` + `modified: bool` shape and the same `_max_suffix_overlap` trick. When the third filter is needed, `coderouter/output_filters.py` should be factored so the Protocol lives alongside a `SimpleNeedleFilter` or `SimplePairFilter` base class rather than duplicating the hold-back logic. v1.0-A didn't do this factoring because two filters share ~10 lines of hold-back code and abstracting across N=2 is premature.

The **symptom-orthogonality heuristic** is the one pattern that's genuinely v1.0-native. Before v1.0 the probe chain was a flat list added to in sub-release order (auth / tool_calls / thinking / reasoning-leak, in historical order). v1.0-B and v1.0-C forced the question "where should the new probe go?" — and the answer turned out to be non-trivial both times. The heuristic "interferes-goes-first, orthogonal-goes-last" is stated here to save the next probe author the 20 minutes of re-deriving it.

## What worked

- **Transformation + probe in the same release paid for itself immediately.** v1.0-A's doctor extension is 3 tests and a patch-emitter function. Without it, operators deploying `output_filters` would have no way to verify the filter is actually catching the leakage they expect — "my config has `output_filters: [strip_thinking]`, is it working?" would become a shell-history-and-grep problem. The probe answers it in one command. Same-release shipping also means the probe's patch body (`output_filters: [strip_thinking, strip_stop_markers]`) and the adapter's consumption path came out of the same design conversation, so they can't drift.
- **The `_is_ollama_like` gate was the right abstraction.** A 2-signal heuristic (`:11434` port OR `extra_body.options.num_ctx` declared) felt risky when first proposed — a single boolean config flag would have been more explicit. But the signal-based form has the property that the operator doesn't need to know the flag exists; the signals that matter for the probe's correctness are the same signals that indicate Ollama. v1.0-C reused the gate verbatim — zero extension needed, shared semantics for free. The cost of the right abstraction was about 10 lines of code and one added runtime helper.
- **Probe ordering as a design decision, not an accident.** v1.0-B explicitly sequenced `num_ctx` before `tool_calls` because the older ordering had been silently producing wrong remediations. v1.0-C explicitly sequenced `streaming` last because it would otherwise dilute the dominant signals of preceding probes. Both decisions were documented in CHANGELOG design-notes so a future refactor (e.g., parallel probe execution) knows which orderings are semantic and which are incidental.
- **Live verify as a symmetric assertion.** v1.0-verify asserts the delta between bare and tuned, not an absolute verdict on either side. That symmetry is what distinguishes "the verify actually exercised the gate" from "everything was OK because nothing was tested." The v0.5-verify runner had the same property; v1.0-verify confirmed it generalizes.
- **`DEFAULT_STOP_MARKERS` was sized from field observation, not imagination.** Six markers, all observed in production Ollama / HF-Ollama traffic through Claude Code. Future additions gate on a CHANGELOG note + regression test (`test_default_stop_markers_contents`). This discipline keeps the default short enough to reason about while avoiding the temptation to pre-add markers from model cards that might never fire in Claude Code use.
- **+71 tests vs a loose target of +40 for a v1.0-labelled minor.** The overshoot is concentrated in v1.0-A (+49). 31 of those are pure-unit tests on the scanner behavior (chunk-boundary correctness, chain composition, the partial-suffix helper in isolation), which is the right place to bank coverage because the adapter-level tests are integration-cost expensive. The ratio held: "characterization tests for every new subsystem," now a multi-minor habit.
- **No new runtime deps, still.** 5 → 5. v1.0-A's filter chain is pure-Python string slicing; v1.0-B's canary is padding + string matching; v1.0-C's SSE parser reuses `httpx.AsyncClient().stream("POST", ...)` + `aiter_lines()` with pure-string `data: <json>` parsing. Same restraint as v0.6, v0.7. Has held for 10+ sub-releases now and should be called out in the v1.0 README Status bullet as a sustained design property, not just an incidental observation.

## What was sharp

- **Scenario A bare-side stochasticity is real, and the advisory-downgrade is a compromise.** qwen2.5-coder's `<think>` emission is not deterministic on the 1-to-30-count prompt (which is a deliberately different prompt from the v1.0-C streaming probe's — scenario A is exercising a different transformation). On a bad draw, bare-side scenario A reports "symptom could not be induced" and the tuned-side positive assertion carries the PASS alone. This is defensible but less than ideal. Two remediations are possible for v1.1-verify: (a) multiple trials on the bare side with a stricter prompt, accept longer runtime; (b) switch to a model with reliable `<think>` emission (a DeepSeek-R1-Distill) and accept the additional `ollama pull`. Option (b) is probably cleaner; held for a v1.0-verify-B pass.
- **Per-text-block Anthropic chain isolation was a near-miss.** The first implementation of `_process_stream_event_for_filters` used a single chain shared across all content blocks. Tests caught the cross-block state leak (block 0's partial `<think>` consumed block 1's leading text) only because the integration test covered a multi-block response. If the test had stopped at a single-block response, the bug would have shipped. Lesson: when a filter is stateful, the tests MUST exercise multiple reset-inducing boundaries, not just one.
- **`output_filters` on `FallbackChain` wasn't included and will be asked for.** v1.0-A put filters on `ProviderConfig` because filter necessity is model-family-coupled, not chain-coupled (v0.6-B's `timeout_s` / `append_system_prompt` precedent). But staging/prod split use cases (disable filters in staging for debugging, enable in prod) have no configuration surface today. The operator workaround is provider duplication with different filter lists, which is clunky. `FallbackChain.output_filters: list[str] | None` as an override layer, matching v0.6-B's shape, is a cheap v1.0-D or v1.1-A item.
- **The 6-probe chain is reaching the size where grouping makes sense.** `auth → num_ctx → tool_calls → thinking → reasoning-leak → streaming` is still a flat sequence. A reader who just wants to know "does this provider handle Claude Code's tool-use?" has to know that tool_calls, thinking, and reasoning-leak are all tool-use-adjacent while num_ctx and streaming are truncation-adjacent. A grouping (`[auth] → [truncation: num_ctx, streaming] → [toolcall: tool_calls, thinking, reasoning-leak]`) would help `format_report()` output legibility and make the probe chain extensible without more global ordering debates. Not urgent; noted for v1.1.
- **Doc-edit touchpoint cost, unchanged.** Same warning as v0.6 and v0.7. Three sub-releases × ~9 doc places = ~27 manual edits for v1.0 alone, plus the v1.0-verify closeout. `scripts/release-close.py` is now on its fourth retrospective without being written. The cost of writing it has been creeping up as plan.md's release-history table grows — a v1.1-adjacent chore that's overdue.
- **`num_predict` probe doesn't check `max_tokens` on the request side.** The streaming probe sends `max_tokens=128` because it wants a short probe, but that also constrains the output — meaning if the model legitimately responds in <128 tokens with `finish_reason=stop`, the probe is fine, and if the model would have responded in >128 tokens without the cap, the probe correctly triggers on `finish_reason=length`. But this means the probe is actually testing "does the chain respect client-specified `max_tokens`" as much as "is `num_predict` too low". The two are entangled. A second probe that omits `max_tokens` entirely and checks whether `finish_reason=length` appears at the upstream's natural boundary would disentangle them, at the cost of a more expensive probe. Held for v1.1 scope.
- **Python 3.10 sandbox still excludes `tests/test_openrouter_roster_diff.py`.** Third retro in a row to name this. The right fix is "bump sandbox Python to 3.11+," the wrong fix is "add a `datetime.UTC` shim." Neither has been done. Counts as "accumulating debt that we keep paying interest on" at this point.
- **No Anthropic-native variant of the v1.0-verify scenarios.** v1.0-verify's scenario A routes through `/v1/chat/completions`. Scenario B/C hit the doctor CLI which is adapter-agnostic. But an Anthropic-native scenario A — `/v1/messages` → `kind: anthropic` provider with `output_filters` declared — would give the first live evidence that per-text-block chain isolation holds under real streaming with multiple content blocks. This is flagged in `v1.0-verify.md`'s Follow-ons; it's the single most valuable extension for v1.0-verify-B.

## Follow-ons worth remembering

- **`num_ctx` and `num_predict` joint probe.** The two probes share Ollama-shape gating and are semantically related (input-side cap / output-side cap on the same upstream). A combined "truncation probe" that emits a single verdict and a single merged patch (`extra_body.options: {num_ctx: 32768, num_predict: 4096}`) would reduce report clutter and match how operators actually fix the symptom (one YAML edit, not two). Risk: losing the per-symptom legibility. Design call: probably keep them separate in the probe chain but add a post-processing step in `format_report()` that detects both-present and collapses the patches for display.
- **`_has_output_length_knob` / `_has_context_length_knob` generalization.** The current `_is_ollama_like` is gated on two Ollama-specific signals. vLLM exposes `--max-model-len` and `max_tokens`; Together / Groq's streaming behavior differs from Ollama in ways the streaming probe might care about. When the second non-Ollama upstream with a configurable context/output-length knob lands, rename and generalize.
- **`FallbackChain.output_filters: list[str] | None`.** v0.6-B shape. Mentioned above in "What was sharp"; v1.0-D or v1.1-A item.
- **Doctor probe-grouping refactor.** Move `format_report()` from flat list-of-probes to group-of-probes (`[auth] → [truncation] → [toolcall]`), with each group headered and probe entries indented. Also opens the door to `--only truncation` / `--only toolcall` CLI flags for targeted re-verification. v1.1 scope.
- **Anthropic-native variant of scenario A in v1.0-verify.** As above — the single highest-ROI v1.0-verify extension. v1.0-verify-B or v1.1-adjacent.
- **Streaming probe timeout knob.** `_STREAMING_PROBE_NUM_PREDICT_DEFAULT = 4096` plus a count-1-to-30 prompt can take 2–4s on CPU inference with a 14B model. The probe currently uses `provider.doctor_probe_timeout_s` (default 5.0s) which is tight. `CodeRouterConfig.doctor.streaming_probe_timeout_s: float = 10.0` as an opt-in would prevent CI flakiness on slower runners. Defer until a flake is actually observed.
- **`recover_garbled_tool_json()` and the v1.0 body scope.** v1.0's §10.1 originally scoped five claude-code-local mechanics: tool-call format conversion, garbled-JSON recovery, Code Mode, prompt cache reuse, output cleaning. Only output cleaning shipped under v1.0-A. The remaining four sit unbuilt. v1.0.0 tag reflects the observation-loop-closed accomplishment but not the claude-code-local feature-completeness the §10.1 table promises. A v1.1 or v1.2 planning pass should either expand v1.0.0's scope claim in plan.md §10 or explicitly re-scope those four to v1.1+ (my recommendation is the latter; what v1.0 delivered is substantial on its own terms and the remaining four don't share the "observation loop" theme).
- **14-case regression suite.** Still on the §10.1 list, still unbuilt, still the natural forcing function for cross-minor regression. A v1.1 or v1.2 scope item; calling it a v1.0 omission would be wrong — the suite is conceptually independent from the v1.0 observability minor and can land any time.
- **Real-machine re-verify for v0.7.** Named in v0.7 retrospective follow-ons; v1.0-verify covers the v1.0 gates but v0.7-B's four probes (auth / tool_calls / thinking / reasoning-leak against live providers) have never been live-verified. v1.0-verify's scenario B+C exercises thinking + reasoning-leak incidentally (they're part of the 6-probe chain output), but auth and tool_calls haven't been live-asserted. v1.1 scope or a `verify_v0_7.sh` addendum.
- **`scripts/release-close.py`.** Fourth retro in a row. Overdue.
- **Test-count auto-updater.** Third retro in a row. Also overdue.

## Numbers

```
Tests:        382  →  453              (+71, +18.6%)
  v1.0-A:     382  →  431              (+49, output_filters + doctor probe extension)
  v1.0-B:     431  →  441              (+10, _probe_num_ctx + patch emitter + fixture migration)
  v1.0-C:     441  →  453              (+12, _probe_streaming + patch emitter + 2xx-0-chunk advisory)
  v1.0-verify: 453  →  453             (±0,  shell runner + YAML verify profiles only)
LOC (src):    ~5100 → ~5850            (+~750, concentrated in output_filters.py + doctor.py + adapters/openai_compat.py + adapters/anthropic_native.py)
LOC (tests):  ~7950 → ~8950            (+~1000, test_output_filters.py + test_output_filters_adapters.py + test_doctor.py probe suite)
LOC (scripts): ~300 → ~770             (+~470, scripts/verify_v1_0.sh as new file)
Docs (adds):  README Status section pivoted 3 times (v1.0-A → v1.0-B → v1.0-C), +~40 lines net
              CHANGELOG +~460 lines    ([v1.0-A] + [v1.0-B] + [v1.0-C] full entries with design-notes + follow-ons)
              plan.md ~9 edits / sub   (state line / release history / tag index / §6.2 detail / test-count chart / §10.2 DoD / §18 log)
              docs/retrospectives/v1.0-verify.md  (new, ~200 lines, evidence placeholder pending first live run)
Commits:      3 on 2026-04-20          (one per sub-release; v1.0-verify is a docs+scripts commit companion)
Runtime deps: 5    →  5                (unchanged — pure-Python scanners, no new SDK surface)
```

Baselines: v0.7.0 umbrella (382 tests). LOC numbers are rough; the commit log is the source of truth.

v1.0's test delta (+71) is the smallest per-minor since v0.4 in raw terms, but v1.0's minors are heavily production-surface (output_filters ships new behavior visible to clients; doctor probes emit new patches consumable by operators) while v0.6 / v0.7 were comparatively internal. Per-sub-release v1.0-A is the largest (+49), which is consistent with it being the transformation-shipping release; v1.0-B (+10) and v1.0-C (+12) are probe additions where each new probe verdict gets one unit test.

## How to read this

This retro is the primary source for v1.0.x+ planning. Four constraints worth respecting when adding the next sub-release or starting the next minor:

- **Every transformation ships with a probe.** v1.0-A demonstrated this at the required granularity. A v1.1+ transformation (tool-call format conversion, `recover_garbled_tool_json`, Code Mode prompt rewriting, prompt-cache prefix scrubbing) without an accompanying probe in the same release should be flagged in review. The probe is part of the release, not a follow-on.
- **Ollama-shape signals are the gating contract for Ollama-specific probes.** `:11434` port OR `extra_body.options.num_ctx` declared. Any future probe targeted at Ollama specifically should reuse `_is_ollama_like`, not invent its own heuristic. If a third signal emerges (e.g., `Ollama-Version` response header), extend the helper with it rather than forking.
- **Probe-chain ordering is semantic, not incidental.** `num_ctx` first, `streaming` last, and the three declaration-layer probes in the middle. If a new probe is added, decide whether it interferes with preceding probes' verdicts (→ front) or is orthogonal to them (→ back) before deciding its position.
- **Bare/tuned delta assertion is now the standard pattern for live verify.** v0.5-verify established it; v1.0-verify confirms it generalizes. Any future live-verify (v1.1-verify, v1.5-verify) should define a bare provider, a tuned provider, and assert the delta — not the absolute verdict on either side.

The full sub-release detail lives in `CHANGELOG.md` (`[v1.0-A]` / `[v1.0-B]` / `[v1.0-C]`), and the live-verify evidence has its own doc at [`docs/retrospectives/v1.0-verify.md`](./v1.0-verify.md). This document is the narrative layer on top.
