v0.5.1
26 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7106af5c09 |
style(tui): UI polish pass (v0.5.1)
Cosmetic refinements on top of v0.5.0's content-only main pane. No
behavior change; ships as a patch bump.
## Color signal — terminal labels tinted per outcome
The transcript's [done]/[error]/[cancelled] labels were plain
foreground (Australis #a9bcc3 white), which made them slow to scan
against the surrounding assistant text. Now tinted per outcome:
- [done] → Aurora green (#16B866 / $success)
- [error] → Dawn red (#ff491a / $error)
- [cancelled] → Dawn yellow (#e1c631 / $warning)
The post-Done Rule() separator is also tinted to Australis dark-60
(#86929d) so the streamed-text → markdown-body boundary reads as
chrome, not a content artifact.
## Empty-state placeholders
Tools and Debug panes were stark-empty before any turn fired — easy
to misread as "the pane is broken." Now show placeholder lines on
mount in Sea dark-50 italic:
Tools tab: (no tool events yet — start a turn that uses tools)
Debug tab: (waiting for telemetry — start a turn)
The placeholders scroll off naturally as real events fill the panes.
## Live thinking widget self-explains
The thinking-current Static at the top of the right column used to
just display raw thinking content with no context — an operator
glancing at the screen mid-stream might not realize they were
looking at LLM chain-of-thought. Now prefixed with "thinking… " so
the widget self-identifies.
## Spacing + chrome
- Transcript / tools-log / debug-log: 1-cell horizontal padding so
content doesn't hug the column border.
- thinking-current: italic text-style on top of the dark-60 color,
so the live-preview band is visually distinct from solid-colored
log content.
- Active tab in TabbedContent: Aurora bright-cyan label + bold
text-style, so the eye lands on the currently selected pane.
- Input placeholder text: tinted to Sea dark-50 so it reads as
placeholder, not content.
## Test impact
3 new tests added (test_done_label_styled_success,
test_empty_state_placeholders_present, plus the polish hits
test_thinking_widget_truncation / test_thinking_coalesce updated for
the "thinking… " prefix). 4 existing tests that checked
`isinstance(w, str) and w.startswith("[done]")` updated to use the
_text_of helper (terminal labels are now RichText, not str).
_spy_writes helper widened to accept positional args after Textual's
internal deferred-render path started passing them positionally
post-Resize.
241/241 GREEN; ruff clean. Live smoke against personal Worldtree
confirmed: Done line renders in Aurora green #16B866 verbatim;
both placeholder lines appear in dark-50; thinking widget shows
"thinking… <content>" during a turn.
Patch bump (v0.5.0 → v0.5.1) per SemVer etiquette: purely cosmetic;
no signature change; no caller-visible behavioral shift.
|
||
|
|
ffd22fb587 |
refactor(tui): content-only main pane + Debug tab + dark chrome (v0.5.0)
Two operator-driven changes off v0.4.1: 1. **Main pane is content-only.** Pre-v0.5.0 the transcript mixed assistant text with telemetry (Thinking closed runs, WorkerPhase, TextBoundary) — only tool events were factored out per #13. The transcript now receives ONLY: user-prompt echo, assistant Text deltas, [done]/[error]/[cancelled] terminal labels, and the post-Done Markdown render. All telemetry routes to a new Debug tab in the right column. 2. **Chrome no longer blue.** Textual's default Header / Footer / active-tab styling tints with `$primary` (Aurora blue under Australis), which read as garish on dark terminals. Header, Footer, and the TabbedContent tab strip get explicit `background: $surface` (Sea bright-black #373b46) so the chrome sits cool and unobtrusive against the Ice black background. ## Layout reshape ``` LEFT COLUMN (content only): RIGHT COLUMN (telemetry): transcript (RichLog, 1fr) thinking-current (Static, dock top) prompt (Input, dock bottom) TabbedContent: Tools (tool_start, tool_result) Debug (thinking, worker_phase, text_boundary) ``` The thinking-current live-preview Static moves from left → right column so the left column is genuinely content-only. Live thinking visibility now persists across tab switches (it docks above the TabbedContent, not inside any tab). ## Presenter routing (TuiPresenterState.render) Signature widens with `debug_log: RichLog`. Routing matrix: Text → log (transcript) Done / Error / Cancelled → log (transcript) [terminal labels] ToolStart / ToolResult → tools_log (Tools tab) Thinking (closed run) → debug_log (Debug tab) WorkerPhase → debug_log (Debug tab) TextBoundary → debug_log (Debug tab) Thinking (per-delta) → thinking_widget (live preview) INV-009 render-exception fallback preserves routing per event class (new INV-020) — ToolStart/Result falls back to tools_log; Thinking/WorkerPhase/TextBoundary to debug_log; everything else to log. ## Keybindings - Ctrl+1 → Tools tab (existing, unchanged) - Ctrl+2 → Debug tab (NEW) `pane-name` footer widget updates dynamically as the operator switches tabs ("Tools" ↔ "Debug"). This was previously deferred to "the multi-tab issue" per the Volva contract-review amendment; multi-tab now exists, so the dynamic update lands here. ## Contract amendments docs/contracts/issues/13.contract.md amended in-place: - INV-015 amended: transcript is content-only; telemetry routes to debug_log. Old routing (telemetry in transcript) retired under the no-backwards-compat rule. - INV-017 amended: thinking-current docks to right column (was left). - INV-019 new: two TabPanes (Tools + Debug), Ctrl+1/Ctrl+2 bindings, dynamic pane-name update. - INV-020 new: render-exception fallback preserves per-event-class routing. - Layout-spec snapshot ASCII diagram updated. Drift-check clean. ## Tests 239/239 GREEN (+3 new: debug_tab_exists, ctrl_2_activates_debug_tab, pane_name_updates_on_tab_switch). 6 existing tests adjusted for the new routing (test_thinking_closes_one_debuglog_entry, test_multiple_thinking_runs_each_get_debuglog_entry, test_render_exception_fallback, test_cancelled_mid_thinking_closes, test_worker_phase_demoted_to_debug_log, test_left_column_content_only). ruff clean. Live smoke against personal Worldtree: mimir KB-search turn populated tools_log with 11 lines of tool events (search_library + read_note); debug_log with 20 lines of worker_phase + thinking content; transcript stayed content-only with `❯ user-prompt` (Aurora bright-cyan) + assistant text deltas. Routing matrix holds end-to-end. (Diagnostic note: RichLog.lines is the rendered- output buffer; inactive TabPane content shows lines=0 until the tab activates and renders. Internal write store is correct — this is a Textual rendering quirk, not a routing bug.) Minor bump (v0.4.1 → v0.5.0) per SemVer etiquette: visible routing surface change for operators; transcript and Debug tab contents look different from yesterday's v0.4.1. |
||
|
|
2756f5f1dd |
style(tui): apply Australis theme to TUI chrome + widgets (v0.4.1)
Retheme the Textual TUI under the Australis Dark colorscheme (github.com/lkraven/australis) — Aurora blue/cyan/green primary, Ice neutrals (#222531 bg, #a9bcc3 fg, #cce7ec highlight), Sea darks for chrome separators, Dawn accents reserved for terminal- event labels (red/yellow). 16-color cool-tone palette with medium contrast. Mechanism: Textual `Theme` API. AUSTRALIS_THEME defined at module scope mapped to semantic tokens (primary/secondary/accent/success/ warning/error/foreground/background/surface/panel) plus a Sea variables block (`au-dark-30..60`, `au-bright-70/80/white`, plus Aurora bright variants). Both `RatatoskrApp` and `AgentPickerApp` register the theme in `__init__` and set `self.theme = "australis"`. Per-widget styling via DEFAULT_CSS theme variables (no hex sprinkled in CSS): - `#identity` → `$au-bright-blue` (Aurora bright-blue). - `#pane-name` → `$au-bright-cyan` (current-pane indicator). - `#hint` → `$au-dark-60` (subtle Ctrl-C state line). - `#prompt` border → `$panel` unfocused / `$primary` focused (focus highlight in Aurora blue). - `#left-column` gets a `border-right: solid $panel` separator. - `#thinking-current` color → `$au-dark-60` (matches demoted style). - `#transcript` + `#tools-log` background → `$background`. - Agent picker's highlighted ListItem → `$primary` bg + `$au-bright-white` fg. Rich Text styling (where Textual's theme system doesn't apply): - `_dim()` helper for demoted telemetry now uses explicit `#86929d` (Sea dark-60) instead of the terminal-dim filter `"dim"`. Renders consistently across emulators and stays anchored to the brand palette. - User-prompt echo (`❯ <content>` in transcript) wrapped in `RichText` styled with `#42dcd1` (Aurora bright-cyan) — calls out the operator's voice in the primary palette. CLI mode (`--send`) is unaffected by design — raw stdout has no theme concept. The retheme is TUI-only. 236/236 tests GREEN; ruff clean. Live smoke against personal Worldtree: `app.theme == "australis"`, all widget colors resolve to expected Australis hex values (identity → `#a4c4ff`, pane-name → `#42dcd1`, hint → `#86929d`). One existing test adjusted: `test_worker_phase_demoted` previously asserted `style == "dim"`; now asserts non-empty style (the demoted style is now an explicit Australis hex, not the terminal "dim" sentinel). Behavior-equivalent — the demotion intent is preserved. Patch bump (v0.4.0 → v0.4.1) per SemVer etiquette: cosmetic refinement of just-shipped surface, no public-API change. |
||
|
|
24e4371ec7 |
feat(tui): issue #13 — §5 layout reshape + Tools pane (v0.4.0)
Reshape the TUI from vertical-stack single-pane to Horizontal two-column with TabbedContent on the right; v1 has a single Tools tab that consumes ToolStart/ToolResult SSE events previously rendered inline in the transcript. Foundation for the rest of design-brief §5; subsequent panes (Persona/AdminEvents/BifrostState/ ServerLog) plug in as sibling TabPanes when their substrate blockers resolve. Three coupled pieces, all in-place amendments to issues #4 + #12: - **Layout**: compose() yields Horizontal#main-row containing Vertical#left-column (transcript + thinking-current + prompt) and Vertical#right-column (TabbedContent#side-panes with TabPane#tools-tab → RichLog#tools-log). Width split 2fr:1fr. CSS dock rules narrow to per-container scope so thinking-current toggling doesn't reflow the right column. - **Tools pane**: TuiPresenterState.render() signature widens with tools_log: RichLog. ToolStart/ToolResult route there per INV-014; every other event keeps its issue-#12 routing. Plain-label fallback under render-exception preserves routing (INV-009). - **Ctrl+1 binding + pane-name widget**: BINDINGS gains Binding("ctrl+1", "focus_tools") which programmatically sets TabbedContent.active; Textual's default preserves Input focus per INV-016 (test asserts; regression path documented). Static#pane-name in the footer renders "Tools" v1 (static — no tab-switch handler wiring lands in #13 per amendment-2 from Volva paraphrase review). CLI mode (--send) is unaffected by design per INV-018 — non- interactive, no tabs concept; CLI keeps inline tool-event rendering. Contract: docs/contracts/issues/13.contract.md (drift-check clean, two amendments applied from Volva contract-paraphrase pass). Tests: +9 net (TestLayoutShape × 7 + TestTuiPresenterState routing × 3, minus 1 deprecated test_tool_start_demoted superseded by test_tool_start_routes_to_tools_log). 236 total GREEN; ruff clean. Live smoke against personal Worldtree's mimir: tool-using turn (KB search) populated tools_log with tool_start + tool_result for search_library + read_note; transcript stayed chat-only with worker_phase + thinking. Routing-not-duplication confirmed end-to-end. |
||
|
|
d30be12deb |
feat(sessions,cli,tui): issue #8 — startup agent picker (v0.3.0)
Adds GET /agents fetch + ListView picker for bare `--new` (TUI mode without --agent). Three in-place amendments: - ratatoskr.sessions: new `list_agents()` + `AgentInfo` frozen dataclass with omit-when-null/empty defaults mirroring SessionInfo's INV-001/INV-002 origin-conditional pattern. Non-200 responses raise the existing SessionApiFailed (no new exception). - ratatoskr.cli: `_parse_args` softens `--agent` from absolute to mode-conditional — required for `--send --new`, optional for bare `--new`, forbidden with `--session` (unchanged INV-004). - ratatoskr.tui: new `AgentPickerApp(App[str | None])` — separate Textual App (not Screen-within-RatatoskrApp) so list_agents errors land on real stderr before any alt-screen opens (preserves issue #6's INV-001). `_resolve_then_run` gains a pre-create branch: fetch agents → empty list → exit 13; non-200 → exit 20; network error → exit 21; picker dismissed → exit 0; otherwise thread chosen agent_id into create_session. Contract: docs/contracts/issues/8.contract.md (drift-check clean). Tests: +18 (227 total, was 209). Live smoke against personal Worldtree (:8081) returned 12 agents; programmatic picker drive auto-picked lofn and created a real session with `end_user_id="ratatoskr-tui"`. |
||
|
|
a77a872810 |
snapshot: persistent-memory — v0.2.1 layout fix + §5 sequencing decision + Worldtree-stall diagnostic
Captures three things accumulated since the v0.2.0 snapshot in |
||
|
|
3b9c610587 |
feat(cli,tui): issue #12 — presenter contract semantics amendment (v0.2.0)
Replaces the stateless _render_event / _render_event_to_log helpers with stateful per-turn presenters (CliPresenterState / TuiPresenterState). Coalesces thinking-event deltas into a single growing display per run; demotes telemetry events with editorial hierarchy; formats duration + usage for human reading. Headline behavior change: a 50-token thinking phase now renders as ONE coalesced growing line in CLI (or one closed RichLog entry + per-delta live Static widget in TUI), not 50 lines of [thinking] spam. Editorial promotion line (issue #12 INV-002): - Load-bearing (no demotion prefix): Text, Done, Error, Cancelled - Demoted telemetry (`. ` ASCII prefix in CLI; dim `· ` in TUI): WorkerPhase, Thinking, TextBoundary, ToolStart, ToolResult Stateful coalescing: - Thinking deltas accumulate into thinking_buffer; first non-thinking event closes the run with a single \n boundary in CLI / one closed dim RichLog entry in TUI. - TUI adds a dedicated Static(id="thinking-current") widget that shows the last ~200 chars of the active run, mirroring per-delta updates. Two-views-of-thinking decoupling per INV-004: chronological RichLog + always-visible widget. - CLI INV-005: when stdout text was streamed mid-line, text_written_since_newline triggers a stdout flush + \n before the next stderr terminal label — guarantees [done] / [error] / [cancelled] land on their own line in a TTY without breaking pipe-to-file scripted consumers. Formatting helpers (issue #12 INV-006 / INV-007): - _format_duration_ms — autoscale `347ms` / `5.5s` / `1.2m` - _format_usage — natural-language `6756 in -> 126 out (6882 total, 0 cached)` with arrow="->" CLI / "→" TUI Cross-frontier design pass (eitri-smithy-dev, althing 01KSBE52YZR5E3SPTKA672JE43) returned 16-of-16 confirmed decisions + 4 material divergences applied: - ASCII `. ` prefix in CLI (`·` is U+00B7, not ASCII) - RichLog one-closed-entry-per-run + Static per-delta updates (not inline-mirror as initially proposed) - presenter-state object instead of pure-function rendering - Framed as "contract semantics amendment", not "polish" Volva paraphrase round (5 prose-precision fixes applied to 12.contract.md): INV-001 "growing display" semantics; single hide mechanism for the Static widget (Textual reactive `display: bool`); [render_error] security clause (type-only, no exception message); text_written_since_newline `\n`-terminated text corner case; [create_session] integration path (bypasses state.render — not an SSE Event variant). Volva code-review round (5 findings applied): - F1 drift: render-exception fallback now writes BOTH a plain-label fallback line for the original event AND the `[render_error] <type>` line (was missing the fallback half). - F2 drift: dim Rich style applied to all demoted-telemetry RichLog writes via `rich.text.Text(..., style="dim")` (was plain str). - F3 drift: belt-and-braces widget clear+hide on EVERY terminal event (Done/Error/Cancelled), even when thinking_open was False. - F4 precision: _format_usage gains PRE-001 assertion on the four expected usage keys. - F5 precision: _run_turn signature amended in issue #3 contract to document the new `state: CliPresenterState | None = None` test- injection kwarg. [create_session] lifecycle line demoted to `. create_session:` (written directly by _amain; bypasses state.render since it's not a wire-level SSE Event variant). Pre-amendment _render_event / _render_event_to_log and their test classes removed under the no-backwards-compat rule. Issues #3 and #4 contracts amended in-place: #3 (CliPresenterState CLASS + FN block + helper FN blocks + _run_turn signature + _amain create_session demotion); #4 (TuiPresenterState CLASS + FN block + compose Static widget + _stream_turn_worker state construction). 209 tests GREEN; ruff clean. Bumps v0.1.0 → v0.2.0 (minor — output shape change breaks pre-amendment grep patterns like `[thinking] '`; no public API surface change beyond the rendering contract). Persistent-memory commit-along: captures the issue #12 decision, forward direction (require end_user_id for every access — declined worldtree-dev's requires_end_user_id offer because we'll send it universally), and the Heimdall scope-model foot-gun note (the "per-Tier-1-agent scope add" diagnosis was a phantom ask resolved by worldtree-dev's correction; agent.call:* baseline covers all Tier 1). |
||
|
|
82821561e6 |
snapshot: persistent-memory — Heimdall scope-model foot-gun note (post-v0.1.0)
Captures the lesson from today's lofn-scope chase: "per-Tier-1-agent scope add" is a phantom ask. The `agent.call:*` (singular) baseline rule in config/policies.yaml covers ALL Tier 1 foundational agents (mimir, lofn, all Asgardians) for every authenticated tier; there is no per-agent grant in this path. The plural `agents.call:<owner>:<agent>` namespace is Tier 3 only (consumer-defined agents via POST /agents/define). Worldtree-dev shipped a corresponding doc fix (dd6e091) — new "Authorization model — agent invocation" section at docs/conversation-api-spec.md lines 58-114 + a heimdall.contract.md fix removing a misleading agent.call:mimir example. Don't ping infra-ops for "per-Tier-1-agent scope adds" again. Real future infra-ops asks remain: admin-tier key for the AdminEvents pane (admin.events.read scope, different tier) and Tier 3 custom-agent registration (POST /agents/define flow, different from scope-add). |
||
|
|
804c2df6eb |
feat(sessions,cli,tui): issues #5 + #6 + worldtree-dev consumer-API follow-up
Issue #6 (TUI startup error visibility): restructure run_tui lifecycle so pre-App.run() failures land on real stderr instead of getting eaten by the alt-screen teardown. New _resolve_then_run async helper opens the AsyncClient via async-with, does pre-flight session resolution, routes AgentNotFound / SessionApiFailed / network errors to sys.stderr (verbatim same labels + exit codes as cli._amain), then constructs RatatoskrApp with pre-resolved state and awaits app.run_async(). RatatoskrApp.__init__ signature widens to (args, *, session_id, agent_id, client) — all three required. on_mount narrows to identity-widget population; on_unmount becomes a no-op (client lifetime owned by run_tui's async-with). Issue #5 (--end-user-id for per-end-user agents): sessions.create_session gains keyword-only end_user_id kwarg with PRE-003 non-empty assertion; ParsedArgs.end_user_id field added (default None); --end-user-id flag with non-empty validation; _amain + _resolve_then_run thread it to their create_session calls. RATATOSKR_END_USER_ID env-var fallback (flag > env > None) per the post-2026-05-23 amendment; env.sh (gitignored) ships "ratatoskr-tui" as project-stable partition default. Worldtree-dev consumer-API follow-up (althing 01KSBARG2B8M): User-Agent header added (ratatoskr/<version> (vh@phasefinal.com), version pulled via importlib.metadata) to both AsyncClient constructions so server logs can distinguish ratatoskr traffic from other consumers. Volva code-review (2 rounds on #6) found 8 test-precision gaps + 1 PRE assertion drift, all Category 1 fixed: missing PRE-001 at _resolve_then_run entry; Rule separator assertions on markdown render; RichLog-write spy on empty submit; input-cleared + no-new-worker on cancelling busy; worker.cancel observation on three force-exit paths; on_unmount-no-close focused test (the prior client-lifetime test patched run_async so on_unmount was never exercised); happy --new resolve test verifying POST count + identity propagation. Issues #2/#3/#4/#5 contracts amended in-place to reflect: - create_session widened (PRE-003, body construction step, body shape POST) - ParsedArgs description + _parse_args STEPS + _amain create_session call + new TESTS for end_user_id + env-var fallback - _resolve_then_run STEPS + new TEST entries; on_mount narrowed; INV-007 amended for new client ownership - Post-#6 adjustment note on issue #5 (_resolve_then_run replaces on_mount as the threading site since #6 moved session resolution out of the alt-screen) 188 tests GREEN; ruff clean. Bumps to v0.1.0 — first minor release, the load-bearing reason is RatatoskrApp.__init__'s breaking signature change (additive end_user_id alone wouldn't have triggered a minor pre-v1.x). Files Gitea issues #9 (spec-pin refresh v0.19.0 → v0.22.1), #10 (track Worldtree #196 subject:{type,id} migration), #11 (AdminEvents pane auth prerequisite admin.events.read). Infra-ops pinged via althing for agents.call:lofn scope add (broker pattern; they forwarded to worldtree-dev because personal Worldtree exposes no public scope-mutation endpoint). |
||
|
|
c713208585 |
feat(sse_client,cli,tui): implement issue #7 — empty-data skip + MalformedSseData
Bundles initial TDD impl + Volva-code-review F1/F3 amendments. sse_client.py: - New MalformedSseData(raw) exception; truncates raw to 200 chars at __init__ (mirrors MalformedSseId.raw[:64] precedent). - _iter_events gains `if sse.data == '': continue` BEFORE _parse_sse_id. Empty-data frames are silently skipped per issue #7 INV-001 (keepalive semantics). Empty-data + bad-id is still a keepalive; intentional ordering, don't reorder. - _iter_events json.loads(sse.data) now wrapped — JSONDecodeError → MalformedSseData(raw=sse.data). cli.py: - Imports MalformedSseData; _run_turn ERROR_ROUTING gains the case → stderr `[malformed_sse_data] raw={exc.raw!r}` + exit 22 (protocol- failure bucket, same as MalformedSseId/TurnIdFlip). tui.py: - Imports MalformedSseData; _stream_turn_worker ERROR_ROUTING gains the case → transcript label; finally block restores state→idle per INV-008 (mid-session errors don't exit the app). Tests (6 new): - test_sse_client.py: empty_data_skipped (tracer — 4 frames in, 3 events out), malformed_data_raises, whitespace_data_raises, malformed_data_truncation, AND empty_data_skip_preserves_last_seen_sse_id (F1 from Volva code-review — drop-after-empty probes internal last_sse_id non-advancement via SseConnectionDropped.last_seen_sse_id). - test_cli.py: malformed_sse_data (tightened to assert exact `[malformed_sse_data] raw='not-json'` shape per F3), malformed_sse_data_truncation (5000-char payload — verifies truncation carries through presenter rendering, F3). - test_tui.py: malformed_sse_data_returns_to_idle (state→idle per INV-008; app does NOT exit). Smoke validation (2026-05-22): the original crashing prompt ("what about system 1 and system 2 framing?") now completes cleanly end-to-end. mimir streamed 3193 tokens (50 seconds, 374980-token context), `[done] turn_id=96 duration_ms=50436`. Empty-data frames somewhere in the stream silently skipped; no crash. 172/172 tests GREEN; ruff clean; all 5 issue contracts (#1, #3, #4, #5, #7) drift-check clean. Persistent-memory updated per the commit-along rule: status reflects v0+#7 milestone; new dated decisions for #5/#6/#7 filing + #7 implementation; foot-gun entry for unguarded json.loads(sse.data). |
||
|
|
6f7192f8db |
snapshot: persistent-memory after v0 milestone (4 issues landed)
Tighten Current state — drop file-by-file inventory (redundant with ls src/ratatoskr/ + each module's docstring + CLAUDE.md's architecture-map pointer to docs/architecture.md). Keep high-level status + next moves. Add two dated decisions capturing session-level lessons: - Volva paraphrase + code-review calibration consistent across all 4 issues (hit rates + recurring gap classes the post-TDD review catches) - Manual smoke is load-bearing — found a real defect (httpx 5s read timeout killing SSE) the mock-only test layer couldn't surface Add two foot-gun entries to Tried and abandoned: - RichLog markup=True silently strips [xxx] labels - Worker query_one timing trap (widened signature dodge → reverted; real fix was test-side pilot.pause) Also fold per-issue TDD detail entries into a single git-log pointer — the structured commit messages (9703eb2..61c3941) carry the per-issue trail; persistent-memory shouldn't duplicate. Net: 114 → 106 lines. Well under 300 soft cap; no archival needed. |
||
|
|
61c3941ec3 |
fix(client): disable SSE read timeout — caught by personal Worldtree smoke
First manual smoke against personal Worldtree (10.250.50.152:8081) produced httpx.ReadTimeout mid-stream after the worker_phase BuildingPrompt event. Root cause: httpx's default 5s read timeout killed the connection during mimir's thinking phase (LLM streaming has multi-second idle gaps between SSE events). Fix at the caller layer (where the AsyncClient is owned): - cli._amain and tui.on_mount now construct AsyncClient with timeout=httpx.Timeout(connect=10.0, read=None, write=10.0, pool=10.0). read=None disables the SSE-killing timeout; connect/write/pool keep modest timeouts so true network failures still surface promptly. Defense in depth in sse_client.stream_turn: - ERROR_ROUTING now also catches httpx.ReadTimeout (was just ReadError | RemoteProtocolError) and surfaces it as SseConnectionDropped, so if a caller misconfigures their client the failure is at least a named exception the presenters handle. Contract amendments (in-place): - Issue #1: new [compatibility] constraint documents the read=None recommendation; ERROR_ROUTING for stream_turn lists ReadTimeout alongside ReadError/RemoteProtocolError. - Issues #3 + #4: AsyncClient construction step now spells out the timeout shape explicitly. Smoke after fix: SSE stream consumed cleanly, agent responded, [done] turn_id=88 model=qwen3.6-35-a3b duration_ms=2351. Stdout-only (2>/dev/null) returned clean agent text + exit 0 — INV-002 stdout/stderr split holds end-to-end against real wire. Wire-compat envelope (personal v0.16.2 vs ratatoskr's v0.19.0 pin) confirmed. 164/164 tests GREEN; ruff clean; all three drift checks clean. Note: TUI mode not smoke-tested from this CC session (needs a TTY; operator-side check via `source env.sh && uv run ratatoskr --new --agent mimir`). |
||
|
|
942e33898c |
fix(tui): address Volva code-vs-contract drift (issue #4)
Volva code-review surfaced 8 findings against the TDD-passing
TUI shell. All 8 addressed.
Drift fixes (code):
- Primary: INV-002 + INV-003 require visible Footer-area rendering
of session-identity + Ctrl-C state hint. Implementation stored
the strings in `self.sub_title` (which lands in the Header, not
Footer) and `self.hint` (a plain attribute, never rendered). Fixed
by adding two `Static` widgets (id="identity" and id="hint") in
compose; the `_set_hint()` helper mirrors state into the widget on
every state transition. Same-model TDD missed this because tests
asserted internal state, not visible widget content.
- Reverted `_stream_turn_worker(content, log)` to single-param
`(content)` per the contract FN signature. The widened signature
was a TDD-time workaround for a NoMatches-during-worker
execution; root cause was test timing (added `await pilot.pause()`
before the polling loop in `_submit_and_wait`).
- Restored `exclusive=True` on `self.run_worker(...)` per the
contract STEP 6 spec.
- Added missing `isinstance(args, ParsedArgs)` PRE assertion to
`run_tui`. Required hoisting `from ratatoskr.cli import
ParsedArgs` out of TYPE_CHECKING — runtime import is fine (no
circular dependency: cli lazy-imports tui inside main; tui
imports cli unconditionally at module load).
- Added missing union-type PRE assertion to `_render_event_to_log`.
Contract amendments (precision):
- COMPOSE shape: RichLog `markup=False, highlight=False` (was True,
True). Explanatory comment in-line: bracketed labels like
[cancel_failed] would otherwise be interpreted+stripped as Rich
style spans; the post-Done Markdown rendering still works via
Markdown() Renderable.
- INV-002 reworded: identity rendered via dedicated
Static(id="identity") widget composed adjacent to Footer (Textual's
built-in Footer renders BINDINGS descriptions; a sibling Static
carries custom content in the same visual region).
- on_mount POST-003 amended to allow `agent_id is None` when
--session is used without --agent (matches INV-002 carve-out;
GET /sessions/{id} agent lookup is out of scope for this shell).
- run_tui happy_returns_zero_on_quit test description clarified:
App.run() is sync and can't be driven by Pilot, so run_tui's
wrapping behavior is tested via monkeypatch; the piloted Ctrl-D
exit path is covered separately by TestActionQuit.
Test fixes:
- footer_identity_visible_first_frame, footer_hint_flips_to_cancel,
streaming_first_ctrl_c_cancels: now query the Static(#identity) /
Static(#hint) widgets via `widget.render()` instead of asserting
on `app.sub_title` / `app.hint` internal state. The internal
state still exists (mirror), but the load-bearing assertion is
on visible widget content.
Meta-note from Volva: "TDD pass caught most stream/session/error
mechanics, but tested internal state where the contract required
visible Footer behavior, so same-model TDD would plausibly miss the
primary drift." Calibration shape continues across all four issues:
the post-TDD cross-model review consistently catches assert-boundary
+ observability-shape gaps the test-author's hypotheses don't cover
(#1: 4 findings, #2: 3, #3: 5, #4: 8).
164/164 tests GREEN; ruff clean; both contract drift checks clean.
|
||
|
|
dd89239c34 |
feat(tui): implement issue #4 contract via TDD; amend cli for TUI dispatch
47 contract-listed tests authored + GREEN (43 tui + 4 issue-#3 amendments). 164/164 tests GREEN suite-wide; ruff clean. Vertical-slice ordering: _render_event_to_log → _cancel_via_sse → CLI amendments → RatatoskrApp class + on_mount + on_unmount → on_input_submitted → _stream_turn_worker → action_interrupt + action_quit → run_tui. Two in-flight contract amendments caught during TDD: - PRE-002 of run_tui was `(args.session_id is None) != args.new` — backwards (fails when --session is set + new=False). Corrected to `bool(args.session_id) != bool(args.new)`. - RichLog created with markup=False (contract drafted markup=True). Rich interprets `[xxx]` as style markup and strips it, which would break every labeled stderr-style line ([cancel_failed], [done], [error], etc.). The post-Done Markdown rendering still works because rich.markdown.Markdown is a Renderable and doesn't need widget-level markup. Implementation notes: - _stream_turn_worker takes the log widget as a parameter passed from on_input_submitted. Querying #transcript from inside a Textual worker context fails with NoMatches; capturing the reference once at handler-time and threading it through the worker sidesteps the issue. - _spy_writes(monkeypatch) test helper records every RichLog.write call. RichLog's `.lines` Strip buffer isn't populated synchronously after .write() returns, which makes post-app-shutdown inspection unreliable; a write-spy gives deterministic verification. - SIGINT-mid-stream tests use custom httpx.AsyncByteStream subclasses with asyncio.Event gates to make timing deterministic without sleep-based polling — the cancel-respx-mock sets the gate event when its endpoint is observed, releasing the next SSE chunk. - _submit_and_wait test helper needs `await pilot.pause()` BEFORE the polling loop so the Input.Submitted message has a chance to dispatch. Discovered via debug-print trace; tracked in the test helper. CLI amendments (per issue #4 in-place amendment of #3 contract): - ParsedArgs.send_content: str | None (was str) - ParsedArgs.raw: bool added - _parse_args: --send default=None; empty-string still rejected; --raw added - main: branches on args.send_content — None → lazy `from ratatoskr.tui import run_tui` + run_tui(args); else asyncio.run(_amain(args)). Lazy import preserves issue #3 INV-001. Persistent-memory updated per the commit-along rule: tui module landed, recent-decisions entries for #4 (contract + Volva + TDD), next natural moves rotated to Volva code-review + manual smoke against the personal Worldtree (key landed in env.sh per infra-ops's earlier delivery). |
||
|
|
9717fb80e2 |
fix(cli): address Volva code-vs-contract drift (issue #3)
Volva code-review surfaced 5 findings against the TDD-passing implementation; all 5 addressed. Drift fixes (code): - Add `assert argv is None or all(isinstance(a, str) for a in argv)` at both `main` and `_parse_args` entry points (PRE-001 was unenforced). - `main` now catches `SystemExit` and returns `exc.code` verbatim — argparse's --help (SystemExit(0)) was escaping through main as an unhandled exception. Contract amended in-place to spell out the SystemExit-from-argparse-clean-exits passthrough in both `main` and `_parse_args` ERROR_ROUTING. New `help_exits_cleanly` test added per the contract amendment. - Add the PRE-001 union-type assert at `_render_event` entry — unmatched Event variants would have silently no-op'd. - `_run_turn` now awaits `cancel_task` in the `finally` block before returning. Under fast-stream + slow-cancel scenarios the `[cancel_failed]` line could miss being written before _run_turn returns, AND _amain could close the AsyncClient while the cancel POST was still in flight. `_cancel_and_log` swallows all errors per INV-009 so the await is safe. Test gap fix: - New `_FlushCountingIO` subclass counts flush() calls; `test_text_to_stdout_only` and `test_done_writes_newline_and_label` now assert `flush_count == 1` to verify INV-010 (per-chunk flush). Previously the tests would have passed even with flush removed. Meta-note carried in persistent-memory: TDD caught central behavior (stdout/stderr routing, exit-code mapping, create-session ordering, SIGINT idempotence); the cross-model code review consistently catches assert-boundary + observability-shape gaps across all three issues (#1: 4 findings, #2: 3 findings, #3: 5 findings). 118/118 tests GREEN; ruff clean; drift check clean. |
||
|
|
db27774c51 |
feat(cli): implement issue #3 contract via TDD
54 contract-listed tests authored + GREEN per the vertical-slice ordering (_parse_args → _render_event → _cancel_and_log → _run_turn → _amain → main). 117/117 tests GREEN suite-wide; ruff clean. The _run_turn race-loop is the load-bearing piece. Per iteration, the await on the next event is raced against sigint_event.wait() when NOT cancelling. Once SIGINT fires (with last_turn_id known), _cancel_and_log is spawned, cancelling=True flips, and subsequent iterations skip wait()-task creation entirely — the bug Volva flagged in contract review would otherwise busy-wake on the already-set event each iteration. Implementation notes: - _UsageErrorParser subclasses argparse.ArgumentParser and overrides error() to raise _ArgparseError instead of calling sys.exit; _parse_args catches and re-raises as UsageError per the contract's ERROR_ROUTING. - _GatedStream test helper (custom httpx.AsyncByteStream that pauses on asyncio.Event entries) makes SIGINT-mid-stream tests deterministic without sleep-based timing — gates release via side-channels (the cancel-mock sets an event when its endpoint is observed). - _sse_resp test helper wraps respx Response with the text/event-stream content-type, dedupes the boilerplate across the 13 _run_turn tests. - Strong-ref cancel_task local in _run_turn holds the fire-and-forget cancel task to suppress RUF006 / asyncio GC warning. One in-flight contract amendment during TDD: no_busy_loop_after_cancel test description originally said "exactly ONE wait()-shaped task" but the natural race-loop shape produces 2 (iter 1 raced w/ text, iter 2 raced w/ sigint → flipped cancelling; iter 3+ skipped). Amended to "TWO total wait() coroutines" with rationale; the busy-loop check is preserved (iter 3+ MUST skip). Persistent-memory updated per the commit-along rule: new module landed, recent-decisions log entries for #3 (contract + Volva paraphrase + TDD), next natural moves rotated to /volva-code-review on the implementation. |
||
|
|
d6f9327ec1 |
fix(sessions): address Volva code-vs-contract drift (issue #2)
Volva's code-spec review (thread 01KS4EKVKKGF) surfaced three findings
on the TDD-passing sessions module. All three addressed; one carries
a collateral contract amendment to keep INV-002 truthful.
1) drift: archived=item.get("archived", False) returned None for an
explicit "archived": null in the response. dict.get(k, default) only
fires the default when the key is absent — it does NOT default for
explicit-null values. The dataclass type is `bool` (not `bool | None`)
and INV-002 says explicit-null → False; the .get() form silently
violated both. Fixed: archived=item.get("archived") or False
(handles absent, null, false, and true cleanly).
INV-002 wording was the source of the bug — I introduced the
mis-spelled form during the Volva amendment round. Updated to spell
out the .get(default) foot-gun explicitly so future readers (and
future paraphrase rounds) don't fall back to the broken pattern.
2) test-gap: no test exercised explicit-null archived/tags. The
_list_item() helper had its own defaulting layer (tags=None →
["work"]) so a happy path test couldn't catch the underlying drift.
Added test_explicit_null_list_defaults using a raw dict to bypass
the helper. Catches the drift directly.
3) precision: message_count=body.get("message_count") could silently
default to None while POST-003 required it non-None. INV-001 prose
literally said "body['message_count']" (bracket access) so the
STEP 5 .get() was the contract's own internal inconsistency.
Aligned the code to bracket access (matches sibling required
fields like session_id) and amended STEP 5 + INV-001 to spell out
the strict semantics explicitly.
Volva's meta-note: "modest weight" — TDD caught the main surface;
this round caught a narrow Python .get() semantics edge that no
human reading would have spotted without explicit-null priors.
Still pulls real weight: that's the kind of bug that ships and
shows up months later when a server starts emitting null where
it used to omit a field.
63 tests GREEN (42 sse_client + 20 sessions + 1 boundary).
Ruff clean. Drift check still GREEN against the pinned issue body.
|
||
|
|
4ba143c563 |
feat(sessions): implement issue #2 contract via TDD
Implements docs/contracts/issues/2.contract.md. Two functions (create_session, list_sessions), two frozen dataclasses (SessionInfo, SessionPage), three exception types (AgentNotFound, InvalidCursor, SessionApiFailed). 19 contract-listed tests cover every TESTS: entry verbatim per the tracer-bullet vertical-slice ordering. SessionInfo uses one shape across both endpoints with origin- conditional defaults per INV-001 (create) and INV-002 (list). create- origin always sets list-only fields to (name=None, archived=False, tags=[]); list-origin reads them from the response item with absent/null treated as those same defaults — keeps the dataclass uniform without forcing callers to handle two types. Spotted an internal-inconsistency in the contract at TDD start — POST-003 and happy_create's test description still said "archived is None, tags is None" while the freshly-applied Volva amendment had moved INV-001 to (archived=False, tags=[]). Fixed in-place before writing any tests so the spec stayed coherent. SessionApiFailed.body truncates to <= 1024 bytes at construction, matching the SseConnectFailed / CancelFailed precedent from issue #1. No code shared with sse_client.py (convention-dependency only per issue #2's dependencies: block). 62 tests GREEN total (42 sse_client + 19 sessions + 1 boundary smoke). Ruff clean. No refactor pass — the two functions are ~25 LOC each with distinct error-routing branches that don't naturally share more than they already do. |
||
|
|
a6e6c1bbd8 |
contract(issue#2): amend per Volva paraphrase — defaults, query, metadata
Volva's contract paraphrase round (thread 01KS4DTCW8CV) surfaced five
ambiguities; three are real contract-text gaps and addressed here.
1) tags/archived/name defaulting was inconsistent across prose, INV-002,
and STEP 6. open_questions said "defaulting to sensible None/empty",
INV-002 said "populated from the response item shape", STEP 6 said
`item.get("tags", []) if "tags" in item else None` (which collapses
absent and explicit-null into the same None branch while letting an
explicit [] pass through). Tightened to: tags is always list[str]
defaulting to [] for absent/null/empty in list items; archived is
always bool defaulting to False; name remains str | None (the only
field where None is a meaningful value). create_session always sets
list-only fields to their fixed defaults (name=None, archived=False,
tags=[]) instead of None to keep the dataclass shape uniform.
2) The include_archived_query test said "URL has no include_archived
param OR explicit false". STEP 2 prescribes "ADD include_archived='true'
iff include_archived" — the OR-clause weakened the test against the
prescribed behavior. Tightened to: default (include_archived=False)
asserts NO include_archived param at all, not an explicit false.
5) metadata's populated semantics: INV-001 said "populated from the
201 response", STEP 5 said body.get("metadata", {}) — two valid
readings (trust the spec vs defensive default). Aligned to the
defensive shape: INV-001 + INV-002 now explicitly state "defaults
to {} when absent" as spec-drift tolerance.
Volva flags #3 (exception .body sensitivity — truncation reduces
size not sensitivity) and #4 (assert for runtime validation — Python
-O disables) reviewed and kept as-is. Both are intentional carryovers
from issue #1's precedent: exception .body is for caller debugging
bound to 1024 bytes (caller's responsibility to not log raw); assert
chosen for fast-path validation, trading -O robustness for normal-mode
speed.
Drift check still clean — amendments don't touch the pinned issue
body, so prd: hashes remain valid.
|
||
|
|
9df9bb8757 |
contract(issue#2): scaffold ratatoskr.sessions — create + list
Issue #2: ratatoskr.sessions covers the session-lifecycle endpoints needed by --send --new (POST /sessions) and the eventual TUI startup picker (GET /sessions). Two FN blocks (create_session, list_sessions) plus two shared frozen dataclasses (SessionInfo, SessionPage). complexity=low; estimated 150 LOC. Bundles both endpoints in one contract because they share the response- envelope shape — SessionInfo carries the union of POST-response fields (message_count) and list-item fields (name, archived, tags), with the origin-conditional fields defaulting to None. INV-001/002 spell out which fields come from which source so callers can rely on the discriminator. INV-006 refuses out-of-range limit (< 1 or > 200) client-side: spec §GET /sessions says the server returns 422; the client checks first so a 422 from this endpoint indicates server-side spec drift, not a client bug. INV-003 codifies the opaque-cursor discipline (spec §Pagination: "Cursors are opaque to clients — do not parse or construct them."). list_sessions threads next_cursor verbatim; never base64-decodes. Exception .body truncation to [:1024] inherited from issue #1's SseConnectFailed/CancelFailed precedent. First contract in this repo to carry a ## Out of scope H2. Future /volva-code-review consults will auto-resolve that section instead of needing --out-of-scope overrides. Six explicit exclusions: Bifrost binding (Worldtree #160), ephemeral/Saga sessions, single- session fetch, PATCH/DELETE mutation, history pagination, transparent multi-page iteration. Server retry/backoff is caller's policy. dependencies: lists issue #1 as a convention-dependency only — no code import; same API-consumption posture (caller-owned httpx client, async-native, frozen dataclasses, no Worldtree-source imports). prd: pinned to issue #2 body SHA-256 01fbbd52b6d90eb0 at 2026-05-21T04:45:06+00:00; scripts/contract_drift_check.py returns clean. |
||
|
|
c17af18351 |
fix(sse_client): address Volva code-vs-contract drift (issue #1)
Volva's code-spec review (thread 01KS4CP6ZZ1F) surfaced four code-vs-
contract drift findings on the TDD-passing implementation. All four
addressed here; no contract amendments required.
1. _iter_events fell off the end of aiter_sse() normally on clean EOF
before any Done/Error/Cancelled. Per INV-001 the iterator MUST NOT
raise StopAsyncIteration before a terminal event unless the HTTP
connection drops, in which case it raises SseConnectionDropped.
Clean EOF before terminal is the same semantic — the stream ended
without delivering its contracted invariant. Fix: track terminal_seen
inside _iter_events; after the async-for completes, if not seen,
raise SseConnectionDropped(last_seen_sse_id=...). Two new tests:
test_clean_eof_before_terminal (one text then EOF) and
test_zero_event_eof (empty stream — last_seen_sse_id is None).
2. SseConnectFailed and CancelFailed both store .body without
truncation; ERROR_ROUTING specifies resp.read()[:1024]. Fix
truncates in each exception's __init__ before storing. New test
test_connect_failed_body_truncated (503 + 5000-byte body → 1024)
and test_cancel_failed_truncates_body (same shape on cancel).
3. _parse_sse_id PRE-001 specifies `assert isinstance(raw, str)`.
Previous code called raw.split(":") directly, which raises an
incidental AttributeError on non-str inputs — not the contracted
precondition path. Fix adds the assert. New test
test_non_string_input covers int and None.
4. Cancel ERROR_ROUTING said httpx.HTTPStatusError other status →
CancelFailed, but no test exercised the branch. test_cancel_failed_
truncates_body covers this (above) — single test double-covers
findings 2 and 4.
43 tests GREEN (42 sse_client + boundary smoke); ruff clean.
Meta-note from Volva: TDD caught the main happy/adversarial SSE shape,
resume header/body, turn-id flip, and cancel races. The remaining
misses were "negative space" cases (clean premature EOF, exception
payload truncation, untested generic cancel branch). Calibration
evidence that cross-model review pulls weight on what same-model
TDD's hypothesis-space doesn't probe.
|
||
|
|
02f2a04b37 |
feat(sse_client): implement issue #1 contract via TDD
Implements docs/contracts/issues/1.contract.md. Four entry points
(stream_turn, reconnect_turn, cancel_turn, _parse_sse_id) + nine
typed Event variants + ten domain exceptions. 37 tests covering
every TESTS: entry verbatim, plus the boundary smoke test still
passes.
Tracer-bullet ordering per the contract's per-FN tracer tags:
_parse_sse_id (foundation; happy_simple) → stream_turn
(happy_one_text_done) → reconnect_turn (happy_resume_from_seq_3) →
cancel_turn (happy_cancel). Each FN's tracer went RED then GREEN
before its other tests landed.
Shared SSE-iteration logic (INV-002 sse_id presence + INV-003
turn_id stability + terminal-break) lives in private _iter_events
helper. expected_turn_id=None gives stream_turn's "establish from
first event" semantics; expected_turn_id=N gives reconnect_turn's
"first event is already a flip-candidate" semantics — the
two-entry-point distinction Volva surfaced during the paraphrase
round.
A few implementation choices worth recording:
- _parse_sse_id uses a `^-?\\d+$` regex pre-check to reject any
whitespace before int() is called. Python's `int(" 3 ")` silently
strips, which would have made the trailing_whitespace adversarial
test pass for the wrong reason.
- The connection_drop test uses a custom httpx.AsyncByteStream
subclass (_DropAfter) that yields chunks then raises
RemoteProtocolError mid-stream. respx alone can't simulate
mid-stream HTTP errors.
- ToolResult.result and ToolStart.arguments are typed as Any
because the server's tool wire shape varies per tool; the spec
doesn't pin a generic schema.
- Boundary smoke test (no core.* / worldtree.* imports under
src/ratatoskr/) still GREEN — INV-005 holds.
Also: one E501 line-length fix in test_no_worldtree_imports.py
that ruff flagged once the new tests pulled it into scope.
|
||
|
|
1526f0bc8e |
contract(issue#1): amend per Volva paraphrase — body, id range, INV-003
Volva's contract paraphrase round (thread 01KS4B3B0Y62) surfaced three
real contract-time ambiguities — addressing each here before applying
ready-for-agent.
1) reconnect_turn body was a punt. STEP 2 literally said "json={'content':
''} OR with no body (TBD per spec — confirm during implementation)".
The spec §Reconnect flow example shows POST with Content-Type:
application/json and a body shaped {"content": "..."} — the wire schema
requires content; the server identifies the resume target via the
Last-Event-ID header and does NOT re-process content (spec line 732:
"agent's tools and LLM call run exactly once regardless of disconnects/
reconnects"). reconnect_turn now takes content: str explicitly; STEP 2
sends json={"content": content}. Caller convention: pass the original
content sent to stream_turn. POST-002 added to assert byte-for-byte
body shape; new test body_threads_content covers it.
2) _parse_sse_id allowed turn_id and seq ≥ 0 — too loose. Spec §SSE id
format line 705 says seq starts at 1 (resets per turn); turn_id is
from SQLite turns.id (autoincrement, ≥1). Tightened POST-001 to require
both ≥1; STEP 5 raises ValueError on either < 1. Test happy_zero_seq
flipped to adversarial zero_seq; added zero_turn_id and negative_seq.
INV-002 tightened to reflect the same range.
3) INV-003 (TurnIdFlip) had a subtle wording gap between stream_turn
(first event ESTABLISHES turn_id; cannot be a flip) and reconnect_turn
(expected turn_id parsed FROM last_event_id BEFORE connect; first event
is already a flip-candidate). Volva noticed the reconnect test said
"first event was not yielded" while stream_turn semantics depend on
the first event being yielded unconditionally. Spelled out both entry
points in INV-003 as a numbered sub-list. reconnect_turn STEP 4 and
test turn_id_flip_on_first_event reworded to match.
Volva flags #3 (MalformedSseId-vs-ValueError split) and #5 (exactly-
one-terminal as server-assumed, not client-verified) reviewed and kept
as-is — both intentional. Drift check unchanged: amending the contract
does not touch the pinned issue body, so prd: hashes remain valid.
|
||
|
|
999b0b4765 |
contract(issue#1): pin sse_client to gitea issue + seed default labels
Convert the sse_client contract into an issue-scoped contract bound to the freshly-filed gitea issue #1. Frontmatter migrates from module-shape (module:/purpose:) to issue-shape (target_module:/scope:/prd:) per CONTRACT-FORMAT §2.1.I. The prd: block pins to issue #1's body SHA-256 (abcbc49467e86f1d at 2026-05-21T03:57:37+00:00); drift check verifies the pin matches the live issue body. scripts/contract_drift_check.py needs pyyaml; added to [dev] in pyproject.toml. Without it the drift check (and the contract parser) fail with ModuleNotFoundError — that's a scaffold hole I'd hit again on a fresh checkout. Also seed 17 default labels on gitea via tea so issue tracking has a working vocabulary out of the gate. Five buckets: Sleipnir gating (ready-for-agent, blocked-needs-contract, blocked-needs-dependency), triage (needs-triage, needs-architect-decision, needs-info), type (bug, enhancement, task, documentation), resolution (duplicate, wontfix, invalid), Ratatoskr-specific area (sse-client, tui, cli, observability). Labels are gitea-side state — not in this commit. Known: contract_parser.py --validate ERRORs on the issue-scoped frontmatter because the parser is v2.0-shape. CONTRACT-FORMAT §2.1.L H10 explicitly marks parser kind-aware validation as a Brokkr-side follow-up. Parser is a canonical-synced file so we do NOT patch it locally (would drift from corviduo-project-template). |
||
|
|
72d477f516 |
contract(sse_client): first contract — SSE consumer, reconnect, cancel
The natural smallest unit to TDD against per design-brief §3. Bundles
stream_turn + reconnect_turn + cancel_turn + private _parse_sse_id into
one module because the SSE-resume flow is structurally coupled — cancel
needs the turn_id parsed from the SSE wire id:, reconnect re-uses the
same parsed SseId, and stream_turn is what produces them.
Hard invariant INV-002 forces every yielded Event to carry a parsed
SseId(turn_id, seq) lifted from the composite {turn_id}:{seq} id:
wire field. This closes the foot-gun design-brief §3 explicitly names:
hand-rolled data:-only parsing silently drops the id: line and breaks
SSE-resume invisibly.
v2.1 format used; test categories adversarial/scenario/trace flagged
warn-only by the v2.0 parser (CONTRACT-FORMAT §2.1.L H10 is a known
Brokkr-side parser follow-up). FN block list parses cleanly.
Scaffold also verified at this commit: uv pip install -e ".[dev]"
resolves clean against the lockfile (now committed), and the boundary
smoke test (tests/test_no_worldtree_imports.py) passes.
|
||
|
|
9703eb2b6b |
init: seed Ratatoskr from corviduo-project-template + ship v0 scaffold
Worldtree Conversation API debug TUI. Multi-pane observability dashboard: chat transcript + persona/Vili affect log + tool events + admin events + Bifrost state + tool inventory + (opt-in) raw server log. Design locked at docs/design-brief.md (originated as brokkr-smithy/docs/ratatoskr-design-brief.md). Operator-locked decisions: - Textual application-shell framework (multi-pane dashboard, not REPL). - Separate repo + separate dev team (no Worldtree-source imports). - httpx-sse for SSE consumption (reference Python SSE-resume impl). - Triple version-skew mitigation: spec-pin in pyproject.toml + recorded SSE snapshot tests + conformance smoke. Initial pin: Worldtree v0.19.0 at 55101e909abcd2219833266b6f905c5bc956e0f0. - Persona pane: label-don't-refuse PII posture. - Server-log pane: opt-in via --server-log <path>. - Two-stage Ctrl-C (cancel then exit). - Markdown rendering default-on; --raw opt-out. In the box: - docs/design-brief.md — the locked design with full rationale. - docs/SPEC-PIN.md — Worldtree spec pin + bump procedure. - docs/conversation-api-spec.md + docs/conversation_api.contract.md — vendored Worldtree spec snapshots at the pinned SHA. - pyproject.toml — Python 3.12, hatchling, uv-managed, deps locked. - src/ratatoskr/ — stub package (cli.py raises NotImplementedError). - tests/test_no_worldtree_imports.py — boundary smoke test PASSING. - tests/snapshots/README.md — recording convention for SSE snapshot tests. Not in the box yet: - Gitea remote (operator/infra-ops to register at vh/ratatoskr). - Implementation — the dev team owns this; design brief is the spec. Origin: althing thread 01KS3R34XD3N6HMK91VXESHGW7 (worldtree-dev → brokkr-smithy-dev, 2026-05-20). Volva consulted via thread 01KS3VF6W33N3V5FNMGQ91YNVD. |