57fdc48e20ed842e7d00a86ea6e1800bb92e2bd0
15 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c7016f23a6 |
feat(#19): ephemeral-template (Echo) session creation
create_session could only mint foundational sessions; an ephemeral template
(agent_id="echo") returned 422 ephemeral_requires_config because ratatoskr never
sent the required config block — Echo was uncreatable, surfacing as an opaque
session_api_failed at the CLI. Thread an opaque, role/model-agnostic config
passthrough through the create path so Echo sessions are creatable.
- sessions.py: create_session(config=...) verbatim passthrough (PRE-004 Mapping /
PRE-005 config-xor-bifrost guards); SessionInfo gains kind + config, captured
defensively (.get) on both create and list.
- cli.py: --system-prompt flag builds config={"system_prompt": ...} (validation:
non-empty, requires --new+--agent, xor bifrost); _amain surfaces kind=; the
--whoami renderer now reads allowed_roles/default_role (was reading the dead
allowed_models/default_model) and tolerates a malformed capabilities shape.
- contract #2 amended (Amendment 2026-07-18); Heid-panel contract-reviewed +
diff-scoped bug-hunted (one whoami null-join gap found + fixed).
Canonical grounding: config.role, never config.model (worldtree-dev althing
01KXT976NN91DRBZBPXNZ2BVZR; ADR-0012 role cutover). Verified end-to-end against
the live v0.16.2 target. TDD across create + CLI; full suite green (534).
Closes #19.
|
||
|
|
3f3a9f7b0f |
refactor(cli)!: remove deprecated textual TUI; web console is the interactive surface
The textual TUI (tui.py) is superseded by the web console (ratatoskr-web) and is removed per the no-backwards-compat rule. The `ratatoskr` command stays as a headless client: --send / --whoami / --characters / --set-persona-pad / --seed-first-message still work; invoking it with no --send now returns a usage error (rc 10) instead of launching the TUI. Removed: src/ratatoskr/tui.py, tests/test_tui.py, the textual + textual-dev deps, and cli.py's run_tui launch path. cli.py's shared exports (USER_AGENT, ParsedArgs, formatters) stay — web/entrypoint.py and tier3.py depend on them. BREAKING CHANGE: the interactive `ratatoskr --agent X` TUI is gone; use the web console (ratatoskr-web) for interactive debugging, or --send for scripted. Verified: full suite 520 passed; ratatoskr --help exit 0; no-send -> rc 10; web/provider/tier3 import clean; textual absent from the lockfile. |
||
|
|
e643d38f58 |
fix: persona_state SET body → canonical {pad:{pleasure,arousal,dominance}} + re-vendor Tier-3 prose (v0.19.7)
worldtree-dev landed the Tier-3 persona/memory/persona_state prose docs
(c9e59ec) — shapes that serialize as freeform Any in the OpenAPI, so the
prose markdown is their source of truth. Re-vendored docs/conversation-api-spec.md
(tolerate_drift markdown pin; worldtree-spec-rev 879cefe→c9e59ec).
Consumer alignment: --set-persona-pad / _set_persona_probe was building
{pad:[list]}, but the canonical POST /sessions/{id}/persona_state body (#317)
is {pad:{pleasure,arousal,dominance}} (named dict). Aligned the probe to the
named dict + a len!=3 guard; updated contract #2's note, the set_persona_state
docstring, and the tests. The set_persona_state wrapper was already correct
(freeform pass-through) — only the CLI probe's body construction drifted.
Suite 602 green. (Also this session: heid-code-review on the #347 slice
returned unanimous zero drift across all three panel arms.)
|
||
|
|
6bf2a84ccd |
feat: authored-history-write consumer side (Worldtree #347) — v0.19.6
Consumer side of Worldtree's #347 authored-history-write (the SillyTavern first-message primitive), shipped via direct in-session TDD: - write_authored_history (POST /sessions/{id}/history): v1 author=assistant, effects=none, per-session idempotency; body server-pinned (AuthoredWriteRequest extra=forbid) so null effects/claimed_original_at are omitted; 200 replay / 201 fresh both return the AuthoredTurnResponse dict. - AuthoredHistoryUnavailable: the hide-existence 404 (feature-absent / ungranted / session-absent, indistinguishable by design — INV-347-1) raised DISTINCT from SessionApiFailed so callers branch feature-absent and never capability-probe. - get_session_messages (GET /sessions/{id}/messages): un-deferred as the seed read-back — confirms a seed renders as a normal role=assistant turn (model-invisible provenance). - --seed-first-message probe: create session -> seed -> read-back; a 404 reports a benign feature-absent result (exit 0), never a capability-probe. Contract #2 amended (2 FNs, validated OK). 19 new tests (12 wrapper + 7 cli), suite 601 green. Coverage-map re-converged: REST 19/41 (the #347 route + the messages read-back close the one gap the 2.3.0 re-vendor opened). Live-proof pending the session.history.write grant (requested infra-ops). |
||
|
|
af07a2329a |
feat(#2): Tier-2 — transient characters + persona-state write; audit converges
v1 coverage-audit: the last in-scope client I/O points. The audit now CONVERGES — REST 17/40 covered with zero in-scope gaps (23 excluded-by- design), SSE 11/11, Bifrost planes 8/8. - sessions.py: list_character_models / create_character / get_character_state / delete_character (#161, character.read/write) + set_persona_state (POST /sessions/{id}/persona_state — freeform body, unpinned in the frozen surface). 200/201 -> dict (or None on 204), off-status -> SessionApiFailed. - cli.py: two one-shot probes (mirror --whoami): --characters (CRUD lifecycle report) + --set-persona-pad "p,a,d" (requires --session). New ParsedArgs.characters/set_persona_pad + probe mutual-exclusion. - Contract #2 amended (5 FNs) + validated. TDD: 7 wrapper + 5 cli tests. Suite 573 green; touched code ruff-clean. - Char read side live-proven (GET /models/available-for-characters -> 200). Coverage-map: convergence frontier CLOSED — scope-A "done" (every frozen I/O point classified) is met; ratatoskr cuts v1 when Worldtree tags 1.0. |
||
|
|
387ac4ab2c |
feat(#2): consume GET /me + GET /capabilities via --whoami one-shot
v1 coverage-audit slice (capabilities+me). Both endpoints had no caller; add them as cheap boot-time debug primitives. - sessions.py: get_me (GET /me — identity/whoami) + get_capabilities (GET /capabilities — Echo ephemeral-template discovery). Mirror get_persona_state: 200 -> parsed dict verbatim, non-200 -> SessionApiFailed. Freeform dicts (frozen OpenAPI types both as objects). - cli.py: new --whoami one-shot mode (mirrors --send). Fetches both, prints an identity + capabilities report, exits. Standalone probe: mutually exclusive with --send/--session/--new/--agent; opens no session. New ParsedArgs.whoami field + main() dispatch. - Contract #2 amended (2 FNs) + validated. TDD: 5 wrapper tests + 5 cli tests (validation + mode + error). Coverage map: REST 9/40. Suite 538 green; touched code ruff-clean. Audit note: /capabilities is the Echo ephemeral-template discovery endpoint, not a generic server-caps endpoint (coverage-map framing corrected). TUI-surfacing of /me + /capabilities deferred. |
||
|
|
5c1b9816d4 |
feat(#6): startup session picker for bare TUI mode
v1 coverage-audit slice b2. The audit found list_sessions had no caller — the startup session picker (design-brief §4) was never built; bare TUI mode was a hard usage error. Add SessionPickerApp (mirrors AgentPickerApp) and resolve bare mode in _resolve_then_run. - Bare TUI mode (no --session/--new) now valid → session picker. Resolution: 0 sessions -> [no_sessions] exit 14 (resume-only per §4 "no in-app creation, --new only"); exactly 1 -> auto-resume (§4 "picker only when >1"); >=2 -> SessionPickerApp -> resume pick (Esc/Ctrl-D -> exit 0). - cli._parse: bare TUI valid; --send still requires one flag; --agent forbidden in bare mode. run_tui PRE-002 xor -> mutually-exclusive. - Contract #6 amended (SessionPickerApp + bare-mode resolution) + validated. TDD: 3 picker pilot tests + 5 resolution tests + 3 cli validation tests. Suite 528 green; touched code ruff-clean. Design note: bare + 0 sessions errors (honors §4's no-in-app-creation clause); the friendlier auto-fall-through-to-new is deferred pending operator preference. |
||
|
|
0bebad74ad |
feat(#17): CLI Bifrost-bind trigger (slice 3a of the INV-008 lockstep)
Slice 3a of issue #17 — the CLI surface of the bind trigger (TUI + web follow, INV-008 lockstep). ratatoskr can now self-drive a bound session from the CLI: - New flags: --bifrost-plane {memory,affect} (dev shortcut -> endpoint_for_plane over --bifrost-host / RATATOSKR_PROVIDER_VISIBLE_HOST) and --bifrost-url (the direct HTTPS/prod endpoint, bypassing the plane shortcut). Mutually exclusive; a binding is a session-CREATE concern (forbidden with --session). - Consumer key resolved from RATATOSKR_BIFROST_CONSUMER_KEY only (the privileged handshake identity — never a CLI flag, distinct from the canary WORLDTREE_API_KEY). - _amain threads bifrost + consumer_key into create_session and routes the bind failures: BifrostConsumerKeyMissing -> exit 22; BifrostHandshakeFailed -> exit 23 with the 401-scoping hint ("use the consumer key, not WORLDTREE_API_KEY") keyed on bifrost_error == bifrost.auth_rejected. - Bound-state indicator on success: ". bifrost: status=bound plane=... endpoint=..." — shows WHICH identity/endpoint bound, not a bare boolean. Also fixes a pre-existing test-isolation bug: test_no_textual_import did a live importlib.reload(ratatoskr.cli) that mutated the shared module in place, breaking class identity (isinstance / pytest.raises) for every test after it. The real check is the static source grep; the reload was vestigial and is removed. 9 new CLI bind tests; full suite 462 green; ruff clean (no new mypy errors). |
||
|
|
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"`. |
||
|
|
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). |
||
|
|
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). |
||
|
|
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. |