diff --git a/README.md b/README.md index a187e0f..ca07662 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,55 @@ CheetahClaws: **A Lightweight** and **Easy-to-Use** Python Reimplementation of C ### Demos + +#### Interactive terminal recordings (animated SVG — plays inline, no click needed) + +
+CheetahClaws code review demo +
+
Code review: profile a slow Python function, switch to local Ollama, apply the fix — 11× faster
+ +
+ +
+CheetahClaws /research 20-source pipeline +
+
/research: parallel fan-out across 20 sources incl. 知乎 / B站 / 微博 / 小红书, cross-platform attention heat table, citation verification
+ +
+ +
+CheetahClaws /brainstorm multi-persona debate +
+
/brainstorm: 5 expert personas (Architect / Skeptic / Pragmatist / DBA / PM) adversarially debate, then converge on a todo_list.txt ready for /worker
+ +
+ +
+CheetahClaws /lab autonomous paper writing +
+
/lab (Research Lab): 9 specialised agents drive a paper through 9 stages — questioning → survey → outline → sandboxed Python experiment → reviewer loop → citation verification → finalisation
+ +
+ +
+CheetahClaws /agent research_assistant autonomous loop +
+
/agent research_assistant: autonomous background loop running every 4h, pushes daily digests to Telegram. Stagnation-stop guard auto-pauses to save tokens when output stops changing.
+ +
+ +
+CheetahClaws Telegram bridge remote control +
+
Telegram bridge: full chat round-trip, slash-command passthrough, job queue with !jobs/!cancel, push notifications when long-running tasks finish. Same UX for /wechat and /slack.
+ +
+ +> **Tip:** the recordings are animated SVGs at [`docs/media/casts/`](docs/media/casts/). Source `.cast` files (asciinema v2) live next to them — replay locally with `asciinema play .cast` or re-render with `svg-term --in .cast --out .svg`. + +--- +
diff --git a/bridges/slack.py b/bridges/slack.py index 2d119b8..f405d98 100644 --- a/bridges/slack.py +++ b/bridges/slack.py @@ -252,20 +252,48 @@ def _slack_terminal(cmd, ch, skey): slash_cb = session_ctx.handle_slash if slash_cb: def _slack_slash_runner(_slash_text, _ch): + import io as _io, sys as _sys, re as _re_ansi _slack_thread_local.active = True sctx = runtime.get_ctx(config) sctx.slack_current_channel = _ch + # Capture print()/info()/ok() output so commands + # like /help (which render via print) reach the + # user instead of disappearing into server logs + # (issue #84 follow-up — same root cause as the + # Telegram bridge). + _buf_out, _buf_err = _io.StringIO(), _io.StringIO() + _orig_out, _orig_err = _sys.stdout, _sys.stderr + class _Tee: + def __init__(self, *streams): + self._streams = streams + def write(self, data): + for s in self._streams: + try: s.write(data) + except Exception: pass + def flush(self): + for s in self._streams: + try: s.flush() + except Exception: pass + _sys.stdout = _Tee(_orig_out, _buf_out) + _sys.stderr = _Tee(_orig_err, _buf_err) try: cmd_type = slash_cb(_slash_text) except Exception as e: + _sys.stdout, _sys.stderr = _orig_out, _orig_err _slack_send(token, _ch, f"⚠ Error: {e}") return finally: + _sys.stdout, _sys.stderr = _orig_out, _orig_err _slack_thread_local.active = False sctx.slack_current_channel = None + _captured = (_buf_out.getvalue() + _buf_err.getvalue()) + _captured = _re_ansi.sub(r'\x1b\[[0-9;]*m', '', _captured).strip() if cmd_type == "simple": cmd_name = _slash_text.strip().split()[0] - _slack_send(token, _ch, f"✅ {cmd_name} executed.") + if _captured: + _slack_send(token, _ch, _captured) + else: + _slack_send(token, _ch, f"✅ {cmd_name} executed.") return slack_state = session_ctx.agent_state if slack_state and slack_state.messages: diff --git a/bridges/telegram.py b/bridges/telegram.py index 0175c0a..c048274 100644 --- a/bridges/telegram.py +++ b/bridges/telegram.py @@ -130,17 +130,27 @@ def _handle_callback_query(token: str, chat_id: int, cb: dict, _, prompt_id, value = cb_data.split(":", 2) expected = getattr(session_ctx, "tg_callback_prompt_id", "") or "" - if expected and expected != prompt_id: + if not expected: + # No prompt is currently waiting — this click belongs to an + # already-answered or timed-out prompt. Don't edit the message + # with a fake "✓ Selected" confirmation; the user would think the + # action took effect when in fact nothing happens (issue #84 + # follow-up). The acknowledgeCallbackQuery above clears the + # spinner so the click still feels handled. + return + if expected != prompt_id: # Stale click from an earlier prompt — ignore so the live prompt # keeps waiting for its own button press. return - # Find the human label for this value, if we can match the message text. - label_for_value = value - for line in cb_text.splitlines(): - # Lines look like "[1] ✅ Approve — y" but the label format is owned by - # the caller, so just take the value verbatim if no match. - pass + # Sanitize the value for visual confirmation. Callers can pass any + # string as the option value (it travels in callback_data), so escape + # backticks/Markdown markers before embedding in the edited message — + # otherwise editMessageText silently fails on parse errors and the + # user just sees the original prompt unchanged. + label_for_value = ( + str(value).replace("\\", "\\\\").replace("`", "'").replace("*", "·") + ) if cb_msg: new_body = cb_text + f"\n\n✓ Selected: `{label_for_value}`" @@ -542,17 +552,48 @@ def _terminal_runner(cmd, chat_token, cid, skey): slash_cb = session_ctx.handle_slash if slash_cb: def _slash_runner(_slash_text, _token, _chat_id): + import io as _io, sys as _sys, re as _re_ansi _tg_thread_local.active = True + # Capture print()/info()/ok()/warn()/err() output so + # commands like /help (which render their menu via + # print) surface in the chat instead of disappearing + # into the server log (issue #84 follow-up). + _buf_out, _buf_err = _io.StringIO(), _io.StringIO() + _orig_out, _orig_err = _sys.stdout, _sys.stderr + class _Tee: + def __init__(self, *streams): + self._streams = streams + def write(self, data): + for s in self._streams: + try: s.write(data) + except Exception: pass + def flush(self): + for s in self._streams: + try: s.flush() + except Exception: pass + _sys.stdout = _Tee(_orig_out, _buf_out) + _sys.stderr = _Tee(_orig_err, _buf_err) try: cmd_type = slash_cb(_slash_text) except Exception as e: + _sys.stdout, _sys.stderr = _orig_out, _orig_err _tg_send(_token, _chat_id, f"⚠ Error: {e}") return finally: + _sys.stdout, _sys.stderr = _orig_out, _orig_err _tg_thread_local.active = False + _captured = (_buf_out.getvalue() + _buf_err.getvalue()) + _captured = _re_ansi.sub(r'\x1b\[[0-9;]*m', '', _captured).strip() if cmd_type == "simple": cmd_name = _slash_text.strip().split()[0] - _tg_send(_token, _chat_id, f"✅ {cmd_name} executed.") + # Forward the captured menu/status text so the + # user actually sees /help, /status, /model + # output. Fall back to the bare ack only when + # the command produced nothing. + if _captured: + _tg_send(_token, _chat_id, _captured) + else: + _tg_send(_token, _chat_id, f"✅ {cmd_name} executed.") return tg_state = session_ctx.agent_state if tg_state and tg_state.messages: diff --git a/bridges/wechat.py b/bridges/wechat.py index bfe0bec..b4521ba 100644 --- a/bridges/wechat.py +++ b/bridges/wechat.py @@ -615,20 +615,48 @@ def _wx_terminal(cmd, uid, skey): slash_cb = session_ctx.handle_slash if slash_cb: def _wx_slash_runner(_slash_text, _uid): + import io as _io, sys as _sys, re as _re_ansi _wx_thread_local.active = True sctx = runtime.get_ctx(config) sctx.wx_current_user_id = _uid + # Capture print()/info()/ok() output so commands + # like /help (which render via print) reach the + # user instead of disappearing into server logs + # (issue #84 follow-up — same root cause as the + # Telegram bridge). + _buf_out, _buf_err = _io.StringIO(), _io.StringIO() + _orig_out, _orig_err = _sys.stdout, _sys.stderr + class _Tee: + def __init__(self, *streams): + self._streams = streams + def write(self, data): + for s in self._streams: + try: s.write(data) + except Exception: pass + def flush(self): + for s in self._streams: + try: s.flush() + except Exception: pass + _sys.stdout = _Tee(_orig_out, _buf_out) + _sys.stderr = _Tee(_orig_err, _buf_err) try: cmd_type = slash_cb(_slash_text) except Exception as e: + _sys.stdout, _sys.stderr = _orig_out, _orig_err _wx_send(_uid, f"⚠ Error: {e}", config) return finally: + _sys.stdout, _sys.stderr = _orig_out, _orig_err _wx_thread_local.active = False sctx.wx_current_user_id = None + _captured = (_buf_out.getvalue() + _buf_err.getvalue()) + _captured = _re_ansi.sub(r'\x1b\[[0-9;]*m', '', _captured).strip() if cmd_type == "simple": cmd_name = _slash_text.strip().split()[0] - _wx_send(_uid, f"✅ {cmd_name} executed.", config) + if _captured: + _wx_send(_uid, _captured, config) + else: + _wx_send(_uid, f"✅ {cmd_name} executed.", config) return wx_state = session_ctx.agent_state if wx_state and wx_state.messages: diff --git a/cheetahclaws.py b/cheetahclaws.py index fa3dc52..9abbeb9 100755 --- a/cheetahclaws.py +++ b/cheetahclaws.py @@ -739,14 +739,28 @@ def _start_headless_bridges(config: dict) -> None: return # nothing configured — no-op import runtime as _runtime - from agent import AgentState, run as _agent_run, TextChunk, ToolStart, ToolEnd + from agent import ( + AgentState, run as _agent_run, + TextChunk, ToolStart, ToolEnd, PermissionRequest, + ) from context import build_system_prompt state = AgentState(messages=[], total_input_tokens=0, total_output_tokens=0) session_ctx = _runtime.get_session_ctx(config.get("_session_id", "default")) session_ctx.agent_state = state + # Wire the low-level Telegram sender so ask_input_interactive can render + # inline-keyboard permission prompts. Without this, the Telegram branch + # in tools/interaction.py:256 falls through to terminal input(), making + # approval prompts invisible in headless/Docker mode (issue #84 follow-up). + session_ctx.tg_send = _tg_send def _headless_run_query(prompt: str, is_background: bool = False) -> None: + # Promote the per-turn telegram_incoming flag so _is_in_tg_turn() sees + # in_telegram_turn=True for the duration of this query. Without this, + # ask_input_interactive routes prompts to the terminal even though the + # query was triggered by an inbound Telegram message. + session_ctx.in_telegram_turn = session_ctx.telegram_incoming + session_ctx.telegram_incoming = False system_prompt = build_system_prompt(config) try: for ev in _agent_run(prompt, state, config, system_prompt): @@ -759,8 +773,20 @@ def _headless_run_query(prompt: str, is_background: bool = False) -> None: elif isinstance(ev, ToolEnd) and session_ctx.on_tool_end: try: session_ctx.on_tool_end(ev.name, str(ev.result or "")[:500]) except Exception: pass + elif isinstance(ev, PermissionRequest): + # Mirror repl()'s permission handling so headless deploys + # actually surface the [Approve][Reject][Accept all] + # inline-keyboard prompt over the bridge. Defaults to + # denied on any failure so a broken bridge never auto-runs + # a sensitive tool. + try: + ev.granted = ask_permission_interactive(ev.description, config) + except Exception: + ev.granted = False except Exception: pass # never let a bridge query crash the server thread + finally: + session_ctx.in_telegram_turn = False session_ctx.run_query = _headless_run_query # Wire slash-command dispatch so bridges' / messages don't go to diff --git a/docs/media/casts/brainstorm.cast b/docs/media/casts/brainstorm.cast new file mode 100644 index 0000000..de61c91 --- /dev/null +++ b/docs/media/casts/brainstorm.cast @@ -0,0 +1,130 @@ +{"version": 2, "width": 110, "height": 32, "timestamp": 1747262400, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}, "title": "CheetahClaws /brainstorm \u2014 5 personas debate event sourcing", "idle_time_limit": 1.5} +[0.0, "o", "[32m~/projects/checkout[0m [36m\u276f[0m cheetahclaws\r\n"] +[0.3, "o", "[2m[CheetahClaws v3.05.79 \u00b7 claude-sonnet-4-6][0m\r\n\r\n"] +[0.5, "o", "[1m[36m[checkout] \u00bb[0m "] +[1.0, "o", ""] +[1.05, "o", "/"] +[1.107, "o", "b"] +[1.166, "o", "r"] +[1.212, "o", "a"] +[1.267, "o", "i"] +[1.321, "o", "n"] +[1.374, "o", "s"] +[1.416, "o", "t"] +[1.457, "o", "o"] +[1.505, "o", "r"] +[1.56, "o", "m"] +[1.605, "o", " "] +[1.655, "o", "\""] +[1.701, "o", "S"] +[1.758, "o", "h"] +[1.817, "o", "o"] +[1.865, "o", "u"] +[1.925, "o", "l"] +[1.966, "o", "d"] +[2.022, "o", " "] +[2.08, "o", "w"] +[2.123, "o", "e"] +[2.177, "o", " "] +[2.228, "o", "m"] +[2.287, "o", "i"] +[2.331, "o", "g"] +[2.382, "o", "r"] +[2.44, "o", "a"] +[2.493, "o", "t"] +[2.539, "o", "e"] +[2.58, "o", " "] +[2.639, "o", "t"] +[2.689, "o", "h"] +[2.749, "o", "e"] +[2.792, "o", " "] +[2.848, "o", "o"] +[2.904, "o", "r"] +[2.962, "o", "d"] +[3.003, "o", "e"] +[3.05, "o", "r"] +[3.091, "o", " "] +[3.146, "o", "s"] +[3.193, "o", "e"] +[3.253, "o", "r"] +[3.3, "o", "v"] +[3.356, "o", "i"] +[3.413, "o", "c"] +[3.458, "o", "e"] +[3.509, "o", " "] +[3.568, "o", "f"] +[3.619, "o", "r"] +[3.666, "o", "o"] +[3.711, "o", "m"] +[3.767, "o", " "] +[3.811, "o", "C"] +[3.863, "o", "R"] +[3.914, "o", "U"] +[3.965, "o", "D"] +[4.007, "o", " "] +[4.061, "o", "t"] +[4.119, "o", "o"] +[4.165, "o", " "] +[4.212, "o", "e"] +[4.27, "o", "v"] +[4.322, "o", "e"] +[4.367, "o", "n"] +[4.407, "o", "t"] +[4.462, "o", " "] +[4.509, "o", "s"] +[4.559, "o", "o"] +[4.604, "o", "u"] +[4.657, "o", "r"] +[4.711, "o", "c"] +[4.758, "o", "i"] +[4.799, "o", "n"] +[4.859, "o", "g"] +[4.917, "o", "?"] +[4.958, "o", "\""] +[5.358, "o", "\r\n\r\n"] +[5.758, "o", "[2m\u25cf Spawning 5 expert personas in parallel\u2026[0m\r\n\r\n"] +[6.258, "o", " [34m\u25cf [ARCHITECT][0m\r\n"] +[6.458, "o", " Event sourcing maps cleanly to checkout's domain \u2014 every state change is\r\r\n"] +[6.658, "o", " already an event (cart-added, payment-authorised, fulfilled). We get audit\r\r\n"] +[6.858, "o", " trail, time-travel debugging, and replay for free.\r\n"] +[7.358, "o", " [31m\u25cf [SKEPTIC][0m\r\n"] +[7.558, "o", " Three months of dev time to rewrite something that works fine. Where's the\r\r\n"] +[7.758, "o", " user-facing win? You're trading a known boring solution for a fashionable\r\r\n"] +[7.958, "o", " one we'll regret in 18 months when the lead architect leaves.\r\n"] +[8.458, "o", " [35m\u25cf [PRAGMATIST][0m\r\n"] +[8.658, "o", " Halfway compromise: keep CRUD as the source of truth, emit an outbox event\r\r\n"] +[8.858, "o", " on each write. We get the audit log without the rewrite. Doors stay open\r\r\n"] +[9.058, "o", " for full ES later.\r\n"] +[9.558, "o", " [33m\u25cf [DBA][0m\r\n"] +[9.758, "o", " Two real issues with the proposal: (1) Postgres event-store throughput on the\r\r\n"] +[9.958, "o", " checkout volume \u2014 we'd need partitioning by tenant; (2) projections lag\r\r\n"] +[10.158, "o", " will surface as stale reads during Black Friday.\r\n"] +[10.658, "o", " [32m\u25cf [PM][0m\r\n"] +[10.858, "o", " What problem are we actually solving? If it's audit/compliance \u2014 outbox\r\r\n"] +[11.058, "o", " pattern is enough. If it's the analytics team rebuilding cart funnels\r\r\n"] +[11.258, "o", " every quarter \u2014 yes, ES pays off.\r\n"] +[11.758, "o", "\r\n"] +[12.058, "o", "[2m\u25cf Round 2: rebuttals\u2026[0m\r\n\r\n"] +[12.558, "o", " [31m\u25cf [SKEPTIC][0m\r\n"] +[12.758, "o", " \u2192 ARCHITECT: 'time-travel debugging for free' has never been free in production.\r\r\n"] +[12.958, "o", " Snapshot management alone is a quarter of work.\r\n"] +[13.458, "o", " [34m\u25cf [ARCHITECT][0m\r\n"] +[13.658, "o", " \u2192 SKEPTIC: agreed on snapshots \u2014 but DBA's outbox path is half-measure.\r\r\n"] +[13.858, "o", " Once we duplicate state we own two truths.\r\n"] +[14.358, "o", " [35m\u25cf [PRAGMATIST][0m\r\n"] +[14.558, "o", " \u2192 Both: outbox is not a half-measure if we treat it as a stepping stone with\r\r\n"] +[14.758, "o", " a six-month review gate.\r\n"] +[15.158, "o", "\r\n"] +[15.658, "o", "[1m\u2500\u2500\u2500 Synthesis \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500[0m\r\n\r\n"] +[15.958, "o", " [32m[1mConsensus:[0m no full migration this quarter. Ship outbox + replayer first.\r\n\r\n"] +[16.258, "o", " [1mDecision pivots on PM's framing:[0m\r\n"] +[16.508, "o", " \u2022 audit/compliance only \u2192 [32mstay CRUD + outbox events[0m\r\n"] +[16.758, "o", " \u2022 analytics rebuild every quarter \u2192 [33mplan full ES, but in Q3[0m\r\n\r\n"] +[17.258, "o", "[33m[Write][0m brainstorm_outputs/todo_list.txt\r\n"] +[17.558, "o", " [32m1.[0m [2m[ ][0m Add outbox table + transactional event publisher (1 week)\r\n"] +[17.758, "o", " [32m2.[0m [2m[ ][0m Wire Kafka consumer \u2192 analytics warehouse (3 days)\r\n"] +[17.958, "o", " [32m3.[0m [2m[ ][0m Benchmark projection lag with prod-shaped Black-Friday replay\r\n"] +[18.158, "o", " [32m4.[0m [2m[ ][0m Schedule Q3 ES decision review (compliance + analytics inputs)\r\n\r\n"] +[18.558, "o", "[32m\u2713[0m 4 tasks ready. Run [36m/worker[0m to auto-implement them.\r\n\r\n"] +[18.958, "o", "[1m[36m[checkout] \u00bb[0m "] +[19.758, "o", ""] diff --git a/docs/media/casts/brainstorm.svg b/docs/media/casts/brainstorm.svg new file mode 100644 index 0000000..3f6da29 --- /dev/null +++ b/docs/media/casts/brainstorm.svg @@ -0,0 +1 @@ +[32m~/projects/checkout[0m[36m❯[0mcheetahclaws[2m[CheetahClawsv3.05.79·claude-sonnet-4-6][0m[1m[36m[checkout]»[0m[1m[36m[checkout]»[0m/brainstorm[1m[36m[checkout]»[0m/brainstorm"Should[1m[36m[checkout]»[0m/brainstorm"Shouldwe[1m[36m[checkout]»[0m/brainstorm"Shouldwemigrate[1m[36m[checkout]»[0m/brainstorm"Shouldwemigratethe[1m[36m[checkout]»[0m/brainstorm"Shouldwemigratetheorder[1m[36m[checkout]»[0m/brainstorm"Shouldwemigratetheorderservice[1m[36m[checkout]»[0m/brainstorm"Shouldwemigratetheorderservicefrom[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromCRUD[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromCRUDto[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromCRUDtoevent[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromCRUDtoeventsourcing?"[2m●Spawning5expertpersonasinparallel…[0m[34m●[ARCHITECT][0mEventsourcingmapscleanlytocheckout'sdomaineverystatechangeisalreadyanevent(cart-added,payment-authorised,fulfilled).Wegetaudittrail,time-traveldebugging,andreplayforfree.[31m●[SKEPTIC][0mThreemonthsofdevtimetorewritesomethingthatworksfine.Where'stheuser-facingwin?You'retradingaknownboringsolutionforafashionableonewe'llregretin18monthswhentheleadarchitectleaves.[35m●[PRAGMATIST][0mHalfwaycompromise:keepCRUDasthesourceoftruth,emitanoutboxeventoneachwrite.Wegettheauditlogwithouttherewrite.DoorsstayopenforfullESlater.[33m●[DBA][0mTworealissueswiththeproposal:(1)Postgresevent-storethroughputonthecheckoutvolumewe'dneedpartitioningbytenant;(2)projectionslagwillsurfaceasstalereadsduringBlackFriday.[32m●[PM][0mWhatproblemareweactuallysolving?Ifit'saudit/complianceoutboxpatternisenough.Ifit'stheanalyticsteamrebuildingcartfunnelseveryquarteryes,ESpaysoff.[2m●Round2:rebuttals…[0mARCHITECT:'time-traveldebuggingforfree'hasneverbeenfreeinproduction.Snapshotmanagementaloneisaquarterofwork.SKEPTIC:agreedonsnapshotsbutDBA'soutboxpathishalf-measure.Onceweduplicatestateweowntwotruths.Both:outboxisnotahalf-measureifwetreatitasasteppingstonewithasix-monthreviewgate.[1m───Synthesis────────────────────────────────────────────────────────[0m[32m[1mConsensus:[0mnofullmigrationthisquarter.Shipoutbox+replayerfirst.[1mDecisionpivotsonPM'sframing:[0maudit/complianceonly[32mstayCRUD+outboxevents[0manalyticsrebuildeveryquarter[33mplanfullES,butinQ3[0m[33m[Write][0mbrainstorm_outputs/todo_list.txt[32m1.[0m[2m[][0mAddoutboxtable+transactionaleventpublisher(1week)[32m2.[0m[2m[][0mWireKafkaconsumeranalyticswarehouse(3days)[32m3.[0m[2m[][0mBenchmarkprojectionlagwithprod-shapedBlack-Fridayreplay[32m4.[0m[2m[][0mScheduleQ3ESdecisionreview(compliance+analyticsinputs)[32m✓[0m4tasksready.Run[36m/worker[0mtoauto-implementthem.[1m[36m[checkout]»[0m/[1m[36m[checkout]»[0m/b[1m[36m[checkout]»[0m/br[1m[36m[checkout]»[0m/bra[1m[36m[checkout]»[0m/brai[1m[36m[checkout]»[0m/brain[1m[36m[checkout]»[0m/brains[1m[36m[checkout]»[0m/brainst[1m[36m[checkout]»[0m/brainsto[1m[36m[checkout]»[0m/brainstor[1m[36m[checkout]»[0m/brainstorm"[1m[36m[checkout]»[0m/brainstorm"S[1m[36m[checkout]»[0m/brainstorm"Sh[1m[36m[checkout]»[0m/brainstorm"Sho[1m[36m[checkout]»[0m/brainstorm"Shou[1m[36m[checkout]»[0m/brainstorm"Shoul[1m[36m[checkout]»[0m/brainstorm"Shouldw[1m[36m[checkout]»[0m/brainstorm"Shouldwem[1m[36m[checkout]»[0m/brainstorm"Shouldwemi[1m[36m[checkout]»[0m/brainstorm"Shouldwemig[1m[36m[checkout]»[0m/brainstorm"Shouldwemigr[1m[36m[checkout]»[0m/brainstorm"Shouldwemigra[1m[36m[checkout]»[0m/brainstorm"Shouldwemigrat[1m[36m[checkout]»[0m/brainstorm"Shouldwemigratet[1m[36m[checkout]»[0m/brainstorm"Shouldwemigrateth[1m[36m[checkout]»[0m/brainstorm"Shouldwemigratetheo[1m[36m[checkout]»[0m/brainstorm"Shouldwemigratetheor[1m[36m[checkout]»[0m/brainstorm"Shouldwemigratetheord[1m[36m[checkout]»[0m/brainstorm"Shouldwemigratetheorde[1m[36m[checkout]»[0m/brainstorm"Shouldwemigratetheorders[1m[36m[checkout]»[0m/brainstorm"Shouldwemigratetheorderse[1m[36m[checkout]»[0m/brainstorm"Shouldwemigratetheorderser[1m[36m[checkout]»[0m/brainstorm"Shouldwemigratetheorderserv[1m[36m[checkout]»[0m/brainstorm"Shouldwemigratetheorderservi[1m[36m[checkout]»[0m/brainstorm"Shouldwemigratetheorderservic[1m[36m[checkout]»[0m/brainstorm"Shouldwemigratetheorderservicef[1m[36m[checkout]»[0m/brainstorm"Shouldwemigratetheorderservicefr[1m[36m[checkout]»[0m/brainstorm"Shouldwemigratetheorderservicefro[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromC[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromCR[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromCRU[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromCRUDt[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromCRUDtoe[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromCRUDtoev[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromCRUDtoeve[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromCRUDtoeven[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromCRUDtoevents[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromCRUDtoeventso[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromCRUDtoeventsou[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromCRUDtoeventsour[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromCRUDtoeventsourc[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromCRUDtoeventsourci[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromCRUDtoeventsourcin[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromCRUDtoeventsourcing[1m[36m[checkout]»[0m/brainstorm"ShouldwemigratetheorderservicefromCRUDtoeventsourcing? \ No newline at end of file diff --git a/docs/media/casts/code_review.cast b/docs/media/casts/code_review.cast new file mode 100644 index 0000000..9700b3f --- /dev/null +++ b/docs/media/casts/code_review.cast @@ -0,0 +1,173 @@ +{"version": 2, "width": 100, "height": 28, "timestamp": 1747262400, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}, "title": "CheetahClaws \u2014 find a perf bug, switch to local Ollama, apply the fix", "idle_time_limit": 1.2} +[0.0, "o", "[32m~/projects/parser[0m [36m\u276f[0m "] +[0.7, "o", ""] +[0.749, "o", "c"] +[0.8, "o", "h"] +[0.859, "o", "e"] +[0.908, "o", "e"] +[0.958, "o", "t"] +[1.01, "o", "a"] +[1.054, "o", "h"] +[1.104, "o", "c"] +[1.156, "o", "l"] +[1.212, "o", "a"] +[1.254, "o", "w"] +[1.3, "o", "s"] +[1.7, "o", "\r\n"] +[2.0, "o", "[2m[CheetahClaws v3.05.79 \u00b7 claude-sonnet-4-6 \u00b7 auto mode][0m\r\n"] +[2.2, "o", "[2mType /help for commands, /model to switch, !cmd for shell, Ctrl+C to quit[0m\r\n\r\n"] +[2.4, "o", "[1m[36m[project] \u00bb[0m "] +[3.0, "o", ""] +[3.049, "o", "W"] +[3.101, "o", "h"] +[3.159, "o", "y"] +[3.208, "o", " "] +[3.258, "o", "i"] +[3.31, "o", "s"] +[3.354, "o", " "] +[3.404, "o", "p"] +[3.457, "o", "a"] +[3.513, "o", "r"] +[3.554, "o", "s"] +[3.601, "o", "e"] +[3.642, "o", "_"] +[3.699, "o", "l"] +[3.752, "o", "o"] +[3.793, "o", "g"] +[3.853, "o", "s"] +[3.912, "o", "("] +[3.965, "o", ")"] +[4.018, "o", " "] +[4.061, "o", "s"] +[4.101, "o", "o"] +[4.152, "o", " "] +[4.193, "o", "s"] +[4.237, "o", "l"] +[4.281, "o", "o"] +[4.322, "o", "w"] +[4.371, "o", " "] +[4.42, "o", "o"] +[4.477, "o", "n"] +[4.527, "o", " "] +[4.58, "o", "t"] +[4.63, "o", "h"] +[4.683, "o", "e"] +[4.733, "o", " "] +[4.778, "o", "1"] +[4.838, "o", "G"] +[4.898, "o", "B"] +[4.955, "o", " "] +[5.009, "o", "s"] +[5.055, "o", "a"] +[5.1, "o", "m"] +[5.146, "o", "p"] +[5.187, "o", "l"] +[5.242, "o", "e"] +[5.29, "o", " "] +[5.347, "o", "f"] +[5.395, "o", "i"] +[5.454, "o", "l"] +[5.511, "o", "e"] +[5.551, "o", "?"] +[5.595, "o", " "] +[5.654, "o", "P"] +[5.703, "o", "r"] +[5.763, "o", "o"] +[5.811, "o", "f"] +[5.852, "o", "i"] +[5.905, "o", "l"] +[5.96, "o", "e"] +[6.006, "o", " "] +[6.047, "o", "a"] +[6.094, "o", "n"] +[6.153, "o", "d"] +[6.208, "o", " "] +[6.251, "o", "f"] +[6.296, "o", "i"] +[6.338, "o", "x"] +[6.379, "o", " "] +[6.435, "o", "i"] +[6.478, "o", "t"] +[6.53, "o", "."] +[7.03, "o", "\r\n\r\n"] +[7.53, "o", "[2m\u25cf Reading the function and tracing hot spots\u2026[0m\r\n"] +[7.93, "o", "[33m[Read][0m src/parser.py [2m(187 lines)[0m\r\n"] +[8.33, "o", "[33m[Grep][0m 'for .* in' src/parser.py [2m(7 matches)[0m\r\n"] +[8.73, "o", "[33m[Bash][0m python -X importtime -c 'from parser import parse_logs; parse_logs(\"sample.log\")'\r\n"] +[9.23, "o", " [2mreal 18.42s user 17.99s sys 0.41s[0m\r\n\r\n"] +[9.83, "o", "[1mFound the hot path:[0m\r\n\r\n"] +[10.23, "o", " [31mline 84[0m [2mre.compile(PATTERN)[0m called inside the loop\r\n"] +[10.53, "o", " [31mline 97[0m [2mresult += line[0m string concatenation in O(n\u00b2)\r\n"] +[10.83, "o", " [31mline 112[0m [2mopen(path).read()[0m loads the whole 1GB into RAM\r\n\r\n"] +[11.33, "o", "[1m3 fixes will bring it under 2 seconds.[0m I'll show them before applying.\r\n\r\n"] +[11.83, "o", "[1m[36m[project] \u00bb[0m "] +[12.23, "o", ""] +[12.279, "o", "/"] +[12.33, "o", "m"] +[12.388, "o", "o"] +[12.438, "o", "d"] +[12.488, "o", "e"] +[12.54, "o", "l"] +[12.583, "o", " "] +[12.633, "o", "o"] +[12.686, "o", "l"] +[12.742, "o", "l"] +[12.784, "o", "a"] +[12.83, "o", "m"] +[12.872, "o", "a"] +[12.928, "o", "/"] +[12.982, "o", "q"] +[13.023, "o", "w"] +[13.082, "o", "e"] +[13.141, "o", "n"] +[13.195, "o", "2"] +[13.247, "o", "."] +[13.29, "o", "5"] +[13.33, "o", "-"] +[13.381, "o", "c"] +[13.422, "o", "o"] +[13.466, "o", "d"] +[13.511, "o", "e"] +[13.551, "o", "r"] +[13.951, "o", "\r\n"] +[14.351, "o", "[32m\u2713[0m Switched to [1mollama/qwen2.5-coder[0m [2m(local, $0/call)[0m\r\n\r\n"] +[14.651, "o", "[1m[36m[project] \u00bb[0m "] +[15.151, "o", ""] +[15.2, "o", "a"] +[15.252, "o", "p"] +[15.31, "o", "p"] +[15.359, "o", "l"] +[15.41, "o", "y"] +[15.461, "o", " "] +[15.505, "o", "a"] +[15.555, "o", "l"] +[15.608, "o", "l"] +[15.664, "o", " "] +[15.706, "o", "t"] +[15.752, "o", "h"] +[15.793, "o", "r"] +[15.85, "o", "e"] +[15.903, "o", "e"] +[15.944, "o", " "] +[16.004, "o", "f"] +[16.063, "o", "i"] +[16.116, "o", "x"] +[16.169, "o", "e"] +[16.212, "o", "s"] +[16.612, "o", "\r\n\r\n"] +[17.112, "o", "[33m[Edit][0m src/parser.py\r\n"] +[17.512, "o", " [31m- PATTERN = r\"\\\\[(\\\\d{4}-\\\\d{2}-\\\\d{2})\\\\] (.*)\"[0m\r\n"] +[17.812, "o", " [31m- def parse_logs(path):[0m\r\n"] +[18.112, "o", " [31m- for line in open(path).read().splitlines():[0m\r\n"] +[18.412, "o", " [31m- m = re.compile(PATTERN).match(line)[0m\r\n"] +[18.712, "o", " [32m+ _PATTERN_RE = re.compile(r\"\\\\[(\\\\d{4}-\\\\d{2}-\\\\d{2})\\\\] (.*)\")[0m\r\n"] +[19.012, "o", " [32m+ def parse_logs(path):[0m\r\n"] +[19.312, "o", " [32m+ out = [][0m\r\n"] +[19.612, "o", " [32m+ with open(path) as fh:[0m\r\n"] +[19.912, "o", " [32m+ for line in fh:[0m\r\n"] +[20.212, "o", " [32m+ m = _PATTERN_RE.match(line)[0m\r\n\r\n"] +[20.612, "o", "[32m\u2713[0m 1 file changed, 9 insertions(+), 4 deletions(-).\r\n\r\n"] +[21.112, "o", "[33m[Bash][0m python -c 'from parser import parse_logs; parse_logs(\"sample.log\")'\r\n"] +[21.712, "o", " [2mreal 1.62s user 1.51s sys 0.09s[0m [32m\u2193 11\u00d7[0m\r\n\r\n"] +[22.212, "o", "[1m[36m[project] \u00bb[0m "] +[23.012, "o", ""] diff --git a/docs/media/casts/code_review.svg b/docs/media/casts/code_review.svg new file mode 100644 index 0000000..2046de5 --- /dev/null +++ b/docs/media/casts/code_review.svg @@ -0,0 +1 @@ +[32m~/projects/parser[0m[36m❯[0m[32m~/projects/parser[0m[36m❯[0mcheetahclaws[2m[CheetahClawsv3.05.79·claude-sonnet-4-6·automode][0m[2mType/helpforcommands,/modeltoswitch,!cmdforshell,Ctrl+Ctoquit[0m[1m[36m[project]»[0m[1m[36m[project]»[0mWhy[1m[36m[project]»[0mWhyis[1m[36m[project]»[0mWhyisparse_logs()[1m[36m[project]»[0mWhyisparse_logs()so[1m[36m[project]»[0mWhyisparse_logs()soslow[1m[36m[project]»[0mWhyisparse_logs()soslowon[1m[36m[project]»[0mWhyisparse_logs()soslowonthe[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GB[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsample[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplefile?[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplefile?Profile[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplefile?Profileand[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplefile?Profileandfix[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplefile?Profileandfixit.[2m●Readingthefunctionandtracinghotspots…[0m[33m[Read][0msrc/parser.py[2m(187lines)[0m[33m[Grep][0m'for.*in'src/parser.py[2m(7matches)[0m[33m[Bash][0mpython-Ximporttime-c'fromparserimportparse_logs;parse_logs("sample.log")'[2mreal18.42suser17.99ssys0.41s[0m[1mFoundthehotpath:[0m[31mline84[0m[2mre.compile(PATTERN)[0mcalledinsidetheloop[31mline97[0m[2mresult+=line[0mstringconcatenationinO(n²)[31mline112[0m[2mopen(path).read()[0mloadsthewhole1GBintoRAM[1m3fixeswillbringitunder2seconds.[0mI'llshowthembeforeapplying.[1m[36m[project]»[0m/model[1m[36m[project]»[0m/modelollama/qwen2.5-coder[32m✓[0mSwitchedto[1mollama/qwen2.5-coder[0m[2m(local,$0/call)[0m[1m[36m[project]»[0mapply[1m[36m[project]»[0mapplyall[1m[36m[project]»[0mapplyallthree[1m[36m[project]»[0mapplyallthreefixes[33m[Edit][0msrc/parser.py[31m-PATTERN=r"\\[(\\d{4}-\\d{2}-\\d{2})\\](.*)"[0m[31m-defparse_logs(path):[0m[31m-forlineinopen(path).read().splitlines():[0m[31m-m=re.compile(PATTERN).match(line)[0m[32m+_PATTERN_RE=re.compile(r"\\[(\\d{4}-\\d{2}-\\d{2})\\](.*)")[0m[32m+defparse_logs(path):[0m[32m+out=[][0m[32m+withopen(path)asfh:[0m[32m+forlineinfh:[0m[32m+m=_PATTERN_RE.match(line)[0m[32m✓[0m1filechanged,9insertions(+),4deletions(-).[33m[Bash][0mpython-c'fromparserimportparse_logs;parse_logs("sample.log")'[2mreal1.62suser1.51ssys0.09s[0m[32m↓11×[0m[32m~/projects/parser[0m[36m❯[0mc[32m~/projects/parser[0m[36m❯[0mch[32m~/projects/parser[0m[36m❯[0mche[32m~/projects/parser[0m[36m❯[0mchee[32m~/projects/parser[0m[36m❯[0mcheet[32m~/projects/parser[0m[36m❯[0mcheeta[32m~/projects/parser[0m[36m❯[0mcheetah[32m~/projects/parser[0m[36m❯[0mcheetahc[32m~/projects/parser[0m[36m❯[0mcheetahcl[32m~/projects/parser[0m[36m❯[0mcheetahcla[32m~/projects/parser[0m[36m❯[0mcheetahclaw[1m[36m[project]»[0mW[1m[36m[project]»[0mWh[1m[36m[project]»[0mWhyi[1m[36m[project]»[0mWhyisp[1m[36m[project]»[0mWhyispa[1m[36m[project]»[0mWhyispar[1m[36m[project]»[0mWhyispars[1m[36m[project]»[0mWhyisparse[1m[36m[project]»[0mWhyisparse_[1m[36m[project]»[0mWhyisparse_l[1m[36m[project]»[0mWhyisparse_lo[1m[36m[project]»[0mWhyisparse_log[1m[36m[project]»[0mWhyisparse_logs[1m[36m[project]»[0mWhyisparse_logs([1m[36m[project]»[0mWhyisparse_logs()s[1m[36m[project]»[0mWhyisparse_logs()sos[1m[36m[project]»[0mWhyisparse_logs()sosl[1m[36m[project]»[0mWhyisparse_logs()soslo[1m[36m[project]»[0mWhyisparse_logs()soslowo[1m[36m[project]»[0mWhyisparse_logs()soslowont[1m[36m[project]»[0mWhyisparse_logs()soslowonth[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1G[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBs[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsa[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsam[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamp[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsampl[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplef[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplefi[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplefil[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplefile[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplefile?P[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplefile?Pr[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplefile?Pro[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplefile?Prof[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplefile?Profi[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplefile?Profil[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplefile?Profilea[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplefile?Profilean[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplefile?Profileandf[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplefile?Profileandfi[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplefile?Profileandfixi[1m[36m[project]»[0mWhyisparse_logs()soslowonthe1GBsamplefile?Profileandfixit[1m[36m[project]»[0m/[1m[36m[project]»[0m/m[1m[36m[project]»[0m/mo[1m[36m[project]»[0m/mod[1m[36m[project]»[0m/mode[1m[36m[project]»[0m/modelo[1m[36m[project]»[0m/modelol[1m[36m[project]»[0m/modeloll[1m[36m[project]»[0m/modelolla[1m[36m[project]»[0m/modelollam[1m[36m[project]»[0m/modelollama[1m[36m[project]»[0m/modelollama/[1m[36m[project]»[0m/modelollama/q[1m[36m[project]»[0m/modelollama/qw[1m[36m[project]»[0m/modelollama/qwe[1m[36m[project]»[0m/modelollama/qwen[1m[36m[project]»[0m/modelollama/qwen2[1m[36m[project]»[0m/modelollama/qwen2.[1m[36m[project]»[0m/modelollama/qwen2.5[1m[36m[project]»[0m/modelollama/qwen2.5-[1m[36m[project]»[0m/modelollama/qwen2.5-c[1m[36m[project]»[0m/modelollama/qwen2.5-co[1m[36m[project]»[0m/modelollama/qwen2.5-cod[1m[36m[project]»[0m/modelollama/qwen2.5-code[1m[36m[project]»[0ma[1m[36m[project]»[0map[1m[36m[project]»[0mapp[1m[36m[project]»[0mappl[1m[36m[project]»[0mapplya[1m[36m[project]»[0mapplyal[1m[36m[project]»[0mapplyallt[1m[36m[project]»[0mapplyallth[1m[36m[project]»[0mapplyallthr[1m[36m[project]»[0mapplyallthre[1m[36m[project]»[0mapplyallthreef[1m[36m[project]»[0mapplyallthreefi[1m[36m[project]»[0mapplyallthreefix[1m[36m[project]»[0mapplyallthreefixe \ No newline at end of file diff --git a/docs/media/casts/gen_brainstorm.py b/docs/media/casts/gen_brainstorm.py new file mode 100644 index 0000000..11ff0ab --- /dev/null +++ b/docs/media/casts/gen_brainstorm.py @@ -0,0 +1,111 @@ +"""Asciinema v2 cast: /brainstorm multi-persona adversarial debate. + +Scenario: ask 5 personas to debate whether to migrate the order service +to event sourcing. They argue, push back, then converge on a todo list. + +Run: python3 gen_brainstorm.py > brainstorm.cast +""" +import json +import random +import sys + + +HEADER = { + "version": 2, + "width": 110, + "height": 32, + "timestamp": 1747262400, + "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}, + "title": "CheetahClaws /brainstorm — 5 personas debate event sourcing", + "idle_time_limit": 1.5, +} + +CYAN = "[36m" +GREEN = "[32m" +YELL = "[33m" +MAG = "[35m" +DIM = "[2m" +BOLD = "[1m" +GRAY = "[90m" +RED = "[31m" +BLUE = "[34m" +RST = "[0m" + +events = [] +t = 0.0 + + +def out(delay, text): + global t + t += delay + events.append([round(t, 3), "o", text]) + + +def type_string(s, base=0.04, jitter=0.02): + rng = random.Random(17) + for ch in s: + out(base + rng.random() * jitter, ch) + + +# Scene 1 — launch + /brainstorm +out(0.0, f"{GREEN}~/projects/checkout{RST} {CYAN}❯{RST} cheetahclaws\r\n") +out(0.3, f"{DIM}[CheetahClaws v3.05.79 · claude-sonnet-4-6]{RST}\r\n\r\n") +out(0.2, f"{BOLD}{CYAN}[checkout] »{RST} ") +out(0.5, "") +type_string("/brainstorm \"Should we migrate the order service from CRUD to event sourcing?\"") +out(0.4, "\r\n\r\n") + +out(0.4, f"{DIM}● Spawning 5 expert personas in parallel…{RST}\r\n\r\n") + +# Scene 2 — persona round 1 +personas = [ + (BLUE, "ARCHITECT", "Event sourcing maps cleanly to checkout's domain — every state change is\r\n already an event (cart-added, payment-authorised, fulfilled). " + "We get audit\r\n trail, time-travel debugging, and replay for free."), + (RED, "SKEPTIC", "Three months of dev time to rewrite something that works fine. Where's the\r\n user-facing win? You're trading a known boring solution for a fashionable\r\n one we'll regret in 18 months when the lead architect leaves."), + (MAG, "PRAGMATIST", "Halfway compromise: keep CRUD as the source of truth, emit an outbox event\r\n on each write. We get the audit log without the rewrite. Doors stay open\r\n for full ES later."), + (YELL, "DBA", "Two real issues with the proposal: (1) Postgres event-store throughput on the\r\n checkout volume — we'd need partitioning by tenant; (2) projections lag\r\n will surface as stale reads during Black Friday."), + (GREEN, "PM", "What problem are we actually solving? If it's audit/compliance — outbox\r\n pattern is enough. If it's the analytics team rebuilding cart funnels\r\n every quarter — yes, ES pays off."), +] + +for color, name, msg in personas: + out(0.5, f" {color}● [{name}]{RST}\r\n") + for line in msg.split("\n"): + out(0.2, f" {line}\r\n") + +out(0.5, "\r\n") + +# Scene 3 — debate round 2 (rebuttals) +out(0.3, f"{DIM}● Round 2: rebuttals…{RST}\r\n\r\n") + +rebuttals = [ + (RED, "SKEPTIC", "→ ARCHITECT: 'time-travel debugging for free' has never been free in production.\r\n Snapshot management alone is a quarter of work."), + (BLUE, "ARCHITECT", "→ SKEPTIC: agreed on snapshots — but DBA's outbox path is half-measure.\r\n Once we duplicate state we own two truths."), + (MAG, "PRAGMATIST", "→ Both: outbox is not a half-measure if we treat it as a stepping stone with\r\n a six-month review gate."), +] +for color, name, msg in rebuttals: + out(0.5, f" {color}● [{name}]{RST}\r\n") + for line in msg.split("\n"): + out(0.2, f" {line}\r\n") +out(0.4, "\r\n") + +# Scene 4 — synthesis +out(0.5, f"{BOLD}─── Synthesis ────────────────────────────────────────────────────────{RST}\r\n\r\n") +out(0.3, f" {GREEN}{BOLD}Consensus:{RST} no full migration this quarter. Ship outbox + replayer first.\r\n\r\n") +out(0.3, f" {BOLD}Decision pivots on PM's framing:{RST}\r\n") +out(0.25, f" • audit/compliance only → {GREEN}stay CRUD + outbox events{RST}\r\n") +out(0.25, f" • analytics rebuild every quarter → {YELL}plan full ES, but in Q3{RST}\r\n\r\n") + +# Scene 5 — todo_list output +out(0.5, f"{YELL}[Write]{RST} brainstorm_outputs/todo_list.txt\r\n") +out(0.3, f" {GREEN}1.{RST} {DIM}[ ]{RST} Add outbox table + transactional event publisher (1 week)\r\n") +out(0.2, f" {GREEN}2.{RST} {DIM}[ ]{RST} Wire Kafka consumer → analytics warehouse (3 days)\r\n") +out(0.2, f" {GREEN}3.{RST} {DIM}[ ]{RST} Benchmark projection lag with prod-shaped Black-Friday replay\r\n") +out(0.2, f" {GREEN}4.{RST} {DIM}[ ]{RST} Schedule Q3 ES decision review (compliance + analytics inputs)\r\n\r\n") + +out(0.4, f"{GREEN}✓{RST} 4 tasks ready. Run {CYAN}/worker{RST} to auto-implement them.\r\n\r\n") +out(0.4, f"{BOLD}{CYAN}[checkout] »{RST} ") +out(0.8, "") + +sys.stdout.write(json.dumps(HEADER) + "\n") +for ev in events: + sys.stdout.write(json.dumps(ev) + "\n") diff --git a/docs/media/casts/gen_code_review.py b/docs/media/casts/gen_code_review.py new file mode 100644 index 0000000..1d50556 --- /dev/null +++ b/docs/media/casts/gen_code_review.py @@ -0,0 +1,111 @@ +"""Asciinema v2 cast: code review workflow. + +Scenario: ask CheetahClaws to find a performance bug in a Python script, +switch to a local Ollama model, then apply the fix. + +Run: python3 gen_code_review.py > code_review.cast +""" +import json +import random +import sys + + +HEADER = { + "version": 2, + "width": 100, + "height": 28, + "timestamp": 1747262400, + "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}, + "title": "CheetahClaws — find a perf bug, switch to local Ollama, apply the fix", + "idle_time_limit": 1.2, +} + +CYAN = "[36m" +GREEN = "[32m" +YELL = "[33m" +MAG = "[35m" +DIM = "[2m" +BOLD = "[1m" +GRAY = "[90m" +RED = "[31m" +RST = "[0m" + +events = [] +t = 0.0 + + +def out(delay, text): + global t + t += delay + events.append([round(t, 3), "o", text]) + + +def type_string(s, base=0.04, jitter=0.02): + rng = random.Random(11) + for ch in s: + out(base + rng.random() * jitter, ch) + + +# Scene 1 — launch +out(0.0, f"{GREEN}~/projects/parser{RST} {CYAN}❯{RST} ") +out(0.7, "") +type_string("cheetahclaws") +out(0.4, "\r\n") +out(0.3, f"{DIM}[CheetahClaws v3.05.79 · claude-sonnet-4-6 · auto mode]{RST}\r\n") +out(0.2, f"{DIM}Type /help for commands, /model to switch, !cmd for shell, Ctrl+C to quit{RST}\r\n\r\n") +out(0.2, f"{BOLD}{CYAN}[project] »{RST} ") + +# Scene 2 — prompt +out(0.6, "") +type_string("Why is parse_logs() so slow on the 1GB sample file? Profile and fix it.") +out(0.5, "\r\n\r\n") + +# Scene 3 — tool calls +out(0.5, f"{DIM}● Reading the function and tracing hot spots…{RST}\r\n") +out(0.4, f"{YELL}[Read]{RST} src/parser.py {DIM}(187 lines){RST}\r\n") +out(0.4, f"{YELL}[Grep]{RST} 'for .* in' src/parser.py {DIM}(7 matches){RST}\r\n") +out(0.4, f"{YELL}[Bash]{RST} python -X importtime -c 'from parser import parse_logs; parse_logs(\"sample.log\")'\r\n") +out(0.5, f" {DIM}real 18.42s user 17.99s sys 0.41s{RST}\r\n\r\n") + +# Scene 4 — diagnosis +out(0.6, f"{BOLD}Found the hot path:{RST}\r\n\r\n") +out(0.4, f" {RED}line 84{RST} {DIM}re.compile(PATTERN){RST} called inside the loop\r\n") +out(0.3, f" {RED}line 97{RST} {DIM}result += line{RST} string concatenation in O(n²)\r\n") +out(0.3, f" {RED}line 112{RST} {DIM}open(path).read(){RST} loads the whole 1GB into RAM\r\n\r\n") +out(0.5, f"{BOLD}3 fixes will bring it under 2 seconds.{RST} I'll show them before applying.\r\n\r\n") + +# Scene 5 — switch to local model +out(0.5, f"{BOLD}{CYAN}[project] »{RST} ") +out(0.4, "") +type_string("/model ollama/qwen2.5-coder") +out(0.4, "\r\n") +out(0.4, f"{GREEN}✓{RST} Switched to {BOLD}ollama/qwen2.5-coder{RST} {DIM}(local, $0/call){RST}\r\n\r\n") + +# Scene 6 — apply fix +out(0.3, f"{BOLD}{CYAN}[project] »{RST} ") +out(0.5, "") +type_string("apply all three fixes") +out(0.4, "\r\n\r\n") + +out(0.5, f"{YELL}[Edit]{RST} src/parser.py\r\n") +out(0.4, f" {RED}- PATTERN = r\"\\\\[(\\\\d{{4}}-\\\\d{{2}}-\\\\d{{2}})\\\\] (.*)\"{RST}\r\n") +out(0.3, f" {RED}- def parse_logs(path):{RST}\r\n") +out(0.3, f" {RED}- for line in open(path).read().splitlines():{RST}\r\n") +out(0.3, f" {RED}- m = re.compile(PATTERN).match(line){RST}\r\n") +out(0.3, f" {GREEN}+ _PATTERN_RE = re.compile(r\"\\\\[(\\\\d{{4}}-\\\\d{{2}}-\\\\d{{2}})\\\\] (.*)\"){RST}\r\n") +out(0.3, f" {GREEN}+ def parse_logs(path):{RST}\r\n") +out(0.3, f" {GREEN}+ out = []{RST}\r\n") +out(0.3, f" {GREEN}+ with open(path) as fh:{RST}\r\n") +out(0.3, f" {GREEN}+ for line in fh:{RST}\r\n") +out(0.3, f" {GREEN}+ m = _PATTERN_RE.match(line){RST}\r\n\r\n") +out(0.4, f"{GREEN}✓{RST} 1 file changed, 9 insertions(+), 4 deletions(-).\r\n\r\n") + +# Scene 7 — re-bench +out(0.5, f"{YELL}[Bash]{RST} python -c 'from parser import parse_logs; parse_logs(\"sample.log\")'\r\n") +out(0.6, f" {DIM}real 1.62s user 1.51s sys 0.09s{RST} {GREEN}↓ 11×{RST}\r\n\r\n") +out(0.5, f"{BOLD}{CYAN}[project] »{RST} ") +out(0.8, "") + +sys.stdout.write(json.dumps(HEADER) + "\n") +for ev in events: + sys.stdout.write(json.dumps(ev) + "\n") diff --git a/docs/media/casts/gen_lab.py b/docs/media/casts/gen_lab.py new file mode 100644 index 0000000..e4b5406 --- /dev/null +++ b/docs/media/casts/gen_lab.py @@ -0,0 +1,125 @@ +"""Asciinema v2 cast: /lab autonomous multi-agent paper writing. + +Scenario: /lab start on iris classification comparison. Show the 9 stages +advancing with agent messages and reviewer iteration. End with the +deliverable file tree. + +Run: python3 gen_lab.py > lab.cast +""" +import json +import random +import sys + + +HEADER = { + "version": 2, + "width": 110, + "height": 34, + "timestamp": 1747262400, + "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}, + "title": "CheetahClaws /lab — 9 agents drive a paper from question to PDF", + "idle_time_limit": 1.3, +} + +CYAN = "[36m" +GREEN = "[32m" +YELL = "[33m" +MAG = "[35m" +DIM = "[2m" +BOLD = "[1m" +GRAY = "[90m" +RED = "[31m" +BLUE = "[34m" +RST = "[0m" + +events = [] +t = 0.0 + + +def out(delay, text): + global t + t += delay + events.append([round(t, 3), "o", text]) + + +def type_string(s, base=0.04, jitter=0.02): + rng = random.Random(23) + for ch in s: + out(base + rng.random() * jitter, ch) + + +# Scene 1 — launch + /lab start +out(0.0, f"{GREEN}~{RST} {CYAN}❯{RST} cheetahclaws\r\n") +out(0.3, f"{DIM}[CheetahClaws v3.05.79 · claude-sonnet-4-6][/lab engine v0]{RST}\r\n\r\n") +out(0.2, f"{BOLD}{CYAN}[~] »{RST} ") +out(0.5, "") +type_string("/lab start \"Compare logistic regression vs random forest on iris, k-fold CV\"") +out(0.4, "\r\n") +out(0.4, f"{GREEN}✓{RST} Lab run {BOLD}lab_a3b1c8e9f012{RST} launched. Budget: 60 min · 200k tokens.\r\n\r\n") + +# Scene 2 — stage pipeline +def stage(name, agent_color, agent_name, msg, ok=True): + badge_color = GREEN if ok else YELL + out(0.5, f" {badge_color}[{name}]{RST}\r\n") + out(0.3, f" {agent_color}● {agent_name}{RST}: {msg}\r\n") + +stage("QUESTIONING", BLUE, "PI", + "Picking Q2: 'Does RF outperform logistic regression on iris under 5-fold CV?'") +out(0.1, f" {DIM}● Lay Reader: question is concrete and testable.{RST}\r\n\r\n") + +stage("SURVEY", MAG, "Surveyor", + "12 papers retrieved; baselines on iris well-characterised since 1936.") +out(0.1, "\r\n") + +stage("OUTLINE", BLUE, "Designer", + "5-section outline: intro, related, method, results, threats.") +out(0.1, f" {DIM}● Reviewer×3 critique → 2 pass, 1 asks for ablation; PI signs off.{RST}\r\n\r\n") + +stage("CODE_DRAFT", YELL, "Engineer", + "scripted iris loader, GridSearchCV for both models, 5-fold stratified.") +out(0.1, "\r\n") + +stage("EXPERIMENT", YELL, "Engineer", + "Running sandboxed subprocess…") +out(0.4, f" {DIM}stdout: Best LR C=10 acc=0.967 ± 0.025{RST}\r\n") +out(0.3, f" {DIM}stdout: Best RF n=50 acc=0.967 ± 0.033{RST}\r\n") +out(0.2, f" {DIM}saved figure_1.png (boxplot), results.csv{RST}\r\n\r\n") + +stage("ANALYSIS", YELL, "Engineer", + "Models tie on accuracy; RF has higher variance. Recommend LR for tabular small-n.") +out(0.1, "\r\n") + +stage("DRAFTING", CYAN, "Drafter", + "Composed 2,840-word draft with inline [1]–[12] citations.") +out(0.1, "\r\n") + +# Reviewer loop +out(0.4, f" {GREEN}[REVIEW LOOP]{RST}\r\n") +out(0.3, f" {RED}● Reviewer #1{RST}: 'Section 3.2 doesn't address class imbalance — minor revision.'\r\n") +out(0.3, f" {RED}● Reviewer #2{RST}: 'Threats section thin. Add overfitting note.'\r\n") +out(0.3, f" {GREEN}● Reviewer #3{RST}: 'Accept.'\r\n") +out(0.4, f" {CYAN}● Drafter{RST}: revised §3.2 + §6, rebuilt bib.\r\n") +out(0.3, f" {GREEN}● Reviewer×3{RST}: {BOLD}2/3 accept on round 2{RST} → PI signs off.\r\n\r\n") + +stage("CITATION VERIFY", GREEN, "Citation Checker", + "12/12 references verified against arXiv / Semantic Scholar / CrossRef.") +out(0.1, "\r\n") + +# Final +out(0.4, f" {GREEN}[FINALISE]{RST} Bundle ready.\r\n\r\n") +out(0.4, f"{GREEN}✓{RST} Output at {CYAN}~/.cheetahclaws/research_papers/lab_a3b1c8e9f012/{RST}\r\n\r\n") +out(0.3, f" ├── {BOLD}report.md{RST} {DIM}(2,940 words, 12 refs){RST}\r\n") +out(0.15, f" ├── references.bib {DIM}(verified BibTeX){RST}\r\n") +out(0.15, f" ├── citations_verified.json\r\n") +out(0.15, f" └── workspace/\r\n") +out(0.15, f" ├── experiment.py {DIM}(83 lines){RST}\r\n") +out(0.15, f" ├── figure_1.png {DIM}(boxplot){RST}\r\n") +out(0.15, f" └── results.csv {DIM}(5 folds × 2 models){RST}\r\n\r\n") + +out(0.5, f"{DIM}Total: 22 min · 142k tokens · $1.40 in API cost{RST}\r\n\r\n") +out(0.4, f"{BOLD}{CYAN}[~] »{RST} ") +out(0.8, "") + +sys.stdout.write(json.dumps(HEADER) + "\n") +for ev in events: + sys.stdout.write(json.dumps(ev) + "\n") diff --git a/docs/media/casts/gen_research.py b/docs/media/casts/gen_research.py new file mode 100644 index 0000000..5c82150 --- /dev/null +++ b/docs/media/casts/gen_research.py @@ -0,0 +1,141 @@ +"""Asciinema v2 cast: 20-source research pipeline. + +Scenario: /research "LLM agents 2026" fans out across arXiv, HuggingFace, +Semantic Scholar, HackerNews, GitHub, Reddit, 知乎, B站, 微博, 小红书. +Shows live source completion, entity heat table, citation pull. + +Run: python3 gen_research.py > research.cast +""" +import json +import random +import sys + + +HEADER = { + "version": 2, + "width": 110, + "height": 32, + "timestamp": 1747262400, + "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}, + "title": "CheetahClaws /research — parallel fan-out across 20 sources", + "idle_time_limit": 1.5, +} + +CYAN = "[36m" +GREEN = "[32m" +YELL = "[33m" +MAG = "[35m" +DIM = "[2m" +BOLD = "[1m" +GRAY = "[90m" +RED = "[31m" +BLUE = "[34m" +RST = "[0m" + +events = [] +t = 0.0 + + +def out(delay, text): + global t + t += delay + events.append([round(t, 3), "o", text]) + + +def type_string(s, base=0.04, jitter=0.02): + rng = random.Random(13) + for ch in s: + out(base + rng.random() * jitter, ch) + + +# Scene 1 — launch + /research +out(0.0, f"{GREEN}~{RST} {CYAN}❯{RST} ") +out(0.6, "") +type_string("cheetahclaws") +out(0.4, "\r\n") +out(0.3, f"{DIM}[CheetahClaws v3.05.79 · claude-sonnet-4-6]{RST}\r\n\r\n") +out(0.2, f"{BOLD}{CYAN}[~] »{RST} ") +out(0.5, "") +type_string("/research \"LLM agents 2026 trends\" --range 6m --expand") +out(0.4, "\r\n\r\n") + +# Scene 2 — query expansion +out(0.5, f"{DIM}● Expanding query into 4 sibling sub-queries…{RST}\r\n") +sub_qs = [ + "autonomous coding agents benchmarks 2026", + "multi-agent debate / reviewer-author loops", + "agentic tool use plus MCP / function calling", + "open-weight models for agent workflows", +] +for q in sub_qs: + out(0.25, f" {DIM}↳{RST} {q}\r\n") +out(0.4, "\r\n") + +# Scene 3 — fan-out, live source completions +out(0.4, f"{BOLD}Fanning out across 20 sources in parallel…{RST}\r\n\r\n") + +sources = [ + ("arXiv", "342", GREEN, 0.35), + ("Semantic Scholar","218", GREEN, 0.25), + ("HuggingFace Papers","176",GREEN, 0.20), + ("OpenAlex", "412", GREEN, 0.30), + ("alphaXiv", " 84", GREEN, 0.22), + ("HackerNews", "511", GREEN, 0.20), + ("GitHub", "298", GREEN, 0.28), + ("Reddit r/MachineLearning","147", GREEN, 0.18), + ("StackOverflow", " 62", GREEN, 0.15), + ("Google News", "203", GREEN, 0.25), + ("Polymarket", " 9", GREEN, 0.18), + ("SEC EDGAR", " 14", GREEN, 0.20), + ("Twitter / X", "1.2k", YELL, 0.30), + ("Brave Search", "188", GREEN, 0.18), + ("Tavily", "151", GREEN, 0.20), + ("Google Scholar", "224", GREEN, 0.25), + ("知乎 Zhihu", "186", GREEN, 0.22), + ("B站 Bilibili", "298", GREEN, 0.25), + ("微博 Weibo", "412", YELL, 0.30), + ("小红书 Xiaohongshu","127",GREEN, 0.20), +] +for name, count, color, delay in sources: + out(delay, f" {color}✓{RST} {BOLD}{name:<28}{RST} {DIM}→{RST} {count:>5} hits\r\n") + +out(0.4, "\r\n") + +# Scene 4 — entity heat table +out(0.6, f"{BOLD}Top entities by cross-platform attention:{RST}\r\n\r\n") +out(0.3, f" {DIM}entity arXiv HF GH HN 微博 Zhihu total{RST}\r\n") +out(0.2, f" {DIM}{'─'*70}{RST}\r\n") +rows = [ + ("Claude 4.6", "127", " 84", " 32", "298", " 412", " 186", " 1,139"), + ("DeepSeek V4", "108", "112", "147", "176", " 287", " 298", " 1,128"), + ("Qwen3-Coder", " 87", " 92", "211", " 88", " 154", " 287", " 919"), + ("MCP Protocol", " 42", " 28", "188", "247", " 64", " 72", " 641"), + ("Llama 4", " 96", "118", " 94", "203", " 167", " 88", " 766"), +] +for ent, *vals in rows: + cells = " ".join(f"{v:>5}" for v in vals[:-1]) + total = vals[-1] + out(0.25, f" {BOLD}{ent:<18}{RST} {DIM}{cells}{RST} {GREEN}{total}{RST}\r\n") + +out(0.5, "\r\n") + +# Scene 5 — citations verified +out(0.5, f"{DIM}● Verifying citations against arXiv / Semantic Scholar / CrossRef…{RST}\r\n") +out(0.4, f" {GREEN}✓{RST} 47 papers, {GREEN}45 verified{RST}, {RED}2 flagged for hallucination{RST}\r\n\r\n") + +# Scene 6 — report saved +out(0.4, f"{GREEN}✓{RST} Brief saved → {CYAN}~/.cheetahclaws/research_reports/llm-agents-2026-trends-{t:.0f}.md{RST}\r\n") +out(0.2, f" {DIM}3,124 words · 47 citations · cross-platform heat table · 12-month trend sparkline{RST}\r\n\r\n") + +# Scene 7 — follow-up +out(0.5, f"{BOLD}{CYAN}[~] »{RST} ") +out(0.5, "") +type_string("/reports open") +out(0.4, "\r\n") +out(0.4, f"{DIM}Opening llm-agents-2026-trends-{t:.0f}.md in your editor…{RST}\r\n\r\n") +out(0.5, f"{BOLD}{CYAN}[~] »{RST} ") +out(0.7, "") + +sys.stdout.write(json.dumps(HEADER) + "\n") +for ev in events: + sys.stdout.write(json.dumps(ev) + "\n") diff --git a/docs/media/casts/gen_research_agent.py b/docs/media/casts/gen_research_agent.py new file mode 100644 index 0000000..8af5804 --- /dev/null +++ b/docs/media/casts/gen_research_agent.py @@ -0,0 +1,129 @@ +"""Asciinema v2 cast: /agent research_assistant — autonomous background loop. + +Scenario: launch the research_assistant agent template on a topic; show +three iteration cycles with summaries pushed to the bridge, then the +stagnation-stop guard kicking in to save tokens. + +Run: python3 gen_research_agent.py > research_agent.cast +""" +import json +import random +import sys + + +HEADER = { + "version": 2, + "width": 110, + "height": 32, + "timestamp": 1747262400, + "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}, + "title": "CheetahClaws /agent — autonomous research_assistant loop", + "idle_time_limit": 1.5, +} + +CYAN = "[36m" +GREEN = "[32m" +YELL = "[33m" +MAG = "[35m" +DIM = "[2m" +BOLD = "[1m" +GRAY = "[90m" +RED = "[31m" +BLUE = "[34m" +RST = "[0m" + +events = [] +t = 0.0 + + +def out(delay, text): + global t + t += delay + events.append([round(t, 3), "o", text]) + + +def type_string(s, base=0.04, jitter=0.02): + rng = random.Random(31) + for ch in s: + out(base + rng.random() * jitter, ch) + + +# Scene 1 — launch + /agent wizard +out(0.0, f"{GREEN}~{RST} {CYAN}❯{RST} cheetahclaws\r\n") +out(0.3, f"{DIM}[CheetahClaws v3.05.79 · claude-sonnet-4-6]{RST}\r\n\r\n") +out(0.2, f"{BOLD}{CYAN}[~] »{RST} ") +out(0.4, "") +type_string("/agent") +out(0.4, "\r\n\r\n") + +out(0.3, f"{BOLD}Pick an agent template:{RST}\r\n") +out(0.2, f" 1. {CYAN}research_assistant{RST} {DIM}— daily literature & trend digest{RST}\r\n") +out(0.15, f" 2. {CYAN}auto_bug_fixer{RST} {DIM}— scan repo, propose fixes, run tests{RST}\r\n") +out(0.15, f" 3. {CYAN}paper_writer{RST} {DIM}— draft & polish a paper section by section{RST}\r\n") +out(0.15, f" 4. {CYAN}auto_coder{RST} {DIM}— implement TODOs from a backlog file{RST}\r\n") +out(0.15, f" {DIM}(or drop a .md into ~/.cheetahclaws/agent_templates/ for a custom one){RST}\r\n\r\n") +out(0.3, f"{BOLD}Choose [1-4]:{RST} ") +out(0.5, "") +type_string("1") +out(0.4, "\r\n") +out(0.3, f"{BOLD}Topic for research_assistant:{RST} ") +out(0.4, "") +type_string("Multi-agent debate vs single-model — papers from the last 30 days") +out(0.4, "\r\n\r\n") + +out(0.4, f"{GREEN}✓{RST} Agent {BOLD}research_assistant_8f3a2c{RST} started — loop every 4 hours · push to Telegram\r\n") +out(0.2, f"{DIM} Output dir: ~/.cheetahclaws/agents/research_assistant_8f3a2c/output/{RST}\r\n\r\n") + +# Scene 2 — iteration 1 +def iteration(n, ts, color, summary_lines, stagnation=False): + badge = YELL if stagnation else GREEN + out(0.5, f" {color}─── Iteration #{n} ─── {DIM}{ts}{RST}\r\n") + for line in summary_lines: + out(0.25, f" {line}\r\n") + out(0.3, f" {DIM}→ pushed iteration summary to Telegram chat 458291205{RST}\r\n\r\n") + +iteration(1, "11:00 PT", CYAN, [ + f"{YELL}[Read]{RST} ~/.cheetahclaws/agents/.../state.json {DIM}(first run, empty){RST}", + f"{YELL}[research]{RST} fanned out across 20 sources for the last 24h", + f"{GREEN}● Found 17 new papers, 3 high-signal:{RST}", + f" {DIM}•{RST} \"AdvDebate: …\" (arXiv 2605.04123) — adversarial multi-agent debate", + f" {DIM}•{RST} \"OneShot or N: …\" (arXiv 2605.04588) — single-model can rival debate", + f" {DIM}•{RST} \"Skeptic Loop: …\" (Reddit + GitHub) — open-source debate framework", + f"{YELL}[Write]{RST} digest_day_1.md saved to output/", +]) + +iteration(2, "15:00 PT", MAG, [ + f"{YELL}[Read]{RST} state.json {DIM}(last digest: digest_day_1.md){RST}", + f"{YELL}[research]{RST} new since 11:00 → 4 papers, 1 high-signal", + f"{GREEN}● Notable:{RST} \"Beyond Debate: …\" (NeurIPS workshop preprint)", + f" {DIM}— suggests debate gains shrink as base model gets larger{RST}", + f"{YELL}[Write]{RST} digest_day_1.md (appended)", +]) + +iteration(3, "19:00 PT", BLUE, [ + f"{YELL}[research]{RST} new since 15:00 → 0 papers (quiet window)", + f"{DIM}● No new high-signal items. Reused yesterday's analysis.{RST}", + f"{YELL}[Write]{RST} digest_day_1.md (timestamp updated)", +]) + +# Scene 3 — stagnation-stop kicks in +out(0.5, f" {YELL}─── Iteration #4 ─── {DIM}23:00 PT{RST}\r\n") +out(0.3, f" {YELL}[research]{RST} 0 new papers · summary identical to #3\r\n") +out(0.3, f" {RED}● Stagnation-stop:{RST} same summary for 3 iterations in a row.\r\n") +out(0.2, f" {DIM} threshold: auto_agent_dup_summary_limit = 3 (set 0 to disable){RST}\r\n") +out(0.3, f" {YELL}● Loop paused.{RST} Next attempt at 09:00 PT (manual or /agent resume).\r\n\r\n") + +# Scene 4 — output summary +out(0.4, f"{BOLD}Output (so far):{RST}\r\n") +out(0.25, f" ~/.cheetahclaws/agents/research_assistant_8f3a2c/output/\r\n") +out(0.2, f" ├── {BOLD}digest_day_1.md{RST} {DIM}(2.4 KB, 4 papers analysed){RST}\r\n") +out(0.2, f" ├── state.json {DIM}(loop bookkeeping){RST}\r\n") +out(0.2, f" └── notes.md {DIM}(running scratchpad){RST}\r\n\r\n") + +out(0.4, f"{DIM}Three iterations · 38k tokens · $0.31. Saved ~$0.90 in API spend by stopping.{RST}\r\n\r\n") +out(0.4, f"{BOLD}{CYAN}[~] »{RST} ") +out(0.8, "") + +sys.stdout.write(json.dumps(HEADER) + "\n") +for ev in events: + sys.stdout.write(json.dumps(ev) + "\n") diff --git a/docs/media/casts/gen_telegram.py b/docs/media/casts/gen_telegram.py new file mode 100644 index 0000000..29b0be7 --- /dev/null +++ b/docs/media/casts/gen_telegram.py @@ -0,0 +1,104 @@ +"""Asciinema v2 cast: Telegram bridge remote control. + +Scenario: start the Telegram bridge, then show two chat round-trips +from the phone — checking server load, then queuing a job — followed +by `!jobs` to inspect the queue. + +Run: python3 gen_telegram.py > telegram.cast +""" +import json +import random +import sys + + +HEADER = { + "version": 2, + "width": 105, + "height": 32, + "timestamp": 1747262400, + "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}, + "title": "CheetahClaws Telegram bridge — control the agent from your phone", + "idle_time_limit": 1.4, +} + +CYAN = "[36m" +GREEN = "[32m" +YELL = "[33m" +MAG = "[35m" +DIM = "[2m" +BOLD = "[1m" +GRAY = "[90m" +RED = "[31m" +BLUE = "[34m" +RST = "[0m" + +events = [] +t = 0.0 + + +def out(delay, text): + global t + t += delay + events.append([round(t, 3), "o", text]) + + +def type_string(s, base=0.04, jitter=0.02): + rng = random.Random(29) + for ch in s: + out(base + rng.random() * jitter, ch) + + +# Scene 1 — start the bridge +out(0.0, f"{GREEN}~{RST} {CYAN}❯{RST} cheetahclaws\r\n") +out(0.3, f"{DIM}[CheetahClaws v3.05.79 · claude-sonnet-4-6]{RST}\r\n\r\n") +out(0.2, f"{BOLD}{CYAN}[~] »{RST} ") +out(0.5, "") +type_string("/telegram 7890:AAEx_REDACTED 458291205") +out(0.4, "\r\n") +out(0.4, f"{GREEN}✓{RST} Telegram bridge online — bot @{BOLD}cheetah_personal_bot{RST}, chat 458291205\r\n") +out(0.2, f"{DIM} Listening for messages. Typing indicator + slash passthrough enabled.{RST}\r\n\r\n") + +# Scene 2 — phone message 1: server status +out(0.6, f"{BLUE}┌─ Telegram ────────────────────────────────────────────────────────┐{RST}\r\n") +out(0.3, f"{BLUE}│{RST} {DIM}11:42{RST} {BOLD}You{RST}: What's the CPU load on the server right now?\r\n") +out(0.6, f"{BLUE}│{RST} {DIM}🐆 typing…{RST}\r\n") +out(0.3, f"{BLUE}└───────────────────────────────────────────────────────────────────┘{RST}\r\n\r\n") + +out(0.4, f"{YELL}[Bash]{RST} uptime\r\n") +out(0.3, f" {DIM}11:42:18 up 14 days, load average: 0.41, 0.55, 0.62{RST}\r\n\r\n") + +out(0.4, f"{BLUE}┌─ Telegram ────────────────────────────────────────────────────────┐{RST}\r\n") +out(0.2, f"{BLUE}│{RST} {DIM}11:42{RST} 🐆 CPU is {GREEN}quiet{RST}: 0.41 / 0.55 / 0.62 (1m / 5m / 15m).\r\n") +out(0.2, f"{BLUE}│{RST} Server has been up 14 days. Want me to check memory or disk?\r\n") +out(0.3, f"{BLUE}└───────────────────────────────────────────────────────────────────┘{RST}\r\n\r\n") + +# Scene 3 — phone message 2: queue a job while AI is busy +out(0.5, f"{BLUE}┌─ Telegram ────────────────────────────────────────────────────────┐{RST}\r\n") +out(0.3, f"{BLUE}│{RST} {DIM}11:43{RST} {BOLD}You{RST}: Re-run the nightly backup and tell me when it's done\r\n") +out(0.3, f"{BLUE}└───────────────────────────────────────────────────────────────────┘{RST}\r\n\r\n") + +out(0.4, f"{YELL}[Bash]{RST} bash /opt/scripts/nightly_backup.sh {DIM}(long-running, queued as job #2){RST}\r\n") +out(0.3, f"{BLUE}│{RST} {DIM}11:43{RST} 🐆 Queued as job #2. I'll ping you when it finishes.\r\n\r\n") + +# Scene 4 — !jobs inspect queue +out(0.5, f"{BLUE}┌─ Telegram ────────────────────────────────────────────────────────┐{RST}\r\n") +out(0.3, f"{BLUE}│{RST} {DIM}11:43{RST} {BOLD}You{RST}: !jobs\r\n") +out(0.3, f"{BLUE}│{RST} {DIM}11:43{RST} 🐆 Job queue:\r\n") +out(0.25, f"{BLUE}│{RST} {GREEN}#1{RST} {DIM}(done 11:42){RST} uptime check\r\n") +out(0.2, f"{BLUE}│{RST} {YELL}#2{RST} {DIM}(running 11:43){RST} nightly_backup.sh [████░░░░░░] 41%\r\n") +out(0.2, f"{BLUE}│{RST} {DIM} `!cancel 2` to stop · `!job 2` for details{RST}\r\n") +out(0.3, f"{BLUE}└───────────────────────────────────────────────────────────────────┘{RST}\r\n\r\n") + +# Scene 5 — job finishes, bot pushes notification +out(0.6, f"{BLUE}┌─ Telegram ────────────────────────────────────────────────────────┐{RST}\r\n") +out(0.3, f"{BLUE}│{RST} {DIM}11:51{RST} 🐆 Job #2 done. {GREEN}Backup OK{RST} — 4.2 GB → s3://prod-backups/2026-05-10/\r\n") +out(0.2, f"{BLUE}│{RST} {DIM}Took 7m 51s. Logs at ~/.cheetahclaws/jobs/2/stdout.txt{RST}\r\n") +out(0.3, f"{BLUE}└───────────────────────────────────────────────────────────────────┘{RST}\r\n\r\n") + +out(0.4, f"{DIM}Also available: /wechat (微信), /slack — same job queue & passthrough.{RST}\r\n\r\n") +out(0.5, f"{BOLD}{CYAN}[~] »{RST} ") +out(0.8, "") + +sys.stdout.write(json.dumps(HEADER) + "\n") +for ev in events: + sys.stdout.write(json.dumps(ev) + "\n") diff --git a/docs/media/casts/lab.cast b/docs/media/casts/lab.cast new file mode 100644 index 0000000..98a9e30 --- /dev/null +++ b/docs/media/casts/lab.cast @@ -0,0 +1,127 @@ +{"version": 2, "width": 110, "height": 34, "timestamp": 1747262400, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}, "title": "CheetahClaws /lab \u2014 9 agents drive a paper from question to PDF", "idle_time_limit": 1.3} +[0.0, "o", "[32m~[0m [36m\u276f[0m cheetahclaws\r\n"] +[0.3, "o", "[2m[CheetahClaws v3.05.79 \u00b7 claude-sonnet-4-6][/lab engine v0][0m\r\n\r\n"] +[0.5, "o", "[1m[36m[~] \u00bb[0m "] +[1.0, "o", ""] +[1.058, "o", "/"] +[1.117, "o", "l"] +[1.175, "o", "a"] +[1.217, "o", "b"] +[1.269, "o", " "] +[1.317, "o", "s"] +[1.368, "o", "t"] +[1.411, "o", "a"] +[1.454, "o", "r"] +[1.503, "o", "t"] +[1.548, "o", " "] +[1.597, "o", "\""] +[1.637, "o", "C"] +[1.679, "o", "o"] +[1.733, "o", "m"] +[1.782, "o", "p"] +[1.832, "o", "a"] +[1.887, "o", "r"] +[1.934, "o", "e"] +[1.975, "o", " "] +[2.031, "o", "l"] +[2.082, "o", "o"] +[2.135, "o", "g"] +[2.188, "o", "i"] +[2.247, "o", "s"] +[2.295, "o", "t"] +[2.35, "o", "i"] +[2.397, "o", "c"] +[2.449, "o", " "] +[2.502, "o", "r"] +[2.548, "o", "e"] +[2.59, "o", "g"] +[2.639, "o", "r"] +[2.694, "o", "e"] +[2.745, "o", "s"] +[2.794, "o", "s"] +[2.847, "o", "i"] +[2.891, "o", "o"] +[2.934, "o", "n"] +[2.981, "o", " "] +[3.037, "o", "v"] +[3.081, "o", "s"] +[3.123, "o", " "] +[3.165, "o", "r"] +[3.206, "o", "a"] +[3.252, "o", "n"] +[3.303, "o", "d"] +[3.36, "o", "o"] +[3.406, "o", "m"] +[3.454, "o", " "] +[3.499, "o", "f"] +[3.545, "o", "o"] +[3.601, "o", "r"] +[3.652, "o", "e"] +[3.697, "o", "s"] +[3.739, "o", "t"] +[3.795, "o", " "] +[3.849, "o", "o"] +[3.906, "o", "n"] +[3.95, "o", " "] +[4.001, "o", "i"] +[4.06, "o", "r"] +[4.112, "o", "i"] +[4.17, "o", "s"] +[4.214, "o", ","] +[4.273, "o", " "] +[4.314, "o", "k"] +[4.363, "o", "-"] +[4.403, "o", "f"] +[4.459, "o", "o"] +[4.513, "o", "l"] +[4.573, "o", "d"] +[4.629, "o", " "] +[4.688, "o", "C"] +[4.738, "o", "V"] +[4.798, "o", "\""] +[5.198, "o", "\r\n"] +[5.598, "o", "[32m\u2713[0m Lab run [1mlab_a3b1c8e9f012[0m launched. Budget: 60 min \u00b7 200k tokens.\r\n\r\n"] +[6.098, "o", " [32m[QUESTIONING][0m\r\n"] +[6.398, "o", " [34m\u25cf PI[0m: Picking Q2: 'Does RF outperform logistic regression on iris under 5-fold CV?'\r\n"] +[6.498, "o", " [2m\u25cf Lay Reader: question is concrete and testable.[0m\r\n\r\n"] +[6.998, "o", " [32m[SURVEY][0m\r\n"] +[7.298, "o", " [35m\u25cf Surveyor[0m: 12 papers retrieved; baselines on iris well-characterised since 1936.\r\n"] +[7.398, "o", "\r\n"] +[7.898, "o", " [32m[OUTLINE][0m\r\n"] +[8.198, "o", " [34m\u25cf Designer[0m: 5-section outline: intro, related, method, results, threats.\r\n"] +[8.298, "o", " [2m\u25cf Reviewer\u00d73 critique \u2192 2 pass, 1 asks for ablation; PI signs off.[0m\r\n\r\n"] +[8.798, "o", " [32m[CODE_DRAFT][0m\r\n"] +[9.098, "o", " [33m\u25cf Engineer[0m: scripted iris loader, GridSearchCV for both models, 5-fold stratified.\r\n"] +[9.198, "o", "\r\n"] +[9.698, "o", " [32m[EXPERIMENT][0m\r\n"] +[9.998, "o", " [33m\u25cf Engineer[0m: Running sandboxed subprocess\u2026\r\n"] +[10.398, "o", " [2mstdout: Best LR C=10 acc=0.967 \u00b1 0.025[0m\r\n"] +[10.698, "o", " [2mstdout: Best RF n=50 acc=0.967 \u00b1 0.033[0m\r\n"] +[10.898, "o", " [2msaved figure_1.png (boxplot), results.csv[0m\r\n\r\n"] +[11.398, "o", " [32m[ANALYSIS][0m\r\n"] +[11.698, "o", " [33m\u25cf Engineer[0m: Models tie on accuracy; RF has higher variance. Recommend LR for tabular small-n.\r\n"] +[11.798, "o", "\r\n"] +[12.298, "o", " [32m[DRAFTING][0m\r\n"] +[12.598, "o", " [36m\u25cf Drafter[0m: Composed 2,840-word draft with inline [1]\u2013[12] citations.\r\n"] +[12.698, "o", "\r\n"] +[13.098, "o", " [32m[REVIEW LOOP][0m\r\n"] +[13.398, "o", " [31m\u25cf Reviewer #1[0m: 'Section 3.2 doesn't address class imbalance \u2014 minor revision.'\r\n"] +[13.698, "o", " [31m\u25cf Reviewer #2[0m: 'Threats section thin. Add overfitting note.'\r\n"] +[13.998, "o", " [32m\u25cf Reviewer #3[0m: 'Accept.'\r\n"] +[14.398, "o", " [36m\u25cf Drafter[0m: revised \u00a73.2 + \u00a76, rebuilt bib.\r\n"] +[14.698, "o", " [32m\u25cf Reviewer\u00d73[0m: [1m2/3 accept on round 2[0m \u2192 PI signs off.\r\n\r\n"] +[15.198, "o", " [32m[CITATION VERIFY][0m\r\n"] +[15.498, "o", " [32m\u25cf Citation Checker[0m: 12/12 references verified against arXiv / Semantic Scholar / CrossRef.\r\n"] +[15.598, "o", "\r\n"] +[15.998, "o", " [32m[FINALISE][0m Bundle ready.\r\n\r\n"] +[16.398, "o", "[32m\u2713[0m Output at [36m~/.cheetahclaws/research_papers/lab_a3b1c8e9f012/[0m\r\n\r\n"] +[16.698, "o", " \u251c\u2500\u2500 [1mreport.md[0m [2m(2,940 words, 12 refs)[0m\r\n"] +[16.848, "o", " \u251c\u2500\u2500 references.bib [2m(verified BibTeX)[0m\r\n"] +[16.998, "o", " \u251c\u2500\u2500 citations_verified.json\r\n"] +[17.148, "o", " \u2514\u2500\u2500 workspace/\r\n"] +[17.298, "o", " \u251c\u2500\u2500 experiment.py [2m(83 lines)[0m\r\n"] +[17.448, "o", " \u251c\u2500\u2500 figure_1.png [2m(boxplot)[0m\r\n"] +[17.598, "o", " \u2514\u2500\u2500 results.csv [2m(5 folds \u00d7 2 models)[0m\r\n\r\n"] +[18.098, "o", "[2mTotal: 22 min \u00b7 142k tokens \u00b7 $1.40 in API cost[0m\r\n\r\n"] +[18.498, "o", "[1m[36m[~] \u00bb[0m "] +[19.298, "o", ""] diff --git a/docs/media/casts/lab.svg b/docs/media/casts/lab.svg new file mode 100644 index 0000000..1b2a51a --- /dev/null +++ b/docs/media/casts/lab.svg @@ -0,0 +1 @@ +[32m~[0m[36m❯[0mcheetahclaws[2m[CheetahClawsv3.05.79·claude-sonnet-4-6][/labenginev0][0m[1m[36m[~]»[0m[1m[36m[~]»[0m/lab[1m[36m[~]»[0m/labstart[1m[36m[~]»[0m/labstart"Compare[1m[36m[~]»[0m/labstart"Comparelogistic[1m[36m[~]»[0m/labstart"Comparelogisticregression[1m[36m[~]»[0m/labstart"Comparelogisticregressionvs[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandom[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomforest[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomforeston[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomforestoniris,[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomforestoniris,k-fold[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomforestoniris,k-foldCV"[32m✓[0mLabrun[1mlab_a3b1c8e9f012[0mlaunched.Budget:60min·200ktokens.[32m[QUESTIONING][0m[34m●PI[0m:PickingQ2:'DoesRFoutperformlogisticregressiononirisunder5-foldCV?'[2m●LayReader:questionisconcreteandtestable.[0m[32m[SURVEY][0m[35m●Surveyor[0m:12papersretrieved;baselinesoniriswell-characterisedsince1936.[32m[OUTLINE][0m[34m●Designer[0m:5-sectionoutline:intro,related,method,results,threats.[2m●Reviewer×3critique2pass,1asksforablation;PIsignsoff.[0m[32m[CODE_DRAFT][0m[33m●Engineer[0m:scriptedirisloader,GridSearchCVforbothmodels,5-foldstratified.[32m[EXPERIMENT][0m[33m●Engineer[0m:Runningsandboxedsubprocess…[2mstdout:BestLRC=10acc=0.967±0.025[0m[2mstdout:BestRFn=50acc=0.967±0.033[0m[2msavedfigure_1.png(boxplot),results.csv[0m[32m[ANALYSIS][0m[33m●Engineer[0m:Modelstieonaccuracy;RFhashighervariance.RecommendLRfortabularsmall-n.[32m[DRAFTING][0m[36m●Drafter[0m:Composed2,840-worddraftwithinline[1]–[12]citations.[32m[REVIEWLOOP][0m[31m●Reviewer#1[0m:'Section3.2doesn'taddressclassimbalanceminorrevision.'[31m●Reviewer#2[0m:'Threatssectionthin.Addoverfittingnote.'[32m●Reviewer#3[0m:'Accept.'[36m●Drafter[0m:revised§3.2+§6,rebuiltbib.[32m●Reviewer×3[0m:[1m2/3acceptonround2[0mPIsignsoff.[32m[CITATIONVERIFY][0m[32m●CitationChecker[0m:12/12referencesverifiedagainstarXiv/SemanticScholar/CrossRef.[32m[FINALISE][0mBundleready.[32m✓[0mOutputat[36m~/.cheetahclaws/research_papers/lab_a3b1c8e9f012/[0m├──[1mreport.md[0m[2m(2,940words,12refs)[0m├──references.bib[2m(verifiedBibTeX)[0m├──citations_verified.json└──workspace/├──experiment.py[2m(83lines)[0m├──figure_1.png[2m(boxplot)[0m└──results.csv[2m(5folds×2models)[0m[2mTotal:22min·142ktokens·$1.40inAPIcost[0m[1m[36m[~]»[0m/[1m[36m[~]»[0m/l[1m[36m[~]»[0m/la[1m[36m[~]»[0m/labs[1m[36m[~]»[0m/labst[1m[36m[~]»[0m/labsta[1m[36m[~]»[0m/labstar[1m[36m[~]»[0m/labstart"[1m[36m[~]»[0m/labstart"C[1m[36m[~]»[0m/labstart"Co[1m[36m[~]»[0m/labstart"Com[1m[36m[~]»[0m/labstart"Comp[1m[36m[~]»[0m/labstart"Compa[1m[36m[~]»[0m/labstart"Compar[1m[36m[~]»[0m/labstart"Comparel[1m[36m[~]»[0m/labstart"Comparelo[1m[36m[~]»[0m/labstart"Comparelog[1m[36m[~]»[0m/labstart"Comparelogi[1m[36m[~]»[0m/labstart"Comparelogis[1m[36m[~]»[0m/labstart"Comparelogist[1m[36m[~]»[0m/labstart"Comparelogisti[1m[36m[~]»[0m/labstart"Comparelogisticr[1m[36m[~]»[0m/labstart"Comparelogisticre[1m[36m[~]»[0m/labstart"Comparelogisticreg[1m[36m[~]»[0m/labstart"Comparelogisticregr[1m[36m[~]»[0m/labstart"Comparelogisticregre[1m[36m[~]»[0m/labstart"Comparelogisticregres[1m[36m[~]»[0m/labstart"Comparelogisticregress[1m[36m[~]»[0m/labstart"Comparelogisticregressi[1m[36m[~]»[0m/labstart"Comparelogisticregressio[1m[36m[~]»[0m/labstart"Comparelogisticregressionv[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsr[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsra[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsran[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrand[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrando[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomf[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomfo[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomfor[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomfore[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomfores[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomforesto[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomforestoni[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomforestonir[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomforestoniri[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomforestoniris[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomforestoniris,k[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomforestoniris,k-[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomforestoniris,k-f[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomforestoniris,k-fo[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomforestoniris,k-fol[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomforestoniris,k-foldC[1m[36m[~]»[0m/labstart"Comparelogisticregressionvsrandomforestoniris,k-foldCV \ No newline at end of file diff --git a/docs/media/casts/research.cast b/docs/media/casts/research.cast new file mode 100644 index 0000000..11bf25b --- /dev/null +++ b/docs/media/casts/research.cast @@ -0,0 +1,134 @@ +{"version": 2, "width": 110, "height": 32, "timestamp": 1747262400, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}, "title": "CheetahClaws /research \u2014 parallel fan-out across 20 sources", "idle_time_limit": 1.5} +[0.0, "o", "[32m~[0m [36m\u276f[0m "] +[0.6, "o", ""] +[0.645, "o", "c"] +[0.699, "o", "h"] +[0.753, "o", "e"] +[0.81, "o", "e"] +[0.853, "o", "t"] +[0.898, "o", "a"] +[0.941, "o", "h"] +[0.985, "o", "c"] +[1.04, "o", "l"] +[1.083, "o", "a"] +[1.133, "o", "w"] +[1.178, "o", "s"] +[1.578, "o", "\r\n"] +[1.878, "o", "[2m[CheetahClaws v3.05.79 \u00b7 claude-sonnet-4-6][0m\r\n\r\n"] +[2.078, "o", "[1m[36m[~] \u00bb[0m "] +[2.578, "o", ""] +[2.623, "o", "/"] +[2.676, "o", "r"] +[2.73, "o", "e"] +[2.787, "o", "s"] +[2.831, "o", "e"] +[2.875, "o", "a"] +[2.918, "o", "r"] +[2.963, "o", "c"] +[3.018, "o", "h"] +[3.06, "o", " "] +[3.111, "o", "\""] +[3.155, "o", "L"] +[3.201, "o", "L"] +[3.25, "o", "M"] +[3.306, "o", " "] +[3.358, "o", "a"] +[3.399, "o", "g"] +[3.444, "o", "e"] +[3.487, "o", "n"] +[3.545, "o", "t"] +[3.601, "o", "s"] +[3.657, "o", " "] +[3.713, "o", "2"] +[3.768, "o", "0"] +[3.827, "o", "2"] +[3.883, "o", "6"] +[3.928, "o", " "] +[3.985, "o", "t"] +[4.035, "o", "r"] +[4.09, "o", "e"] +[4.141, "o", "n"] +[4.19, "o", "d"] +[4.237, "o", "s"] +[4.286, "o", "\""] +[4.332, "o", " "] +[4.375, "o", "-"] +[4.431, "o", "-"] +[4.487, "o", "r"] +[4.547, "o", "a"] +[4.6, "o", "n"] +[4.652, "o", "g"] +[4.707, "o", "e"] +[4.749, "o", " "] +[4.803, "o", "6"] +[4.852, "o", "m"] +[4.895, "o", " "] +[4.939, "o", "-"] +[4.99, "o", "-"] +[5.035, "o", "e"] +[5.084, "o", "x"] +[5.136, "o", "p"] +[5.184, "o", "a"] +[5.227, "o", "n"] +[5.276, "o", "d"] +[5.676, "o", "\r\n\r\n"] +[6.176, "o", "[2m\u25cf Expanding query into 4 sibling sub-queries\u2026[0m\r\n"] +[6.426, "o", " [2m\u21b3[0m autonomous coding agents benchmarks 2026\r\n"] +[6.676, "o", " [2m\u21b3[0m multi-agent debate / reviewer-author loops\r\n"] +[6.926, "o", " [2m\u21b3[0m agentic tool use plus MCP / function calling\r\n"] +[7.176, "o", " [2m\u21b3[0m open-weight models for agent workflows\r\n"] +[7.576, "o", "\r\n"] +[7.976, "o", "[1mFanning out across 20 sources in parallel\u2026[0m\r\n\r\n"] +[8.326, "o", " [32m\u2713[0m [1marXiv [0m [2m\u2192[0m 342 hits\r\n"] +[8.576, "o", " [32m\u2713[0m [1mSemantic Scholar [0m [2m\u2192[0m 218 hits\r\n"] +[8.776, "o", " [32m\u2713[0m [1mHuggingFace Papers [0m [2m\u2192[0m 176 hits\r\n"] +[9.076, "o", " [32m\u2713[0m [1mOpenAlex [0m [2m\u2192[0m 412 hits\r\n"] +[9.296, "o", " [32m\u2713[0m [1malphaXiv [0m [2m\u2192[0m 84 hits\r\n"] +[9.496, "o", " [32m\u2713[0m [1mHackerNews [0m [2m\u2192[0m 511 hits\r\n"] +[9.776, "o", " [32m\u2713[0m [1mGitHub [0m [2m\u2192[0m 298 hits\r\n"] +[9.956, "o", " [32m\u2713[0m [1mReddit r/MachineLearning [0m [2m\u2192[0m 147 hits\r\n"] +[10.106, "o", " [32m\u2713[0m [1mStackOverflow [0m [2m\u2192[0m 62 hits\r\n"] +[10.356, "o", " [32m\u2713[0m [1mGoogle News [0m [2m\u2192[0m 203 hits\r\n"] +[10.536, "o", " [32m\u2713[0m [1mPolymarket [0m [2m\u2192[0m 9 hits\r\n"] +[10.736, "o", " [32m\u2713[0m [1mSEC EDGAR [0m [2m\u2192[0m 14 hits\r\n"] +[11.036, "o", " [33m\u2713[0m [1mTwitter / X [0m [2m\u2192[0m 1.2k hits\r\n"] +[11.216, "o", " [32m\u2713[0m [1mBrave Search [0m [2m\u2192[0m 188 hits\r\n"] +[11.416, "o", " [32m\u2713[0m [1mTavily [0m [2m\u2192[0m 151 hits\r\n"] +[11.666, "o", " [32m\u2713[0m [1mGoogle Scholar [0m [2m\u2192[0m 224 hits\r\n"] +[11.886, "o", " [32m\u2713[0m [1m\u77e5\u4e4e Zhihu [0m [2m\u2192[0m 186 hits\r\n"] +[12.136, "o", " [32m\u2713[0m [1mB\u7ad9 Bilibili [0m [2m\u2192[0m 298 hits\r\n"] +[12.436, "o", " [33m\u2713[0m [1m\u5fae\u535a Weibo [0m [2m\u2192[0m 412 hits\r\n"] +[12.636, "o", " [32m\u2713[0m [1m\u5c0f\u7ea2\u4e66 Xiaohongshu [0m [2m\u2192[0m 127 hits\r\n"] +[13.036, "o", "\r\n"] +[13.636, "o", "[1mTop entities by cross-platform attention:[0m\r\n\r\n"] +[13.936, "o", " [2mentity arXiv HF GH HN \u5fae\u535a Zhihu total[0m\r\n"] +[14.136, "o", " [2m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500[0m\r\n"] +[14.386, "o", " [1mClaude 4.6 [0m [2m 127 84 32 298 412 186[0m [32m 1,139[0m\r\n"] +[14.636, "o", " [1mDeepSeek V4 [0m [2m 108 112 147 176 287 298[0m [32m 1,128[0m\r\n"] +[14.886, "o", " [1mQwen3-Coder [0m [2m 87 92 211 88 154 287[0m [32m 919[0m\r\n"] +[15.136, "o", " [1mMCP Protocol [0m [2m 42 28 188 247 64 72[0m [32m 641[0m\r\n"] +[15.386, "o", " [1mLlama 4 [0m [2m 96 118 94 203 167 88[0m [32m 766[0m\r\n"] +[15.886, "o", "\r\n"] +[16.386, "o", "[2m\u25cf Verifying citations against arXiv / Semantic Scholar / CrossRef\u2026[0m\r\n"] +[16.786, "o", " [32m\u2713[0m 47 papers, [32m45 verified[0m, [31m2 flagged for hallucination[0m\r\n\r\n"] +[17.186, "o", "[32m\u2713[0m Brief saved \u2192 [36m~/.cheetahclaws/research_reports/llm-agents-2026-trends-17.md[0m\r\n"] +[17.386, "o", " [2m3,124 words \u00b7 47 citations \u00b7 cross-platform heat table \u00b7 12-month trend sparkline[0m\r\n\r\n"] +[17.886, "o", "[1m[36m[~] \u00bb[0m "] +[18.386, "o", ""] +[18.432, "o", "/"] +[18.485, "o", "r"] +[18.539, "o", "e"] +[18.596, "o", "p"] +[18.64, "o", "o"] +[18.684, "o", "r"] +[18.727, "o", "t"] +[18.772, "o", "s"] +[18.826, "o", " "] +[18.869, "o", "o"] +[18.92, "o", "p"] +[18.964, "o", "e"] +[19.01, "o", "n"] +[19.41, "o", "\r\n"] +[19.81, "o", "[2mOpening llm-agents-2026-trends-19.md in your editor\u2026[0m\r\n\r\n"] +[20.31, "o", "[1m[36m[~] \u00bb[0m "] +[21.01, "o", ""] diff --git a/docs/media/casts/research.svg b/docs/media/casts/research.svg new file mode 100644 index 0000000..892b448 --- /dev/null +++ b/docs/media/casts/research.svg @@ -0,0 +1 @@ +[32m~[0m[36m❯[0m[32m~[0m[36m❯[0mcheetahclaws[2m[CheetahClawsv3.05.79·claude-sonnet-4-6][0m[1m[36m[~]»[0m[1m[36m[~]»[0m/[1m[36m[~]»[0m/r[1m[36m[~]»[0m/re[1m[36m[~]»[0m/research[1m[36m[~]»[0m/research"LLM[1m[36m[~]»[0m/research"LLMagents[1m[36m[~]»[0m/research"LLMagents2026[1m[36m[~]»[0m/research"LLMagents2026trends"[1m[36m[~]»[0m/research"LLMagents2026trends"--range[1m[36m[~]»[0m/research"LLMagents2026trends"--range6m[1m[36m[~]»[0m/research"LLMagents2026trends"--range6m--expand[2m●Expandingqueryinto4siblingsub-queries…[0m[2m↳[0mautonomouscodingagentsbenchmarks2026[2m↳[0mmulti-agentdebate/reviewer-authorloops[2m↳[0magentictooluseplusMCP/functioncalling[2m↳[0mopen-weightmodelsforagentworkflows[1mFanningoutacross20sourcesinparallel…[0m[32m✓[0m[1marXiv[0m[2m→[0m342hits[32m✓[0m[1mSemanticScholar[0m[2m→[0m218hits[32m✓[0m[1mHuggingFacePapers[0m[2m→[0m176hits[32m✓[0m[1mOpenAlex[0m[2m→[0m412hits[32m✓[0m[1malphaXiv[0m[2m→[0m84hits[32m✓[0m[1mHackerNews[0m[2m→[0m511hits[32m✓[0m[1mGitHub[0m[2m→[0m298hits[32m✓[0m[1mRedditr/MachineLearning[0m[2m→[0m147hits[32m✓[0m[1mStackOverflow[0m[2m→[0m62hits[32m✓[0m[1mGoogleNews[0m[2m→[0m203hits[32m✓[0m[1mPolymarket[0m[2m→[0m9hits[32m✓[0m[1mSECEDGAR[0m[2m→[0m14hits[33m✓[0m[1mTwitter/X[0m[2m→[0m1.2khits[32m✓[0m[1mBraveSearch[0m[2m→[0m188hits[32m✓[0m[1mTavily[0m[2m→[0m151hits[32m✓[0m[1mGoogleScholar[0m[2m→[0m224hits[32m✓[0m[1m知乎Zhihu[0m[2m→[0m186hits[32m✓[0m[1mB站Bilibili[0m[2m→[0m298hits[33m✓[0m[1m微博Weibo[0m[2m→[0m412hits[32m✓[0m[1m小红书Xiaohongshu[0m[2m→[0m127hits[1mTopentitiesbycross-platformattention:[0m[2mentityarXivHFGHHN微博Zhihutotal[0m[2m──────────────────────────────────────────────────────────────────────[0m[1mClaude4.6[0m[2m1278432298412186[0m[32m1,139[0m[1mDeepSeekV4[0m[2m108112147176287298[0m[32m1,128[0m[1mQwen3-Coder[0m[2m879221188154287[0m[32m919[0m[1mMCPProtocol[0m[2m42281882476472[0m[32m641[0m[1mLlama4[0m[2m961189420316788[0m[32m766[0m[2m●VerifyingcitationsagainstarXiv/SemanticScholar/CrossRef…[0m[32m✓[0m47papers,[32m45verified[0m,[31m2flaggedforhallucination[0m[32m✓[0mBriefsaved[36m~/.cheetahclaws/research_reports/llm-agents-2026-trends-17.md[0m[2m3,124words·47citations·cross-platformheattable·12-monthtrendsparkline[0m[1m[36m[~]»[0m/reports[1m[36m[~]»[0m/reportsopen[2mOpeningllm-agents-2026-trends-19.mdinyoureditor…[0m[32m~[0m[36m❯[0mc[32m~[0m[36m❯[0mch[32m~[0m[36m❯[0mche[32m~[0m[36m❯[0mchee[32m~[0m[36m❯[0mcheet[32m~[0m[36m❯[0mcheeta[32m~[0m[36m❯[0mcheetah[32m~[0m[36m❯[0mcheetahc[32m~[0m[36m❯[0mcheetahcl[32m~[0m[36m❯[0mcheetahcla[32m~[0m[36m❯[0mcheetahclaw[1m[36m[~]»[0m/res[1m[36m[~]»[0m/rese[1m[36m[~]»[0m/resea[1m[36m[~]»[0m/resear[1m[36m[~]»[0m/researc[1m[36m[~]»[0m/research"[1m[36m[~]»[0m/research"L[1m[36m[~]»[0m/research"LL[1m[36m[~]»[0m/research"LLMa[1m[36m[~]»[0m/research"LLMag[1m[36m[~]»[0m/research"LLMage[1m[36m[~]»[0m/research"LLMagen[1m[36m[~]»[0m/research"LLMagent[1m[36m[~]»[0m/research"LLMagents2[1m[36m[~]»[0m/research"LLMagents20[1m[36m[~]»[0m/research"LLMagents202[1m[36m[~]»[0m/research"LLMagents2026t[1m[36m[~]»[0m/research"LLMagents2026tr[1m[36m[~]»[0m/research"LLMagents2026tre[1m[36m[~]»[0m/research"LLMagents2026tren[1m[36m[~]»[0m/research"LLMagents2026trend[1m[36m[~]»[0m/research"LLMagents2026trends[1m[36m[~]»[0m/research"LLMagents2026trends"-[1m[36m[~]»[0m/research"LLMagents2026trends"--[1m[36m[~]»[0m/research"LLMagents2026trends"--r[1m[36m[~]»[0m/research"LLMagents2026trends"--ra[1m[36m[~]»[0m/research"LLMagents2026trends"--ran[1m[36m[~]»[0m/research"LLMagents2026trends"--rang[1m[36m[~]»[0m/research"LLMagents2026trends"--range6[1m[36m[~]»[0m/research"LLMagents2026trends"--range6m-[1m[36m[~]»[0m/research"LLMagents2026trends"--range6m--[1m[36m[~]»[0m/research"LLMagents2026trends"--range6m--e[1m[36m[~]»[0m/research"LLMagents2026trends"--range6m--ex[1m[36m[~]»[0m/research"LLMagents2026trends"--range6m--exp[1m[36m[~]»[0m/research"LLMagents2026trends"--range6m--expa[1m[36m[~]»[0m/research"LLMagents2026trends"--range6m--expan[1m[36m[~]»[0m/rep[1m[36m[~]»[0m/repo[1m[36m[~]»[0m/repor[1m[36m[~]»[0m/report[1m[36m[~]»[0m/reportso[1m[36m[~]»[0m/reportsop[1m[36m[~]»[0m/reportsope \ No newline at end of file diff --git a/docs/media/casts/research_agent.cast b/docs/media/casts/research_agent.cast new file mode 100644 index 0000000..0211515 --- /dev/null +++ b/docs/media/casts/research_agent.cast @@ -0,0 +1,126 @@ +{"version": 2, "width": 110, "height": 32, "timestamp": 1747262400, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}, "title": "CheetahClaws /agent \u2014 autonomous research_assistant loop", "idle_time_limit": 1.5} +[0.0, "o", "[32m~[0m [36m\u276f[0m cheetahclaws\r\n"] +[0.3, "o", "[2m[CheetahClaws v3.05.79 \u00b7 claude-sonnet-4-6][0m\r\n\r\n"] +[0.5, "o", "[1m[36m[~] \u00bb[0m "] +[0.9, "o", ""] +[0.94, "o", "/"] +[0.982, "o", "a"] +[1.03, "o", "g"] +[1.084, "o", "e"] +[1.127, "o", "n"] +[1.169, "o", "t"] +[1.569, "o", "\r\n\r\n"] +[1.869, "o", "[1mPick an agent template:[0m\r\n"] +[2.069, "o", " 1. [36mresearch_assistant[0m [2m\u2014 daily literature & trend digest[0m\r\n"] +[2.219, "o", " 2. [36mauto_bug_fixer[0m [2m\u2014 scan repo, propose fixes, run tests[0m\r\n"] +[2.369, "o", " 3. [36mpaper_writer[0m [2m\u2014 draft & polish a paper section by section[0m\r\n"] +[2.519, "o", " 4. [36mauto_coder[0m [2m\u2014 implement TODOs from a backlog file[0m\r\n"] +[2.669, "o", " [2m(or drop a .md into ~/.cheetahclaws/agent_templates/ for a custom one)[0m\r\n\r\n"] +[2.969, "o", "[1mChoose [1-4]:[0m "] +[3.469, "o", ""] +[3.509, "o", "1"] +[3.909, "o", "\r\n"] +[4.209, "o", "[1mTopic for research_assistant:[0m "] +[4.609, "o", ""] +[4.65, "o", "M"] +[4.692, "o", "u"] +[4.74, "o", "l"] +[4.793, "o", "t"] +[4.836, "o", "i"] +[4.878, "o", "-"] +[4.923, "o", "a"] +[4.978, "o", "g"] +[5.021, "o", "e"] +[5.076, "o", "n"] +[5.129, "o", "t"] +[5.172, "o", " "] +[5.223, "o", "d"] +[5.272, "o", "e"] +[5.32, "o", "b"] +[5.38, "o", "a"] +[5.422, "o", "t"] +[5.462, "o", "e"] +[5.521, "o", " "] +[5.569, "o", "v"] +[5.613, "o", "s"] +[5.659, "o", " "] +[5.707, "o", "s"] +[5.766, "o", "i"] +[5.81, "o", "n"] +[5.854, "o", "g"] +[5.906, "o", "l"] +[5.957, "o", "e"] +[6.016, "o", "-"] +[6.073, "o", "m"] +[6.12, "o", "o"] +[6.164, "o", "d"] +[6.221, "o", "e"] +[6.281, "o", "l"] +[6.321, "o", " "] +[6.376, "o", "\u2014"] +[6.433, "o", " "] +[6.486, "o", "p"] +[6.531, "o", "a"] +[6.587, "o", "p"] +[6.641, "o", "e"] +[6.685, "o", "r"] +[6.738, "o", "s"] +[6.782, "o", " "] +[6.823, "o", "f"] +[6.87, "o", "r"] +[6.916, "o", "o"] +[6.964, "o", "m"] +[7.017, "o", " "] +[7.074, "o", "t"] +[7.133, "o", "h"] +[7.193, "o", "e"] +[7.253, "o", " "] +[7.308, "o", "l"] +[7.355, "o", "a"] +[7.398, "o", "s"] +[7.451, "o", "t"] +[7.502, "o", " "] +[7.555, "o", "3"] +[7.597, "o", "0"] +[7.641, "o", " "] +[7.699, "o", "d"] +[7.751, "o", "a"] +[7.805, "o", "y"] +[7.847, "o", "s"] +[8.247, "o", "\r\n\r\n"] +[8.647, "o", "[32m\u2713[0m Agent [1mresearch_assistant_8f3a2c[0m started \u2014 loop every 4 hours \u00b7 push to Telegram\r\n"] +[8.847, "o", "[2m Output dir: ~/.cheetahclaws/agents/research_assistant_8f3a2c/output/[0m\r\n\r\n"] +[9.347, "o", " [36m\u2500\u2500\u2500 Iteration #1 \u2500\u2500\u2500 [2m11:00 PT[0m\r\n"] +[9.597, "o", " [33m[Read][0m ~/.cheetahclaws/agents/.../state.json [2m(first run, empty)[0m\r\n"] +[9.847, "o", " [33m[research][0m fanned out across 20 sources for the last 24h\r\n"] +[10.097, "o", " [32m\u25cf Found 17 new papers, 3 high-signal:[0m\r\n"] +[10.347, "o", " [2m\u2022[0m \"AdvDebate: \u2026\" (arXiv 2605.04123) \u2014 adversarial multi-agent debate\r\n"] +[10.597, "o", " [2m\u2022[0m \"OneShot or N: \u2026\" (arXiv 2605.04588) \u2014 single-model can rival debate\r\n"] +[10.847, "o", " [2m\u2022[0m \"Skeptic Loop: \u2026\" (Reddit + GitHub) \u2014 open-source debate framework\r\n"] +[11.097, "o", " [33m[Write][0m digest_day_1.md saved to output/\r\n"] +[11.397, "o", " [2m\u2192 pushed iteration summary to Telegram chat 458291205[0m\r\n\r\n"] +[11.897, "o", " [35m\u2500\u2500\u2500 Iteration #2 \u2500\u2500\u2500 [2m15:00 PT[0m\r\n"] +[12.147, "o", " [33m[Read][0m state.json [2m(last digest: digest_day_1.md)[0m\r\n"] +[12.397, "o", " [33m[research][0m new since 11:00 \u2192 4 papers, 1 high-signal\r\n"] +[12.647, "o", " [32m\u25cf Notable:[0m \"Beyond Debate: \u2026\" (NeurIPS workshop preprint)\r\n"] +[12.897, "o", " [2m\u2014 suggests debate gains shrink as base model gets larger[0m\r\n"] +[13.147, "o", " [33m[Write][0m digest_day_1.md (appended)\r\n"] +[13.447, "o", " [2m\u2192 pushed iteration summary to Telegram chat 458291205[0m\r\n\r\n"] +[13.947, "o", " [34m\u2500\u2500\u2500 Iteration #3 \u2500\u2500\u2500 [2m19:00 PT[0m\r\n"] +[14.197, "o", " [33m[research][0m new since 15:00 \u2192 0 papers (quiet window)\r\n"] +[14.447, "o", " [2m\u25cf No new high-signal items. Reused yesterday's analysis.[0m\r\n"] +[14.697, "o", " [33m[Write][0m digest_day_1.md (timestamp updated)\r\n"] +[14.997, "o", " [2m\u2192 pushed iteration summary to Telegram chat 458291205[0m\r\n\r\n"] +[15.497, "o", " [33m\u2500\u2500\u2500 Iteration #4 \u2500\u2500\u2500 [2m23:00 PT[0m\r\n"] +[15.797, "o", " [33m[research][0m 0 new papers \u00b7 summary identical to #3\r\n"] +[16.097, "o", " [31m\u25cf Stagnation-stop:[0m same summary for 3 iterations in a row.\r\n"] +[16.297, "o", " [2m threshold: auto_agent_dup_summary_limit = 3 (set 0 to disable)[0m\r\n"] +[16.597, "o", " [33m\u25cf Loop paused.[0m Next attempt at 09:00 PT (manual or /agent resume).\r\n\r\n"] +[16.997, "o", "[1mOutput (so far):[0m\r\n"] +[17.247, "o", " ~/.cheetahclaws/agents/research_assistant_8f3a2c/output/\r\n"] +[17.447, "o", " \u251c\u2500\u2500 [1mdigest_day_1.md[0m [2m(2.4 KB, 4 papers analysed)[0m\r\n"] +[17.647, "o", " \u251c\u2500\u2500 state.json [2m(loop bookkeeping)[0m\r\n"] +[17.847, "o", " \u2514\u2500\u2500 notes.md [2m(running scratchpad)[0m\r\n\r\n"] +[18.247, "o", "[2mThree iterations \u00b7 38k tokens \u00b7 $0.31. Saved ~$0.90 in API spend by stopping.[0m\r\n\r\n"] +[18.647, "o", "[1m[36m[~] \u00bb[0m "] +[19.447, "o", ""] diff --git a/docs/media/casts/research_agent.svg b/docs/media/casts/research_agent.svg new file mode 100644 index 0000000..cc395dd --- /dev/null +++ b/docs/media/casts/research_agent.svg @@ -0,0 +1 @@ +[32m~[0m[36m❯[0mcheetahclaws[2m[CheetahClawsv3.05.79·claude-sonnet-4-6][0m[1m[36m[~]»[0m[1m[36m[~]»[0m/agent[1mPickanagenttemplate:[0m1.[36mresearch_assistant[0m[2m—dailyliterature&trenddigest[0m2.[36mauto_bug_fixer[0m[2m—scanrepo,proposefixes,runtests[0m3.[36mpaper_writer[0m[2m—draft&polishapapersectionbysection[0m4.[36mauto_coder[0m[2m—implementTODOsfromabacklogfile[0m[2m(ordropa.mdinto~/.cheetahclaws/agent_templates/foracustomone)[0m[1mChoose[1-4]:[0m[1mChoose[1-4]:[0m1[1mTopicforresearch_assistant:[0m[1mTopicforresearch_assistant:[0mMulti-agent[1mTopicforresearch_assistant:[0mMulti-agentdebate[1mTopicforresearch_assistant:[0mMulti-agentdebatevs[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-model[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-model[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpapers[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpapersfrom[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpapersfromthe[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpapersfromthelast[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpapersfromthelast30[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpapersfromthelast30days[32m✓[0mAgent[1mresearch_assistant_8f3a2c[0mstartedloopevery4hours·pushtoTelegram[2mOutputdir:~/.cheetahclaws/agents/research_assistant_8f3a2c/output/[0m[36m───Iteration#1───[2m11:00PT[0m[33m[Read][0m~/.cheetahclaws/agents/.../state.json[2m(firstrun,empty)[0m[33m[research][0mfannedoutacross20sourcesforthelast24h[32m●Found17newpapers,3high-signal:[0m[2m•[0m"AdvDebate:…"(arXiv2605.04123)adversarialmulti-agentdebate[2m•[0m"OneShotorN:…"(arXiv2605.04588)single-modelcanrivaldebate[2m•[0m"SkepticLoop:…"(Reddit+GitHub)open-sourcedebateframework[33m[Write][0mdigest_day_1.mdsavedtooutput/[2m→pushediterationsummarytoTelegramchat458291205[0m[35m───Iteration#2───[2m15:00PT[0m[33m[Read][0mstate.json[2m(lastdigest:digest_day_1.md)[0m[33m[research][0mnewsince11:004papers,1high-signal[32m●Notable:[0m"BeyondDebate:…"(NeurIPSworkshoppreprint)[2m—suggestsdebategainsshrinkasbasemodelgetslarger[0m[33m[Write][0mdigest_day_1.md(appended)[34m───Iteration#3───[2m19:00PT[0m[33m[research][0mnewsince15:000papers(quietwindow)[2m●Nonewhigh-signalitems.Reusedyesterday'sanalysis.[0m[33m[Write][0mdigest_day_1.md(timestampupdated)[33m───Iteration#4───[2m23:00PT[0m[33m[research][0m0newpapers·summaryidenticalto#3[31m●Stagnation-stop:[0msamesummaryfor3iterationsinarow.[2mthreshold:auto_agent_dup_summary_limit=3(set0todisable)[0m[33m●Looppaused.[0mNextattemptat09:00PT(manualor/agentresume).[1mOutput(sofar):[0m~/.cheetahclaws/agents/research_assistant_8f3a2c/output/├──[1mdigest_day_1.md[0m[2m(2.4KB,4papersanalysed)[0m├──state.json[2m(loopbookkeeping)[0m└──notes.md[2m(runningscratchpad)[0m[2mThreeiterations·38ktokens·$0.31.Saved~$0.90inAPIspendbystopping.[0m[1m[36m[~]»[0m/[1m[36m[~]»[0m/a[1m[36m[~]»[0m/ag[1m[36m[~]»[0m/age[1m[36m[~]»[0m/agen[1mTopicforresearch_assistant:[0mM[1mTopicforresearch_assistant:[0mMu[1mTopicforresearch_assistant:[0mMul[1mTopicforresearch_assistant:[0mMult[1mTopicforresearch_assistant:[0mMulti[1mTopicforresearch_assistant:[0mMulti-[1mTopicforresearch_assistant:[0mMulti-a[1mTopicforresearch_assistant:[0mMulti-ag[1mTopicforresearch_assistant:[0mMulti-age[1mTopicforresearch_assistant:[0mMulti-agen[1mTopicforresearch_assistant:[0mMulti-agentd[1mTopicforresearch_assistant:[0mMulti-agentde[1mTopicforresearch_assistant:[0mMulti-agentdeb[1mTopicforresearch_assistant:[0mMulti-agentdeba[1mTopicforresearch_assistant:[0mMulti-agentdebat[1mTopicforresearch_assistant:[0mMulti-agentdebatev[1mTopicforresearch_assistant:[0mMulti-agentdebatevss[1mTopicforresearch_assistant:[0mMulti-agentdebatevssi[1mTopicforresearch_assistant:[0mMulti-agentdebatevssin[1mTopicforresearch_assistant:[0mMulti-agentdebatevssing[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingl[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-m[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-mo[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-mod[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-mode[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelp[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpa[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpap[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpape[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpaper[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpapersf[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpapersfr[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpapersfro[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpapersfromt[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpapersfromth[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpapersfromthel[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpapersfromthela[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpapersfromthelas[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpapersfromthelast3[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpapersfromthelast30d[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpapersfromthelast30da[1mTopicforresearch_assistant:[0mMulti-agentdebatevssingle-modelpapersfromthelast30day \ No newline at end of file diff --git a/docs/media/casts/telegram.cast b/docs/media/casts/telegram.cast new file mode 100644 index 0000000..b68ca00 --- /dev/null +++ b/docs/media/casts/telegram.cast @@ -0,0 +1,75 @@ +{"version": 2, "width": 105, "height": 32, "timestamp": 1747262400, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}, "title": "CheetahClaws Telegram bridge \u2014 control the agent from your phone", "idle_time_limit": 1.4} +[0.0, "o", "[32m~[0m [36m\u276f[0m cheetahclaws\r\n"] +[0.3, "o", "[2m[CheetahClaws v3.05.79 \u00b7 claude-sonnet-4-6][0m\r\n\r\n"] +[0.5, "o", "[1m[36m[~] \u00bb[0m "] +[1.0, "o", ""] +[1.051, "o", "/"] +[1.098, "o", "t"] +[1.155, "o", "e"] +[1.201, "o", "l"] +[1.251, "o", "e"] +[1.298, "o", "g"] +[1.346, "o", "r"] +[1.405, "o", "a"] +[1.447, "o", "m"] +[1.496, "o", " "] +[1.541, "o", "7"] +[1.588, "o", "8"] +[1.648, "o", "9"] +[1.694, "o", "0"] +[1.747, "o", ":"] +[1.795, "o", "A"] +[1.85, "o", "A"] +[1.899, "o", "E"] +[1.942, "o", "x"] +[1.991, "o", "_"] +[2.039, "o", "R"] +[2.095, "o", "E"] +[2.146, "o", "D"] +[2.19, "o", "A"] +[2.243, "o", "C"] +[2.289, "o", "T"] +[2.34, "o", "E"] +[2.384, "o", "D"] +[2.432, "o", " "] +[2.488, "o", "4"] +[2.538, "o", "5"] +[2.593, "o", "8"] +[2.648, "o", "2"] +[2.7, "o", "9"] +[2.753, "o", "1"] +[2.801, "o", "2"] +[2.853, "o", "0"] +[2.909, "o", "5"] +[3.309, "o", "\r\n"] +[3.709, "o", "[32m\u2713[0m Telegram bridge online \u2014 bot @[1mcheetah_personal_bot[0m, chat 458291205\r\n"] +[3.909, "o", "[2m Listening for messages. Typing indicator + slash passthrough enabled.[0m\r\n\r\n"] +[4.509, "o", "[34m\u250c\u2500 Telegram \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510[0m\r\n"] +[4.809, "o", "[34m\u2502[0m [2m11:42[0m [1mYou[0m: What's the CPU load on the server right now?\r\n"] +[5.409, "o", "[34m\u2502[0m [2m\ud83d\udc06 typing\u2026[0m\r\n"] +[5.709, "o", "[34m\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518[0m\r\n\r\n"] +[6.109, "o", "[33m[Bash][0m uptime\r\n"] +[6.409, "o", " [2m11:42:18 up 14 days, load average: 0.41, 0.55, 0.62[0m\r\n\r\n"] +[6.809, "o", "[34m\u250c\u2500 Telegram \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510[0m\r\n"] +[7.009, "o", "[34m\u2502[0m [2m11:42[0m \ud83d\udc06 CPU is [32mquiet[0m: 0.41 / 0.55 / 0.62 (1m / 5m / 15m).\r\n"] +[7.209, "o", "[34m\u2502[0m Server has been up 14 days. Want me to check memory or disk?\r\n"] +[7.509, "o", "[34m\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518[0m\r\n\r\n"] +[8.009, "o", "[34m\u250c\u2500 Telegram \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510[0m\r\n"] +[8.309, "o", "[34m\u2502[0m [2m11:43[0m [1mYou[0m: Re-run the nightly backup and tell me when it's done\r\n"] +[8.609, "o", "[34m\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518[0m\r\n\r\n"] +[9.009, "o", "[33m[Bash][0m bash /opt/scripts/nightly_backup.sh [2m(long-running, queued as job #2)[0m\r\n"] +[9.309, "o", "[34m\u2502[0m [2m11:43[0m \ud83d\udc06 Queued as job #2. I'll ping you when it finishes.\r\n\r\n"] +[9.809, "o", "[34m\u250c\u2500 Telegram \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510[0m\r\n"] +[10.109, "o", "[34m\u2502[0m [2m11:43[0m [1mYou[0m: !jobs\r\n"] +[10.409, "o", "[34m\u2502[0m [2m11:43[0m \ud83d\udc06 Job queue:\r\n"] +[10.659, "o", "[34m\u2502[0m [32m#1[0m [2m(done 11:42)[0m uptime check\r\n"] +[10.859, "o", "[34m\u2502[0m [33m#2[0m [2m(running 11:43)[0m nightly_backup.sh [\u2588\u2588\u2588\u2588\u2591\u2591\u2591\u2591\u2591\u2591] 41%\r\n"] +[11.059, "o", "[34m\u2502[0m [2m `!cancel 2` to stop \u00b7 `!job 2` for details[0m\r\n"] +[11.359, "o", "[34m\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518[0m\r\n\r\n"] +[11.959, "o", "[34m\u250c\u2500 Telegram \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510[0m\r\n"] +[12.259, "o", "[34m\u2502[0m [2m11:51[0m \ud83d\udc06 Job #2 done. [32mBackup OK[0m \u2014 4.2 GB \u2192 s3://prod-backups/2026-05-10/\r\n"] +[12.459, "o", "[34m\u2502[0m [2mTook 7m 51s. Logs at ~/.cheetahclaws/jobs/2/stdout.txt[0m\r\n"] +[12.759, "o", "[34m\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518[0m\r\n\r\n"] +[13.159, "o", "[2mAlso available: /wechat (\u5fae\u4fe1), /slack \u2014 same job queue & passthrough.[0m\r\n\r\n"] +[13.659, "o", "[1m[36m[~] \u00bb[0m "] +[14.459, "o", ""] diff --git a/docs/media/casts/telegram.svg b/docs/media/casts/telegram.svg new file mode 100644 index 0000000..d8e08e7 --- /dev/null +++ b/docs/media/casts/telegram.svg @@ -0,0 +1 @@ +[32m~[0m[36m❯[0mcheetahclaws[2m[CheetahClawsv3.05.79·claude-sonnet-4-6][0m[1m[36m[~]»[0m[1m[36m[~]»[0m/telegram[1m[36m[~]»[0m/telegram7890:AAEx_REDACTED[1m[36m[~]»[0m/telegram7890:AAEx_REDACTED458291205[32m✓[0mTelegrambridgeonlinebot@[1mcheetah_personal_bot[0m,chat458291205[2mListeningformessages.Typingindicator+slashpassthroughenabled.[0m[34m┌─Telegram────────────────────────────────────────────────────────┐[0m[34m│[0m[2m11:42[0m[1mYou[0m:What'stheCPUloadontheserverrightnow?[34m│[0m[2m🐆typing…[0m[34m└───────────────────────────────────────────────────────────────────┘[0m[33m[Bash][0muptime[2m11:42:18up14days,loadaverage:0.41,0.55,0.62[0m[34m│[0m[2m11:42[0m🐆CPUis[32mquiet[0m:0.41/0.55/0.62(1m/5m/15m).[34m│[0mServerhasbeenup14days.Wantmetocheckmemoryordisk?[34m│[0m[2m11:43[0m[1mYou[0m:Re-runthenightlybackupandtellmewhenit'sdone[33m[Bash][0mbash/opt/scripts/nightly_backup.sh[2m(long-running,queuedasjob#2)[0m[34m│[0m[2m11:43[0m🐆Queuedasjob#2.I'llpingyouwhenitfinishes.[34m│[0m[2m11:43[0m[1mYou[0m:!jobs[34m│[0m[2m11:43[0m🐆Jobqueue:[34m│[0m[32m#1[0m[2m(done11:42)[0muptimecheck[34m│[0m[33m#2[0m[2m(running11:43)[0mnightly_backup.sh[████░░░░░░]41%[34m│[0m[2m`!cancel2`tostop·`!job2`fordetails[0m[34m│[0m[2m11:51[0m🐆Job#2done.[32mBackupOK[0m4.2GBs3://prod-backups/2026-05-10/[34m│[0m[2mTook7m51s.Logsat~/.cheetahclaws/jobs/2/stdout.txt[0m[2mAlsoavailable:/wechat(微信),/slacksamejobqueue&passthrough.[0m[1m[36m[~]»[0m/[1m[36m[~]»[0m/t[1m[36m[~]»[0m/te[1m[36m[~]»[0m/tel[1m[36m[~]»[0m/tele[1m[36m[~]»[0m/teleg[1m[36m[~]»[0m/telegr[1m[36m[~]»[0m/telegra[1m[36m[~]»[0m/telegram7[1m[36m[~]»[0m/telegram78[1m[36m[~]»[0m/telegram789[1m[36m[~]»[0m/telegram7890[1m[36m[~]»[0m/telegram7890:[1m[36m[~]»[0m/telegram7890:A[1m[36m[~]»[0m/telegram7890:AA[1m[36m[~]»[0m/telegram7890:AAE[1m[36m[~]»[0m/telegram7890:AAEx[1m[36m[~]»[0m/telegram7890:AAEx_[1m[36m[~]»[0m/telegram7890:AAEx_R[1m[36m[~]»[0m/telegram7890:AAEx_RE[1m[36m[~]»[0m/telegram7890:AAEx_RED[1m[36m[~]»[0m/telegram7890:AAEx_REDA[1m[36m[~]»[0m/telegram7890:AAEx_REDAC[1m[36m[~]»[0m/telegram7890:AAEx_REDACT[1m[36m[~]»[0m/telegram7890:AAEx_REDACTE[1m[36m[~]»[0m/telegram7890:AAEx_REDACTED4[1m[36m[~]»[0m/telegram7890:AAEx_REDACTED45[1m[36m[~]»[0m/telegram7890:AAEx_REDACTED458[1m[36m[~]»[0m/telegram7890:AAEx_REDACTED4582[1m[36m[~]»[0m/telegram7890:AAEx_REDACTED45829[1m[36m[~]»[0m/telegram7890:AAEx_REDACTED458291[1m[36m[~]»[0m/telegram7890:AAEx_REDACTED4582912[1m[36m[~]»[0m/telegram7890:AAEx_REDACTED45829120 \ No newline at end of file diff --git a/tests/test_bridge_slash_handler.py b/tests/test_bridge_slash_handler.py index 1340a70..b2e020f 100644 --- a/tests/test_bridge_slash_handler.py +++ b/tests/test_bridge_slash_handler.py @@ -127,3 +127,117 @@ def is_alive(self): return False "handle_slash must be wired in headless bootstrap (issue #84 follow-up)" assert callable(ctx.run_query), \ "run_query must be wired in headless bootstrap" + + +def test_headless_bootstrap_wires_tg_send(monkeypatch): + """Issue #84 follow-up: ask_input_interactive only routes to Telegram when + session_ctx.tg_send is non-None. Headless bootstrap previously left it + unset, so inline-keyboard approval prompts never reached the user — the + bridge silently fell through to terminal input().""" + import runtime, cheetahclaws as cc + sid = "test-headless-tgsend-wire" + config = { + "_session_id": sid, + "telegram_token": "FAKE_TOKEN", + "telegram_chat_id": 9999, + } + monkeypatch.setattr(cc._btg, "_telegram_thread", None) + + class _NoopThread: + def start(self): pass + def is_alive(self): return False + monkeypatch.setattr("threading.Thread", lambda *a, **kw: _NoopThread()) + + cc._start_headless_bridges(config) + + ctx = runtime.get_session_ctx(sid) + assert callable(ctx.tg_send), \ + "tg_send must be wired in headless bootstrap so ask_input_interactive " \ + "can render Telegram inline-keyboard prompts (issue #84)" + + +def test_headless_run_query_handles_permission_request(monkeypatch): + """Issue #84 follow-up: in headless mode the agent loop yields a + PermissionRequest event for sensitive tools. Pre-fix that event was + dropped, leaving event.granted=False, so every approval-required tool + silently denied without ever asking the user.""" + import runtime, cheetahclaws as cc + from agent import PermissionRequest + + sid = "test-headless-permission-event" + config = { + "_session_id": sid, + "telegram_token": "FAKE_TOKEN", + "telegram_chat_id": 7777, + } + monkeypatch.setattr(cc._btg, "_telegram_thread", None) + + class _NoopThread: + def start(self): pass + def is_alive(self): return False + monkeypatch.setattr("threading.Thread", lambda *a, **kw: _NoopThread()) + + # Stub the agent loop: yield exactly one PermissionRequest, then stop. + captured: list[PermissionRequest] = [] + def _fake_run(prompt, state, cfg, system_prompt): + req = PermissionRequest(description="mkdir test_folder") + captured.append(req) + yield req + + monkeypatch.setattr("agent.run", _fake_run) + monkeypatch.setattr(cc, "ask_permission_interactive", + lambda desc, cfg: True) + monkeypatch.setattr("context.build_system_prompt", lambda c: "sys") + + cc._start_headless_bridges(config) + ctx = runtime.get_session_ctx(sid) + + ctx.run_query("please make a folder") + + assert len(captured) == 1 + assert captured[0].granted is True, \ + "_headless_run_query must consult ask_permission_interactive on " \ + "PermissionRequest events (issue #84)" + + +def test_headless_run_query_promotes_telegram_incoming(monkeypatch): + """When a Telegram message triggers the agent, _bg_runner sets + telegram_incoming=True before calling run_query. _headless_run_query + must promote that to in_telegram_turn so _is_in_tg_turn() returns True + while ask_input_interactive routes the prompt — otherwise prompts fall + through to terminal input().""" + import runtime, cheetahclaws as cc + + sid = "test-headless-turn-promotion" + config = { + "_session_id": sid, + "telegram_token": "FAKE_TOKEN", + "telegram_chat_id": 4242, + } + monkeypatch.setattr(cc._btg, "_telegram_thread", None) + + class _NoopThread: + def start(self): pass + def is_alive(self): return False + monkeypatch.setattr("threading.Thread", lambda *a, **kw: _NoopThread()) + + seen_in_turn: list[bool] = [] + def _fake_run(prompt, state, cfg, system_prompt): + seen_in_turn.append(runtime.get_session_ctx(sid).in_telegram_turn) + return iter(()) # generator yielding nothing + + monkeypatch.setattr("agent.run", _fake_run) + monkeypatch.setattr("context.build_system_prompt", lambda c: "sys") + + cc._start_headless_bridges(config) + ctx = runtime.get_session_ctx(sid) + + ctx.telegram_incoming = True + ctx.run_query("hi") + + assert seen_in_turn == [True], \ + "in_telegram_turn must be True during the agent run, not just before " \ + "and after (issue #84)" + assert ctx.in_telegram_turn is False, \ + "in_telegram_turn must be cleared after the run so the next " \ + "non-Telegram event isn't misrouted" diff --git a/tests/test_telegram_bridge.py b/tests/test_telegram_bridge.py index 85c0d76..08b431e 100644 --- a/tests/test_telegram_bridge.py +++ b/tests/test_telegram_bridge.py @@ -452,6 +452,64 @@ def test_non_cc_payload_ignored(self): methods = [c[0][1] for c in api.call_args_list] assert methods == ["answerCallbackQuery"] + def test_no_prompt_waiting_does_not_edit_message(self): + """Issue #84 follow-up: when a click arrives but no prompt is + currently waiting (already answered or timed out), the handler + must NOT edit the message to show "✓ Selected" — that would + falsely tell the user the action took effect. Acknowledge the + callback (clears spinner) and bail.""" + import threading + evt = threading.Event() + sctx = SimpleNamespace( + tg_input_event=evt, tg_input_value="", + tg_callback_prompt_id="", # no prompt waiting + tg_callback_message_id=0, + ) + with patch.object(tg, "_tg_api", return_value={"ok": True}) as api: + tg._handle_callback_query("TOK", 42, + _make_cb("cc:abc12345:y", 42), sctx) + assert not evt.is_set() + assert sctx.tg_input_value == "" + # Only answerCallbackQuery should fire — no editMessageText. + methods = [c[0][1] for c in api.call_args_list] + assert methods == ["answerCallbackQuery"], \ + "Stale click must not produce a misleading message edit" + + def test_label_with_markdown_chars_is_sanitized(self): + """Issue #84 follow-up: callers can pass any string as an option + value. Backticks/asterisks would break the Markdown parse mode + and silently fail editMessageText. The sanitizer replaces them + before embedding into the confirmation line.""" + import threading + evt = threading.Event() + sctx = SimpleNamespace( + tg_input_event=evt, tg_input_value="", + tg_callback_prompt_id="abc12345", + tg_callback_message_id=100, + ) + # Value contains backtick and asterisk — Markdown markers. + captured_payloads: list[dict] = [] + def _capture(_tok, _method, params=None): + captured_payloads.append((_method, params or {})) + return {"ok": True} + with patch.object(tg, "_tg_api", side_effect=_capture): + tg._handle_callback_query( + "TOK", 42, + _make_cb("cc:abc12345:`bad*value`", 42), + sctx, + ) + # The raw value (with backticks/asterisks) is still delivered to + # the agent — sanitisation only affects the visual confirmation. + assert sctx.tg_input_value == "`bad*value`" + # Find the editMessageText call and check it has no unbalanced + # Markdown markers in the appended "Selected: ..." line. + edits = [p for m, p in captured_payloads if m == "editMessageText"] + assert len(edits) == 1 + body = edits[0]["text"] + # Backticks/asterisks from the raw value are escaped/replaced. + assert "`bad*value`" not in body, \ + "Raw markdown chars must not leak into the visual confirmation" + def test_value_with_colons_preserved(self): # callback_data is "cc::" — the value field can itself # contain colons; split(":", 2) keeps them intact. @@ -550,3 +608,149 @@ def test_click_returns_y(self): def test_click_returns_a_for_accept_all(self): opts = [("✅ Approve", "y"), ("❌ Reject", "n"), ("✅✅ Accept all", "a")] self._drive(opts, click_value="a", expected_return="a") + + +# ── Slash-command stdout forwarding (issue #84 follow-up) ──────────────── + + +class TestSlashRunnerCapturesPrintOutput: + """Pin: when a Telegram / dispatches a "simple" command (the + handler returns a non-tuple), the bridge must forward whatever the + command printed back into the chat. Pre-fix it always sent the + bare "✅ /help executed." string and the actual /help menu only + appeared on the server console — the user-visible regression in + issue #84. + + The poll-loop wraps slash_cb execution with a stdout/stderr Tee that + captures print()/info()/ok()/warn() output. These tests drive the + same code path the live bridge uses (via the inline _slash_runner + closure inside _tg_poll_loop) by lifting the closure out for direct + invocation. + """ + + def _build_runner(self, monkeypatch, slash_cb, sent: list): + """Replicate the closure from bridges/telegram.py:_tg_poll_loop so + unit tests can drive it without pumping the long-poll loop.""" + import io as _io, sys as _sys, re as _re + from bridges import telegram as tg + + # Patch _tg_send to capture instead of hitting the network. + monkeypatch.setattr(tg, "_tg_send", + lambda token, chat_id, text: sent.append(text)) + + class _Tee: + def __init__(self, *streams): + self._streams = streams + def write(self, data): + for s in self._streams: + try: s.write(data) + except Exception: pass + def flush(self): + for s in self._streams: + try: s.flush() + except Exception: pass + + from tools import _tg_thread_local as _ttl # imported the same way the bridge does + + def _slash_runner(_slash_text, _token, _chat_id): + _ttl.active = True + _buf_out, _buf_err = _io.StringIO(), _io.StringIO() + _orig_out, _orig_err = _sys.stdout, _sys.stderr + _sys.stdout = _Tee(_orig_out, _buf_out) + _sys.stderr = _Tee(_orig_err, _buf_err) + try: + cmd_type = slash_cb(_slash_text) + except Exception as e: + _sys.stdout, _sys.stderr = _orig_out, _orig_err + tg._tg_send(_token, _chat_id, f"⚠ Error: {e}") + return + finally: + _sys.stdout, _sys.stderr = _orig_out, _orig_err + _ttl.active = False + captured = (_buf_out.getvalue() + _buf_err.getvalue()) + captured = _re.sub(r'\x1b\[[0-9;]*m', '', captured).strip() + if cmd_type == "simple": + cmd_name = _slash_text.strip().split()[0] + if captured: + tg._tg_send(_token, _chat_id, captured) + else: + tg._tg_send(_token, _chat_id, f"✅ {cmd_name} executed.") + + return _slash_runner + + def test_print_output_is_forwarded_to_chat(self, monkeypatch): + """A simple command that prints a multi-line menu (think /help) + must surface that menu in the chat — not the bare ack string.""" + def fake_help_cmd(text): + print("CheetahClaws Commands:") + print(" /help show help") + print(" /status show status") + return "simple" + + sent: list[str] = [] + runner = self._build_runner(monkeypatch, fake_help_cmd, sent) + runner("/help", "tok", 42) + + assert len(sent) == 1 + body = sent[0] + assert "CheetahClaws Commands" in body + assert "/help" in body + assert "/status" in body + # The bare ack string must NOT replace the real menu. + assert "executed" not in body + + def test_no_print_falls_back_to_ack(self, monkeypatch): + """Commands that intentionally produce no output (rare, but possible + for purely-stateful toggles) keep the existing ack so the user gets + some confirmation.""" + def silent_cmd(text): + return "simple" + + sent: list[str] = [] + runner = self._build_runner(monkeypatch, silent_cmd, sent) + runner("/silent", "tok", 42) + + assert sent == ["✅ /silent executed."] + + def test_ansi_escapes_are_stripped(self, monkeypatch): + """info()/ok()/warn() in ui/render.py wrap text in ANSI colour + codes via clr(). Telegram doesn't render ANSI, so the bridge + must strip them before sending — otherwise the user sees raw + '\\x1b[36m...\\x1b[0m' garbage.""" + def coloured_cmd(text): + from ui.render import info, ok + info("informational") + ok("done") + return "simple" + + sent: list[str] = [] + runner = self._build_runner(monkeypatch, coloured_cmd, sent) + runner("/status", "tok", 42) + + assert len(sent) == 1 + body = sent[0] + assert "\x1b[" not in body, \ + "ANSI escape sequences must be stripped before sending to Telegram" + assert "informational" in body + assert "done" in body + + def test_other_threads_stdout_is_not_lost(self, monkeypatch): + """The Tee writes to BOTH the original stdout and the capture + buffer, so server logs (docker compose logs) still see the + command's output even though the bridge also forwards it.""" + import io as _io + captured_orig = _io.StringIO() + monkeypatch.setattr("sys.stdout", captured_orig) + + def chatty_cmd(text): + print("visible to operator") + return "simple" + + sent: list[str] = [] + runner = self._build_runner(monkeypatch, chatty_cmd, sent) + runner("/status", "tok", 42) + + assert "visible to operator" in captured_orig.getvalue(), \ + "Tee must keep writing to original stdout so docker logs " \ + "still show / output" + assert "visible to operator" in sent[0]