Atlas gets a Dock voice

A workspace-Dock turn that answers "what is this page?" and proposes a navigation — distinct from wizard intake.
Story-1664 · Epic-1646 (Dock-chat brain rail · Lane A) · branch feature/1664-atlas-dock-chat · 2026-06-23

Until now Atlas had exactly one chat behaviour: wizard intake. Every chat turn ran stream_atlas_turn, which requires an AtlasIntakeInput and assembles a Matter Frame proposal. So when counsel opened the Agent Dock on, say, the matters page and asked "what is this page about?", they got matter-intake clarifications — wrong. This Story gives Atlas a real Dock-chat behaviour: given the surface context (the page counsel is on + the per-turn available_surfaces manifest) + their message + history, it answers about the current surface and may propose a typed navigate_to. The Wave-0 contract (Chore #1663) added the kind: "intake" | "dock_chat" discriminator + the DockChatContext; the sibling broker (#1660) routes kind=dock_chat here.

Two boundaries shape every line: (1) Dock scope is page answer + navigate_to ONLY (PLAN D1) — no server-plane read tools this pass; "what is this page" is answered purely from the route-registry surface.description carried in available_surfaces (D2 — no KB retrieval). (2) The brain proposes a typed intent and holds no router credentials; any navigate_to is validated against the injected manifest, so it can only navigate to a surface it was shown (and a URL-shaped key is rejected at the Pydantic boundary).

What to review

AreaWhereWhat it does
Typed contractsagents/atlas/models.pyDockChatContext (surface_key + available_surfaces + history + counsel_message + thinking_level), the FLAT generation DTO AtlasDockOutput, and the assembled AtlasDockTurn (Markdown answer + optional NavigateTo + typed error handles).
The Dock brainagents/atlas/dock_chat.pystream_dock_chat_turn — a flat-DTO PromptedOutput agent + output-function, the SAME non-streaming bounded run() path as intake (Bug-1370), fail-closed to a TYPED error turn. Reuses route_intent.assert_surface_in_manifest for the no-invented-key guarantee.
The Dock promptagents/atlas/prompts/dock_chat.pyDOCK_CHAT_SYSTEM + builder — renders the CURRENT page's description + the navigable manifest + history + counsel's message. Answers in counsel's language (incl. Hindi).
Runtime kind-branchagents/atlas/runtime.pyhandle_request branches on kind BEFORE build_intake_input → the Dock path needs no wizard input. Same turn.delta→turn.completed cadence + cost/usage stamping; intake path byte-unchanged.
Span + entrypointagents/atlas/manual_span.py · main.pyNew approved manual span agents.atlas.dock_chat (ADR-124). main.py exempts kind=dock_chat from the "input required" rule.
Evals + offline gatesagents/atlas/evals/dock_*.py · tests/test_dock_*.py6 golden Dock cases (3 answer-about-page on distinct surfaces, 2 navigate, 1 Hindi) graded by the deterministic DockTurnMatchesExpectation gate; offline + FunctionModel wiring tests run in CI without a key.

The contract the Dock turn emits

// AtlasDockTurn — a page answer, optionally carrying a typed navigation
{ next_message_md: "This is your matters list — browse, filter, and open cases.",
  navigate_to: null,                              // a plain "what is this page" answer
  is_terminal: true }

// "show my clients" on the matters page → the same turn, now navigating
{ next_message_md: "Taking you to your clients.",
  navigate_to: { kind:"navigate_to", surface_key:"clients", params:{} } }  // a key, NEVER "/clients"

How to verify

# 1. offline gates + the real PromptedOutput wiring (no key — runs in CI)
cd agents/atlas && pytest tests/test_dock_chat.py tests/test_dock_evals.py -q   # passed

# 2. repo gate
ruff check agents/atlas && python ../../scripts/check_brain_egress.py && pytest -q  # 289 passed, clean

# 3. golden Dock-evals LIVE against the real model (needs OPENROUTER_API_KEY)
cd agents/atlas && python -m evals.dock_run

The FunctionModel test proves the full chain end-to-end without an API key: the flat DTO is validated, the output-function assembles the AtlasDockTurn, a navigation builds a typed NavigateTo, and an invented surface key is rejected until retries exhaust. The LIVE Dock-evals (DeepSeek v3.2 via OpenRouter) run in CI where the key is injected.

Acceptance criteria — held

Scope & boundaries honoured