* v2 architecture phase 1 * feat(engine): Phase 2 — execution loop, capability system, thread runtime Add the core execution engine to ironclaw_engine crate: - CapabilityRegistry: register/get/list capabilities and actions - LeaseManager: async lease lifecycle (grant, check, consume, revoke, expire) - PolicyEngine: deterministic effect-level allow/deny/approve - ThreadTree: parent-child relationship tracking - ThreadSignal/ThreadOutcome: inter-thread messaging via mpsc - ThreadManager: spawn threads as tokio tasks, stop, inject messages, join - ExecutionLoop: core loop replacing run_agentic_loop() with signals, context building, LLM calls, action execution, and event recording - Structured executor (Tier 0): lease lookup → policy check → effect execution - Tool intent nudge detection - MemoryStore + RetrievalEngine stubs for Phase 4 - Full 8-phase architecture plan in docs/plans/ - CLAUDE.md spec for the engine crate 74 tests passing, zero clippy warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): Phase 3 — Monty Python executor with RLM pattern Add CodeAct execution (Tier 1) using the Monty embedded Python interpreter, following the Recursive Language Model (RLM) pattern from arXiv:2512.24601. Key additions: - executor/scripting.rs: Monty integration with FunctionCall-based tool dispatch, catch_unwind panic safety, resource limits (30s, 64MB, 1M allocs) - LlmResponse::Code variant + ExecutionTier::Scripting - Context-as-variables (RLM 3.4): thread messages, goal, step_number, previous_results injected as Python variables — LLM context stays lean while code accesses data selectively - llm_query(prompt, context) (RLM 3.5): recursive subagent calls from within Python code — results stored as variables, not injected into parent's attention window (symbolic composition) - Compact output metadata between code steps instead of full stdout - MontyObject ↔ serde_json::Value bidirectional conversion - Updated architecture plan with RLM design principles 74 tests passing, zero clippy warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): RLM best-practices enhancements from cross-reference analysis Cross-referenced our implementation against the official RLM (alexzhang13/rlm), fast-rlm (avbiswas/fast-rlm), and Prime Intellect's verifiers implementation. Key enhancements: - FINAL(answer) / FINAL_VAR(name): explicit termination pattern matching all three reference implementations. Code can signal completion at any point, not just via return value. - llm_query_batched(prompts): parallel recursive sub-calls via tokio::spawn, matching fast-rlm's asyncio.gather pattern and Prime Intellect's llm_batch. - Output truncation increased to 8000 chars (from 120), matching Prime Intellect's 8192 default. Shows [TRUNCATED: last N chars] or [FULL OUTPUT]. - Step 0 orientation preamble: auto-injects context metadata (message count, total chars, goal, last user message preview) before first code step, matching fast-rlm's auto-print pattern. - Error-to-LLM flow: Python parse errors, runtime errors, NameErrors, OS errors, and async errors now flow back as stdout content instead of terminating the step, enabling LLM self-correction on next iteration. Only VM panics (catch_unwind) terminate as EngineError. 74 tests passing, zero clippy warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(engine): update architecture plan with RLM cross-reference learnings Comprehensive update after cross-referencing against official RLM (alexzhang13/rlm), fast-rlm (avbiswas/fast-rlm), Prime Intellect (verifiers/RLMEnv), rlm-rs (zircote/rlm-rs), and Google ADK RLM. Changes: - Mark Phases 1-3 as DONE with commit refs and test counts - Add "Key Influences" section documenting all reference implementations - Phase 3: full table of implemented RLM features with sources - Phase 3: "Remaining gaps" table with which phase addresses each - Phase 4: expanded with compaction (85% context), rlm_query() (full recursive sub-agent), dual model routing, budget controls (USD, timeout, tokens, consecutive errors), lazy loading, pass-by-reference - Add "RLM Execution Model" cross-cutting section - Add "Implementation Progress" tracking table - Remove stale "TO IMPLEMENT" markers (all Phase 3 work is done) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): Phase 4 — budget controls, compaction, reflection pipeline Budget enforcement in ExecutionLoop: - max_tokens_total: cumulative token limit, checked before each iteration - max_duration: wall-clock timeout for entire thread - max_consecutive_errors: consecutive error steps threshold (resets on success, matching official RLM behavior) - All produce ThreadOutcome::Failed with descriptive messages Context compaction (from RLM paper, 85% threshold): - estimate_tokens(): char-based estimation (chars/4, matching RLM) - should_compact(): triggers when tokens >= threshold_pct * context_limit - compact_messages(): asks LLM to summarize progress, replaces history with [system, summary, continuation_note], preserves intermediate results - Configurable via ThreadConfig: model_context_limit, compaction_threshold Dual model routing: - LlmCallConfig gains depth field (0=root, 1+=sub-call) - Implementations can route to cheaper models for sub-calls - ExecutionLoop passes thread depth to every LLM call Reflection pipeline (reflection/pipeline.rs): - reflect(thread, llm): analyzes completed thread via LLM - Produces Summary doc (always), Lesson doc (if errors), Issue doc (if failed) - Builds transcript from thread messages + error events - Returns ReflectionResult with docs + token usage ThreadConfig extended with: max_tokens_total, max_consecutive_errors, model_context_limit, enable_compaction, compaction_threshold, depth, max_depth. 78 tests passing, zero clippy warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): Phase 5 — conversation surface separated from execution Conversation is now a UI layer, not an execution boundary. Multiple threads can run concurrently within one conversation; threads can outlive their originating conversation. New types (types/conversation.rs): - ConversationSurface: channel + user + entries + active_threads - ConversationEntry: sender (User/Agent/System) + content + origin_thread_id - ConversationId, EntryId (UUID newtypes) - EntrySender enum (User, Agent{thread_id}, System) ConversationManager (runtime/conversation.rs): - get_or_create_conversation(channel, user) — indexed by (channel, user) - handle_user_message() — injects into active foreground thread or spawns new - record_thread_outcome() — adds agent/system entries, untracks completed threads - get_conversation(), list_conversations() This enables the key architectural insight: a user can ask "what's the weather?" while a deployment thread is still running. Both produce entries in the same conversation. 85 tests passing, zero clippy warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(engine): simplify execution tiers — Monty-only for CodeAct/RLM Restructure phases 6-8 to clarify execution model: - Monty is the sole Python executor for CodeAct/RLM. No WASM or Docker Python runtimes for LLM-generated code. - WASM sandbox is for third-party tool isolation (existing infra, Phase 8) - Docker containers are for thread-level isolation of high-risk work (Phase 8) - Two-phase commit moves to Phase 6 (integration) at the adapter boundary Phase renumbering: - Old Phase 6 (Tier 2-3) → removed as separate phase - Old Phase 7 (integration) → Phase 6 - Old Phase 8 (cleanup) → Phase 7 - New Phase 8: WASM tools + Docker thread isolation (infra integration) Updated progress table: Phases 1-5 marked DONE with test counts and commits. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): Phase 6 — bridge adapters for main crate integration Strategy C parallel deployment: when ENGINE_V2=true env var is set, user messages route through the engine instead of the existing agentic loop. All existing behavior is unchanged when the flag is off. Bridge module (src/bridge/): - LlmBridgeAdapter: wraps LlmProvider as engine LlmBackend, converts ThreadMessage↔ChatMessage, ActionDef↔ToolDefinition, depth-based model routing (primary vs cheap_llm) - EffectBridgeAdapter: wraps ToolRegistry+SafetyLayer as EffectExecutor, routes tool calls through existing execute_tool_with_safety pipeline - InMemoryStore: HashMap-backed Store impl (no DB tables needed yet) - EngineRouter: is_engine_v2_enabled() + handle_with_engine() that builds engine from Agent deps and processes messages end-to-end Integration touchpoint (4 lines in agent_loop.rs): After hook processing, before session resolution, check ENGINE_V2 flag and route UserInput through the engine path. Accessor visibility widened: llm(), cheap_llm(), safety(), tools() changed from pub(super) to pub(crate) for bridge access. 85 engine tests + main crate clippy clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(engine): add user message and system prompt to thread before execution The ExecutionLoop was sending empty messages to the LLM because the thread was spawned with the user's input as the goal but no messages. Fixes: - ThreadManager.spawn_thread() now adds the goal as an initial user message before starting the execution loop - ExecutionLoop.run() injects a default system prompt if none exists Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(bridge): match existing LLM request format to prevent 400 errors The LLM bridge was missing several defaults that the existing Reasoning.respond_with_tools() sets: - tool_choice: "auto" when tools are present (required by some providers) - max_tokens: 4096 (default) - temperature: 0.7 (default) - When no tools (force_text): use plain complete() instead of complete_with_tools() with empty tools array — matches existing no-tools fallback path Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(engine): persist conversation context across messages The engine was creating a fresh ThreadManager and InMemoryStore per message, losing all context between turns. A follow-up question like "what are the latest 10 issues?" had no memory of the prior "how many issues" response. Fixes: - EngineState (ThreadManager, ConversationManager, InMemoryStore) now persists across messages via OnceLock, initialized on first use - ConversationManager builds message history from prior conversation entries (user messages + agent responses) and passes it to new threads - ThreadManager.spawn_thread_with_history() accepts initial_messages that are prepended before the current user message - System notifications (thread started/completed) are filtered out of the history (not useful as LLM context) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): enable CodeAct/RLM mode with code block detection The engine now operates in CodeAct/RLM mode: System prompt (executor/prompt.rs): - Instructs LLM to write Python in ```repl fenced blocks - Documents available tools as callable Python functions - Documents llm_query(), llm_query_batched(), FINAL() - Documents context variables (context, goal, step_number, previous_results) - Strategy guidance: examine context, break into steps, use tools, call FINAL() Code block detection (bridge/llm_adapter.rs): - extract_code_block() scans LLM text responses for ```repl or ```python blocks - When detected, returns LlmResponse::Code instead of LlmResponse::Text - The ExecutionLoop routes Code responses through Monty for execution No structured tool definitions sent to LLM: - Tools are described in the system prompt as Python functions - The LLM call sends empty actions array, forcing text-mode responses - This ensures the LLM writes code blocks (CodeAct) instead of structured tool calls (which would bypass the REPL) 85 tests passing, zero clippy warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(engine): add 8 CodeAct/RLM E2E tests with mock LLM Comprehensive test coverage for the Monty Python execution path: - codeact_simple_final: Python code calls FINAL('answer') → thread completes - codeact_tool_call_then_final: code calls test_tool() → FunctionCall suspends VM → MockEffects returns result → code resumes → FINAL() - codeact_pure_python_computation: sum([1,2,3,4,5]) → FINAL('Sum is 15') with no tool calls — pure Python in Monty - codeact_multi_step: first step prints output (no FINAL), second step sees output metadata and calls FINAL — tests iterative REPL flow - codeact_error_recovery: first step has NameError → error flows to LLM as stdout → second step recovers with FINAL — tests error transparency - codeact_context_variables_available: code accesses `goal` and `context` variables injected by the RLM context builder - codeact_multiple_tool_calls_in_loop: for loop calls test_tool() 3 times → 3 FunctionCall suspensions → all results collected → FINAL - codeact_llm_query_recursive: code calls llm_query('prompt') → VM suspends → MockLlm provides sub-agent response → result returned as Python string variable 93 tests passing (85 prior + 8 new), zero clippy warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(bridge): detect code blocks in plain completion path + multi-block support Two bugs fixed: 1. The no-tools completion path (used by CodeAct since we send empty actions) returned LlmResponse::Text without checking for code blocks. Code blocks were rendered as markdown text instead of being executed. 2. extract_code_block now: - Handles bare ``` fences (skips non-Python languages) - Collects ALL code blocks in the response and concatenates them (models often split code across multiple blocks with explanation) - Tries markers in order: ```repl, ```python, ```py, then bare ``` Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(bridge): add 11 regression tests for code block extraction Covers the exact failure modes discovered during live testing: - extract_repl_block: standard ```repl fenced block - extract_python_block: ```python marker - extract_py_block: ```py shorthand - extract_bare_backtick_block: bare ``` with Python content - skip_non_python_language: ```json should NOT be extracted - no_code_blocks_returns_none: plain text, no fences - multiple_code_blocks_concatenated: two ```repl blocks with explanation between them → concatenated with \n\n - mixed_thinking_and_code: model outputs explanation + two ```python blocks (the Hyperliquid case) → both extracted - repl_preferred_over_bare: ```repl takes priority over bare ``` - empty_code_block_skipped: empty fenced block returns None - unclosed_block_returns_none: no closing ``` returns None Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(engine): detect FINAL() in text responses + regression tests Models sometimes write FINAL() outside code blocks — as plain text after an explanation. The Hyperliquid case: model outputs a long analysis then FINAL("""...""") at the end, not inside ```repl fences. Fixes: - extract_final_from_text(): regex-based FINAL detection in text responses, matching the official RLM's find_final_answer() fallback - Handles: double-quoted, single-quoted, triple-quoted, unquoted, nested parens - Checked in LlmResponse::Text handler BEFORE tool intent nudge (FINAL takes priority) 9 new tests: - codeact_final_in_text_response: FINAL("answer") in plain text - codeact_final_triple_quoted_in_text: FINAL("""multi\nline""") in text - final_double_quoted, final_single_quoted, final_triple_quoted, final_unquoted, final_with_nested_parens, final_after_long_text, no_final_returns_none 102 tests passing (93 + 9 new), zero clippy warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add crate extraction & cleanup roadmap Documents architectural recommendations from the engine v2 design process for future reference: - Root directory consolidation (channels-src + tools-src → extensions/) - Crate extraction tiers: zero-coupling (estimation, observability, tunnel), trivial-coupling (document_extraction, pairing, hooks), medium-coupling (secrets, MCP, db, workspace, llm, skills), heavy-coupling (web gateway, agent, extensions) - src/ module reorganization into logical groups (core, persistence, infra, media, support) - main.rs/app.rs slimming targets (100/500 lines after migration) - WASM module candidates (document_extraction) and non-candidates (REPL, web gateway → separate crates instead) - Priority ordering for extraction work - Tracks completed items (ironclaw_safety, ironclaw_engine, transcription move) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): live progress status updates via event broadcast Engine v2 now shows live progress in the CLI (and any channel): - "Thinking..." when a step starts - Tool name + success/error when actions execute - "Processing results..." when a step completes Implementation: - ThreadManager holds a broadcast::Sender<ThreadEvent> (capacity 256) - ExecutionLoop.emit_event() writes to thread.events AND broadcasts - ThreadManager.subscribe_events() returns a receiver - Router uses tokio::select! to listen for events while waiting for thread completion, forwarding them as StatusUpdate to the channel This replaces the polling approach with zero-latency event streaming. Agent.channels visibility widened to pub(crate) for bridge access. 102 tests passing, zero clippy warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(engine): include tool results in code step output for LLM context The LLM was ignoring tool results and answering from training data because the compact output metadata didn't include what tools returned. Tool results lived only as ActionResult messages (role: Tool) which some providers flatten or the model ignores. Now the code step output includes: - stdout from Python print() statements - [tool_name result] with the actual output (truncated to 4K per tool) - [tool_name error] for failed tools - [return] for the code's return value - Total output truncated to 8K chars to prevent context bloat This ensures the model sees web_search results, API responses, etc. in the next iteration and can reason about them instead of hallucinating. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): add debug/trace logging for CodeAct execution Three verbosity levels for debugging the engine: RUST_LOG=ironclaw_engine=debug: - LLM call: message count, iteration, force_text - LLM response: type (text/code/action_calls), token usage - Code execution: code length, action count, had_error, final_answer - Text response: length, FINAL() detection RUST_LOG=ironclaw_engine=trace: - Full message list sent to LLM (role, length, first 200 chars each) - Full code block being executed - stdout preview (first 500 chars) - Per-tool results (name, success, first 300 chars of output) - Text response preview (first 500 chars) Usage: ENGINE_V2=true RUST_LOG=ironclaw_engine=debug cargo run ENGINE_V2=true RUST_LOG=ironclaw_engine=trace cargo run Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): execution trace recording + retrospective analysis Enable with ENGINE_V2_TRACE=1 to get full execution traces and automatic issue detection after each thread completes. Trace recording (executor/trace.rs): - build_trace(): captures full thread state — messages (with full content), events, step count, token usage, detected issues - write_trace(): writes JSON to engine_trace_{timestamp}.json - log_trace_summary(): logs summary + issues at info/warn level Retrospective analyzer detects 8 issue categories: - thread_failure: thread ended in Failed state - no_response: no assistant message generated - tool_error: specific tool failures with error details - code_error: Python errors (NameError, SyntaxError, etc.) in output - missing_tool_output: tool results exist but not in system messages - excessive_steps: >10 steps (may be stuck in loop) - no_tools_used: single-step answer without tools (hallucination risk) - mixed_mode: text responses without code blocks (prompt not followed) Thread state now saved to store after execution completes (for trace access after join_thread). Usage: ENGINE_V2=true ENGINE_V2_TRACE=1 cargo run # After each message: trace JSON + issue log in terminal Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): wire reflection pipeline + trace analysis into thread lifecycle After every thread completes, ThreadManager now automatically runs: 1. Retrospective trace analysis (non-LLM, always): - Detects 8 issue categories (tool errors, code errors, missing outputs, excessive steps, hallucination risk, etc.) - Logs issues at warn level when found 2. Trace file recording (when ENGINE_V2_TRACE=1): - Writes full JSON trace to engine_trace_{timestamp}.json 3. LLM reflection (when enable_reflection=true): - Calls reflection pipeline to produce Summary, Lesson, Issue docs - Saves docs to store for future context retrieval - Enabled by default in the bridge router All three run inside the spawned tokio task after exec.run() completes, before saving the final thread state. No external wiring needed. Removed duplicate trace recording from the router — it's now handled by ThreadManager automatically. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(bridge): convert tool name hyphens to underscores for Python compatibility Root cause from trace analysis: the LLM writes `web_search()` (valid Python identifier) but the tool registry has `web-search` (with hyphen). The EffectBridgeAdapter couldn't find the tool → "Tool not found" error → model fabricated fake data instead. Fixes: - available_actions(): converts tool names from hyphens to underscores (web-search → web_search) so the system prompt lists valid Python names - execute_action(): tries the original name first, then falls back to hyphenated form (web_search → web-search) for tool registry lookup - Same conversion in router's capability registry builder Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(bridge): parse JSON tool output to prevent double-serialization From trace analysis: web_search returned a JSON string, which was wrapped as serde_json::json!(string) creating a Value::String containing JSON. When Monty got this as MontyObject::String, the Python code couldn't index it with result['title'] → TypeError. Fix: try parsing the tool output string as JSON first. If valid, use the parsed Value (becomes a Python dict/list). If not valid JSON, keep as string. This means web_search results are directly indexable in Python: results = web_search(query="...") print(results["results"][0]["title"]) # works now Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): persist variables across code steps via `state` dict Monty creates a fresh runtime per code step, so variables are lost between steps. This caused the model to re-paste tool results from system messages, wasting tokens. Fix: maintain a `persisted_state` JSON dict in the ExecutionLoop that accumulates across steps: - Tool results stored by tool name: state["web_search"] = {results...} - Return values stored: state["last_return"], state["step_0_return"] - Injected as a `state` Python variable in each new MontyRun Now the model can do: Step 1: results = web_search(query="...") # tool result saved in state Step 2: data = state["web_search"] # access previous result summary = llm_query("summarize", str(data)) FINAL(summary) System prompt updated to document the `state` variable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(engine): add state hint on code errors + retrieval engine integration When code fails with NameError/UnboundLocalError (model trying to access variables from a previous step), the error output now includes: [HINT] Variables don't persist between code blocks. Use the `state` dict to access data from previous steps. Available keys: ["web_search", "last_return"] This teaches the model to use `state["web_search"]` instead of `result` after a NameError, reducing wasted steps from 3-4 to 1. Also integrates RetrievalEngine into context building and ThreadManager: - build_step_context() now accepts optional RetrievalEngine to inject relevant memory docs (Lessons, Specs, Playbooks) into LLM context - RetrievalEngine uses keyword matching with doc-type priority scoring - Memory docs from reflection (Phase 4) now feed back into future threads Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: remove trace files and add to .gitignore Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(engine): replace web_fetch example with web_search in CodeAct prompt The system prompt example used web_fetch(url="...") which doesn't exist as a tool. The model learned from the example and tried web_fetch, getting "Tool not found". Changed to web_search(query="...") which is an actual registered tool. Found via trace analysis — reflection pipeline correctly identified this as a "Tool Name Correction" spec doc. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(engine): extract prompt templates to markdown files Prompt templates moved from inline Rust strings to plain markdown files at crates/ironclaw_engine/prompts/ for easy inspection and iteration: - prompts/codeact_preamble.md — main instructions, special functions, context variables, rules - prompts/codeact_postamble.md — strategy section Loaded at compile time via include_str!(), so no runtime file I/O. Edit the .md files and rebuild to iterate on prompts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(engine): replace byte-index slicing with char-safe truncation Panic: 'byte index 80 is not a char boundary; it is inside ''' when tool output contained multi-byte UTF-8 characters (smart quotes from web search results). Fixed 4 unsafe byte-index slices: - thread.rs:281: message preview &content[..80] → chars().take(80) - loop_engine.rs:556: tool output &str[..4000] → chars().take(4000) - loop_engine.rs:579: output tail &str[len-8000..] → chars().skip() - scripting.rs:82: stdout tail &str[len-N..] → chars().skip() All now use .chars().take() or .chars().skip() which respect character boundaries. Follows CLAUDE.md rule: "Never use byte-index slicing on user-supplied or external strings." Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(engine): fix false positive missing_tool_output warning in trace analyzer The check was looking for "[" + "result]" in System-role messages only, but tool output metadata is added with patterns like "[shell result]" and may appear in messages with any role. Changed to scan all messages for " result]" or " error]" patterns regardless of role. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(engine): update architecture plan with Phase 6 status and approval flow design Phase 6 updated to reflect what was actually built: - Bridge adapters (LLM, Effect, InMemoryStore, Router) — all done - Integration touchpoint (4 lines in handle_message) — done - Live progress via broadcast events — done - Conversation persistence across messages — done - Trace recording + retrospective analysis — done - 8 bugs found and fixed via trace analysis — documented Phase 6 remaining work documented: - Approval flow: detailed 5-step design (send to channel, pause thread, route response, resume execution, always handling) with v1 reference - Database persistence (InMemoryStore → real DB tables) - Acceptance testing (TestRig + TraceLlm fixtures) - Two-phase commit for high-stakes effects Progress table updated: Phase 6 marked as DONE (partial), 134 tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add self-improving engine design plan Designs a system where the engine debugs and improves itself, based on the pattern observed in the last session: 5 consecutive bug fixes all followed trace → read → identify → edit → test, using tools the engine already has access to. Three levels of self-improvement: - Level 1 (Prompt): edit prompts/*.md to prevent LLM mistakes. Auto-apply. - Level 2 (Config): adjust defaults/mappings. Branch + test + PR. - Level 3 (Code): Rust patches for engine bugs. Branch + test + clippy + PR. Architecture: Self-improvement Mission spawns a Reflection thread that reads traces, reads source, proposes fixes, validates via cargo test, and either auto-applies (Level 1) or creates a PR (Level 2-3). Includes: fix pattern database (seeded from our 8 debugging session fixes), feedback loop diagram, safety model, implementation phases (A through D), and what exists vs what's new. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add engine v2 security model and audit Comprehensive security analysis of engine v2 covering: Threat model: 4 attacker profiles (malicious input, prompt injection via tools, poisoned memory, supply chain). Current state audit: 9 controls working (Monty sandbox, safety layer, policy engine, leases, provenance, events) and 9 gaps identified. Critical finding: ALL tools granted by default — CodeAct code can call shell, write_file, apply_patch without approval. Proposed fix: 3-tier tool classification (auto/approve-once/always-approve). CodeAct-specific threats: tool call amplification, prompt injection via search results, data exfiltration via tool chains, Monty escape. Self-improvement security: poisoned trace attacks, memory poisoning via reflection. Mitigations: edit validation, frequency caps, audit trail, auto-rollback, reflection output scanning. 6-layer security architecture proposed: input validation, capability gating, output sanitization, execution sandboxing, self-improvement controls, observability. Prioritized implementation plan with severity/effort ratings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(security): cross-reference v1 controls — use, don't reinvent Updated security plan with detailed audit of ALL existing v1 security controls and how they map to engine v2 bridge gaps: Key finding: v1 already has solutions for every security gap identified. The bridge just needs to wire them in: - Tool::requires_approval() exists but bridge doesn't call it - safety.wrap_for_llm() exists but tool results enter context unwrapped - RateLimiter exists but bridge doesn't check rate limits - BeforeToolCall hooks exist but bridge doesn't run them - redact_params() exists but bridge doesn't redact sensitive params - Shell risk classification (Low/Medium/High) is inherited but ignored Revised priority: most fixes are small wiring tasks in EffectBridgeAdapter, not new security infrastructure. The bridge is the security boundary. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): add missions, reliability tracker, reflection executor, and provenance-aware policy - Add Mission type and MissionManager for recurring thread scheduling - Add ReliabilityTracker for per-capability success/failure/latency tracking - Add reflection executor that spawns CodeAct threads for post-completion reflection - Extend PolicyEngine with provenance-aware taint checking (LLM-generated data requires approval for financial/external-write effects) - Extend Store trait with mission CRUD methods - Add conversation surface tracking, compaction token fix, context memory injection - Wire new modules through lib.rs re-exports and bridge adapters Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(bridge): wire v1 security controls into engine v2 adapter Zero engine crate changes. All security controls enforced at the bridge boundary in EffectBridgeAdapter: 1. Tool approval (v1: Tool::requires_approval): - Checks each tool's approval requirement with actual params - Always → returns EngineError::LeaseDenied (blocks execution) - UnlessAutoApproved → checks auto_approved set, blocks if not approved - Never → proceeds - Per-session auto_approved HashSet (for future "always" handling) 2. Hook interception (v1: BeforeToolCall): - Runs HookEvent::ToolCall before every execution - HookOutcome::Reject → blocks with reason - HookError::Rejected → blocks with reason - Hook errors → fail-open (logged, execution continues) 3. Output sanitization (v1: sanitize_tool_output + wrap_for_llm): - Leak detection: API keys in tool output are redacted - Policy enforcement: content policy rules applied - Length truncation: output capped at 100KB - XML boundary protection: prevents injection via tool output 4. Sensitive param redaction (v1: redact_params): - Tool's sensitive_params() consulted before hooks see parameters - Redacted params sent to hooks, original params used for execution 5. available_actions() now sets requires_approval based on each tool's default approval requirement, so the engine's PolicyEngine can gate tools it hasn't seen before. 6. Actual execution timing measured via Instant::now() (replaces placeholder Duration::from_millis(1)). Accessor visibility: hooks() widened to pub(crate). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(bridge): implement tool approval flow for engine v2 Adds a complete approval flow that mirrors v1 behavior, using the existing v1 security controls (Tool::requires_approval, auto-approve sets, StatusUpdate::ApprovalNeeded). ## How it works ### Step 1: Tool blocked at execution When the LLM's code calls a tool (e.g., `shell("ls")`): 1. EffectBridgeAdapter.execute_action() looks up the Tool object 2. Calls tool.requires_approval(¶ms) — returns ApprovalRequirement 3. If Always → EngineError::LeaseDenied (always blocks) 4. If UnlessAutoApproved → checks auto_approved HashSet → if not in set, returns EngineError::LeaseDenied 5. If Never → proceeds to execution ### Step 2: Engine returns NeedApproval The LeaseDenied error propagates through: - CodeAct path: becomes Python RuntimeError, code halts, thread returns NeedApproval with action_name + parameters - Structured path: same via ActionResult.is_error ### Step 3: Router stores pending approval - PendingApproval { action_name, original_content } stored on EngineState - StatusUpdate::ApprovalNeeded sent to channel (shows approval card in CLI/web with tool name, parameters, yes/always/no buttons) - Returns text: "Tool 'shell' requires approval. Reply yes/always/no." ### Step 4: User responds handle_message() intercepts Submission::ApprovalResponse when ENGINE_V2: - 'yes' → auto_approve_tool(name) on EffectBridgeAdapter, re-processes original message (tool now passes the approval check on second run) - 'always' → same + logs for session persistence - 'no' → returns "Denied: tool was not executed." ### Key design choice Instead of pausing/resuming mid-execution (which needs engine changes to freeze/restore the Monty VM state), we auto-approve the tool and re-run the full message. The EffectBridgeAdapter's auto_approved set persists across runs, so the second execution passes immediately. This trades one extra LLM call for zero engine modifications. ## Files changed - src/bridge/router.rs: PendingApproval struct, handle_approval(), NeedApproval → StatusUpdate::ApprovalNeeded conversion - src/bridge/mod.rs: export handle_approval - src/agent/agent_loop.rs: intercept ApprovalResponse for engine v2 - src/bridge/effect_adapter.rs: fmt fixes 151 tests passing, clippy + fmt clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(engine): demote trace/reflection logging from info to debug INFO-level log output from background tasks (trace analysis, reflection) corrupts the REPL terminal UI. The trace summary, issue warnings, and reflection doc previews were printing mid-approval-card, breaking the interactive display. Fix: all logging in trace.rs changed from info!/warn! to debug!/warn!. Trace analysis and reflection results now only show when RUST_LOG=ironclaw_engine=debug is set. Also added logging discipline rule to global CLAUDE.md: - info! → user-facing status the REPL intentionally renders - debug! → internal diagnostics (traces, reflection, engine internals) - Background tasks must NEVER use info! — it breaks the TUI Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(bridge): demote all router info! logging to debug! "engine v2: initializing" and "engine v2: handling message" were printing at INFO level, corrupting the REPL UI. All router logging now uses debug! — only visible with RUST_LOG=ironclaw=debug. Zero info! calls remain in crates/ironclaw_engine/ or src/bridge/. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(safety): demote leak detector warn-action logs from warn! to debug! The leak detector's Warn-action matches (high_entropy_hex pattern on web search results containing commit SHAs, CSS colors, URL hashes) were logging at warn! level, corrupting the REPL UI with lines like: WARN Potential secret leak detected pattern=high_entropy_hex preview=a96f********cee5 These are informational false positives — real leaks use LeakAction::Redact which silently modifies the content. Warn-action matches only log for debugging purposes and should not appear in production output. Changed to debug! level — visible with RUST_LOG=ironclaw_safety=debug. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(engine): strengthen CodeAct prompt to prevent shallow text answers The model was answering "Suggested 45 improvements" as a brief text summary from training data without actually searching or listing them. The trace showed: no code block, no tool calls, no FINAL(). Prompt changes: - Rule 1: "ALWAYS respond with a ```repl code block. NEVER answer with plain text only." (was: "Always write code... plain text for brief explanations") - Rule 2 (NEW): "NEVER answer from memory or training data alone. Always use tools to get real, current information before answering." - Rule 3: FINAL answer "should be detailed and complete — not just a summary like 'found 45 items'" - Rule 8 (NEW): "Include the actual content in your FINAL() answer, not just a count or summary. Users want to see the details." Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(bridge): persist reflection docs to workspace for cross-session learning Replaces InMemoryStore with HybridStore: - Ephemeral data (threads, steps, events, leases) stays in-memory - MemoryDocs (lessons, specs, playbooks from reflection) persist to the workspace at engine/docs/{type}/{id}.json On engine init, load_docs_from_workspace() reads existing docs back into the in-memory cache. This means: - Lessons learned in session 1 are available in session 2 - The RetrievalEngine injects relevant past lessons into new threads - The engine genuinely improves over time as reflection accumulates Workspace paths: engine/docs/lessons/{uuid}.json engine/docs/specs/{uuid}.json engine/docs/playbooks/{uuid}.json engine/docs/summaries/{uuid}.json engine/docs/issues/{uuid}.json No new database tables. Uses existing workspace write/read/list. workspace() accessor widened to pub(crate). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(bridge): adapt to execute_tool_with_safety params-by-value change Staging merge changed execute_tool_with_safety to take params by value instead of by reference (perf optimization from PR #926). Updated bridge adapter to clone params before passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(engine): add web gateway integration plan to Phase 6 Documents three gaps between engine v2 and the web gateway: 1. No SSE streaming (engine emits ThreadEvent, gateway expects SseEvent) 2. No conversation persistence (engine uses HybridStore, gateway reads v1 DB) 3. No cross-channel visibility (REPL ↔ web messages invisible to each other) Implementation plan: bridge ThreadEvent→AppEvent, write messages to v1 conversation tables after thread completion. Prerequisite: AppEvent extraction PR (in progress separately). Also updated DB persistence status: HybridStore with workspace-backed MemoryDocs is now implemented (partial persistence). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(engine): document routine/job gap and SIGKILL crash scenario Routines are entirely v1 — not hooked up to engine v2. When a user asks "create a routine" as natural language, engine v2 tries to call routine_create via CodeAct, but the tool needs RoutineEngine + Database refs that the bridge's minimal JobContext doesn't provide. This caused a SIGKILL crash during testing. Options documented: block routine tools in v2 (short term), pass refs through context (medium), replace with Mission system (long term). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: extract AppEvent to crates/ironclaw_common SseEvent was defined in src/channels/web/types.rs but imported by 12+ modules across agent, orchestrator, worker, tools, and extensions — it had become the application-wide event protocol, not a web transport concern. Create crates/ironclaw_common as a shared workspace crate and move the enum there as AppEvent. Also move the truncate_preview utility which was similarly leaked from the web gateway into agent modules. - New crate: crates/ironclaw_common (AppEvent, truncate_preview) - Rename SseEvent → AppEvent, from_sse_event → from_app_event - web/types.rs re-exports AppEvent for internal gateway use - web/util.rs re-exports truncate_preview - Wire format unchanged (serde renames are on variants, not the enum) Aligned with the event bus direction on refactor/architectural-hardening where DomainEvent (≡ AppEvent) is wrapped in a SystemEvent envelope. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(bridge): integrate with web gateway via AppEvent + v1 conversation DB Three changes to make engine v2 visible in the web gateway: 1. SSE event streaming (AppEvent broadcast): - ThreadEvent → AppEvent conversion via thread_event_to_app_event() - Events broadcast to SseManager during the poll loop - Covers: Thinking, ToolCompleted (success/error), Status, Response - Web gateway receives real-time progress without any gateway changes 2. Conversation persistence to v1 database: - After thread completes, writes user message + agent response to v1 ConversationStore via add_conversation_message() - Uses get_or_create_assistant_conversation() for per-user per-channel - Web gateway reads from DB as usual — chat history appears 3. Final response broadcast: - AppEvent::Response with full text + thread_id sent via SSE - Web gateway renders the response in the chat UI New EngineState fields: sse (Option<Arc<SseManager>>), db (Option<Arc<dyn Database>>). Both populated from Agent.deps. Agent.deps visibility widened to pub(crate). Depends on: ironclaw_common crate with AppEvent type (PR #1615). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(bridge): complete Phase 6 — v1-only tool blocking, rate limiting, call limits Three security/stability improvements in EffectBridgeAdapter: 1. V1-only tool blocking: - routine_create, create_job, build_software (and hyphenated variants) return helpful error: "use the slash command instead" - Filtered out of available_actions() so system prompt doesn't list them - Prevents crash from tools needing RoutineEngine/Scheduler refs 2. Per-step tool call limit: - Max 50 tool calls per code block (AtomicU32 counter) - Prevents amplification: `for i in range(10000): shell(...)` - Returns "call limit reached, break into multiple steps" 3. Rate limiting: - Per-user per-tool sliding window via RateLimiter - Checks tool.rate_limit_config() before every execution - Returns "rate limited, try again in Ns" Architecture plan updated: - Gateway integration: DONE - Routines: BLOCKED (gracefully, with slash command fallback) - Rate limiting: DONE - Call limit: DONE - Phase 6 status: DONE (remaining: acceptance tests, two-phase commit) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add Mission system design — goal-oriented autonomous threads Missions replace routines with evolving, knowledge-accumulating autonomous agents. Unlike routines (fixed prompt, stateless), Missions: - Generate prompts from accumulated Project knowledge (lessons, playbooks, issues from prior threads) - Adapt approach when something fails repeatedly - Track progress toward a goal with success criteria - Self-manage: pause when stuck, complete when goal achieved Architecture: MissionManager with cron ticker spawns threads via ThreadManager. Meta-prompt built from mission goal + Project MemoryDocs via RetrievalEngine. Reflection feeds back automatically. 6-step implementation plan: cron trigger, meta-prompt builder, bridge wiring, CodeAct tools, progress tracking, persistence. Includes two worked examples: daily tech news briefing (ongoing) and test coverage improvement (goal-driven, self-completing). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): extend Mission types with webhook/event triggers + evolving strategy Mission types updated to support external activation sources: MissionCadence expanded: - Cron { expression, timezone } — timezone-aware scheduling - OnEvent { event_pattern } — channel message pattern matching - OnSystemEvent { source, event_type } — structured events from tools - Webhook { path, secret } — external HTTP triggers (GitHub, email, etc.) - Manual — explicit triggering only The engine defines trigger TYPES. The bridge implements infrastructure (cron ticker, webhook endpoints, event matchers). GitHub issues, PRs, email, Slack events all use the generic Webhook cadence — no special-casing in the engine. Webhook payload injected as state["trigger_payload"] in the thread's Python context. Mission struct extended: - current_focus: what the next thread should work on (evolving) - approach_history: what we've tried (for adaptation) - max_threads_per_day / threads_today: daily budget - last_trigger_payload: webhook/event data for thread context Plan updated with trigger type table and webhook integration design. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): implement MissionManager execution with meta-prompts The MissionManager now builds evolving meta-prompts and processes thread outcomes for continuous learning: fire_mission() upgraded: - Loads Project MemoryDocs via RetrievalEngine for context - Builds meta-prompt from: goal, current_focus, approach_history, project knowledge docs, trigger payload, thread count - Spawns thread with meta-prompt as user message - Background task waits for completion and processes outcome - Daily thread budget enforcement (max_threads_per_day) Meta-prompt structure: # Mission: {name} Goal: {goal} ## Current Focus (evolves between threads) ## Previous Approaches (what we've tried) ## Knowledge from Prior Threads (lessons, playbooks, issues) ## Trigger Payload (webhook/event data if applicable) ## Instructions (accomplish step, report next focus, check goal) Outcome processing: - Extracts "next focus:" from FINAL() response → updates current_focus - Detects "goal achieved: yes" → completes mission - Records accomplishment in approach_history - Failed threads recorded as "FAILED: {error}" Cron ticker: - start_cron_ticker() spawns tokio task, ticks every 60s - Checks active Cron missions, fires those past next_fire_at 151 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(bridge): wire MissionManager into engine v2 for CodeAct access Missions are now callable from CodeAct Python code: ```python # Create a daily briefing mission result = mission_create( name="Tech News", goal="Daily AI/crypto/software news briefing", cadence="0 9 * * *" ) # List all missions missions = mission_list() # Manually fire a mission mission_fire(id="...") # Pause/resume mission_pause(id="...") mission_resume(id="...") ``` Implementation: - MissionManager created on engine init, cron ticker started - EffectBridgeAdapter intercepts mission_* function calls before tool lookup and routes to MissionManager - parse_cadence() handles: "manual", cron expressions, "event:pattern", "webhook:path" - Mission functions documented in CodeAct system prompt - MissionManager set on adapter via set_mission_manager() after init (avoids circular dependency) System prompt updated with mission_create, mission_list, mission_fire, mission_pause, mission_resume documentation. 151 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(bridge): map routine_* calls to mission operations in v2 When the model calls routine_create, routine_list, routine_fire, routine_pause, routine_resume, or routine_delete, the bridge now routes them to the MissionManager instead of blocking with an error. Mapping: routine_create → mission_create (with cadence parsing) routine_list → mission_list routine_fire → mission_fire routine_pause → mission_pause routine_resume → mission_resume routine_update → mission_pause/resume (based on params) routine_delete → mission_complete (marks as done) Routine tools removed from v1-only blocklist and restored in available_actions(). The model can use either "routine" or "mission" vocabulary — both work. Still blocked: create_job, cancel_job, build_software (need v1 Scheduler/ContainerJobManager refs). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(engine): add E2E mission flow tests — 7 new tests Comprehensive mission lifecycle tests: - fire_mission_builds_meta_prompt_with_goal: verifies thread spawned with project context and recorded in history - outcome_processing_extracts_next_focus: "Next focus: X" in FINAL() response → mission.current_focus updated - outcome_processing_detects_goal_achieved: "Goal achieved: yes" → mission status transitions to Completed - mission_evolves_via_direct_outcome_processing: 3-step evolution: step 1 sets focus to "db module", step 2 evolves to "tools module", step 3 detects goal achieved → mission completes. Tests the full learning loop without background task timing dependencies. - fire_with_trigger_payload: webhook payload stored on mission and threads_today counter incremented - daily_budget_enforced: max_threads_per_day=1 → first fire succeeds, second returns None 157 tests passing (151 prior + 6 new mission E2E). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): self-improving engine via Mission system Wire the self-improvement loop as a Mission with OnSystemEvent cadence, inspired by karpathy/autoresearch's program.md approach. The mission fires when threads complete with issues, receives trace data as trigger payload, and uses tools directly to diagnose and fix problems. Key changes: Engine self-improvement (Phase A+B from design doc): - Add fire_on_system_event() to MissionManager for OnSystemEvent cadence - Add start_event_listener() that subscribes to thread events and fires matching missions when non-Mission threads complete with trace issues - Add ensure_self_improvement_mission() with autoresearch-style goal prompt (concrete loop steps, not vague instructions) - Add process_self_improvement_output() for structured JSON fallback - Seed fix pattern database with 8 known patterns from debugging - Runtime prompt overlay via MemoryDoc (build_codeact_system_prompt now async + Store-aware, appends learned rules from prompt_overlay docs) - Pass Store to ExecutionLoop for overlay loading Bridge review fixes (P1/P2): - Scope engine v2 SSE events to requesting user (broadcast_for_user) - Per-user pending approvals via HashMap instead of global Option - Reset tool-call limit counter before each thread execution - Only persist auto-approval when user chose "always", not one-off "yes" - Remove dead store/mission_manager fields from EngineState Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add checkpoint-based engine thread recovery * feat(engine): add Python orchestrator module and host functions Add the orchestrator infrastructure for replacing the Rust execution loop with versioned Python code. This commit adds the module and host functions without switching over — the existing Rust loop is unchanged. New files: - orchestrator/default.py: v0 Python orchestrator (run_loop + helpers) - executor/orchestrator.rs: host function dispatch, orchestrator loading from Store with version selection, OrchestratorResult parsing Host functions exposed to orchestrator Python via Monty suspension: __llm_complete__, __execute_code_step__ (nested Monty VM), __execute_action__, __check_signals__, __emit_event__, __add_message__, __save_checkpoint__, __transition_to__, __retrieve_docs__, __check_budget__, __get_actions__ Also makes json_to_monty, monty_to_json, monty_to_string pub(crate) in scripting.rs for cross-module use. Design doc: docs/plans/2026-03-25-python-orchestrator.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): switch ExecutionLoop::run() to Python orchestrator Replace the 900-line Rust execution loop with a ~80-line bootstrap that loads and runs the versioned Python orchestrator via Monty VM. The orchestrator Python code (orchestrator/default.py) is the v0 compiled-in version. Runtime versions can override it via MemoryDoc storage (orchestrator:main with tag orchestrator_code). Key fixes during switchover: - Use ExtFunctionResult::NotFound for unknown functions so Monty falls through to Python-defined functions (extract_final, etc.) - Move helper function definitions above run_loop for Monty scoping - Use FINAL result value (not VM return value) in Complete handler - Rename 'final' variable to 'final_answer' to avoid Python keyword Status: 171/177 tests pass. 6 remaining failures are step_count and token tracking bookkeeping — the orchestrator manages these internally but doesn't yet update the thread's counters via host functions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(engine): all 177 tests pass with Python orchestrator - Increment step_count and track tokens in __emit_event__("step_completed") so thread bookkeeping matches the old Rust loop behavior - Remove double-counting of tokens in bootstrap (orchestrator handles it) - Match nudge text to existing TOOL_INTENT_NUDGE constant - Fix FINAL result propagation (use stored final_result, not VM return) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): orchestrator versioning, auto-rollback, and tests Add version lifecycle for the Python orchestrator: - Failure tracking via MemoryDoc (orchestrator:failures) - Auto-rollback: after 3 consecutive failures, skip the latest version and fall back to previous (or compiled-in v0) - Success resets the failure counter - OrchestratorRollback event for observability Update self-improvement Mission goal with Level 1.5 instructions for orchestrator patches — the agent can now modify the execution loop itself via memory_write with versioned orchestrator docs. 12 new tests: version selection (highest wins), rollback after failures, rollback to default, failure counting/resetting, outcome parsing for all 5 ThreadOutcome variants. 189 tests pass, zero clippy warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add engine v2 architecture, self-improvement, and dev history Three new docs for contributors: - engine-v2-architecture.md: Two-layer architecture (Rust kernel + Python orchestrator), five primitives, execution model with nested Monty VMs, bridge layer, memory/reflection, missions, capabilities - self-improvement.md: Three improvement levels (prompt/orchestrator/ config/code), autoresearch-inspired Mission loop, versioned orchestrator with auto-rollback, fix pattern database, safety model - development-history.md: Summary of 6 Claude Code sessions that built the system, key design decisions and debugging moments, architecture evolution from 900-line Rust loop to Python orchestrator Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): complete v2 side-by-side integration with gateway API Wire engine v2 into the full submission pipeline and expose threads, projects, and missions through the web gateway REST API. Bridge routing — route ExecApproval, Interrupt, NewThread, and Clear submissions to engine v2 when ENGINE_V2=true. Previously only UserInput and ApprovalResponse were handled; all other control commands fell through to disconnected v1 sessions. Bridge query layer — add 11 read-only query functions and 6 DTO types so gateway handlers can inspect engine state (threads, steps, events, projects, missions) without direct access to the EngineState singleton. Gateway endpoints — new /api/engine/* routes: GET /threads, /threads/{id}, /threads/{id}/steps, /threads/{id}/events GET /projects, /projects/{id} GET /missions, /missions/{id} POST /missions/{id}/fire, /missions/{id}/pause, /missions/{id}/resume SSE events — add ThreadStateChanged, ChildThreadSpawned, and MissionThreadSpawned AppEvent variants. Expand the bridge event mapper to forward StateChanged and ChildSpawned engine events to the browser. Engine crate — add ConversationManager::clear_conversation() for /new and /clear commands. Code quality — replace 10 .expect() calls with proper error returns, remove dead AgentConfig.engine_v2 field, log silent init errors, fix duplicate doc comment, improve fallthrough documentation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(engine): empty call_id on ActionResult and trace analyzer false positives Fix structured executor not stamping call_id onto ActionResult — the EffectExecutor trait doesn't receive call_id, so the structured executor must copy it from the original ActionCall after execution. Empty call_id caused OpenAI-compatible providers to reject the next LLM request with "Invalid 'input[2].call_id': empty string". Fix trace analyzer false positives: - code_error check now only scans User-role code output messages (prefixed with [stdout]/[stderr]/[code ]/Traceback), not System prompt which contains example error text - missing_tool_output check now recognizes ActionResult messages as valid tool output (Tier 0 structured path) - Add NotImplementedError to detected code error patterns New trace checks: - empty_call_id: detect ActionResult messages with missing/empty call_id before they reach the LLM API (severity: Error) - llm_error: extract LLM provider errors from Failed state reason - orchestrator_error: extract orchestrator errors from Failed state Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(web): add Missions tab to gateway UI Add a full Missions page to the web gateway with list view, detail view, and action buttons (Fire, Pause, Resume). Backend: add /api/engine/missions/summary endpoint returning counts by status (active/paused/completed/failed). Frontend: - New "Missions" tab between Jobs and Routines - Summary cards showing mission counts by status - Table with name, goal, cadence type, thread count, status, actions - Detail view with goal, cadence, current focus, success criteria, approach history, spawned thread list, and action buttons - Fire/Pause/Resume actions with toast notifications - i18n support (English + Chinese) - CSS following the existing routines/jobs patterns Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(engine): eagerly initialize engine v2 at startup The gateway API endpoints (/api/engine/missions, etc.) call bridge query functions that return empty results when the engine state hasn't been initialized yet. Previously, initialization only happened lazily on the first chat message via handle_with_engine(). Now when ENGINE_V2=true, the engine is initialized in Agent::run() before channels start, so the self-improvement mission and other engine state is available to gateway API endpoints immediately. Also rename get_or_init_engine → init_engine and make it public so it can be called from agent_loop.rs at startup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(web): improve mission detail with markdown goal and thread table - Goal rendered as full-width markdown block instead of plain-text meta item (uses existing renderMarkdown/marked) - Current focus and success criteria also rendered as markdown - Spawned threads shown as a clickable table with goal, type, state, steps, tokens, and created date instead of a UUID list - Clicking a thread row opens an inline thread detail view showing metadata grid and full message history with markdown rendering - Back button returns to the mission detail view - Backend: mission detail now returns full thread summaries (goal, state, step_count, tokens) instead of just thread IDs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(web): close SSE connections on page unload to prevent connection starvation The browser limits concurrent HTTP/1.1 connections per origin to 6. Without cleanup, SSE connections from prior page loads linger after refresh/navigation, eating into the pool. After 2-3 refreshes, all 6 slots are consumed by stale SSE streams and new API fetch calls queue indefinitely — the UI shows "connected" (SSE works) but data never loads. Add a beforeunload handler that closes both eventSource (chat events) and logEventSource (log stream) so the browser can reuse connections immediately on page reload. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(web): support multiple gateway tabs by reducing SSE connections Each browser tab opened 2 SSE connections (chat events + log events). With the HTTP/1.1 per-origin limit of 6, the 3rd tab exhausted the pool and couldn't load any data. Three changes: 1. Lazy log SSE — only connect when the logs tab is active, disconnect when switching away. Most users rarely view logs, so this saves a connection slot per tab. 2. Visibility API — close SSE when the browser tab goes to background (user switches to another tab), reconnect when it becomes visible. Background tabs don't need real-time events. 3. Combined with the existing beforeunload cleanup, this means: - Active foreground tab: 1 connection (chat SSE only, +1 if logs tab) - Background tabs: 0 connections - Closed/refreshed tabs: 0 connections (beforeunload cleanup) This allows many gateway tabs to coexist within the 6-connection limit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(engine): route messages to correct conversation by thread scope Messages sent from a new conversation in the gateway always appeared in the default assistant conversation because handle_with_engine ignored the thread_id from the frontend. Two fixes: 1. Engine conversation scoping — when the message carries a thread_id (from the frontend's conversation picker), use it as part of the engine conversation key: "gateway:<thread_id>" instead of just "gateway". This creates a distinct engine conversation per v1 thread, so messages don't cross-contaminate. 2. V1 dual-write targeting — write user messages and assistant responses to the v1 conversation matching the thread_id (via ensure_conversation), not the hardcoded assistant conversation. Falls back to the assistant conversation when no thread_id is present (e.g., default chat). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(web): richer activity indicators for engine v2 execution The gateway UI showed only generic "Thinking..." during engine v2 execution with no visibility into CodeAct code execution, tool calls, or reflection. Now the event mapping produces detailed status updates: Step lifecycle: - "Calling LLM..." when a step starts (was "Thinking...") - "Step complete — N in / M out tokens" when done (was "Processing...") Tool execution: - Emit ToolStarted + ToolCompleted SSE events so the frontend renders proper tool cards with spinner → checkmark/error transitions - Duration shown in parameters field (e.g., "42ms") CodeAct visibility: - "Executing code..." when assistant produces a code block - "Code executed" / "Code executed (no output)" for successful runs - "Code error — retrying..." when Monty raises an exception Reflection: - "Reflecting on execution..." when post-thread analysis starts - "Reflection complete — N insight(s) saved" when done Also refactored thread_event_to_app_event → thread_event_to_app_events (returns Vec<AppEvent>) to support emitting ToolStarted before ToolCompleted in a single event handler pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(engine): resolve tool names as callable stubs in CodeAct runtime When LLM-generated code calls `mission_list()` or any tool function, Monty's Python execution model first resolves the name (`mission_list`) as a NameLookup before invoking it as a FunctionCall. The NameLookup handler always returned Undefined, causing NameError before the function call could dispatch to the effect executor. Fix: before starting the Monty VM, collect all known tool names from the effect executor's available_actions(). In the NameLookup handler, if the name matches a known tool, return a MontyObject::Function stub instead of Undefined. Monty then yields FunctionCall for the stub, which dispatches to the normal tool execution pipeline. This enables CodeAct code to call any registered tool as a Python function: mission_list(), mission_create(), routine_list(), web_search(), memory_search(), etc. — all without explicit imports or __execute_action__ boilerplate. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): consolidate action execution, remove reflection, add learning missions Three major changes to the v2 engine: 1. **Consolidated action execution** — `handle_execute_action` in Rust is now the single source of truth for lease lookup, policy check, lease consumption, action execution, event emission, and ActionResult message recording. The Python orchestrator no longer duplicates event/message logic. This fixes the empty call_id bug (OpenAI HTTP 400) and the missing tool_calls on assistant messages (Codex "No tool call found" error). 2. **Removed reflection system** — Deleted the per-thread reflection pipeline (pipeline.rs, executor.rs), ThreadState::Reflecting, ThreadType::Reflection, enable_reflection config, and all 3 reflection event kinds. Learning is now handled entirely by event-driven missions that fire selectively. 3. **Three learning missions** replace reflection: - `self-improvement` — fires on trace issues (error diagnosis, prompt fixes) - `playbook-extraction` — fires on successful 5+ step threads (reusable procedures) - `conversation-insights` — fires every 5 threads per project (user preferences, domain knowledge, workflow patterns) Additional fixes: - llm_query()/llm_query_batched() always include system message (Codex compat) - handle_llm_complete adds assistant message with structured action_calls for Tier 0 responses (prevents "No tool call found" errors) - Gateway broadcasts without thread_id emit as Status events instead of being dropped - Comprehensive tests for call_id propagation and trace analysis (17 new tests) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(skills): extract ironclaw_skills crate and integrate with v2 engine Extract the skills system into a standalone `ironclaw_skills` crate (following the ironclaw_safety pattern) and wire it into the v2 engine for deterministic skill selection, CodeAct code injection, and confidence tracking. **ironclaw_skills crate** (94 tests): - Core types: SkillManifest, ActivationCriteria, LoadedSkill, SkillTrust - V2 types: V2SkillMetadata, CodeSnippet, SkillMetrics, V2SkillSource - Deterministic 4-phase selector (gating→scoring→budget→attenuation) - apply_confidence_factor() for extracted skill scoring - SKILL.md parser, validation/escaping, gating, registry, catalog - Feature-gated: catalog (reqwest), registry (filesystem) **Engine integration** (14 new tests): - DocType::Skill with retrieval weight 0.45 - SkillSelector bridges MemoryDoc→LoadedSkill for shared scoring - SkillTracker for usage/version/rollback confidence tracking - System prompt injection via <skill> XML blocks - CodeAct snippet injection via Monty NameLookup - Skill extraction mission replaces playbook extraction - ThreadManager.set_skill_selector() for runtime wiring **Bridge + migration**: - skill_migration.rs: v1 SKILL.md → v2 MemoryDoc (idempotent) - init_engine() migrates v1 skills, builds SkillSelector - src/skills/mod.rs → re-export shim **E2E test** (tests/engine_v2_skill_codeact.rs): - Full CodeAct loop: skill selected → LLM returns Python code → Monty executes http() → mock returns canned GitHub JSON → FINAL() terminates → thread completes with canned data - GitHub SKILL.md in skills/github/ as reference implementation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Documenting research around how to extend to more integrations * docs: update engine-v2-architecture for missions and skills - Replace "Reflection Pipeline" with "Learning Missions" (self-improvement, skill-extraction, conversation-insights) - Add "Skills System" section covering ironclaw_skills crate, deterministic selection pipeline, CodeAct integration, confidence tracking, v1 migration - Update MemoryDoc types table (add Skill, remove Playbook as primary) - Update Integration Scaling section: Skills replace Capabilities-as-knowledge as the concrete implementation - Update example from Capability YAML to SKILL.md format with credentials - Fix thread state machine (remove Reflecting state) - Update key files table and test counts - Add self-improvement feedback loop diagram Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: clean up legacy playbook references in engine crate - Rename PLAYBOOK_MIN_STEPS/ACTIONS → SKILL_EXTRACTION_MIN_STEPS/ACTIONS - Fix pattern DB uses DocType::Note instead of DocType::Playbook - Update CLAUDE.md: skill-extraction mission, DocType list, module map Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(skills): credential specs in skill frontmatter, HTTP tool hardening, mission leases Skills can now declare API credentials in YAML frontmatter (SkillCredentialSpec, SkillCredentialLocation, SkillOAuthConfig, ProviderRefreshStrategy). Valid specs are registered into SharedCredentialRegistry at startup; the HttpTool auto-injects credentials for matching hosts — same zero-exposure model as WASM tools. HTTP tool security hardening: - Block LLM-provided auth headers for hosts with registered credentials - Return structured authentication_required error for missing credentials - Strip sensitive response headers (Set-Cookie, WWW-Authenticate, Authorization) - Scan response body through LeakDetector before returning to LLM Mission capability leases: registered mission_create/list/fire/pause/resume/delete as a "missions" capability so threads receive leases. Removed routine_* aliases from effect adapter — descriptions mention "routine" for LLM intent mapping. Includes 10 integration tests (tests/skill_credential_injection.rs) covering the full pipeline: YAML parsing → validation → registry → HttpTool wiring → per-user isolation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(engine): remove legacy Playbook doc type, superseded by Skill Drop DocType::Playbook variant and all references — playbook extraction mission was already renamed to skill extraction in the previous session. Updates CLAUDE.md, architecture docs, context builder, retrieval weights, mission comments, and store adapter path mapping. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(engine): move skill selection and injection to Python orchestrator Skill selection was in Rust (SkillSelector in loop_engine.rs) — now it's in the Python orchestrator where the self-improvement mission can evolve it. Rust provides data access via two new host functions: - __list_skills__() — loads DocType::Skill MemoryDocs from Store - __record_skill_usage__(doc_id, success) — confidence tracking Python orchestrator handles everything else: - score_skill() — keyword/tag/confidence scoring (~40 lines) - select_skills() — budget-aware top-N selection (~15 lines) - format_skills() — XML block injection into system prompt (~20 lines) - Injection at step 0 with active_skill_ids stored in state Removed from Rust: - SkillSelector field + builder on ExecutionLoop and ThreadManager - format_skills_section() from prompt.rs - Rust-side skill injection block in loop_engine.rs - SkillSelector wiring in bridge/router.rs E2E test updated: skills stored in TestStore, Python orchestrator finds them via __list_skills__() and injects based on goal keywords. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: annotate v1-only code for removal after migration Mark modules and functions that exist solely for the v1 agent with "remove after v1 migration" notes: - src/skills/mod.rs ��� shim, attenuation, credential registration - src/skills/attenuation.rs — trust-based tool filtering (v1 only) - ironclaw_skills: selector, gating, registry, catalog modules - ironclaw_engine: skill_selector.rs (superseded by Python orchestrator) - src/bridge/skill_migration.rs — one-time v1→v2 conversion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(engine): remove unused skill_selector.rs Rust-side skill selection was moved to the Python orchestrator in7f87d179. This module had no production callers — only its own tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(skills): compile-time skill bundling infrastructure Add support for embedding skills into the binary at compile time: - build.rs: embed_skills() collects skills/*/SKILL.md into embedded_skills.json - src/skills/bundled.rs: loads embedded skills via include_str! - SkillRegistry: with_bundled_content(), load_from_content(), step 4 in discover_all() - Bundled skills are Trusted (ship with binary), lowest discovery priority - 4 new tests for bundled loading, user override, gating, and removal rejection - Cargo.toml: add serde_json build-dependency [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): non-blocking auth signal, NeedAuthentication flow, timeout safety When the HTTP tool detects a missing credential for a registered host: 1. EffectBridgeAdapter emits SSE AuthRequired event (best-effort, for connected frontends — silently dropped for missions/background threads) 2. Error flows back to LLM as normal ActionResult (non-blocking) 3. LLM tells the user to authenticate This avoids the blocking interruption approach which would hang mission threads and sub-threads that have no channel context. Engine additions: - EngineError::NeedAuthentication variant for structured auth failures - ThreadOutcome::NeedAuthentication for batch interruption when needed - structured.rs handles NeedAuthentication by interrupting the batch (stops subsequent calls, returns outcome to orchestrator) - Auth callback on EffectBridgeAdapter (optional, set by router for SSE) - extract_credential_name parser for HTTP tool error messages - routine_* tools added to is_v1_only_tool blocklist Safety: added 5-minute timeout to await_thread_outcome to prevent infinite hangs (e.g. after denied tool approval where thread fails to resume). Tests: 3 structured executor tests (NeedAuthentication interrupts batch, stops subsequent calls, regular errors don't interrupt) + 7 effect adapter tests (credential extraction, callback firing, v1-only tools). Also adds Linear API skill (skills/linear/SKILL.md). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): platform self-awareness, event pipeline fix, globals() builtin, prompt templates Session 9 changes driven by live trace analysis: - CodeAct event pipeline: handle_execute_code_step now transfers CodeExecutionResult events to thread.events and broadcasts via event_tx (fixes false-positive no_tools_used trace warnings) - Monty globals()/locals() builtins: returns dict of available action names from capability leases, enabling "tool_name" in globals() probing - PlatformInfo injection into system prompts (version, LLM backend, model, database, channels, owner, repo URL) - Mission goal prompts moved to prompts/*.md files (include_str! pattern) - /expected command for triggering self-improvement from user feedback - Session 9 development history Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(engine): auto-approve http calls with registered credentials in v2 The v1 approval flow (interactive yes/no prompt) doesn't exist in v2. When the http tool returned UnlessAutoApproved for credentialed hosts, the effect adapter blocked with LeaseDenied — making all skill-based API calls fail. Fix: credential-backed http calls bypass the v1 approval check. The user authorized by storing the credential; the v1 interactive prompt is redundant in v2's lease-based security model. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(ui): show activated skills in CLI and gateway End-to-end skill activation display: 1. Python orchestrator emits __emit_event__("skill_activated", skill_names=...) after select_skills() picks skills for the conversation 2. Rust host function parses the comma-separated names into EventKind::SkillActivated 3. Router forwards to channels as StatusUpdate::SkillActivated 4. REPL renders: ◈ skills: github, linear (cyan) 5. Web gateway emits AppEvent::SkillActivated SSE event for frontend display Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli): show auth prompt in REPL when credential is missing The AuthRequired SSE event was emitted but only reached the web gateway. The REPL never saw it because it receives events through forward_event_to_channel which converts ThreadEvents to StatusUpdates. Fix: when forward_event_to_channel sees an ActionFailed with "authentication_required" in the error, emit StatusUpdate::AuthRequired to the channel. Also add AuthRequired/AuthCompleted rendering to the REPL (was missing — fell through to unmatched arm). CLI now shows: ⚿ Authentication required: github_token Store the credential with: ironclaw secret set <name> <value> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(ui): show tool arguments in CLI and gateway Add params_summary to ActionExecuted/ActionFailed events so the CLI and gateway can display what tools are doing: ● http(https://api.github.com/repos/nearai/ironclaw/issues) ● web_search(latest AI news) ● memory_read(HEARTBEAT.md) The summarize_params() helper extracts the most relevant argument per tool type (URL for http, query for search, path for memory, etc.) and truncates to 80 chars. Sensitive params are not included. Router forwards the summary in both StatusUpdate (CLI/REPL) and AppEvent (web gateway SSE) display names. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: handle Python None params in http tool, add params_summary to CodeAct dispatch Two fixes from live testing: 1. http tool: treat null headers/body as empty (Python's None becomes JSON null via Monty). Previously headers=None errored with "'headers' must be an object or array of {name, value}". 2. scripting.rs: compute params_summary before dispatching actions in the CodeAct path (was always None). Now http calls show their URL in the CLI: ● http(https://api.github.com/repos/.../issues) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: remove glob re-exports, fix clippy warnings, clean up duplicates - Remove `pub use ironclaw_safety::*` from src/safety/mod.rs and migrate all 20+ call sites to import directly from `ironclaw_safety` - Remove `pub use ironclaw_skills::*` from src/skills/mod.rs and migrate all 15+ call sites to import directly from `ironclaw_skills` - Fix 4 clippy warnings: 2 shadow imports, 2 collapsible if-let chains - Add missing SkillActivated arm to WASM channel StatusUpdate match - Remove duplicate AuthRequired/AuthCompleted arms in repl.rs - Update CLAUDE.md extracted crates guidance and prompt template rule - Fix bench imports (safety_check, safety_pipeline) 46 files changed, zero warnings, 3836 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(auth): guided credential flow — prompt for token and retry When a thread completes with authentication_required, the router enters "auth mode" for that user: 1. Detects credential_name from the error in the thread response 2. Looks up setup_instructions from the skill's credential spec 3. Emits AuthRequired to CLI/gateway with instructions 4. Stores PendingAuth — next user message is treated as a token 5. Stores the token in SecretsStore 6. Retries the original user request automatically CLI flow: › create an issue in github ⚿ Authentication required: github_token Create a PAT at https://github.com/settings/tokens Paste your token below (or type 'cancel'): › ghp_abc123... ✓ github_token authenticated: Credential stored. Retrying... ● http(https://api.github.com/repos/.../issues) Issue created: https://github.com/... Gateway flow: same but AuthRequired SSE event shows the auth modal. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(e2e): skill-based OAuth flow tests 6 E2E tests covering the full skill credential lifecycle via the gateway API: - test_github_skill_loaded: github skill with credential spec loaded - test_no_github_token_initially: no stored secrets before auth - test_http_tool_returns_auth_required: http tool signals missing cred - test_guided_auth_flow: request → auth prompt → paste token → retry - test_auth_required_sse_event: SSE stream includes auth/skill events - test_different_users_isolated: per-user credential scoping Includes mock API server (aiohttp) requiring Bearer auth with token tracking for assertions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: document Monty runtime limitations in CodeAct prompt, fix new-thread read-only - Add "Runtime environment" section to codeact_preamble.md documenting Monty's restrictions: no stdlib imports, single imports only, no classes/ with/match/del/yield, available builtins and modules, workarounds - Add MONTY.md tracking current pin, all limitations, upgrade process, and changelog for future Monty updates - Fix gateway createNewThread() not resetting read-only state — new threads now eagerly enable chat input instead of waiting for async loadThreads() callback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(engine): transition thread to Waiting on NeedApproval The orchestrator Python returned {"outcome": "need_approval"} without calling __transition_to__("waiting"), leaving the thread in Running state. When the user later approved/denied, resume_thread rejected it with "thread is not resumable from Running". - Add __transition_to__("waiting", "approval needed") in both code-step and action-call approval paths in default.py - Add Rust safety net in loop_engine.rs: if orchestrator returns NeedApproval but thread isn't Waiting, force the transition Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine): restructure workspace storage for human readability Rewrite HybridStore (src/bridge/store_adapter.rs) to produce a developer-friendly workspace layout: - Knowledge docs use frontmatter+markdown with slugified filenames instead of UUID.json with wrapped structs - Orchestrator code, prompt overlays, and failure tracker grouped under engine/orchestrator/ - Missions nested under their project in named folders with room for working files alongside mission.json - Runtime state (threads, leases, events) under engine/.runtime/ - Terminal threads archived to compact summaries, dead leases cleaned on startup - Auto-generated engine/README.md with knowledge counts, mission status, and thread stats Also includes: /expected command, approval state fix, platform self-awareness, Monty limitations in preamble, prompt template extraction. See docs/development-history.md Session 10 for details. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(skills): add commitments system — active intake for personal AI assistant Introduces 9 SKILL.md files that implement a complete commitments tracking system using existing workspace storage, routines, and tools — no Rust code changes or new database tables required. Core skills: - commitment-setup: one-time bootstrap (workspace structure + routines) - commitment-triage: in-conversation signal extraction and commitment management - commitment-digest: periodic summary composition and delivery Supporting skills: - decision-capture: detect and record decisions with rationale - delegation-tracker: track delegated items and generate follow-ups - idea-parking: park, resurface, and promote ideas Persona bundles: - ceo-assistant: delegation-heavy, meeting prep, 3x/day triage, responsibility-grouped digests - content-creator-assistant: content pipeline stages, 6h trend expiry, cross-platform cascades - trader-assistant: position-aware scoring, contradictory signal detection, trade journaling Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix cargo fmt in bridge/router.rs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(skills): add companion skill references and explicit workspace setup to persona bundles Each persona setup skill now: - Lists companion skills (commitment-triage, decision-capture, etc.) with activation triggers so the agent knows what activates during conversation - Warns if companion skills are missing from the skills/ directory - Includes explicit workspace creation steps (README schema, directory placeholders) instead of vague "same as commitment-setup" references - Bumps max_context_tokens for content-creator and trader to fit the additional setup instructions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(skills): enrich commitments with plan parity and add requires.skills support Skills updates (v0.2.0): - Switch all routine_create calls to mission_create (v2 engine primitive) - Add immediacy (realtime/prompt/batch) and expires_at to signal schema - Add resolution_path (agent_can_handle/needs_reply/needs_decision/note_only) and stale_after/resolved_by to commitment schema - Add outcome tracking fields to decision schema - Add intelligence destination — informational signals write durable MemoryDocs - Add trust calibration section to README schema (start conservative) - Digest now ends with "Did I miss anything?" to catch false negatives - Agent offers to handle agent_can_handle items with explicit user approval Rust changes: - Add requires.skills field to GatingRequirements for skill dependency declarations. Missing skills produce warnings (not failures) so the skill still loads but the agent knows to surface missing dependencies. - Log skill dependency warnings during load_and_validate_skill and load_from_content - Add test: test_skill_dependencies_produce_warnings Persona bundles now declare their companion skill dependencies via metadata.openclaw.requires.skills in YAML frontmatter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(skills): lift requires to top-level field, implement chain installation Moves skill gating requirements from `metadata.openclaw.requires` to a top-level `requires` field on `SkillManifest`. The legacy nested path still works via `effective_requires()` merge — existing skills with the old format load without changes. Chain installation in `skill_install` tool: - After installing a skill, checks `requires.skills` for dependencies - Missing dependencies are auto-fetched from the catalog and installed - Reports chain_installed, chain_install_failed, and missing_dependencies in the tool output so the agent can inform the user Changes: - types.rs: Add `requires: GatingRequirements` to SkillManifest, add `effective_requires()` merge method - registry.rs: Use effective_requires() instead of nested metadata path - skill_tools.rs: Chain-install missing skill dependencies after install - cli/skills.rs: Display requires.skills in skill info output - All SKILL.md files: Use top-level `requires.skills` instead of `metadata.openclaw.requires.skills` Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(skills): explicit /skill-name activation in messages Users can now write /github or /file-issues anywhere in their message to force-activate a skill. The /skill-name is replaced with the skill's description so the sentence reads naturally for the LLM: "fetch issues from /github" → "fetch issues from GitHub API" "please /file-issues for all bugs" → "please file detailed GitHub issues for all bugs" Implementation: - extract_skill_mentions() in selector.rs scans for /name patterns, matches against available skills, returns matched skills + rewritten message - select_active_skills() returns (skills, rewritten_message) — explicit mentions merged with score-based selection - dispatcher.rs rewrites the last user message in LLM context with expanded text - 8 tests covering: basic mention, description expansion, hyphenated names, multiple mentions, unknown skills, URLs not matched Also includes: seed_orchestrator_v0() for workspace visibility of compiled-in orchestrator code. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(engine): wire NeedAuthentication and NeedApproval through v2 CodeAct path Production traces revealed tool result desync on RequireApproval (no ActionResult message → OpenAI 400), auth flow not triggering in CodeAct (EffectAdapter returned Ok instead of Err(NeedAuthentication)), and HTTP tool blocking unauthenticated requests. Fixes: - Add emit_and_record() to RequireApproval branch in handle_execute_action - Wire NeedAuthentication through scripting.rs DispatchResult, orchestrator host functions, default.py, loop_engine safety net - Add EngineError::NeedApproval variant; effect adapter returns it instead of LeaseDenied for tools needing approval - HTTP tool: inject-if-available (proceed without auth, error only on 401) - HTTP_ALLOW_LOCALHOST env flag for E2E testing with mock servers - host_matches_pattern supports port in pattern (127.0.0.1:8080 matches host_str() output 127.0.0.1) - CodeAct postamble: error recovery guidance - Orchestrator user_id from thread.metadata instead of hardcoded "orchestrator" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(bridge): v1/v2 history, approval routing, cancel cleanup Multiple v2 engine bridge fixes discovered by E2E tests: - Write response to v1 DB for ALL thread outcomes (not just Completed), so history API shows NeedApproval/NeedAuthentication responses - Remove v1 thread_id hint from pending_approval lookup (v1/v2 use different UUID spaces) - Add has_pending_auth() check in agent_loop: route "cancel"/"no" through handle_with_engine when PendingAuth is active (SubmissionParser parsed "cancel" as ApprovalResponse, bypassing auth flow) - Add engine_thread_id to PendingAuth; stop_thread on cancel - Write cancel response to v1 DB - NeedAuthentication handler enters guided auth flow with setup hints Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(e2e): comprehensive v2 engine test suite (12 tests, 5 files) E2E tests for the v2 engine covering auth flow, approval lifecycle, error handling, and edge cases. Uses mock API servers with strict token validation, dedicated ironclaw server fixtures per module, and the mock LLM's tool call pattern system. Tests: - Auth flow: skill activation, NeedAuthentication → token → retry, credential persistence across threads, cancel during auth, empty token treated as cancel, special character injection safety - Approval: approve yes (text-based), deny, always (persists across threads), prompt mentions tool name - Error handling: max iterations (30 step limit), tool intent nudge (LLM recovery after "let me search") Infrastructure: - mock_llm.py: runtime-configurable github_api_url, tool call patterns for issues/loop/drive, canned responses for intent nudge - HTTP_ALLOW_LOCALHOST=true + SECRETS_MASTER_KEY in fixtures - Separate server instances for cancel tests (cancel contaminates conversation state) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add Session 11 — E2E test suite + engine hardening Documents 14 bugs found across two production traces and E2E test execution, the test infrastructure design (mock servers, dedicated fixtures, HTTP_ALLOW_LOCALHOST), and the architecture evolution from trace analysis → code fix → test to prevent regression. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(skills): address PR review — TOCTOU race, unbounded loop, lock thrashing Fixes three issues from code review on #1736: 1. TOCTOU race: re-check has() under the write lock before commit_install to handle concurrent installs 2. Unbounded loop: cap chain dependencies at 10 to bound total fetch time 3. Lock thrashing: batch has() checks and install_target_dir() under a single read lock before entering the fetch loop [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(auth): kernel-level pre-flight auth gate for engine v2 Transform authentication from a reactive post-execution error to a proactive pre-flight check. The EffectBridgeAdapter now checks credentials BEFORE executing tool calls, preventing wasted HTTP requests and 401 errors from reaching the LLM. Key changes: - New AuthManager (src/bridge/auth_manager.rs) centralizes credential checking, setup instruction lookup, and tool readiness queries - Pre-flight auth gate in execute_action() checks SharedCredentialRegistry + SecretsStore before tool execution - Post-install auth pipeline: after tool_install, kernel auto-checks readiness and initiates auth flow or appends setup instructions - tool_auth and tool_activate filtered from v2 LLM tool list and blocked in execute_action() — auth is kernel-level in v2 - Text-based auth detection kept as defense-in-depth fallback with tracing when it fires - Setup instruction lookup deduplicated via AuthManager - ExtensionManager gains check_tool_auth_status_pub() for auth queries Also fixes pre-existing DocType::Plan exhaustiveness errors in the engine crate and re-exports PlanStepDto from ironclaw_common. Includes 10 unit tests (AuthManager + is_v1_auth_tool) and 5 E2E tests covering pre-flight blocking, auth-then-retry, credential persistence, v1 auth tools hidden, and auth cancellation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add Session 12 — kernel-level auth rework decisions and rationale Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(plan): autonomous plan mode via v2 primitives (MemoryDoc, Mission, SSE) Add plan mode for autonomous long-running task execution, composing existing v2 engine primitives rather than new engine states. Inspired by OpenAI Codex's update_plan checklist and Claude Code's file-based plan mode — both enforce planning through prompts, not tool removal. Engine: DocType::Plan variant for MemoryDoc (project-scoped, retrievable). Events: PlanUpdate SSE event with PlanStepDto for live checklist rendering. Tool: plan_update tool broadcasts structured plan progress via SSE. Command: /plan (create/approve/status/revise/list) rewrites to UserInput with [PLAN MODE] prefix to activate the plan-mode skill. Skill: skills/plan-mode/SKILL.md defines full plan protocol — creation (memory_write), approval (mission_create + mission_fire), execution (step-by-step with plan_update), and revision flows. UI: Inline chat checklist widget with status badges, step icons (checkmark/spinner/circle), results, and progress summary. Tests: 5 E2E scenarios + mock LLM patterns + helper selectors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(skills): add developer bundle, generalize workflow orchestrator, add tech debt tracker New skills: - developer-assistant: multi-repo setup, dev-tuned missions (morning brief, stale PR check, weekly retro, tech debt resurface, decision outcome check) - github-workflow: generalized from ironclaw-workflow-orchestrator, works for any GitHub repo with parameterized templates - project-setup: "add repo owner/repo" creates workspace project entity and installs per-repo workflow missions - tech-debt-tracker: passive detection from conversation + PR review comment scanning, severity/category classification, promote-to-commitment flow Removed: - ironclaw-workflow-orchestrator: replaced entirely by github-workflow Updated all persona bundles: - Remove timezone questions (timezone comes from channel automatically) - CEO, content-creator, trader skills updated Projects are now first-class workspace entities under projects/<owner>-<repo>/ with metadata (maintainers, branches, AI agent authors, workflow status). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(skills): integrate v2-architecture features into commitment skills Update skills to reference capabilities from the v2-architecture merge: - /plan command: developer-assistant retro and tech-debt missions now suggest "/plan <description>" for complex items. Tech-debt-tracker promotion flow suggests /plan for multi-step refactors. - /skill-name activation: developer-assistant confirm message lists /commitment-digest, /tech-debt-tracker as quick commands - CEO confirm message mentions /plan for complex initiatives Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(skills): add security, QA, review readiness, and product prioritization skills Inspired by gstack's specialist role model and process completeness tracking. New skills: - security-review: OWASP-based security audit with fix-first model (auto-fix obvious issues, escalate ambiguous ones), health scoring, FP tracking - qa-review: test coverage analysis, edge case identification, test plan generation, regression risk assessment with health scoring - review-readiness: PR readiness dashboard tracking which reviews are complete per branch (code review, tests, security, QA, linting) with merge gating - product-prioritization: evidence-based feature scoring with anti-sycophantic forcing questions, dual effort estimates (human vs AI-assisted time), demand×3 + impact×2 + alignment / effort scoring, user feedback analysis Updated commitment schema: - decision_type: mechanical (auto-act) | taste (auto-act, surface) | challenge (always ask) - effort_human / effort_assisted: dual time estimates - Autonomous resolution now respects decision_type classification Updated developer-assistant bundle: - Requires all 4 new skills - Calibration includes decision classification framework, effort compression principle ("AI makes completeness cheap"), review readiness tracking - Confirm message lists /security-review, /qa-review, /product-prioritization Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(skills): flatten requires to top-level SkillManifest field Remove the metadata.openclaw.requires nesting from staging in favor of the flat manifest.requires path chosen on this branch. Drop SkillMetadata and OpenClawMeta wrapper types. Update all call sites, test YAML, and re-exports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: cargo fmt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(skills): address PR #1736 review feedback - Delete dead-code duplicate capability/skill_tracker.rs (canonical version lives in memory/skill_tracker.rs; this one was never wired into capability/mod.rs) - Delete dead-code executor/intent.rs (never wired into executor/mod.rs) - Correct record_usage docstring to match actual behavior (returns Err on missing doc/invalid metadata; does not log-and-swallow) - Remove unconditional companion-skill warnings from gating; the registry layer owns missing-skill detection since gating has no visibility into installed skills - Chain-install: surface MAX_CHAIN_DEPS overflow as skipped_dependencies instead of silently dropping deps >10 - Chain-install: distinguish transient fetch errors (network/5xx) from genuine catalog misses (HTTP 404/410); transient errors now surface under chain_install_failed with the error message - trader-assistant SKILL: renumber config question list (4→5, was 4→6) - trader-assistant SKILL: correct trade-journal README path from bare decisions/ to commitments/decisions/ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(test): live/replay tests for commitment persona bundles Add three end-to-end persona tests (CEO, content creator, trader) that exercise the full skill activation -> workspace setup flow against a real LLM with recording, and replay deterministically from committed trace fixtures. ### Engine fixes exposed by the tests - **Engine v2 skill scoring missed regex patterns.** The Python orchestrator's `score_skill` only considered keywords and tags, while the v1 selector (and every SKILL.md in the repo) relies on regex patterns like `(?i)I'm a (CEO|manager|...)` for the strongest activation signal. Persona bundles that matched via patterns alone scored 0 and never activated. Added `__regex_match__` host function (backed by the `regex` crate) and regex pattern scoring (20 pts per match, cap 40) to `select_skills`, matching v1 parity. ### Test infrastructure - `TestRigBuilder::with_skills_dir(dir)` to load real SKILL.md files from the repo's `skills/` directory instead of an empty tempdir. The rig now preserves AppBuilder's skill registry rather than blindly overwriting it with an empty one when skills are configured. - Test channel user_id now matches `config.owner_id` so engine v2 resolves the thread to the owner's default project (where skills are migrated) instead of creating a fresh per-user project with no skills. - `TestRig::loaded_skill_names()` and `active_skill_names()` for asserting on the selection pipeline. - `LiveTestHarnessBuilder::with_skills_dir()` wires the above into the live harness. - `hydrate_llm_secrets_into_env()` in the live harness decrypts `llm_nearai_api_key` / `llm_anthropic_api_key` / `llm_openai_api_key` from the user's real secrets store and exports them as env vars before the provider chain is built, bypassing NEAR AI's interactive OAuth prompt in test mode. ### Committed fixtures Each persona test has a recorded trace (`.json`) and a human-readable session log (`.log`) under `tests/fixtures/llm_traces/live/`. Live mode (`IRONCLAW_LIVE_TEST=1`) hits the configured LLM and refreshes these; replay mode (default) plays them back deterministically. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(engine): repair stale unit tests against orchestrator-driven flow Five engine unit tests had been broken since v2 landed but went unnoticed because they exercise the legacy ExecutionLoop path. The tests encoded assumptions that no longer hold: - `loop_engine::action_then_text`, `tool_intent_nudge_injected`, and `codeact_multi_step` checked `thread.messages` for the orchestrator's working transcript. The Python orchestrator now persists working messages into `thread.internal_messages` (via `sync_runtime_state`), while `thread.messages` only carries the system prompt and the final assistant response. Tests now check `internal_messages`. - `executor::trace::trace_serializes_approval_request_payload` indexed `trace.events[0]` for the manually-pushed `ApprovalRequested` event, but `Thread::add_message` now records a `MessageAdded` event for each call, so the approval lives further down the list. Find by kind instead. Also relax the parameter-map serialization assertion to check key presence rather than exact serialized form. - `mission::system_mission_requires_system_user_to_manage` asserted the engine rejects non-owner pause/resume on shared missions. The current security model is intentionally permissive at the engine level — the web handler enforces admin role before forwarding the call (per the `pause_mission` doc comment). Test renamed and rewritten to pin down the contract: shared missions accept any user at the engine layer. All 267 ironclaw_engine unit tests now pass. No production code change. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(personas): multi-turn workflow tests + strict trace error checks Replaces the shallow setup-only persona tests with multi-turn workflows that exercise the actual commitment system: setup → real-world capture → workspace verification. Each test reads the temp workspace via the rig to confirm that meeting outcomes, content publications, and market signals actually land as files in `commitments/` with the right names. ### New scenarios - **CEO**: setup → "Sarah delivers Q2 budget by Friday, Bob drafts term sheet by Tuesday" → verify both commitments captured under commitments/open/ with assignee + deadline references. - **Content creator**: setup → "Just published episode 47, need TikTok cuts and Twitter thread by tomorrow, also park an idea about debugging legacy code" → verify distribution commitments and parked idea files exist. - **Trader**: setup with positions inline → "AAPL/TSMC partnership signal + closed SPY puts at $4.20" → verify signal file in signals/pending/ AND decision in decisions/. ### Strict trace error detection The `LiveTestHarness` now exposes `finish_strict()` which scans the captured status events and fails the test if any non-benign tool failure is found. Three categories of "noise" are explicitly allowed and bypass the strict check: - **Workspace probing**: `Document not found` from memory_read on a file the agent is checking before writing. - **Wrong tool selection**: write_file → memory_write redirects. - **Wrong patch params**: memory_write missing new_string with old_string. Engine bugs (Python SyntaxError, missing leases, registry 404s for locally-loaded skills) still fail the test. ### Engine bugs caught and fixed by the strict check 1. **Effect adapters wrap tool errors as `Ok(ActionResult { is_error: true })` but the engine emitted `ActionExecuted` regardless.** Three call sites in `scripting.rs`, `orchestrator.rs`, and `structured.rs` now inspect `is_error` and emit `ActionFailed` accordingly so traces, observers, and approval flows see failures correctly. 2. **CodeAct SyntaxErrors were silent.** When `__execute_code_step__` returns `had_error: true` (e.g. Monty Python failed to parse the snippet), the orchestrator now emits an `ActionFailed` event with action_name `__codeact__` so the failure surfaces in traces. 3. **`extract_code_block` matched bare ``` blocks containing markdown.** The LLM emitted ``` blocks of markdown lists as examples, the bridge's heuristic extracted them as Python, and Monty crashed with SyntaxError on `- TICKER: SIZE, ...`. Added a `looks_like_python` heuristic that requires bare blocks to contain Python tokens (assignments, function calls, keywords, comments) — markdown lists, tables, blockquotes, prose, and numbered lists are now rejected. Five new unit tests pin the heuristic. 4. **`FINAL` emitted as a structured tool call has no lease.** The agent sometimes calls FINAL via tool_calls instead of inside a CodeAct block. The orchestrator's Python loop now intercepts any tool call named `FINAL` before dispatching to `__execute_actions_parallel__` and treats it as a completion signal with the answer pulled from `params.answer/result/content/text` (or the response content). 5. **`skill_install` for an already-loaded skill 404s against the registry.** Local SKILL.md files aren't published to ClawHub, so any agent that mis-interprets an active persona bundle as "needs to be installed" gets a 404. `skill_install` is now idempotent: if the registry already has a skill with the requested name, return success immediately without hitting the catalog. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Expand live persona commitment workflows * fix skills install and CI regressions * style live harness formatting * fix(skills): address PR #1736 review feedback - skill_tools chain install: check `guard.has(&dep_name)` before applying the MAX_CHAIN_DEPS cap so already-installed deps don't get reported under `skipped_dependencies`. - skill_tools chain install: when a concurrent install wins the race after we've already written the SKILL.md to disk, clean up the orphan dir instead of leaving it on the filesystem. Reworked the commit block so the non-Send RwLockWriteGuard is dropped before the cleanup `.await`. - gating: drop the always-empty `GatingResult.warnings` field — the registry layer is the right place to surface missing companion-skill warnings, and the field promised behavior that was never implemented. - parser: detect the legacy `metadata.openclaw.requires` SKILL.md frontmatter shape and emit a `tracing::warn!` so authors don't silently lose their gating/dep config when serde drops the unknown nested keys. - default.py orchestrator: filter FINAL out of `action_calls` passed to `append_message` so the message history doesn't record a FINAL action with no matching ActionResult (would confuse context replay on resume). - default.py orchestrator FINAL handler: handle string `params`, add a `value` fallback (common LLM pattern), and emit a `final_fallback` trace event when falling back to `response.content` so the ambiguity is visible in traces. - live_harness: fix misleading "read-only" docstring on the libsql DB open path — libsql doesn't expose RO mode here; document the actual guarantee (only `get_decrypted` is called). Adds a regression test for legacy `metadata.openclaw.requires` detection in `crates/ironclaw_skills/src/parser.rs`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(orchestrator): drop duplicate FINAL tool calls Follow-up toa947fd0dfor PR #1736 review comment 3053486977. The previous draft would let a second FINAL call fall through into `executable_calls` and try to run as a normal action (failing with a lease error). Now any FINAL beyond the first is dropped via `continue` so duplicate FINAL emissions degrade gracefully. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(skills,orchestrator): address PR #1736 follow-up review feedback - skill_tools `already_installed` payload: include `trust: "installed"` so the response shape is consistent with the success / chain-install paths. - skill_tools chain install: recover from `RwLock` poisoning via two helper wrappers (`registry_read` / `registry_write`) that log loudly but `into_inner()` instead of permanently bricking `skill_install`. The registry holds replace-on-success state, so recovery is safe. - skill_tools `skipped_dependencies_message`: clarify that `MAX_CHAIN_DEPS` is an *attempt* cap (intentional bound on fetch time from large or malicious manifests), not a success cap, and tell the caller to retry via a follow-up `skill_install`. - llm_adapter `looks_like_python`: tighten the function-call heuristic so it requires an identifier-style char immediately before `(`. Fixes the false positive where markdown links `[text](url)` and prose like "See (docs)" inside a bare ``` block were forwarded to Monty as code. Adds regression test `bare_backtick_markdown_link_is_rejected`. - ironclaw_engine `__regex_match__`: document the ReDoS-safety reliance on the default `regex` crate's linear-time matching guarantee, with a top-of-crate comment warning future maintainers not to add `fancy-regex` to the dep tree without a wall-clock budget. - default.py FINAL handling: emit `duplicate_final_dropped` event when more than one FINAL call is co-emitted, and truncate the `response.content` fallback to 500 chars (with an explanatory suffix and event metadata) so a model dumping its full reasoning into a paramless FINAL doesn't ship thousands of tokens as the answer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(skills): address PR #1736 review (gating fast-path, skills cap, round-trip test) - gating::check_requirements: fast-path return when `bins`/`env`/`config` are all empty, avoiding a `spawn_blocking` + `which` subprocess call per skill load for skills with no subprocess-checkable requirements (the common case). - types: cap `requires.skills` at `MAX_REQUIRED_SKILLS_PER_MANIFEST = 10` via a new `GatingRequirements::enforce_limits()` method, mirroring the host-side `MAX_CHAIN_DEPS` so a hostile manifest can't cause unbounded queue growth in the chain installer before the downstream cap kicks in. Parser calls `enforce_limits()` alongside `activation.enforce_limits()`. Regression test: `test_requires_skills_is_capped_at_parse_time`. - skill_tools: add a round-trip integration test exercising the `install_dependencies=false` → `install_dependencies=true` flow via `install_missing_skill_dependencies`, verifying the pending deps are actually picked up on the second call (PR #1736 review 3058525543). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(skills,orchestrator): address PR #1736 security review - skill_tools chain install: stop enqueueing nested deps once `attempted >= MAX_CHAIN_DEPS`, plus a hard `MAX_CHAIN_QUEUE = 100` belt-and-braces cap. Prevents unbounded BFS queue growth from a hostile manifest with deep `requires.skills` fan-out. - skill_tools: override `SkillInstallTool::requires_approval` to return `Always` when `install_dependencies=true`, forcing a per-call approval prompt for chain installs. Single-skill installs retain `UnlessAutoApproved`. - skill_tools main install path: close the TOCTOU window between the pre-write `guard.has()` check (released read lock + async `prepare_install_to_disk`) and the `commit_install` write. Re-check under the write lock and return the idempotent `already_installed` response on the race (cleaning up the orphan on-disk copy first). - skill_tools chain installer: dependency-confusion guard — reject (and clean up) when `prepare_install_to_disk` returns a manifest name that does not match the requested `dep_name`. Blocks a hostile catalog entry from publishing a skill named "dep-a" whose manifest declares `name: evil-skill`. - orchestrator `handle_regex_match`: add `dfa_size_limit(MAX_REGEX_SIZE)` alongside `size_limit`. Fixes the doc comment that referenced a `compile_error!` guard I'd removed in a previous commit. - orchestrator tests: new `regex_match_host_function_is_callable_from_monty` regression test confirming Monty's NameLookup + FunctionCall dispatch actually reaches `handle_regex_match`. Teaches `eval_python_bool` to route `__regex_match__` calls through the real handler instead of stubbing to `None`. - ironclaw_skills registry: after `discover_all()` completes, walk every loaded skill and emit a `tracing::warn!` for each `requires.skills` entry not present in the registry. Workspace- discovered bundle skills (e.g., `ceo-assistant`) no longer silently degrade when their companion skills aren't installed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(skills,orchestrator,tests): address PR #1736 review (limits, ids, hydration) - skills/commitment-triage: trim activation lists to fit parser caps (`MAX_KEYWORDS_PER_SKILL = 20`, `MAX_PATTERNS_PER_SKILL = 5`). Previously declared 24 keywords + 11 patterns; the overflow was silently dropped at parse time, breaking some intended activations. Added a comment naming the limits so future edits stay in budget. - skills/developer-assistant: trim `requires.skills` from 13 to the cap of 10 (`MAX_REQUIRED_SKILLS_PER_MANIFEST`). Drops `qa-review`, `review-readiness`, and `product-prioritization` — they remain available via manual `skill_install`. - orchestrator CodeAct ActionFailed event: stop emitting empty `call_id`, which trips the `loop_engine.rs:1277` non-empty assertion. Use a synthetic `format!("codeact-step-{}", step_id.0)` so trace correlation still works for snippet failures that have no LLM-provided call_id. - live_harness `hydrate_llm_secrets_into_env`: stop hardcoding `owner_id = "default"`. Read `IRONCLAW_OWNER_ID` first, fall back to the legacy `"default"` scope only when the env var is unset/empty. Users with a non-default owner scope no longer get forced into interactive auth despite having seeded secrets in the real DB. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(orchestrator,skills): address PR #1736 paranoid-architect review - default.py: hoist `CHARS_PER_TOKEN` and `MESSAGE_OVERHEAD_CHARS` above the `estimate_context_tokens` definition (and the `FINAL(result)` entry-point call). The constants were previously defined at lines 846-847 — *after* the entry point — so any execution path that ran `compact_if_needed → estimate_context_tokens` would NameError. The bug was latent because `enable_compaction` defaults to false in CI; this commit removes the dead-zone landmine. - ironclaw_skills validation: add `validate_skill_version` enforcing a semver-ish character class (`[a-zA-Z0-9._\-+~]{1,32}`) and reject hostile values at parse time. Wired into `parser.rs` via a new `SkillParseError::InvalidVersion` variant. Closes the XML attribute injection vector through `format_skills` in default.py, which interpolates `version` directly into `<skill version="...">` and was the only field without character-class validation. Regression test: `test_parser_rejects_xml_breakout_in_version`. - orchestrator handle_execute_action + handle_execute_actions_parallel: replace the non-atomic `find_lease_for_action` + `consume_use` pair with the atomic `find_and_consume` after the policy check passes. Mirrors `structured.rs::execute_action_batch_with_results` and closes the TOCTOU window where two concurrent calls could each observe a one-use lease and both proceed to execute. - skill_credential_injection: add the three missing security tests (#6 LLM auth header rejection on credentialed host, #7 non-auth header passthrough on credentialed host, #8 LLM auth header passthrough on unregistered host) that were listed in the file doc comment but never implemented. Tests drive `HttpTool::execute` directly so they exercise the actual rejection branch in `http.rs:503-520`, not just the upstream `requires_approval` gate. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5.3 KiB
IronClaw E2E Tests
Browser-level end-to-end tests for the IronClaw web gateway using Python + Playwright.
Prerequisites
- Python 3.11+
- Rust toolchain (for building ironclaw)
- Chromium (installed via Playwright)
Setup
cd tests/e2e
pip install -e .
playwright install chromium
Build ironclaw
The tests need the ironclaw binary built with libsql support:
cargo build --no-default-features --features libsql
Run tests
# From repo root
pytest tests/e2e/ -v
# Run a single scenario
pytest tests/e2e/scenarios/test_chat.py -v
# With visible browser (not headless)
HEADED=1 pytest tests/e2e/scenarios/test_connection.py -v
Architecture
Tests start two subprocesses:
- Mock LLM (
mock_llm.py) -- fake OpenAI-compat server with canned responses - IronClaw -- the real binary with gateway enabled, pointing to the mock LLM
Then Playwright drives a headless Chromium browser against the gateway, making DOM assertions.
Scenarios
| File | What it tests |
|---|---|
test_connection.py |
Auth, tab navigation, connection status |
test_chat.py |
Send message, SSE streaming, response rendering |
test_skills.py |
ClawHub search, skill install/remove |
test_tool_approval.py |
Tool approval overlay (approve, deny, always, params toggle) |
test_sse_reconnect.py |
SSE reconnection handling, keepalive comments, restart recovery, stale reconnect IDs, and connection-limit coverage |
test_html_injection.py |
HTML injection security |
test_extensions.py |
Extensions tab: install, remove, configure, OAuth, auth card, activate |
Adding new scenarios
- Create
tests/e2e/scenarios/test_<name>.py - Use the
pagefixture for a fresh browser page - Use selectors from
helpers.py(updateSELdict if new elements are needed) - Keep tests deterministic -- use the mock LLM, not real providers
Live Persona Failure Notes
For the live 20+ turn persona workflows and recurring tool-misuse patterns seen
there, see LIVE_TOOL_FAILURES.md.
Mocking API responses with page.route()
For tabs that depend on external data (extensions, jobs, memory, routines), use
Playwright's page.route() to intercept the browser's HTTP requests to the
ironclaw gateway and return deterministic fixture JSON. This avoids needing
real installed binaries, live external services, or complex database setup.
Basic pattern
import json
async def test_something(page):
# 1. Set up route intercepts BEFORE navigation triggers the fetch
# Always use async def handlers — route.fulfill() is a coroutine and must be awaited.
async def handle_tools(route):
await route.fulfill(
status=200,
content_type="application/json",
body=json.dumps({"tools": [{"name": "echo", "description": "Echo"}]}),
)
await page.route("**/api/extensions/tools", handle_tools)
# 2. Navigate / interact to trigger the fetch
await page.locator('.tab-bar button[data-tab="extensions"]').click()
# 3. Assert on the rendered DOM
rows = page.locator("#tools-tbody tr")
assert await rows.count() == 1
Matching only the exact path
**/api/extensions matches http://host/api/extensions but NOT sub-paths
like http://host/api/extensions/install. For the bare list endpoint, add
a check inside the handler:
async def handle_ext_list(route):
path = route.request.url.split("?")[0]
if path.endswith("/api/extensions"):
await route.fulfill(json={"extensions": []})
else:
await route.continue_() # Let sub-paths through to the real server
await page.route("**/api/extensions*", handle_ext_list)
Mocking method-specific behaviour (GET vs POST)
async def handle_setup(route):
if route.request.method == "GET":
await route.fulfill(json={"secrets": [...]})
else: # POST
await route.fulfill(json={"success": True})
await page.route("**/api/extensions/my-ext/setup", handle_setup)
Counting calls (for reload tests)
calls = []
async def counting_handler(route):
calls.append(1)
await route.fulfill(json={"extensions": []})
await page.route("**/api/extensions", counting_handler)
# ... interact ...
assert len(calls) == 2 # called twice (initial + after some action)
Applying the pattern to other tabs
| Tab | Key API endpoints to mock |
|---|---|
| Jobs | /api/jobs, /api/jobs/{id}, /api/jobs/{id}/events |
| Memory | /api/memory/search, /api/memory/tree, /api/memory/read |
| Routines | /api/routines, /api/routines/{id}/runs |
Injecting state directly via page.evaluate()
For purely client-side UI (components rendered entirely in JS without API calls), call the JavaScript function directly to skip the network layer entirely:
# Show an approval card without needing a real tool execution
await page.evaluate("""
showApproval({
request_id: 'test-001',
thread_id: currentThreadId,
tool_name: 'shell',
description: 'Run something',
})
""")
This is the pattern used in most of test_tool_approval.py and parts of
test_extensions.py (auth card, configure modal). The waiting-approval
regression in test_tool_approval.py uses a real tool call instead so it can
exercise backend approval state.