* feat: robust plan quality gate, iterative refinement, and amend UX
Plan agent output from weak models often produced garbage (11-char plans
that echo the prompt). Two fixes:
1. Quality validation (_validate_plan) checks length, section structure,
echo detection, and refusal patterns. Fails trigger one automatic
retry with a coaching message injected into the agent's existing
conversation, preserving all prior exploration context.
2. Iterative feedback loop — user feedback at plan review re-runs the
plan agent via _refine_plan() instead of appending text to the tool
result. Up to 5 refinement rounds. The plan file path is always
included in the tool result so the outer model knows where it lives.
UI improvements:
- Web: Reject button dynamically becomes "Amend" (amber) when feedback
is typed. Key hint badges (Esc/Enter) on plan buttons. Main input
disabled during review. Light-theme contrast fix via --on-color var.
- CLI: Prompt shows all three actions (approve/amend/reject).
- Bridge: Race condition fix — clear pending entry before HTTP POST so
sequential plan reviews from the refinement loop aren't skipped.
15 new tests covering validation, retry, and refinement.
* fix: address PR 41 review feedback
- Escape key in plan dialog now mirrors the Amend button: if feedback is
typed, Esc sends the feedback (amend); if empty, Esc rejects. Previously
Esc always hard-coded "reject", discarding typed feedback.
- Coaching message for plan retry now says "should include at least two of"
instead of "MUST include these", matching the actual validation rule
(_MIN_PLAN_SECTIONS = 2).
* feat: render plan inline in chat after approval
After the plan review dialog closes, the plan content is now rendered
as a collapsible inline block in the chat stream — styled with a
status header (approved/rejected/amending), markdown-rendered body,
and feedback note when amending. Uses the same makeCollapsible pattern
as tool output blocks.
* fix: prevent plan approval hang when inline render fails
The authFetch call that unblocks the server must fire before the
cosmetic inline plan rendering. Previously _addInlinePlan ran first
and any JS error (e.g. from renderMarkdown) prevented the API call,
leaving the session thread blocked forever.
- Move authFetch before _addInlinePlan
- Wrap _addInlinePlan in try-catch
- Guard against empty content
- Only auto-collapse plans longer than 12 lines
* fix: address PR 41 review feedback (round 2)
- Max refinement rounds no longer implicitly approve: the loop now
shows the final plan for explicit approve/reject before proceeding.
Previously exhausting 5 rounds silently accepted the last revision.
- Plan inline block: correct aria-label from "Tool output" to
"Plan content" when makeCollapsible is applied.
- XSS concern (not applicable): renderMarkdown is used for all
assistant messages — plan content follows the same trust model.
- Test loop concern (acknowledged): refinement tests verify component
logic; full _execute_tools integration would require extensive
mocking for marginal coverage gain.
* feat: thinking spinner + inline plan hardening
* fix lint
* ci: add GitHub Release creation on tag push
* refactor: rename plan tool to create_plan
Rename plan → create_plan to resolve cross-provider tool selection
failures. Models consistently treated "plan" as a reasoning concept
rather than a callable tool. The new name is an unambiguous verb+noun
action. Also rename the parameter from prompt → goal for clarity,
add web_search to the default system prompt tool patterns
* feat: eval harness improvements inspired by autoresearch patterns
Major enhancements to turnstone-eval:
- Per-test timeout (--test-timeout, default 300s) and suite timeout
(--suite-timeout) prevent stuck runs from blocking the suite
- Fast-fail skips remaining runs after ceil(n/2) consecutive zeros
- Summary table with colored PASS/WEAK/FAIL and append-only TSV output
- Progress reporting with running pass rate, token count, and ETA
- Parallel test execution via ProcessPoolExecutor (--parallel N)
- Per-role model assignment: test/optimizer/observer can use different
models and providers (--optimizer-model, --observer-model, etc.)
with auto-detection from base URL
- Improved optimizer and observer system prompts with structured
failure-mode diagnosis, keep/discard rules, and trend analysis
- Fixed token counting (prompt tokens use last-turn value, not sum)
- Added math-calculation and web-search-query test cases
- Fixed multi-file-edit test (both files now contain the target string)
* fix: address Copilot review feedback
- Revert prompt token counting to sum (reflects billed usage)
- Add tool_args to fast-fail skipped run dicts for schema consistency
- Align approval_label with func_name ("create_plan")
- Add timeout to future.result() in parallel path (test_timeout + 30s)
- Document thread-leak trade-off on serial timeout path
* feat: add vision/image support to read_file tool
read_file now detects image files (PNG, JPEG, GIF, WebP, BMP, TIFF, ICO)
and returns base64-encoded content parts for vision-capable models.
Non-vision models receive a text description instead. A new
supports_vision flag on ModelCapabilities gates the feature, with
config.toml [models.*.capabilities] overrides for local models
(vLLM, llama.cpp, NIM).
* fix: address PR review feedback
- Discard _read_files on no-vision OSError path, include exception detail
- Discard _read_files on oversized image error (not a successful read)
- Validate capabilities type from config.toml (reject non-dict)
- Clarify tool description re: vision behavior and offset/limit scope
- Remove unused os import in tests, fix import sort order
- Handle list content (image tool results) in eval.py tool result loop
* Normalize session_id into ws_id as sole persistent identity
Eliminate the separate session_id concept. The workstream ID (ws_id) is
now the single identity used for both real-time routing and conversation
persistence, removing a layer of indirection that was 1:1 in practice
and buggy on resume (stale pointers, orphaned rows).
Schema changes (migration 006):
- Drop sessions table; add alias/title columns to workstreams
- Rename conversations.session_id → ws_id
- Rename session_config table → workstream_config (ws_id column)
- Data migration remaps existing conversations to ws_id
Storage/API renames:
- register_session → register_workstream (already existed, merged)
- save_message/load_messages now keyed by ws_id
- resolve_session → resolve_workstream
- ChatSession.session_id property → ws_id
- ChatSession.resume_session() → resume()
- resume_session field → resume_ws
- SessionResumedEvent → WorkstreamResumedEvent
- /api/sessions → /api/workstreams/saved
- /sessions slash command → /workstreams
- --session-retention-days → --retention-days
Channel eviction recovery simplified: reuses old ws_id directly
instead of get_session_id_by_ws() reverse lookup.
* Fix Copilot review feedback: stale session wording in docs, regenerate OpenAPI spec
- docs/channels.md: "resumes the session" → "resumes the workstream",
"Session resumed:" → "Resumed:", "old session was pruned" → "old
workstream was pruned"
- docs/api-reference.md: "Each session object" → "Each saved workstream
object", field descriptions updated, removed stale node_id field
- sdk/typescript/openapi-server.json: fully regenerated from Python
models — removes all stale session_id properties from WorkstreamInfo,
DashboardWorkstream, CreateWorkstreamResponse schemas
The developer message role is not supported by all chat templates
(e.g. Qwen). Switch to the standard system role which is universally
supported by OpenAI-compatible APIs.
* Add call_id routing for streaming tool output during parallel execution
Thread call_id through tool_info, approve_request, and tool_result SSE
events so the browser can route streaming output chunks and final results
to the correct tool div when multiple bash tools run in parallel.
Server: include call_id in serialized approval items and tool_result events.
Protocol: add call_id to on_tool_result signature (session, cli, eval, server)
and ToolResultEvent dataclass; pass through MQ bridge.
Client: set data-call-id on tool divs, match by call_id in appendToolOutputChunk
and appendToolOutput with func_name fallback; extract makeCollapsible
helper; use CSS.escape for querySelector safety; fix replayHistory
\\n typo and missing keyboard accessibility on collapsed output.
Bridge: fix pre-existing bug using "name" instead of "func_name" for
auto-approval matching; include call_id in _build_history for replay.
Also adds on_tool_result calls to write_file and edit_file exec methods.
* Update docs/tools.md
Replace subprocess.run() with Popen for bash tool execution, streaming
stdout line-by-line through a new on_tool_output_chunk callback. Web UI
renders chunks incrementally with a pulsing amber border indicator.
Core:
- Add on_tool_output_chunk(call_id, chunk) to SessionUI protocol
- Rewrite _exec_bash() with Popen, process-group kill via
start_new_session + os.killpg, background stderr drain thread,
threading.Event-based timeout detection
- Guard UI callback with contextlib.suppress so errors don't
interrupt output collection
Server/CLI/eval:
- Add tool_output_chunk SSE event type in WebUI
- No-op implementations in TerminalUI, BackgroundTerminalUI, SilentUI
MQ:
- Add ToolOutputChunkEvent to mq/protocol.py and _OUTBOUND_REGISTRY
- Handle tool_output_chunk in bridge._handle_ws_event
Web UI:
- Add appendToolOutputChunk() with call_id-keyed DOM elements,
inner auto-scroll, ARIA attributes, and empty chunk guards
- Fix appendToolOutput() streaming cleanup using adjacency matching
- Make collapsed output keyboard-accessible (tabindex, role, keydown)
- Improve stripAnsi() to handle CSI, OSC, and two-byte escapes;
use it consistently in replayHistory, addInfoMessage, addErrorMessage
- Add .tool-output-stream CSS with soft pulse animation, mobile
max-height cap, and consolidated prefers-reduced-motion support
Docs & diagrams:
- Document tool_output_chunk SSE event in api-reference.md
- Update SessionUI protocol (14 methods) in architecture.md
- Update Phase 3 execution flow in tools.md
- Add on_tool_output_chunk to 03-core-engine-classes.puml
- Update 04-conversation-turn.puml, 05-tool-pipeline.puml
- Add ToolOutputChunkEvent to 06-mq-protocol.puml
- Add to event list in 07-message-routing.puml
- Regenerate all 5 affected PNG diagrams
* Quality overhaul: code tooling, CI/CD, architecture diagrams, UI redesign, and legacy cleanup
- Add ruff (lint+format) and mypy (strict) with zero errors across 37 source files
- Add GitHub Actions CI (lint, typecheck, test matrix 3.11/3.12/3.13) and PyPI publish workflow
- Create 12 PlantUML architecture diagrams with PNG renders covering all subsystems
- Refresh README and docs with badges, diagram links, and current descriptions
- Refactor test_server_live.py with mock streaming helpers for deterministic CI testing
- Update dependencies to current versions (openai>=2.24, httpx>=0.28, redis>=7.2)
Console dashboard:
- Move state indicators from top cards to fixed bottom status bar with cluster metrics
- Replace flat 50-node list with hostname-prefix grouped nodes (expand/collapse, up to 1000)
- Apply "Instrument Panel" visual redesign: IBM Plex Mono + Outfit fonts, warm amber accent,
LED glow state indicators, deep charcoal surfaces, WCAG AA contrast compliance
- Add render cache, stale indicator, active filter highlight, loading states
Server web UI:
- Apply matching Instrument Panel aesthetic for visual consistency with console
- Fix branding (pcode → turnstone), extract inline styles to CSS classes
- Rename pcode localStorage keys and history state to turnstone
Legacy cleanup:
- Remove persona-model-specific --persona flag and /persona slash command
- Remove model_identity from chat_template_kwargs (vLLM-specific mechanism)
- Refactor plan agent to use standard developer message instead of model_identity
- Remove dead code (unused date/has_tools variables, noqa suppressions)
* Fix CI typecheck: add mypy overrides for optional sympy/numpy imports
The math sandbox optionally imports sympy and numpy at runtime (try/except
ImportError). In CI these packages are not installed, so mypy raises
import-not-found rather than import-untyped. Add mypy overrides to
ignore missing imports for these optional dependencies.
* Fix Copilot review findings: ARIA role, status bar cache, and pulse opacity
- Change #node-table from role="tree" to role="list" and group elements
from role="treeitem" to role="listitem" (proper ARIA semantics)
- Include currentView and currentFilter.state in renderStatusBar cache key
so active pill highlight updates when switching views
- Align pulse animation to 0.35 opacity (already applied in CSS)