diff --git a/.env.example b/.env.example
index 55c3adb5..3fd58ef6 100644
--- a/.env.example
+++ b/.env.example
@@ -31,7 +31,7 @@ DATABASE_POOL_SIZE=10
# Base URL defaults to https://private.near.ai
# 2. API key: Set NEARAI_API_KEY to use API key auth from cloud.near.ai.
# Base URL defaults to https://cloud-api.near.ai
-NEARAI_MODEL=zai-org/GLM-5-FP8
+NEARAI_MODEL=Qwen/Qwen3.5-122B-A10B
NEARAI_BASE_URL=https://private.near.ai
NEARAI_AUTH_URL=https://private.near.ai
# NEARAI_SESSION_TOKEN=sess_... # hosting providers: set this
@@ -78,7 +78,7 @@ NEARAI_AUTH_URL=https://private.near.ai
# === MiniMax ===
# LLM_BACKEND=minimax
# MINIMAX_API_KEY=...
-# MINIMAX_MODEL=MiniMax-M2.5
+# MINIMAX_MODEL=MiniMax-M2.7
# MINIMAX_BASE_URL=https://api.minimax.io/v1 # default (global); use https://api.minimaxi.com/v1 for China
# === Anthropic Direct ===
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index e7371677..2f885b16 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -174,7 +174,7 @@ jobs:
- name: Run E2E tests
run: |
- pytest tests/e2e/ -v -x --timeout=120
+ pytest tests/e2e/ -v --timeout=120
env:
RUST_LOG: ironclaw=info
RUST_BACKTRACE: "1"
diff --git a/.github/workflows/regression-test-check.yml b/.github/workflows/regression-test-check.yml
index 6d97c4ce..ef1a4d92 100644
--- a/.github/workflows/regression-test-check.yml
+++ b/.github/workflows/regression-test-check.yml
@@ -43,12 +43,42 @@ jobs:
fi
fi
- if [ "$IS_FIX" = false ]; then
- echo "Not a fix PR — skipping regression test check."
+ # --- 1b. Does this PR touch high-risk state machine or resilience code? ---
+ CHANGED_FILES=$(git diff --name-only "${BASE_REF}...${HEAD_REF}")
+
+ TOUCHES_HIGH_RISK=false
+ HIGH_RISK_PATTERNS=(
+ "src/context/state.rs"
+ "src/agent/session.rs"
+ "src/llm/circuit_breaker.rs"
+ "src/llm/retry.rs"
+ "src/llm/failover.rs"
+ "src/agent/self_repair.rs"
+ "src/agent/agentic_loop.rs"
+ "src/tools/execute.rs"
+ "crates/ironclaw_safety/src/"
+ )
+
+ for pattern in "${HIGH_RISK_PATTERNS[@]}"; do
+ if echo "$CHANGED_FILES" | grep -q "$pattern"; then
+ TOUCHES_HIGH_RISK=true
+ echo "High-risk file matched: $pattern"
+ break
+ fi
+ done
+
+ # Skip only if NEITHER condition holds — no double-firing on fix PRs
+ if [ "$IS_FIX" = false ] && [ "$TOUCHES_HIGH_RISK" = false ]; then
+ echo "Not a fix PR and no high-risk files changed — skipping."
exit 0
fi
- echo "Fix PR detected."
+ if [ "$IS_FIX" = true ]; then
+ echo "Fix PR detected."
+ fi
+ if [ "$TOUCHES_HIGH_RISK" = true ]; then
+ echo "High-risk state machine or resilience code modified."
+ fi
# --- 2. Skip label or commit message marker ---
if grep -qF ',skip-regression-check,' <<< ",$PR_LABELS,"; then
@@ -63,8 +93,6 @@ jobs:
fi
# --- 3. Exempt static-only / docs-only changes ---
- CHANGED_FILES=$(git diff --name-only "${BASE_REF}...${HEAD_REF}")
-
if [ -z "$CHANGED_FILES" ]; then
echo "No changed files — skipping."
exit 0
@@ -110,5 +138,12 @@ jobs:
fi
# --- 5. No tests found ---
- echo "::warning::This PR looks like a bug fix but contains no test changes. Every fix should include a regression test. Add a #[test] or #[tokio::test], or apply the 'skip-regression-check' label if not feasible."
+ if [ "$IS_FIX" = true ]; then
+ echo "::warning::This PR looks like a bug fix but contains no test changes."
+ fi
+ if [ "$TOUCHES_HIGH_RISK" = true ]; then
+ echo "::warning::This PR modifies high-risk state machine or resilience code but includes no test changes."
+ fi
+ echo "::warning::Please add tests exercising the changed behavior, or apply the 'skip-regression-check' label if not feasible."
exit 1
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 36c4d103..6aad4993 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,153 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.19.0](https://github.com/nearai/ironclaw/compare/v0.18.0...v0.19.0) - 2026-03-17
+
+### Added
+
+- verify telegram owner during hot activation ([#1157](https://github.com/nearai/ironclaw/pull/1157))
+- *(config)* unify config resolution with Settings fallback (Phase 2, #1119) ([#1203](https://github.com/nearai/ironclaw/pull/1203))
+- *(sandbox)* add retry logic for transient container failures ([#1232](https://github.com/nearai/ironclaw/pull/1232))
+- *(heartbeat)* fire_at time-of-day scheduling with IANA timezone ([#1029](https://github.com/nearai/ironclaw/pull/1029))
+- Reuse Codex CLI OAuth tokens for ChatGPT backend LLM calls ([#693](https://github.com/nearai/ironclaw/pull/693))
+- add pre-push git hook with delta lint mode ([#833](https://github.com/nearai/ironclaw/pull/833))
+- *(cli)* add `logs` command for gateway log access ([#1105](https://github.com/nearai/ironclaw/pull/1105))
+- add Feishu/Lark WASM channel plugin ([#1110](https://github.com/nearai/ironclaw/pull/1110))
+- add Criterion benchmarks for safety layer hot paths ([#836](https://github.com/nearai/ironclaw/pull/836))
+- *(routines)* human-readable cron schedule summaries in web UI ([#1154](https://github.com/nearai/ironclaw/pull/1154))
+- *(web)* add follow-up suggestion chips and ghost text ([#1156](https://github.com/nearai/ironclaw/pull/1156))
+- *(ci)* include commit history in staging promotion PRs ([#952](https://github.com/nearai/ironclaw/pull/952))
+- *(tools)* add reusable sensitive JSON redaction helper ([#457](https://github.com/nearai/ironclaw/pull/457))
+- configurable hybrid search fusion strategy ([#234](https://github.com/nearai/ironclaw/pull/234))
+- *(cli)* add cron subcommand for managing scheduled routines ([#1017](https://github.com/nearai/ironclaw/pull/1017))
+- adds context-llm tool support ([#616](https://github.com/nearai/ironclaw/pull/616))
+- *(web-chat)* add hover copy button for user/assistant messages ([#948](https://github.com/nearai/ironclaw/pull/948))
+- add Slack approval buttons for tool execution in DMs ([#796](https://github.com/nearai/ironclaw/pull/796))
+- enhance HTTP tool parameter parsing ([#911](https://github.com/nearai/ironclaw/pull/911))
+- *(routines)* enable tool access in lightweight routine execution ([#257](https://github.com/nearai/ironclaw/pull/257)) ([#730](https://github.com/nearai/ironclaw/pull/730))
+- add MiniMax as a built-in LLM provider ([#940](https://github.com/nearai/ironclaw/pull/940))
+- *(cli)* add `ironclaw channels list` subcommand ([#933](https://github.com/nearai/ironclaw/pull/933))
+- *(cli)* add `ironclaw skills list/search/info` subcommands ([#918](https://github.com/nearai/ironclaw/pull/918))
+- add cargo-deny for supply chain safety ([#834](https://github.com/nearai/ironclaw/pull/834))
+- *(setup)* display ASCII art banner during onboarding ([#851](https://github.com/nearai/ironclaw/pull/851))
+- *(extensions)* unify auth and configure into single entrypoint ([#677](https://github.com/nearai/ironclaw/pull/677))
+- *(i18n)* Add internationalization support with Chinese and English translations ([#929](https://github.com/nearai/ironclaw/pull/929))
+- Import OpenClaw memory, history and settings ([#903](https://github.com/nearai/ironclaw/pull/903))
+
+### Fixed
+
+- jobs limit ([#1274](https://github.com/nearai/ironclaw/pull/1274))
+- misleading UI message ([#1265](https://github.com/nearai/ironclaw/pull/1265))
+- bump channel registry versions for promotion ([#1264](https://github.com/nearai/ironclaw/pull/1264))
+- cover staging CI all-features and routine batch regressions ([#1256](https://github.com/nearai/ironclaw/pull/1256))
+- resolve merge conflict fallout and missing config fields
+- web/CLI routine mutations do not refresh live event trigger cache ([#1255](https://github.com/nearai/ironclaw/pull/1255))
+- *(jobs)* make completed->completed transition idempotent to prevent race errors ([#1068](https://github.com/nearai/ironclaw/pull/1068))
+- *(llm)* persist refreshed Anthropic OAuth token after Keychain re-read ([#1213](https://github.com/nearai/ironclaw/pull/1213))
+- *(worker)* prevent orphaned tool_results and fix parallel merging ([#1069](https://github.com/nearai/ironclaw/pull/1069))
+- Telegram bot token validation fails intermittently (HTTP 404) ([#1166](https://github.com/nearai/ironclaw/pull/1166))
+- *(security)* prevent metadata spoofing of internal job monitor flag ([#1195](https://github.com/nearai/ironclaw/pull/1195))
+- *(security)* default webhook server to loopback when tunnel is configured ([#1194](https://github.com/nearai/ironclaw/pull/1194))
+- *(auth)* avoid false success and block chat during pending auth ([#1111](https://github.com/nearai/ironclaw/pull/1111))
+- *(config)* unify ChannelsConfig resolution to env > settings > default ([#1124](https://github.com/nearai/ironclaw/pull/1124))
+- *(web-chat)* normalize chat copy to plain text ([#1114](https://github.com/nearai/ironclaw/pull/1114))
+- *(skill)* treat empty url param as absent when installing skills ([#1128](https://github.com/nearai/ironclaw/pull/1128))
+- preserve AuthError type in oauth_http_client cache ([#1152](https://github.com/nearai/ironclaw/pull/1152))
+- *(web)* prevent Safari IME composition Enter from sending message ([#1140](https://github.com/nearai/ironclaw/pull/1140))
+- *(mcp)* handle 400 auth errors, clear auth mode after OAuth, trim tokens ([#1158](https://github.com/nearai/ironclaw/pull/1158))
+- eliminate panic paths in production code ([#1184](https://github.com/nearai/ironclaw/pull/1184))
+- N+1 query pattern in event trigger loop (routine_engine) ([#1163](https://github.com/nearai/ironclaw/pull/1163))
+- *(llm)* add stop_sequences parity for tool completions ([#1170](https://github.com/nearai/ironclaw/pull/1170))
+- *(channels)* use live owner binding during wasm hot activation ([#1171](https://github.com/nearai/ironclaw/pull/1171))
+- Non-transactional multi-step context updates between metadata/to… ([#1161](https://github.com/nearai/ironclaw/pull/1161))
+- *(webhook)* avoid lock-held awaits in server lifecycle paths ([#1168](https://github.com/nearai/ironclaw/pull/1168))
+- Google Sheets returns 403 PERMISSION_DENIED after completing OAuth ([#1164](https://github.com/nearai/ironclaw/pull/1164))
+- HTTP webhook secret transmitted in request body rather than via header, docs inconsistency and security concern ([#1162](https://github.com/nearai/ironclaw/pull/1162))
+- *(ci)* exclude ironclaw_safety from release automation ([#1146](https://github.com/nearai/ironclaw/pull/1146))
+- *(registry)* bump versions for github, web-search, and discord extensions ([#1106](https://github.com/nearai/ironclaw/pull/1106))
+- *(mcp)* address 14 audit findings across MCP module ([#1094](https://github.com/nearai/ironclaw/pull/1094))
+- *(http)* replace .expect() with match in webhook handler ([#1133](https://github.com/nearai/ironclaw/pull/1133))
+- *(time)* treat empty timezone string as absent ([#1127](https://github.com/nearai/ironclaw/pull/1127))
+- 5 critical/high-priority bugs (auth bypass, relay failures, unbounded recursion, context growth) ([#1083](https://github.com/nearai/ironclaw/pull/1083))
+- *(ci)* checkout promotion PR head for metadata refresh ([#1097](https://github.com/nearai/ironclaw/pull/1097))
+- *(ci)* add missing attachments field and crates/ dir to Dockerfiles ([#1100](https://github.com/nearai/ironclaw/pull/1100))
+- *(registry)* bump telegram channel version for capabilities change ([#1064](https://github.com/nearai/ironclaw/pull/1064))
+- *(ci)* repair staging promotion workflow behavior ([#1091](https://github.com/nearai/ironclaw/pull/1091))
+- *(wasm)* address #1086 review followups -- description hint and coercion safety ([#1092](https://github.com/nearai/ironclaw/pull/1092))
+- *(ci)* repair staging-ci workflow parsing ([#1090](https://github.com/nearai/ironclaw/pull/1090))
+- *(extensions)* fix lifecycle bugs + comprehensive E2E tests ([#1070](https://github.com/nearai/ironclaw/pull/1070))
+- add tool_info schema discovery for WASM tools ([#1086](https://github.com/nearai/ironclaw/pull/1086))
+- resolve bug_bash UX/logging issues (#1054 #1055 #1058) ([#1072](https://github.com/nearai/ironclaw/pull/1072))
+- *(http)* fail closed when webhook secret is missing at runtime ([#1075](https://github.com/nearai/ironclaw/pull/1075))
+- *(service)* set CLI_ENABLED=false in macOS launchd plist ([#1079](https://github.com/nearai/ironclaw/pull/1079))
+- relax approval requirements for low-risk tools ([#922](https://github.com/nearai/ironclaw/pull/922))
+- *(web)* make approval requests appear without page reload ([#996](https://github.com/nearai/ironclaw/pull/996)) ([#1073](https://github.com/nearai/ironclaw/pull/1073))
+- *(routines)* run cron checks immediately on ticker startup ([#1066](https://github.com/nearai/ironclaw/pull/1066))
+- *(web)* recompute cron next_fire_at when re-enabling routines ([#1080](https://github.com/nearai/ironclaw/pull/1080))
+- *(memory)* reject absolute filesystem paths with corrective routing ([#934](https://github.com/nearai/ironclaw/pull/934))
+- remove all inline event handlers for CSP script-src compliance ([#1063](https://github.com/nearai/ironclaw/pull/1063))
+- *(mcp)* include OAuth state parameter in authorization URLs ([#1049](https://github.com/nearai/ironclaw/pull/1049))
+- *(mcp)* open MCP OAuth in same browser as gateway ([#951](https://github.com/nearai/ironclaw/pull/951))
+- *(deploy)* harden production container and bootstrap security ([#1014](https://github.com/nearai/ironclaw/pull/1014))
+- release lock guards before awaiting channel send ([#869](https://github.com/nearai/ironclaw/pull/869)) ([#1003](https://github.com/nearai/ironclaw/pull/1003))
+- *(registry)* use versioned artifact URLs and checksums for all WASM manifests ([#1007](https://github.com/nearai/ironclaw/pull/1007))
+- *(setup)* preserve model selection on provider re-run ([#679](https://github.com/nearai/ironclaw/pull/679)) ([#987](https://github.com/nearai/ironclaw/pull/987))
+- *(mcp)* attach session manager for non-OAuth HTTP clients ([#793](https://github.com/nearai/ironclaw/pull/793)) ([#986](https://github.com/nearai/ironclaw/pull/986))
+- *(security)* migrate webhook auth to HMAC-SHA256 signature header ([#970](https://github.com/nearai/ironclaw/pull/970))
+- *(security)* make unsafe env::set_var calls safe with explicit invariants ([#968](https://github.com/nearai/ironclaw/pull/968))
+- *(security)* require explicit SANDBOX_ALLOW_FULL_ACCESS to enable FullAccess policy ([#967](https://github.com/nearai/ironclaw/pull/967))
+- *(security)* add Content-Security-Policy header to web gateway ([#966](https://github.com/nearai/ironclaw/pull/966))
+- *(test)* stabilize openai compat oversized-body regression ([#839](https://github.com/nearai/ironclaw/pull/839))
+- *(ci)* disambiguate WASM bundle filenames to prevent tool/channel collision ([#964](https://github.com/nearai/ironclaw/pull/964))
+- *(setup)* validate channel credentials during setup ([#684](https://github.com/nearai/ironclaw/pull/684))
+- drain tunnel pipes to prevent zombie process ([#735](https://github.com/nearai/ironclaw/pull/735))
+- *(mcp)* header safety validation and Authorization conflict bug from #704 ([#752](https://github.com/nearai/ironclaw/pull/752))
+- *(agent)* block thread_id-based context pollution across users ([#760](https://github.com/nearai/ironclaw/pull/760))
+- *(mcp)* stdio/unix transports skip initialize handshake ([#890](https://github.com/nearai/ironclaw/pull/890)) ([#935](https://github.com/nearai/ironclaw/pull/935))
+- *(setup)* drain residual events and filter key kind in onboard prompts ([#937](https://github.com/nearai/ironclaw/pull/937)) ([#949](https://github.com/nearai/ironclaw/pull/949))
+- *(security)* load WASM tool description and schema from capabilities.json ([#520](https://github.com/nearai/ironclaw/pull/520))
+- *(security)* resolve DNS once and reuse for SSRF validation to prevent rebinding ([#518](https://github.com/nearai/ironclaw/pull/518))
+- *(security)* replace regex HTML sanitizer with DOMPurify to prevent XSS ([#510](https://github.com/nearai/ironclaw/pull/510))
+- *(ci)* improve Claude Code review reliability ([#955](https://github.com/nearai/ironclaw/pull/955))
+- *(ci)* run gated test jobs during staging CI ([#956](https://github.com/nearai/ironclaw/pull/956))
+- *(ci)* prevent staging-ci tag failure and chained PR auto-close ([#900](https://github.com/nearai/ironclaw/pull/900))
+- *(ci)* WASM WIT compat sqlite3 duplicate symbol conflict ([#953](https://github.com/nearai/ironclaw/pull/953))
+- resolve deferred review items from PRs #883, #848, #788 ([#915](https://github.com/nearai/ironclaw/pull/915))
+- *(web)* improve UX readability and accessibility in chat UI ([#910](https://github.com/nearai/ironclaw/pull/910))
+
+### Other
+
+- Fix Telegram auto-verify flow and routing ([#1273](https://github.com/nearai/ironclaw/pull/1273))
+- *(e2e)* fix approval waiting regression coverage ([#1270](https://github.com/nearai/ironclaw/pull/1270))
+- isolate heavy integration tests ([#1266](https://github.com/nearai/ironclaw/pull/1266))
+- Merge branch 'main' into fix/resolve-conflicts
+- Refactor owner scope across channels and fix default routing fallback ([#1151](https://github.com/nearai/ironclaw/pull/1151))
+- *(extensions)* document relay manager init order ([#928](https://github.com/nearai/ironclaw/pull/928))
+- *(setup)* extract init logic from wizard into owning modules ([#1210](https://github.com/nearai/ironclaw/pull/1210))
+- mention MiniMax as built-in provider in all READMEs ([#1209](https://github.com/nearai/ironclaw/pull/1209))
+- Fix schema-guided tool parameter coercion ([#1143](https://github.com/nearai/ironclaw/pull/1143))
+- Make no-panics CI check test-aware ([#1160](https://github.com/nearai/ironclaw/pull/1160))
+- *(mcp)* avoid reallocating SSE buffer on each chunk ([#1153](https://github.com/nearai/ironclaw/pull/1153))
+- *(routines)* avoid full message history clone each tool iteration ([#1172](https://github.com/nearai/ironclaw/pull/1172))
+- *(registry)* align manifest versions with published artifacts ([#1169](https://github.com/nearai/ironclaw/pull/1169))
+- remove __pycache__ from repo and add to .gitignore ([#1177](https://github.com/nearai/ironclaw/pull/1177))
+- *(registry)* move MCP servers from code to JSON manifests ([#1144](https://github.com/nearai/ironclaw/pull/1144))
+- improve routine schema guidance ([#1089](https://github.com/nearai/ironclaw/pull/1089))
+- add event-trigger routine e2e coverage ([#1088](https://github.com/nearai/ironclaw/pull/1088))
+- enforce no .unwrap(), .expect(), or assert!() in production code ([#1087](https://github.com/nearai/ironclaw/pull/1087))
+- periodic sync main into staging (resolved conflicts) ([#1098](https://github.com/nearai/ironclaw/pull/1098))
+- fix formatting in cli/mod.rs and mcp/auth.rs ([#1071](https://github.com/nearai/ironclaw/pull/1071))
+- Expose the shared agent session manager via AppComponents ([#532](https://github.com/nearai/ironclaw/pull/532))
+- *(agent)* remove unnecessary Worker re-export ([#923](https://github.com/nearai/ironclaw/pull/923))
+- Fix UTF-8 unsafe truncation in WASM emit_message ([#1015](https://github.com/nearai/ironclaw/pull/1015))
+- extract safety module into ironclaw_safety crate ([#1024](https://github.com/nearai/ironclaw/pull/1024))
+- Add Z.AI provider support for GLM-5 ([#938](https://github.com/nearai/ironclaw/pull/938))
+- *(html_to_markdown)* refresh golden files after renderer bump ([#1016](https://github.com/nearai/ironclaw/pull/1016))
+- Migrate GitHub webhook normalization into github tool ([#758](https://github.com/nearai/ironclaw/pull/758))
+- Fix systemctl unit ([#472](https://github.com/nearai/ironclaw/pull/472))
+- add Russian localization (README.ru.md) ([#850](https://github.com/nearai/ironclaw/pull/850))
+- Add generic host-verified /webhook/tools/{tool} ingress ([#757](https://github.com/nearai/ironclaw/pull/757))
+
## [0.18.0](https://github.com/nearai/ironclaw/compare/v0.17.0...v0.18.0) - 2026-03-11
### Other
diff --git a/CLAUDE.md b/CLAUDE.md
index d47292e1..e2d84c1e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -158,6 +158,8 @@ src/
│
├── secrets/ # Secrets management (AES-256-GCM, OS keychain for master key)
│
+├── profile.rs # Psychographic profile types, 9-dimension analysis framework
+│
├── setup/ # 7-step onboarding wizard — see src/setup/README.md
│
├── skills/ # SKILL.md prompt extension system — see .claude/rules/skills.md
diff --git a/Cargo.lock b/Cargo.lock
index 854d103a..2c5547e0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3436,7 +3436,7 @@ dependencies = [
[[package]]
name = "ironclaw"
-version = "0.18.0"
+version = "0.19.0"
dependencies = [
"aes-gcm",
"aho-corasick",
diff --git a/Cargo.toml b/Cargo.toml
index b396b18d..5b452651 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,7 +20,7 @@ exclude = [
[package]
name = "ironclaw"
-version = "0.18.0"
+version = "0.19.0"
edition = "2024"
rust-version = "1.92"
description = "Secure personal AI assistant that protects your data and expands its capabilities on the fly"
diff --git a/FEATURE_PARITY.md b/FEATURE_PARITY.md
index 85348de5..e0002a41 100644
--- a/FEATURE_PARITY.md
+++ b/FEATURE_PARITY.md
@@ -465,7 +465,7 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| Device pairing | ✅ | ❌ | |
| Tailscale identity | ✅ | ❌ | |
| Trusted-proxy auth | ✅ | ❌ | Header-based reverse proxy auth |
-| OAuth flows | ✅ | 🚧 | NEAR AI OAuth |
+| OAuth flows | ✅ | 🚧 | NEAR AI OAuth plus hosted extension/MCP OAuth broker; external auth-proxy rollout still pending |
| DM pairing verification | ✅ | ✅ | ironclaw pairing approve, host APIs |
| Allowlist/blocklist | ✅ | 🚧 | allow_from + pairing store |
| Per-group tool policies | ✅ | ❌ | |
diff --git a/README.ja.md b/README.ja.md
new file mode 100644
index 00000000..887cf67e
--- /dev/null
+++ b/README.ja.md
@@ -0,0 +1,330 @@
+
+
+
+
+IronClaw
+
+
+ あなたの味方になる、安全なパーソナルAIアシスタント
+
+
+
+
+
+
+
+
+
+ English |
+ 简体中文 |
+ Русский |
+ 日本語
+
+
+
+ フィロソフィー •
+ 機能 •
+ インストール •
+ 設定 •
+ セキュリティ •
+ アーキテクチャ
+
+
+---
+
+## フィロソフィー
+
+IronClawはシンプルな原則に基づいて構築されています:**あなたのAIアシスタントは、あなたのために働くべきであり、あなたに不利益をもたらすべきではありません。**
+
+AIシステムがデータの取り扱いについて不透明になり、企業の利益に沿って調整されることが増えている世界で、IronClawは異なるアプローチを取ります:
+
+- **あなたのデータはあなたのもの** - すべての情報はローカルに保存・暗号化され、あなたの管理下から離れることはありません
+- **設計段階からの透明性** - オープンソース、監査可能、隠れたテレメトリやデータ収集なし
+- **自己拡張する能力** - ベンダーのアップデートを待たずに、新しいツールをその場で構築
+- **多層防御** - 複数のセキュリティレイヤーがプロンプトインジェクションやデータ流出から保護
+
+IronClawは、個人生活にも仕事にも本当に信頼できるAIアシスタントです。
+
+## 機能
+
+### セキュリティファースト
+
+- **WASMサンドボックス** - 信頼されていないツールは、機能ベースの権限を持つ隔離されたWebAssemblyコンテナで実行
+- **認証情報の保護** - シークレットはツールに公開されず、リーク検出付きでホスト境界で注入
+- **プロンプトインジェクション防御** - パターン検出、コンテンツサニタイズ、ポリシー適用
+- **エンドポイントの許可リスト** - HTTPリクエストは明示的に許可されたホストとパスのみに制限
+
+### 常時利用可能
+
+- **マルチチャネル** - REPL、HTTPウェブフック、WASMチャネル(Telegram、Slack)、Webゲートウェイ
+- **Dockerサンドボックス** - ジョブごとのトークンとオーケストレーター/ワーカーパターンによる隔離されたコンテナ実行
+- **Webゲートウェイ** - リアルタイムSSE/WebSocketストリーミング対応のブラウザUI
+- **ルーティン** - cronスケジュール、イベントトリガー、ウェブフックハンドラーによるバックグラウンド自動化
+- **ハートビートシステム** - 監視・保守タスクのためのプロアクティブなバックグラウンド実行
+- **並列ジョブ** - 隔離されたコンテキストで複数のリクエストを同時に処理
+- **自己修復** - スタックした操作の自動検出と復旧
+
+### 自己拡張
+
+- **動的ツール構築** - 必要なものを説明すると、IronClawがWASMツールとして構築
+- **MCPプロトコル** - Model Context Protocolサーバーに接続して追加機能を利用
+- **プラグインアーキテクチャ** - 再起動なしで新しいWASMツールやチャネルを追加
+
+### 永続メモリ
+
+- **ハイブリッド検索** - Reciprocal Rank Fusionを使用した全文検索+ベクトル検索
+- **ワークスペースファイルシステム** - メモ、ログ、コンテキストのための柔軟なパスベースストレージ
+- **アイデンティティファイル** - セッション間で一貫した人格と設定を維持
+
+## インストール
+
+### 前提条件
+
+- Rust 1.85+
+- PostgreSQL 15+ ([pgvector](https://github.com/pgvector/pgvector)拡張機能を含む)
+- NEAR AIアカウント(セットアップウィザードで認証を処理)
+
+## ダウンロードまたはビルド
+
+最新のアップデートは[リリースページ](https://github.com/nearai/ironclaw/releases/)をご覧ください。
+
+
+ Windowsインストーラーでインストール(Windows)
+
+[Windowsインストーラー](https://github.com/nearai/ironclaw/releases/latest/download/ironclaw-x86_64-pc-windows-msvc.msi)をダウンロードして実行してください。
+
+
+
+
+ PowerShellスクリプトでインストール(Windows)
+
+```sh
+irm https://github.com/nearai/ironclaw/releases/latest/download/ironclaw-installer.ps1 | iex
+```
+
+
+
+
+ シェルスクリプトでインストール(macOS、Linux、Windows/WSL)
+
+```sh
+curl --proto '=https' --tlsv1.2 -LsSf https://github.com/nearai/ironclaw/releases/latest/download/ironclaw-installer.sh | sh
+```
+
+
+
+ Homebrewでインストール(macOS/Linux)
+
+```sh
+brew install ironclaw
+```
+
+
+
+
+ ソースコードからコンパイル(Windows、Linux、macOSでCargo)
+
+`cargo`でインストールします。コンピューターに[Rust](https://rustup.rs)がインストールされていることを確認してください。
+
+```bash
+# リポジトリをクローン
+git clone https://github.com/nearai/ironclaw.git
+cd ironclaw
+
+# ビルド
+cargo build --release
+
+# テストを実行
+cargo test
+```
+
+**フルリリース**(チャネルソースを変更した後)の場合、まず`./scripts/build-all.sh`を実行してチャネルを再ビルドしてください。
+
+
+
+### データベースのセットアップ
+
+```bash
+# データベースを作成
+createdb ironclaw
+
+# pgvectorを有効化
+psql ironclaw -c "CREATE EXTENSION IF NOT EXISTS vector;"
+```
+
+## 設定
+
+セットアップウィザードを実行してIronClawを設定します:
+
+```bash
+ironclaw onboard
+```
+
+ウィザードは、データベース接続、NEAR AI認証(ブラウザOAuth経由)、シークレットの暗号化(システムキーチェーンを使用)を処理します。設定は接続されたデータベースに永続化されます。ブートストラップ変数(例:`DATABASE_URL`、`LLM_BACKEND`)は、データベース接続前に利用できるよう`~/.ironclaw/.env`に書き込まれます。
+
+### 代替LLMプロバイダー
+
+IronClawはデフォルトでNEAR AIを使用しますが、多くのLLMプロバイダーをすぐに利用できます。組み込みプロバイダーには**Anthropic**、**OpenAI**、**Google Gemini**、**MiniMax**、**Mistral**、**Ollama**(ローカル)が含まれます。**OpenRouter**(300以上のモデル)、**Together AI**、**Fireworks AI**、セルフホストサーバー(**vLLM**、**LiteLLM**)などのOpenAI互換サービスもサポートされています。
+
+ウィザードでプロバイダーを選択するか、環境変数を直接設定してください:
+
+```env
+# 例:MiniMax(組み込み、204Kコンテキスト)
+LLM_BACKEND=minimax
+MINIMAX_API_KEY=...
+
+# 例:OpenAI互換エンドポイント
+LLM_BACKEND=openai_compatible
+LLM_BASE_URL=https://openrouter.ai/api/v1
+LLM_API_KEY=sk-or-...
+LLM_MODEL=anthropic/claude-sonnet-4
+```
+
+完全なプロバイダーガイドは[docs/LLM_PROVIDERS.md](docs/LLM_PROVIDERS.md)をご覧ください。
+
+## セキュリティ
+
+IronClawは、データを保護し悪用を防ぐために多層防御を実装しています。
+
+### WASMサンドボックス
+
+すべての信頼されていないツールは、隔離されたWebAssemblyコンテナで実行されます:
+
+- **機能ベースの権限** - HTTP、シークレット、ツール呼び出しの明示的なオプトイン
+- **エンドポイントの許可リスト** - 許可されたホスト/パスへのHTTPリクエストのみ
+- **認証情報の注入** - シークレットはホスト境界で注入され、WASMコードに公開されない
+- **リーク検出** - リクエストとレスポンスのシークレット流出試行をスキャン
+- **レート制限** - 悪用防止のためのツールごとのリクエスト制限
+- **リソース制限** - メモリ、CPU、実行時間の制約
+
+```
+WASM ──► 許可リスト ──► リーク ──► 認証情報 ──► リクエスト ──► リーク ──► WASM
+ バリデーター スキャン 注入 実行 スキャン
+ (リクエスト) (レスポンス)
+```
+
+### プロンプトインジェクション防御
+
+外部コンテンツは複数のセキュリティレイヤーを通過します:
+
+- パターンベースのインジェクション試行検出
+- コンテンツのサニタイズとエスケープ
+- 重要度レベル付きポリシールール(ブロック/警告/レビュー/サニタイズ)
+- 安全なLLMコンテキスト注入のためのツール出力ラッピング
+
+### データ保護
+
+- すべてのデータはローカルのPostgreSQLデータベースに保存
+- AES-256-GCMでシークレットを暗号化
+- テレメトリ、分析、データ共有なし
+- すべてのツール実行の完全な監査ログ
+
+## アーキテクチャ
+
+```
+┌────────────────────────────────────────────────────────────────┐
+│ チャネル │
+│ ┌──────┐ ┌──────┐ ┌─────────────┐ ┌─────────────┐ │
+│ │ REPL │ │ HTTP │ │WASMチャネル │ │ Web │ │
+│ └──┬───┘ └──┬───┘ └──────┬──────┘ │ ゲートウェイ│ │
+│ │ │ │ │(SSE + WS) │ │
+│ │ │ │ └──────┬──────┘ │
+│ └─────────┴──────────────┴────────────────┘ │
+│ │ │
+│ ┌─────────▼─────────┐ │
+│ │ エージェントループ │ インテントルーティング│
+│ └────┬──────────┬───┘ │
+│ │ │ │
+│ ┌──────────▼────┐ ┌──▼───────────────┐ │
+│ │ スケジューラー │ │ ルーティン │ │
+│ │ (並列ジョブ) │ │ エンジン │ │
+│ └──────┬────────┘ │(cron,event,wh) │ │
+│ │ └────────┬─────────┘ │
+│ ┌─────────────┼────────────────────┘ │
+│ │ │ │
+│ ┌───▼─────┐ ┌────▼────────────────┐ │
+│ │ ローカル │ │ オーケストレーター │ │
+│ │ ワーカー │ │ ┌───────────────┐ │ │
+│ │(プロセス │ │ │ Docker │ │ │
+│ │ 内) │ │ │ サンドボックス│ │ │
+│ └───┬─────┘ │ │ コンテナ │ │ │
+│ │ │ │ ┌───────────┐ │ │ │
+│ │ │ │ │Worker / CC│ │ │ │
+│ │ │ │ └───────────┘ │ │ │
+│ │ │ └───────────────┘ │ │
+│ │ └─────────┬───────────┘ │
+│ └──────────────────┤ │
+│ │ │
+│ ┌───────────▼──────────┐ │
+│ │ ツールレジストリ │ │
+│ │ 組み込み, MCP, WASM │ │
+│ └──────────────────────┘ │
+└────────────────────────────────────────────────────────────────┘
+```
+
+### コアコンポーネント
+
+| コンポーネント | 目的 |
+|---------------|------|
+| **エージェントループ** | メインのメッセージ処理とジョブの調整 |
+| **ルーター** | ユーザーの意図を分類(コマンド、クエリ、タスク) |
+| **スケジューラー** | 優先度付きの並列ジョブ実行を管理 |
+| **ワーカー** | LLM推論とツール呼び出しでジョブを実行 |
+| **オーケストレーター** | コンテナのライフサイクル、LLMプロキシ、ジョブごとの認証 |
+| **Webゲートウェイ** | チャット、メモリ、ジョブ、ログ、拡張機能、ルーティンのブラウザUI |
+| **ルーティンエンジン** | スケジュール(cron)とリアクティブ(イベント、ウェブフック)のバックグラウンドタスク |
+| **ワークスペース** | ハイブリッド検索付き永続メモリ |
+| **セーフティレイヤー** | プロンプトインジェクション防御とコンテンツサニタイズ |
+
+## 使い方
+
+```bash
+# 初回セットアップ(データベース、認証などを設定)
+ironclaw onboard
+
+# インタラクティブREPLを起動
+cargo run
+
+# デバッグログ付き
+RUST_LOG=ironclaw=debug cargo run
+```
+
+## 開発
+
+```bash
+# コードフォーマット
+cargo fmt
+
+# リント
+cargo clippy --all --benches --tests --examples --all-features
+
+# テスト実行
+createdb ironclaw_test
+cargo test
+
+# 特定のテストを実行
+cargo test test_name
+```
+
+- **Telegramチャネル**: セットアップとDMペアリングについては[docs/TELEGRAM_SETUP.md](docs/TELEGRAM_SETUP.md)を参照してください。
+- **チャネルソースの変更**: `cargo build`の前に`./channels-src/telegram/build.sh`を実行して、更新されたWASMをバンドルしてください。
+
+## OpenClawの系譜
+
+IronClawは[OpenClaw](https://github.com/openclaw/openclaw)にインスパイアされたRust再実装です。完全な対応表は[FEATURE_PARITY.md](FEATURE_PARITY.md)をご覧ください。
+
+主な違い:
+
+- **Rust vs TypeScript** - ネイティブパフォーマンス、メモリ安全性、シングルバイナリ
+- **WASMサンドボックス vs Docker** - 軽量、機能ベースのセキュリティ
+- **PostgreSQL vs SQLite** - 本番環境対応の永続化
+- **セキュリティファースト設計** - 複数の防御レイヤー、認証情報の保護
+
+## ライセンス
+
+以下のいずれかのライセンスの下で提供されています:
+
+- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE))
+- MIT License ([LICENSE-MIT](LICENSE-MIT))
+
+お好みに応じて選択してください。
diff --git a/README.md b/README.md
index 9684ee4d..fa73dc45 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,8 @@
English |
简体中文 |
- Русский
+ Русский |
+ 日本語
diff --git a/README.ru.md b/README.ru.md
index c64770a9..0546e7f4 100644
--- a/README.ru.md
+++ b/README.ru.md
@@ -17,7 +17,8 @@
English |
简体中文 |
- Русский
+ Русский |
+ 日本語
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 34023822..a337d713 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -17,7 +17,8 @@
English |
简体中文 |
- Русский
+ Русский |
+ 日本語
diff --git a/channels-src/feishu/src/lib.rs b/channels-src/feishu/src/lib.rs
index 2e7261d8..3094eaa0 100644
--- a/channels-src/feishu/src/lib.rs
+++ b/channels-src/feishu/src/lib.rs
@@ -206,9 +206,17 @@ struct FeishuApiResponse {
data: Option,
}
-/// Tenant access token response.
-#[derive(Debug, Default, Deserialize)]
-struct TenantAccessTokenData {
+/// Tenant access token response (flat format).
+///
+/// Unlike most Feishu APIs that nest results under `data`, the
+/// `/auth/v3/tenant_access_token/internal` endpoint returns `code`, `msg`,
+/// `tenant_access_token`, and `expire` at the top level.
+#[derive(Debug, Deserialize)]
+struct TenantAccessTokenResponse {
+ #[serde(default)]
+ code: i32,
+ #[serde(default)]
+ msg: String,
tenant_access_token: String,
expire: i64,
}
@@ -770,9 +778,8 @@ fn obtain_tenant_token(api_base: &str) -> Result {
));
}
- let token_resp: FeishuApiResponse =
- serde_json::from_slice(&response.body)
- .map_err(|e| format!("Failed to parse token response: {}", e))?;
+ let token_resp: TenantAccessTokenResponse = serde_json::from_slice(&response.body)
+ .map_err(|e| format!("Failed to parse token response: {}", e))?;
if token_resp.code != 0 {
return Err(format!(
@@ -781,23 +788,33 @@ fn obtain_tenant_token(api_base: &str) -> Result {
));
}
- let data = token_resp
- .data
- .ok_or_else(|| "Token response missing data".to_string())?;
+ if token_resp.tenant_access_token.is_empty() {
+ return Err("Token response missing tenant_access_token".to_string());
+ }
+
+ if token_resp.expire <= 0 {
+ return Err(format!(
+ "Token response has invalid expire value: {}",
+ token_resp.expire
+ ));
+ }
// Cache the token with expiry.
let now = channel_host::now_millis();
- let expiry = now + (data.expire as u64) * 1000;
+ let expiry = now.saturating_add((token_resp.expire as u64).saturating_mul(1000));
- let _ = channel_host::workspace_write(TOKEN_PATH, &data.tenant_access_token);
+ let _ = channel_host::workspace_write(TOKEN_PATH, &token_resp.tenant_access_token);
let _ = channel_host::workspace_write(TOKEN_EXPIRY_PATH, &expiry.to_string());
channel_host::log(
channel_host::LogLevel::Debug,
- &format!("Tenant access token refreshed, expires in {}s", data.expire),
+ &format!(
+ "Tenant access token refreshed, expires in {}s",
+ token_resp.expire
+ ),
);
- Ok(data.tenant_access_token)
+ Ok(token_resp.tenant_access_token)
}
Err(e) => Err(format!("Token exchange request failed: {}", e)),
}
@@ -819,3 +836,60 @@ fn json_response(status: u16, body: serde_json::Value) -> OutgoingHttpResponse {
body: body_bytes,
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn parse_flat_token_response() {
+ let json = r#"{
+ "code": 0,
+ "msg": "ok",
+ "tenant_access_token": "t-abc123",
+ "expire": 7200
+ }"#;
+ let resp: TenantAccessTokenResponse = serde_json::from_str(json).unwrap();
+ assert_eq!(resp.code, 0);
+ assert_eq!(resp.msg, "ok");
+ assert_eq!(resp.tenant_access_token, "t-abc123");
+ assert_eq!(resp.expire, 7200);
+ }
+
+ #[test]
+ fn parse_token_response_rejects_missing_token() {
+ let json = r#"{"code": 0, "msg": "ok", "expire": 7200}"#;
+ let result: Result = serde_json::from_str(json);
+ assert!(result.is_err(), "should fail when tenant_access_token is missing");
+ }
+
+ #[test]
+ fn parse_token_response_rejects_missing_expire() {
+ let json = r#"{"code": 0, "msg": "ok", "tenant_access_token": "t-abc"}"#;
+ let result: Result = serde_json::from_str(json);
+ assert!(result.is_err(), "should fail when expire is missing");
+ }
+
+ #[test]
+ fn parse_token_response_defaults_code_and_msg() {
+ let json = r#"{"tenant_access_token": "t-abc", "expire": 3600}"#;
+ let resp: TenantAccessTokenResponse = serde_json::from_str(json).unwrap();
+ assert_eq!(resp.code, 0);
+ assert_eq!(resp.msg, "");
+ assert_eq!(resp.tenant_access_token, "t-abc");
+ assert_eq!(resp.expire, 3600);
+ }
+
+ #[test]
+ fn parse_token_error_response() {
+ let json = r#"{
+ "code": 10003,
+ "msg": "invalid app_id",
+ "tenant_access_token": "",
+ "expire": 0
+ }"#;
+ let resp: TenantAccessTokenResponse = serde_json::from_str(json).unwrap();
+ assert_eq!(resp.code, 10003);
+ assert!(resp.tenant_access_token.is_empty());
+ }
+}
diff --git a/channels-src/telegram/src/lib.rs b/channels-src/telegram/src/lib.rs
index a095ccb3..f34ed68a 100644
--- a/channels-src/telegram/src/lib.rs
+++ b/channels-src/telegram/src/lib.rs
@@ -360,6 +360,8 @@ enum TelegramStatusAction {
}
const TELEGRAM_STATUS_MAX_CHARS: usize = 600;
+/// Telegram's hard limit for message text length.
+const TELEGRAM_MAX_MESSAGE_LEN: usize = 4096;
fn truncate_status_message(input: &str, max_chars: usize) -> String {
let mut iter = input.chars();
@@ -371,6 +373,73 @@ fn truncate_status_message(input: &str, max_chars: usize) -> String {
}
}
+/// Split a long message into chunks that fit within Telegram's 4096-char limit.
+///
+/// Tries to split at the most natural boundary available (in priority order):
+/// 1. Double newline (paragraph break)
+/// 2. Single newline
+/// 3. Sentence end (`. `, `! `, `? `)
+/// 4. Word boundary (space)
+/// 5. Hard cut at the limit (last resort for pathological input)
+fn split_message(text: &str) -> Vec {
+ if text.chars().count() <= TELEGRAM_MAX_MESSAGE_LEN {
+ return vec![text.to_string()];
+ }
+
+ let mut chunks: Vec = Vec::new();
+ let mut remaining = text;
+
+ while !remaining.is_empty() {
+ // Count chars to find the byte offset for our window.
+ let window_bytes = remaining
+ .char_indices()
+ .take(TELEGRAM_MAX_MESSAGE_LEN)
+ .last()
+ .map(|(byte_idx, ch)| byte_idx + ch.len_utf8())
+ .unwrap_or(remaining.len());
+
+ if window_bytes >= remaining.len() {
+ // Remainder fits entirely.
+ chunks.push(remaining.to_string());
+ break;
+ }
+
+ let window = &remaining[..window_bytes];
+
+ // 1. Double newline — best paragraph boundary
+ let split_at = window.rfind("\n\n")
+ // 2. Single newline
+ .or_else(|| window.rfind('\n'))
+ // 3. Sentence-ending punctuation followed by space.
+ // Note: this only detects ASCII punctuation (. ! ?), not CJK
+ // sentence-ending marks (。!?). CJK text falls through to
+ // word-boundary or hard-cut splitting.
+ .or_else(|| {
+ let bytes = window.as_bytes();
+ // Search backwards for '. ', '! ', '? '
+ (1..bytes.len()).rev().find(|&i| {
+ matches!(bytes[i - 1], b'.' | b'!' | b'?') && bytes[i] == b' '
+ })
+ })
+ // 4. Word boundary (last space)
+ .or_else(|| window.rfind(' '))
+ // 5. Hard cut
+ .unwrap_or(window_bytes);
+
+ // Avoid empty chunks (e.g. text starting with \n\n).
+ let split_at = if split_at == 0 { window_bytes } else { split_at };
+
+ // Trim whitespace at chunk boundaries for clean Telegram display.
+ // Note: this drops leading/trailing spaces at split points, which is
+ // acceptable for chat messages but means the concatenation of chunks
+ // may not exactly equal the original text when split at spaces.
+ chunks.push(remaining[..split_at].trim_end().to_string());
+ remaining = remaining[split_at..].trim_start();
+ }
+
+ chunks
+}
+
fn status_message_for_user(update: &StatusUpdate) -> Option {
let message = update.message.trim();
if message.is_empty() {
@@ -1242,26 +1311,64 @@ fn send_response(
return Ok(());
}
- // Try Markdown, fall back to plain text on parse errors
- match send_message(
- chat_id,
- &response.content,
- reply_to_message_id,
- Some("Markdown"),
- message_thread_id,
- ) {
- Ok(_) => Ok(()),
- Err(SendError::ParseEntities(_)) => send_message(
- chat_id,
- &response.content,
- reply_to_message_id,
- None,
- message_thread_id,
- )
- .map(|_| ())
- .map_err(|e| format!("Plain-text retry also failed: {}", e)),
- Err(e) => Err(e.to_string()),
+ // Split large messages into chunks that fit Telegram's limit.
+ let chunks = split_message(&response.content);
+ let total = chunks.len();
+
+ // The first chunk replies to the original message; subsequent chunks
+ // reply to the previously sent chunk so they form a visual thread.
+ let mut reply_to = reply_to_message_id;
+
+ for (i, chunk) in chunks.into_iter().enumerate() {
+ // Try Markdown, fall back to plain text on parse errors
+ let result = send_message(chat_id, &chunk, reply_to, Some("Markdown"), message_thread_id);
+
+ let msg_id = match result {
+ Ok(id) => {
+ channel_host::log(
+ channel_host::LogLevel::Debug,
+ &format!(
+ "Sent message chunk {}/{} to chat {}: message_id={}",
+ i + 1,
+ total,
+ chat_id,
+ id,
+ ),
+ );
+ id
+ }
+ Err(SendError::ParseEntities(detail)) => {
+ channel_host::log(
+ channel_host::LogLevel::Warn,
+ &format!(
+ "Markdown parse failed on chunk {}/{} ({}), retrying as plain text",
+ i + 1,
+ total,
+ detail
+ ),
+ );
+ let id = send_message(chat_id, &chunk, reply_to, None, message_thread_id)
+ .map_err(|e| format!("Plain-text retry also failed: {}", e))?;
+ channel_host::log(
+ channel_host::LogLevel::Debug,
+ &format!(
+ "Sent plain-text chunk {}/{} to chat {}: message_id={}",
+ i + 1,
+ total,
+ chat_id,
+ id,
+ ),
+ );
+ id
+ }
+ Err(e) => return Err(e.to_string()),
+ };
+
+ // Each subsequent chunk threads off the previous sent message.
+ reply_to = Some(msg_id);
}
+
+ Ok(())
}
/// Send a single attachment, choosing sendPhoto or sendDocument based on MIME type.
@@ -2043,6 +2150,102 @@ export!(TelegramChannel);
mod tests {
use super::*;
+ #[test]
+ fn test_split_message_short() {
+ let text = "Hello, world!";
+ let chunks = split_message(text);
+ assert_eq!(chunks, vec![text]);
+ }
+
+ #[test]
+ fn test_split_message_paragraph_boundary() {
+ let para_a = "A".repeat(3000);
+ let para_b = "B".repeat(3000);
+ let text = format!("{}\n\n{}", para_a, para_b);
+ let chunks = split_message(&text);
+ assert_eq!(chunks.len(), 2);
+ assert_eq!(chunks[0], para_a);
+ assert_eq!(chunks[1], para_b);
+ }
+
+ #[test]
+ fn test_split_message_word_boundary() {
+ // Build a string well over the limit with no newlines.
+ let words: Vec = (0..1000).map(|i| format!("word{:04}", i)).collect();
+ let text = words.join(" ");
+ assert!(text.len() > TELEGRAM_MAX_MESSAGE_LEN);
+ let chunks = split_message(&text);
+ assert!(chunks.len() > 1, "expected multiple chunks");
+ for chunk in &chunks {
+ assert!(chunk.chars().count() <= TELEGRAM_MAX_MESSAGE_LEN);
+ }
+ // Rejoined chunks must equal the original text exactly.
+ let rejoined = chunks.join(" ");
+ assert_eq!(rejoined, text);
+ }
+
+ #[test]
+ fn test_split_message_each_chunk_fits() {
+ // Stress-test: 20 000 chars of mixed text.
+ let text: String = (0..500)
+ .map(|i| format!("Sentence number {}. ", i))
+ .collect();
+ assert!(text.len() > TELEGRAM_MAX_MESSAGE_LEN);
+ let chunks = split_message(&text);
+ for chunk in &chunks {
+ assert!(chunk.chars().count() <= TELEGRAM_MAX_MESSAGE_LEN);
+ }
+ }
+
+ #[test]
+ fn test_split_message_sentence_boundary() {
+ // Build text that exceeds the limit, with sentence boundaries inside.
+ let sentence = "This is a test sentence. ";
+ let repeat_count = TELEGRAM_MAX_MESSAGE_LEN / sentence.len() + 5;
+ let text: String = sentence.repeat(repeat_count);
+ assert!(text.chars().count() > TELEGRAM_MAX_MESSAGE_LEN);
+
+ let chunks = split_message(&text);
+ assert!(chunks.len() > 1);
+ // First chunk should end at a sentence boundary (trimmed)
+ let first = &chunks[0];
+ assert!(
+ first.ends_with('.'),
+ "First chunk should end at a sentence boundary, got: ...{}",
+ &first[first.len().saturating_sub(20)..]
+ );
+ }
+
+ #[test]
+ fn test_split_message_hard_cut_no_spaces() {
+ // Pathological input: a single huge "word" with no spaces or newlines.
+ let text = "x".repeat(TELEGRAM_MAX_MESSAGE_LEN * 2 + 100);
+ let chunks = split_message(&text);
+ assert!(chunks.len() >= 2);
+ for chunk in &chunks {
+ assert!(chunk.chars().count() <= TELEGRAM_MAX_MESSAGE_LEN);
+ }
+ // Rejoined must preserve all characters
+ let rejoined: String = chunks.concat();
+ assert_eq!(rejoined, text);
+ }
+
+ #[test]
+ fn test_split_message_multibyte_chars() {
+ // Emoji are 4 bytes each. Ensure we don't panic or split mid-character.
+ let emoji = "\u{1F600}"; // 😀
+ let text: String = emoji.repeat(TELEGRAM_MAX_MESSAGE_LEN + 100);
+ assert!(text.chars().count() > TELEGRAM_MAX_MESSAGE_LEN);
+
+ let chunks = split_message(&text);
+ assert!(chunks.len() >= 2);
+ for chunk in &chunks {
+ assert!(chunk.chars().count() <= TELEGRAM_MAX_MESSAGE_LEN);
+ // Every char should be a complete emoji
+ assert!(chunk.chars().all(|c| c == '\u{1F600}'));
+ }
+ }
+
#[test]
fn test_clean_message_text() {
// Without bot_username: strips any leading @mention
diff --git a/codecov.yml b/codecov.yml
index 3e31b00a..723c1175 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -2,9 +2,13 @@ coverage:
status:
project:
default:
- target: auto
- threshold: 1%
+ target: 80%
+ threshold: 2%
patch:
default:
- target: 80%
- threshold: 5%
\ No newline at end of file
+ target: 90%
+
+comment:
+ layout: "reach,diff,flags"
+ behavior: default
+ require_changes: true
diff --git a/docs/LLM_PROVIDERS.md b/docs/LLM_PROVIDERS.md
index a581a56b..0623ce25 100644
--- a/docs/LLM_PROVIDERS.md
+++ b/docs/LLM_PROVIDERS.md
@@ -15,7 +15,7 @@ configurations.
| io.net | `ionet` | `IONET_API_KEY` | Intelligence API |
| Mistral | `mistral` | `MISTRAL_API_KEY` | Mistral models |
| Yandex AI Studio | `yandex` | `YANDEX_API_KEY` | YandexGPT models |
-| MiniMax | `minimax` | `MINIMAX_API_KEY` | MiniMax-M2.5 models |
+| MiniMax | `minimax` | `MINIMAX_API_KEY` | MiniMax-M2.7 models |
| Cloudflare Workers AI | `cloudflare` | `CLOUDFLARE_API_KEY` | Access to Workers AI |
| Ollama | `ollama` | No | Local inference |
| AWS Bedrock | `bedrock` | AWS credentials | Native Converse API |
@@ -84,7 +84,7 @@ LLM_BACKEND=minimax
MINIMAX_API_KEY=...
```
-Available models: `MiniMax-M2.5` (default), `MiniMax-M2.5-highspeed`
+Available models: `MiniMax-M2.7` (default), `MiniMax-M2.7-highspeed`, `MiniMax-M2.5`, `MiniMax-M2.5-highspeed`
To use the China mainland endpoint, set:
diff --git a/docs/plans/2026-03-18-staging-ci-triage.md b/docs/plans/2026-03-18-staging-ci-triage.md
new file mode 100644
index 00000000..adfd5d05
--- /dev/null
+++ b/docs/plans/2026-03-18-staging-ci-triage.md
@@ -0,0 +1,87 @@
+# Staging CI Review Issues Triage
+
+**Date:** 2026-03-18
+**Branch:** staging (HEAD `b7a1edf`)
+**Total open issues:** 50
+
+---
+
+## Batch 1 — Critical & 100-confidence issues
+
+| # | Title | Severity | Verdict | File(s) | Action |
+|---|-------|----------|---------|---------|--------|
+| 1281 | Logic inversion in Telegram auto-verification | CRITICAL:100 | **FALSE POSITIVE** (closed) | `src/channels/web/server.rs` | Different handlers with intentional different SSE behavior |
+| 908 | Missing consecutive_failures reset | CRITICAL:100 | **STALE** | `src/llm/circuit_breaker.rs` | Close — `record_success()` already resets to 0 |
+| 1282 | Variable shadowing fallback notification | HIGH:100 | **STALE** | `src/agent/agent_loop.rs` | Close — fixed in commit `bcc38ce` |
+| 1283 | Inconsistent fallback logic DRY | HIGH:75 | **STALE** | `src/agent/agent_loop.rs` | Close — fixed in commit `bcc38ce` |
+| 1178 | Workflow linting bypass for test code | CRITICAL:75 | **FALSE POSITIVE** | `.github/workflows/code_style.yml` | Close — script reads full file, not hunk headers |
+
+---
+
+## Remaining Batches (queued)
+
+### Batch 2 — Retry/DRY + CI workflow issues (completed)
+
+| # | Title | Severity | Verdict | Action |
+|---|-------|----------|---------|--------|
+| 1288 | DRY violation: retry-after parsing | HIGH:95 | **LEGIT** | Fixed: extracted shared `parse_retry_after()` |
+| 1289 | Semantic mismatch in RFC2822 test helpers | MEDIUM:85 | **DUPLICATE** (closed) | Duplicate of #1288 |
+| 1290 | Unnecessary eager `chrono::Utc::now()` call | LOW:85 | **FALSE POSITIVE** (closed) | Already deferred inside successful parse branch |
+| 963 | Logical equivalence bug in workflow conditions | HIGH:100 | **FALSE POSITIVE** (closed) | Refactored condition correctly handles `workflow_call` |
+| 1280 | Flaky OAuth wildcard callback tests | Flaky | **LEGIT** | Fixed: added `tokio::sync::Mutex` for env var serialization |
+
+### Batch 3 — Routine engine + notification routing
+- #1365 — too_many_arguments on RoutineEngine::new()
+- #1371 — Discovery schema regeneration on every tool_info call
+- #1364 — Prompt injection via unescaped channel/user in lightweight routines
+- #1284 — notification_target_for_channel() assumes channel owner
+
+### Batch 4 — Telegram/Extension Manager webhook group
+- #1247 — Synchronous 120-second blocking poll in HTTP handler
+- #1248 — Hardcoded channel-specific logic violates architecture
+- #1249 — Telegram-specific business logic bloats ExtensionManager
+- #1250 — Response success/failure logic mismatch in chat auth
+- #1251 — Channel-specific configuration mappings lack extensibility
+
+### Batch 5 — HMAC/Auth/Security
+- #1034 — Signature verification not constant-time
+- #1035 — Incorrect order of operations in HMAC verification
+- #1036 — Double opt-in lacks runtime validation consistency
+- #1037 — API breaking change: auth() signature
+- #1038 — CSP policy allows CDN scripts with risky fallback
+
+### Batch 6 — Webhook handler + config
+- #1039 — Per-request HTTP client creation in hot path
+- #1040 — Complex nested auth logic in webhook_handler
+- #1041 — Redundant JSON deserialization in webhook handler
+- #1042 — Implicit state mutation in config conversion
+- #1005 — Inconsistent double opt-in enforcement
+
+### Batch 7 — Tool schema validation / WASM bounds
+- #974 — Unbounded recursion in resolve_nested()
+- #975 — Unbounded recursion in validate_tool_schema()
+- #976 — Unbounded description string in CapabilitiesFile
+- #977 — Unbounded parameters schema JSON
+- #978 — Unnecessary clone of large JSON in hot path
+
+### Batch 8 — Tool schema + config + security
+- #979 — No size limits on JSON files read
+- #980 — Misleading warning condition for missing parameters
+- #988 — Hardcoded CLI_ENABLED env var in systemd template
+- #990 — Configuration semantics unclear for daemon mode
+- #1103 — SSRF risk via configurable embedding base URL
+
+### Batch 9 — Agent loop / job worker
+- #870 — Unbounded loop without cancellation token
+- #871 — Stringly-typed unsupported parameter filtering
+- #873 — RwLock overhead on hot path
+- #892 — JobDelegate::check_signals() treats non-terminal as terminal
+- #1252 — String concatenation in hot polling loop
+
+### Batch 10 — Agent loop perf + CI scripts
+- #893 — Unnecessary parameter cloning on every tool execution
+- #894 — truncate_for_preview allocates for non-truncated strings
+- #895 — Tool definitions fetched every iteration without caching
+- #1179 — AWK state machine never resets between hunks
+- #1180 — Code fence detection logic flawed in extract_suggestions()
+- #1181 — Unsafe .unwrap() in production code manifest.rs
diff --git a/providers.json b/providers.json
index 12723a6f..550edd64 100644
--- a/providers.json
+++ b/providers.json
@@ -393,8 +393,8 @@
"api_key_required": true,
"base_url_env": "MINIMAX_BASE_URL",
"model_env": "MINIMAX_MODEL",
- "default_model": "MiniMax-M2.5",
- "description": "MiniMax API (MiniMax-M2.5 and MiniMax-M2.5-highspeed models)",
+ "default_model": "MiniMax-M2.7",
+ "description": "MiniMax API (MiniMax-M2.7, MiniMax-M2.7-highspeed, MiniMax-M2.5 and MiniMax-M2.5-highspeed models)",
"setup": {
"kind": "api_key",
"secret_name": "llm_minimax_api_key",
diff --git a/registry/channels/discord.json b/registry/channels/discord.json
index 50ef85ee..dc545d75 100644
--- a/registry/channels/discord.json
+++ b/registry/channels/discord.json
@@ -18,8 +18,8 @@
},
"artifacts": {
"wasm32-wasip2": {
- "url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/discord-0.2.0-wasm32-wasip2.tar.gz",
- "sha256": "efa1b9019fa33e243f8db1e1fcc732731d45836336bdd26ca19b6fe227ca8b69"
+ "url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/channel-discord-0.2.1-wasm32-wasip2.tar.gz",
+ "sha256": "6159cb54aa44a9d8219e29bf0aea9404213b20ff567506fe75f23d4698d6ec18"
}
},
"auth_summary": {
diff --git a/registry/channels/feishu.json b/registry/channels/feishu.json
index 0446a442..66cecf1d 100644
--- a/registry/channels/feishu.json
+++ b/registry/channels/feishu.json
@@ -17,7 +17,12 @@
"capabilities": "feishu.capabilities.json",
"crate_name": "feishu-channel"
},
- "artifacts": {},
+ "artifacts": {
+ "wasm32-wasip2": {
+ "sha256": "5fca74022264d1c8e78a0853766276f7ffa3cf0d8065b2f51ca10985acad4714",
+ "url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/channel-feishu-0.1.1-wasm32-wasip2.tar.gz"
+ }
+ },
"auth_summary": {
"method": "manual",
"provider": "Feishu / Lark",
diff --git a/registry/channels/telegram.json b/registry/channels/telegram.json
index e44061e5..85d793ed 100644
--- a/registry/channels/telegram.json
+++ b/registry/channels/telegram.json
@@ -2,7 +2,7 @@
"name": "telegram",
"display_name": "Telegram Channel",
"kind": "channel",
- "version": "0.2.4",
+ "version": "0.2.5",
"wit_version": "0.3.0",
"description": "Talk to your agent through a Telegram bot",
"keywords": [
@@ -18,8 +18,8 @@
},
"artifacts": {
"wasm32-wasip2": {
- "url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/telegram-0.2.3-wasm32-wasip2.tar.gz",
- "sha256": "b9a83d5a2d1285ce0ec116b354336a1f245f893291ccb01dffbcaccf89d72aed"
+ "url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/channel-telegram-0.2.4-wasm32-wasip2.tar.gz",
+ "sha256": "a7cb300ec1c946831cfceaa95c1dc8f30d0f42a3924f3cb5de8098821573f4b8"
}
},
"auth_summary": {
diff --git a/registry/tools/github.json b/registry/tools/github.json
index e775ac82..e760c4df 100644
--- a/registry/tools/github.json
+++ b/registry/tools/github.json
@@ -19,8 +19,8 @@
},
"artifacts": {
"wasm32-wasip2": {
- "url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/github-0.2.0-wasm32-wasip2.tar.gz",
- "sha256": "da9fac56b6f20197a415489bbaec9fefb085a5cf6324cab79ea48a47eb19c13b"
+ "url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/tool-github-0.2.1-wasm32-wasip2.tar.gz",
+ "sha256": "92c530b3ad172e2372d819744b5233f1d8f65768e26eb5a6c213eba3ce1de758"
}
},
"auth_summary": {
diff --git a/registry/tools/llm-context.json b/registry/tools/llm-context.json
index a647a153..e4e9808c 100644
--- a/registry/tools/llm-context.json
+++ b/registry/tools/llm-context.json
@@ -21,8 +21,8 @@
},
"artifacts": {
"wasm32-wasip2": {
- "url": "https://github.com/nearai/ironclaw/releases/latest/download/llm-context-wasm32-wasip2.tar.gz",
- "sha256": "581cc5867ef3b75116b7ddc8161e63dd92befe2b53e6ad8213c007639aa243c3"
+ "url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/tool-llm-context-0.1.0-wasm32-wasip2.tar.gz",
+ "sha256": "d9ced2b1226b879135891e0ee40e072c7c95412e1b2462925a23853e1f92497e"
}
},
"auth_summary": {
diff --git a/registry/tools/slack.json b/registry/tools/slack.json
index 11bd7fff..8e1df989 100644
--- a/registry/tools/slack.json
+++ b/registry/tools/slack.json
@@ -17,8 +17,8 @@
},
"artifacts": {
"wasm32-wasip2": {
- "url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/slack-0.2.1-wasm32-wasip2.tar.gz",
- "sha256": "d4667e35126986509d862bc3a0088777305d8f41c75de83c1e223b42312ede48"
+ "url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/tool-slack-0.2.0-wasm32-wasip2.tar.gz",
+ "sha256": "ccfb0415d7a04f9497726c712d15216de36e86f498b849101283c017f5ab4efb"
}
},
"auth_summary": {
diff --git a/registry/tools/telegram.json b/registry/tools/telegram.json
index 680d6fdb..12e58c68 100644
--- a/registry/tools/telegram.json
+++ b/registry/tools/telegram.json
@@ -18,8 +18,8 @@
},
"artifacts": {
"wasm32-wasip2": {
- "url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/telegram-0.2.2-wasm32-wasip2.tar.gz",
- "sha256": "b9a83d5a2d1285ce0ec116b354336a1f245f893291ccb01dffbcaccf89d72aed"
+ "url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/tool-telegram-0.2.0-wasm32-wasip2.tar.gz",
+ "sha256": "c17065ca41fae5f2a7c43b36144686718cd310a2f22442313bb1aa82bbad0ae4"
}
},
"auth_summary": {
diff --git a/registry/tools/web-search.json b/registry/tools/web-search.json
index 1722c391..5c1dedef 100644
--- a/registry/tools/web-search.json
+++ b/registry/tools/web-search.json
@@ -18,8 +18,8 @@
},
"artifacts": {
"wasm32-wasip2": {
- "url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/web-search-0.2.0-wasm32-wasip2.tar.gz",
- "sha256": "56834573c54ea2a33cea1eb0f04bbdf59f1ef8d8702995cf431b0921302eeccc"
+ "url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/tool-web-search-0.2.1-wasm32-wasip2.tar.gz",
+ "sha256": "bad275ca4ec314adea5241d6b92c44ccf9cebcbca8e30ba2493cc0bcb4b57218"
}
},
"auth_summary": {
diff --git a/release-plz.toml b/release-plz.toml
index ee7037df..b003952d 100644
--- a/release-plz.toml
+++ b/release-plz.toml
@@ -3,4 +3,5 @@ git_release_enable = false
[[package]]
name = "ironclaw_safety"
+publish = false
release = false
diff --git a/skills/delegation/SKILL.md b/skills/delegation/SKILL.md
new file mode 100644
index 00000000..0163dd32
--- /dev/null
+++ b/skills/delegation/SKILL.md
@@ -0,0 +1,75 @@
+---
+name: delegation
+version: 0.1.0
+description: Helps users delegate tasks, break them into steps, set deadlines, and track progress via routines and memory.
+activation:
+ keywords:
+ - delegate
+ - hand off
+ - assign task
+ - help me with
+ - take care of
+ - remind me to
+ - schedule
+ - plan my
+ - manage my
+ - track this
+ patterns:
+ - "can you.*handle"
+ - "I need (help|someone) to"
+ - "take over"
+ - "set up a reminder"
+ - "follow up on"
+ tags:
+ - personal-assistant
+ - task-management
+ - delegation
+ max_context_tokens: 1500
+---
+
+# Task Delegation Assistant
+
+When the user wants to delegate a task or get help managing something, follow this process:
+
+## 1. Clarify the Task
+
+Ask what needs to be done, by when, and any constraints. Get enough detail to act independently but don't over-interrogate. If the request is clear, skip straight to planning.
+
+## 2. Break It Down
+
+Decompose the task into concrete, actionable steps. Use `memory_write` to persist the task plan to a path like `tasks/{task-name}.md` with:
+- Clear description
+- Steps with checkboxes
+- Due date (if any)
+- Status: pending/in-progress/done
+
+## 3. Set Up Tracking
+
+If the task is recurring or has a deadline:
+- Create a routine using `routine_create` for scheduled check-ins
+- Add a heartbeat item if it needs daily monitoring
+- Set up an event-triggered routine if it depends on external input
+
+## 4. Use Profile Context
+
+Check `USER.md` for the user's preferences:
+- **Proactivity level**: High = check in frequently. Low = only report on completion.
+- **Communication style**: Match their preferred tone and detail level.
+- **Focus areas**: Prioritize tasks that align with their stated goals.
+
+## 5. Execute or Queue
+
+- If you can do it now (search, draft, organize, calculate), do it immediately.
+- If it requires waiting, external action, or follow-up, create a reminder routine.
+- If it requires tools you don't have, explain what's needed and suggest alternatives.
+
+## 6. Report Back
+
+Always confirm the plan with the user before starting execution. After completing, update the task file in memory and notify the user with a concise summary.
+
+## Communication Guidelines
+
+- Be direct and action-oriented
+- Confirm understanding before acting on ambiguous requests
+- When in doubt about autonomy level, ask once then remember the answer
+- Use `memory_write` to track delegation preferences for future reference
diff --git a/skills/ironclaw-workflow-orchestrator/references/workflow-routines.md b/skills/ironclaw-workflow-orchestrator/references/workflow-routines.md
index 8afa857d..5e64a2b2 100644
--- a/skills/ironclaw-workflow-orchestrator/references/workflow-routines.md
+++ b/skills/ironclaw-workflow-orchestrator/references/workflow-routines.md
@@ -8,15 +8,21 @@ Replace `{{...}}` placeholders before use.
{
"name": "wf-issue-plan",
"description": "Create implementation plan when a new issue arrives",
- "trigger_type": "system_event",
- "event_source": "github",
- "event_type": "issue.opened",
- "event_filters": {
- "repository_name": "{{repository}}"
- },
- "action_type": "full_job",
"prompt": "For issue #{{issue_number}} in {{repository}}, produce a concrete implementation plan with milestones, edge cases, and tests. Post/update an issue comment with the plan.",
- "cooldown_secs": 30
+ "request": {
+ "kind": "system_event",
+ "source": "github",
+ "event_type": "issue.opened",
+ "filters": {
+ "repository_name": "{{repository}}"
+ }
+ },
+ "execution": {
+ "mode": "full_job"
+ },
+ "advanced": {
+ "cooldown_secs": 30
+ }
}
```
@@ -28,16 +34,22 @@ Trigger per-maintainer by creating one routine per handle, or maintain a shared
{
"name": "wf-maintainer-comment-gate-{{maintainer}}",
"description": "React to maintainer guidance comments on issues/PRs",
- "trigger_type": "system_event",
- "event_source": "github",
- "event_type": "pr.comment.created",
- "event_filters": {
- "repository_name": "{{repository}}",
- "comment_author": "{{maintainer}}"
- },
- "action_type": "full_job",
"prompt": "Read the maintainer comment and decide: update plan or start/continue implementation. If plan changes are requested, edit the plan artifact first. If implementation is requested, continue on the feature branch and update PR status/comment.",
- "cooldown_secs": 20
+ "request": {
+ "kind": "system_event",
+ "source": "github",
+ "event_type": "pr.comment.created",
+ "filters": {
+ "repository_name": "{{repository}}",
+ "comment_author": "{{maintainer}}"
+ }
+ },
+ "execution": {
+ "mode": "full_job"
+ },
+ "advanced": {
+ "cooldown_secs": 20
+ }
}
```
@@ -47,15 +59,21 @@ Trigger per-maintainer by creating one routine per handle, or maintain a shared
{
"name": "wf-pr-monitor-loop",
"description": "Keep PR healthy: address review comments and refresh branch",
- "trigger_type": "system_event",
- "event_source": "github",
- "event_type": "pr.synchronize",
- "event_filters": {
- "repository_name": "{{repository}}"
- },
- "action_type": "full_job",
"prompt": "For PR #{{pr_number}}, collect open review comments and unresolved threads, apply fixes, push branch updates, and summarize remaining blockers. If conflict with {{main_branch}}, rebase/merge from origin/{{main_branch}} and resolve safely.",
- "cooldown_secs": 20
+ "request": {
+ "kind": "system_event",
+ "source": "github",
+ "event_type": "pr.synchronize",
+ "filters": {
+ "repository_name": "{{repository}}"
+ }
+ },
+ "execution": {
+ "mode": "full_job"
+ },
+ "advanced": {
+ "cooldown_secs": 20
+ }
}
```
@@ -65,16 +83,22 @@ Trigger per-maintainer by creating one routine per handle, or maintain a shared
{
"name": "wf-ci-fix-loop",
"description": "Fix failing CI checks on active PRs",
- "trigger_type": "system_event",
- "event_source": "github",
- "event_type": "ci.check_run.completed",
- "event_filters": {
- "repository_name": "{{repository}}",
- "ci_conclusion": "failure"
- },
- "action_type": "full_job",
"prompt": "Find failing check details for PR #{{pr_number}}, implement minimal safe fixes, rerun or await CI, and post concise status updates. Prioritize deterministic and test-backed fixes.",
- "cooldown_secs": 20
+ "request": {
+ "kind": "system_event",
+ "source": "github",
+ "event_type": "ci.check_run.completed",
+ "filters": {
+ "repository_name": "{{repository}}",
+ "ci_conclusion": "failure"
+ }
+ },
+ "execution": {
+ "mode": "full_job"
+ },
+ "advanced": {
+ "cooldown_secs": 20
+ }
}
```
@@ -84,11 +108,17 @@ Trigger per-maintainer by creating one routine per handle, or maintain a shared
{
"name": "wf-staging-batch-review",
"description": "Batch correctness review through staging, then merge to main",
- "trigger_type": "cron",
- "schedule": "0 0 */{{batch_interval_hours}} * * *",
- "action_type": "full_job",
"prompt": "Every cycle: list ready PRs, merge ready ones into {{staging_branch}}, run deep correctness analysis in batch, fix discovered issues on affected branches, ensure CI green, then merge {{staging_branch}} into {{main_branch}} if clean.",
- "cooldown_secs": 120
+ "request": {
+ "kind": "cron",
+ "schedule": "0 0 */{{batch_interval_hours}} * * *"
+ },
+ "execution": {
+ "mode": "full_job"
+ },
+ "advanced": {
+ "cooldown_secs": 120
+ }
}
```
@@ -98,16 +128,22 @@ Trigger per-maintainer by creating one routine per handle, or maintain a shared
{
"name": "wf-learning-memory",
"description": "Capture merge learnings into shared memory",
- "trigger_type": "system_event",
- "event_source": "github",
- "event_type": "pr.closed",
- "event_filters": {
- "repository_name": "{{repository}}",
- "pr_merged": "true"
- },
- "action_type": "full_job",
"prompt": "From merged PR #{{pr_number}}, extract preventable mistakes, reviewer themes, CI failure causes, and successful patterns. Write/update a shared memory doc with actionable rules to reduce cycle time and regressions.",
- "cooldown_secs": 30
+ "request": {
+ "kind": "system_event",
+ "source": "github",
+ "event_type": "pr.closed",
+ "filters": {
+ "repository_name": "{{repository}}",
+ "pr_merged": "true"
+ }
+ },
+ "execution": {
+ "mode": "full_job"
+ },
+ "advanced": {
+ "cooldown_secs": 30
+ }
}
```
@@ -115,7 +151,7 @@ Trigger per-maintainer by creating one routine per handle, or maintain a shared
```json
{
- "source": "github",
+ "event_source": "github",
"event_type": "issue.opened",
"payload": {
"repository_name": "{{repository}}",
diff --git a/skills/routine-advisor/SKILL.md b/skills/routine-advisor/SKILL.md
new file mode 100644
index 00000000..3bb10c72
--- /dev/null
+++ b/skills/routine-advisor/SKILL.md
@@ -0,0 +1,118 @@
+---
+name: routine-advisor
+version: 0.1.0
+description: Suggests relevant cron routines based on user context, goals, and observed patterns
+activation:
+ keywords:
+ - every day
+ - every morning
+ - every week
+ - routine
+ - automate
+ - remind me
+ - check daily
+ - monitor
+ - recurring
+ - schedule
+ - habit
+ - workflow
+ - keep forgetting
+ - always have to
+ - repetitive
+ - notifications
+ - digest
+ - summary
+ - review daily
+ - weekly review
+ patterns:
+ - "I (always|usually|often|regularly) (check|do|look at|review)"
+ - "every (morning|evening|week|day|monday|friday)"
+ - "I (wish|want) (I|it) (could|would) (automatically|auto)"
+ - "is there a way to (auto|schedule|set up)"
+ - "can you (check|monitor|watch|track).*for me"
+ - "I keep (forgetting|missing|having to)"
+ tags:
+ - automation
+ - scheduling
+ - personal-assistant
+ - productivity
+ max_context_tokens: 1500
+---
+
+# Routine Advisor
+
+When the conversation suggests the user has a repeatable task or could benefit from automation, consider suggesting a routine.
+
+## When to Suggest
+
+Suggest a routine when you notice:
+- The user describes doing something repeatedly ("I check my PRs every morning")
+- The user mentions forgetting recurring tasks ("I keep forgetting to...")
+- The user asks you to do something that sounds periodic
+- You've learned enough about the user to propose a relevant automation
+- The user has installed extensions that enable new monitoring capabilities
+
+## How to Suggest
+
+Be specific and concrete. Not "Want me to set up a routine?" but rather: "I noticed you review PRs every morning. Want me to create a daily 9am routine that checks your open PRs and sends you a summary?"
+
+Always include:
+1. What the routine would do (specific action)
+2. When it would run (specific schedule in plain language)
+3. How it would notify them (which channel they're on)
+
+Wait for the user to confirm before creating.
+
+## Pacing
+
+- First 1-3 conversations: Do NOT suggest routines. Focus on helping and learning.
+- After learning 2-3 user patterns: Suggest your first routine. Keep it simple.
+- After 5+ conversations: Suggest more routines as patterns emerge.
+- Never suggest more than 1 routine per conversation unless the user is clearly interested.
+- If the user declines, wait at least 3 conversations before suggesting again.
+
+## Creating Routines
+
+Use the `routine_create` tool. Before creating, check `routine_list` to avoid duplicates.
+
+Parameters:
+- `trigger_type`: Usually "cron" for scheduled tasks
+- `schedule`: Standard cron format. Common schedules:
+ - Daily 9am: `0 9 * * *`
+ - Weekday mornings: `0 9 * * MON-FRI`
+ - Weekly Monday: `0 9 * * MON`
+ - Every 2 hours during work: `0 9-17/2 * * MON-FRI`
+ - Sunday evening: `0 18 * * SUN`
+- `action_type`: "lightweight" for simple checks, "full_job" for multi-step tasks
+- `prompt`: Clear, specific instruction for what the routine should do
+- `context_paths`: Workspace files to load as context (e.g., `["context/profile.json", "MEMORY.md"]`)
+
+## Routine Ideas by User Type
+
+**Developer:**
+- Daily PR review digest (check open PRs, summarize what needs attention)
+- CI/CD failure alerts (monitor build status)
+- Weekly dependency update check
+- Daily standup prep (summarize yesterday's work from daily logs)
+
+**Professional:**
+- Morning briefing (today's priorities from memory + any pending tasks)
+- End-of-day summary (what was accomplished, what's pending)
+- Weekly goal review (check progress against stated goals)
+- Meeting prep reminders
+
+**Health/Personal:**
+- Daily exercise or habit check-in
+- Weekly meal planning prompt
+- Monthly budget review reminder
+
+**General:**
+- Daily news digest on topics of interest
+- Weekly reflection prompt (what went well, what to improve)
+- Periodic task/reminder check-in
+- Regular cleanup of stale tasks or notes
+- Weekly profile evolution (if the user has a profile in `context/profile.json`, suggest a Monday routine that reads the profile via `memory_read`, searches recent conversations for new patterns with `memory_search`, and updates the profile via `memory_write` if any fields should change with confidence > 0.6 — be conservative, only update with clear evidence)
+
+## Awareness
+
+Before suggesting, consider what tools and extensions are currently available. Only suggest routines the agent can actually execute. If a routine would need a tool that isn't installed, mention that too: "If you connect your calendar, I could also send you a morning briefing with today's meetings."
diff --git a/src/agent/CLAUDE.md b/src/agent/CLAUDE.md
index e55c9591..686753de 100644
--- a/src/agent/CLAUDE.md
+++ b/src/agent/CLAUDE.md
@@ -113,7 +113,7 @@ Check-insert is done under a single write lock to prevent TOCTOU races. A cleanu
4. Detects broken tools via `store.get_broken_tools(5)` (threshold: 5 failures). Requires `with_store()` to be called; returns empty without a store.
5. Attempts to rebuild broken tools via `SoftwareBuilder`. Requires `with_builder()` to be called; returns `ManualRequired` without a builder.
-Note: the `stuck_threshold` duration is stored but currently unused (marked `#[allow(dead_code)]`). Stuck detection relies on `JobState::Stuck` being set by the state machine, not wall-clock time comparison.
+The `stuck_threshold` duration is used for time-based detection of `InProgress` jobs that have been running longer than the threshold. When `detect_stuck_jobs()` finds such jobs, it transitions them to `Stuck` before returning them, enabling the normal `attempt_recovery()` path.
Repair results: `Success`, `Retry`, `Failed`, `ManualRequired`. `Retry` does NOT notify the user (to avoid spam).
diff --git a/src/agent/agent_loop.rs b/src/agent/agent_loop.rs
index 83d971ef..dbc9d38b 100644
--- a/src/agent/agent_loop.rs
+++ b/src/agent/agent_loop.rs
@@ -31,6 +31,13 @@ use crate::skills::SkillRegistry;
use crate::tools::ToolRegistry;
use crate::workspace::Workspace;
+/// Static greeting persisted to DB and broadcast on first launch.
+///
+/// Sent before the LLM is involved so the user sees something immediately.
+/// The conversational onboarding (profile building, channel setup) happens
+/// organically in the subsequent turns driven by BOOTSTRAP.md.
+const BOOTSTRAP_GREETING: &str = include_str!("../workspace/seeds/GREETING.md");
+
/// Collapse a tool output string into a single-line preview for display.
pub(crate) fn truncate_for_preview(output: &str, max_chars: usize) -> String {
let collapsed: String = output
@@ -113,6 +120,17 @@ async fn resolve_routine_notification_target(
.await
}
+pub(crate) fn chat_tool_execution_metadata(message: &IncomingMessage) -> serde_json::Value {
+ serde_json::json!({
+ "notify_channel": message.channel,
+ "notify_user": message
+ .routing_target()
+ .unwrap_or_else(|| message.user_id.clone()),
+ "notify_thread_id": message.thread_id,
+ "notify_metadata": message.metadata,
+ })
+}
+
fn should_fallback_routine_notification(error: &ChannelError) -> bool {
!matches!(error, ChannelError::MissingRoutingTarget { .. })
}
@@ -146,6 +164,10 @@ pub struct AgentDeps {
pub transcription: Option>,
/// Document text extraction middleware for PDF, DOCX, PPTX, etc.
pub document_extraction: Option>,
+ /// Sandbox readiness state for full-job routine dispatch.
+ pub sandbox_readiness: crate::agent::routine_engine::SandboxReadiness,
+ /// Software builder for self-repair tool rebuilding.
+ pub builder: Option>,
}
/// The main agent that coordinates all components.
@@ -161,9 +183,10 @@ pub struct Agent {
pub(super) heartbeat_config: Option,
pub(super) hygiene_config: Option,
pub(super) routine_config: Option,
- /// Optional slot to expose the routine engine to the gateway for manual triggering.
+ /// Shared routine-engine slot used for internal event matching and for exposing
+ /// the engine to gateway/manual trigger entry points.
pub(super) routine_engine_slot:
- Option>>>>,
+ Arc>>>,
}
impl Agent {
@@ -228,16 +251,21 @@ impl Agent {
heartbeat_config,
hygiene_config,
routine_config,
- routine_engine_slot: None,
+ routine_engine_slot: Arc::new(tokio::sync::RwLock::new(None)),
}
}
- /// Set the routine engine slot for exposing the engine to the gateway.
+ /// Replace the routine-engine slot with a shared one so the gateway and
+ /// agent reference the same engine.
pub fn set_routine_engine_slot(
&mut self,
slot: Arc>>>,
) {
- self.routine_engine_slot = Some(slot);
+ self.routine_engine_slot = slot;
+ }
+
+ async fn routine_engine(&self) -> Option> {
+ self.routine_engine_slot.read().await.clone()
}
// Convenience accessors
@@ -330,15 +358,48 @@ impl Agent {
/// Run the agent main loop.
pub async fn run(self) -> Result<(), Error> {
+ // Proactive bootstrap: persist the static greeting to DB *before*
+ // starting channels so the first web client sees it via history.
+ let bootstrap_thread_id = if self
+ .workspace()
+ .is_some_and(|ws| ws.take_bootstrap_pending())
+ {
+ tracing::debug!(
+ "Fresh workspace detected — persisting static bootstrap greeting to DB"
+ );
+ if let Some(store) = self.store() {
+ let thread_id = store
+ .get_or_create_assistant_conversation("default", "gateway")
+ .await
+ .ok();
+ if let Some(id) = thread_id {
+ self.persist_assistant_response(id, "gateway", "default", BOOTSTRAP_GREETING)
+ .await;
+ }
+ thread_id
+ } else {
+ None
+ }
+ } else {
+ None
+ };
+
// Start channels
let mut message_stream = self.channels.start_all().await?;
// Start self-repair task with notification forwarding
- let repair = Arc::new(DefaultSelfRepair::new(
+ let mut self_repair = DefaultSelfRepair::new(
self.context_manager.clone(),
self.config.stuck_threshold,
self.config.max_repair_attempts,
- ));
+ );
+ if let Some(ref store) = self.deps.store {
+ self_repair = self_repair.with_store(Arc::clone(store));
+ }
+ if let Some(ref builder) = self.deps.builder {
+ self_repair = self_repair.with_builder(Arc::clone(builder), Arc::clone(self.tools()));
+ }
+ let repair = Arc::new(self_repair);
let repair_interval = self.config.repair_check_interval;
let repair_channels = self.channels.clone();
let repair_owner_id = self.owner_id().to_string();
@@ -541,6 +602,7 @@ impl Agent {
Some(self.scheduler.clone()),
self.tools().clone(),
self.safety().clone(),
+ self.deps.sandbox_readiness,
));
// Register routine tools
@@ -633,9 +695,7 @@ impl Agent {
// via a local to use in the message loop below.
// Expose engine to gateway for manual triggering
- if let Some(ref slot) = self.routine_engine_slot {
- *slot.write().await = Some(Arc::clone(&engine));
- }
+ *self.routine_engine_slot.write().await = Some(Arc::clone(&engine));
tracing::debug!(
"Routines enabled: cron ticker every {}s, max {} concurrent",
@@ -655,8 +715,29 @@ impl Agent {
None
};
- // Extract engine ref for use in message loop
- let routine_engine_for_loop = routine_handle.as_ref().map(|(_, e)| Arc::clone(e));
+ // Bootstrap phase 2: register the thread in session manager and
+ // broadcast the greeting via SSE for any clients already connected.
+ // The greeting was already persisted to DB before start_all(), so
+ // clients that connect after this point will see it via history.
+ if let Some(id) = bootstrap_thread_id {
+ // Use get_or_create_session (not resolve_thread) to avoid creating
+ // an orphan thread. Then insert the DB-sourced thread directly.
+ let session = self.session_manager.get_or_create_session("default").await;
+ {
+ use crate::agent::session::Thread;
+ let mut sess = session.lock().await;
+ let thread = Thread::with_id(id, sess.id);
+ sess.active_thread = Some(id);
+ sess.threads.entry(id).or_insert(thread);
+ }
+ self.session_manager
+ .register_thread("default", "gateway", id, session)
+ .await;
+
+ let mut out = OutgoingResponse::text(BOOTSTRAP_GREETING.to_string());
+ out.thread_id = Some(id.to_string());
+ let _ = self.channels.broadcast("gateway", "default", out).await;
+ }
// Main message loop
tracing::debug!("Agent {} ready and listening", self.config.name);
@@ -693,29 +774,6 @@ impl Agent {
// Store successfully extracted document text in workspace for indexing
self.store_extracted_documents(&message).await;
- // Event-triggered routines consume plain user input before it enters
- // the normal chat/tool pipeline. This avoids a duplicate turn where
- // the main agent responds and the routine also fires on the same
- // inbound message.
- if !message.is_internal
- && matches!(
- SubmissionParser::parse(&message.content),
- Submission::UserInput { .. }
- )
- && let Some(ref engine) = routine_engine_for_loop
- {
- let fired = engine.check_event_triggers(&message).await;
- if fired > 0 {
- tracing::debug!(
- channel = %message.channel,
- user = %message.user_id,
- fired,
- "Consumed inbound user message with matching event-triggered routine(s)"
- );
- continue;
- }
- }
-
match self.handle_message(&message).await {
Ok(Some(response)) if !response.is_empty() => {
// Hook: BeforeOutbound — allow hooks to modify or suppress outbound
@@ -874,9 +932,6 @@ impl Agent {
}
async fn handle_message(&self, message: &IncomingMessage) -> Result