robot_lab 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.architecture/AGENTS.md +32 -0
- data/.architecture/config.yml +8 -0
- data/.architecture/members.yml +60 -0
- data/.architecture/reviews/feature-free-will.md +490 -0
- data/.architecture/reviews/overall-codebase.md +427 -0
- data/.claude/settings.local.json +57 -0
- data/.codex/config.toml +2 -0
- data/.irbrc +2 -2
- data/.rubocop.yml +172 -0
- data/CHANGELOG.md +72 -0
- data/CLAUDE.md +139 -0
- data/README.md +91 -95
- data/Rakefile +109 -3
- data/agent2agent_review.md +192 -0
- data/agentf_improvements.md +253 -0
- data/agents.md +14 -0
- data/docs/examples/index.md +37 -2
- data/docs/getting-started/configuration.md +20 -7
- data/docs/guides/index.md +16 -16
- data/docs/guides/knowledge.md +7 -1
- data/docs/guides/observability.md +132 -0
- data/docs/index.md +30 -3
- data/docs/superpowers/plans/2026-05-06-agentskills.md +1303 -0
- data/docs/superpowers/specs/2026-05-06-agentskills-design.md +247 -0
- data/examples/.envrc +1 -0
- data/examples/01_simple_robot.rb +5 -9
- data/examples/02_tools.rb +5 -9
- data/examples/03_network.rb +8 -9
- data/examples/04_mcp.rb +21 -29
- data/examples/05_streaming.rb +12 -18
- data/examples/06_prompt_templates.rb +11 -19
- data/examples/07_network_memory.rb +16 -31
- data/examples/08_llm_config.rb +10 -22
- data/examples/09_chaining.rb +16 -27
- data/examples/10_memory.rb +12 -28
- data/examples/11_network_introspection.rb +15 -29
- data/examples/12_message_bus.rb +5 -12
- data/examples/13_spawn.rb +5 -10
- data/examples/14_rusty_circuit/.envrc +1 -0
- data/examples/14_rusty_circuit/comic.rb +2 -0
- data/examples/14_rusty_circuit/heckler.rb +1 -1
- data/examples/14_rusty_circuit/open_mic.rb +1 -3
- data/examples/14_rusty_circuit/scout.rb +2 -0
- data/examples/15_memory_network_and_bus/.envrc +1 -0
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +6 -3
- data/examples/15_memory_network_and_bus/linux_writer.rb +1 -1
- data/examples/15_memory_network_and_bus/output/combined_article.md +6 -6
- data/examples/15_memory_network_and_bus/output/final_article.md +6 -8
- data/examples/15_memory_network_and_bus/output/linux_draft.md +4 -2
- data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
- data/examples/15_memory_network_and_bus/output/memory.json +6 -6
- data/examples/15_memory_network_and_bus/output/revision_1.md +10 -11
- data/examples/15_memory_network_and_bus/output/revision_2.md +6 -8
- data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
- data/examples/16_writers_room/.envrc +1 -0
- data/examples/16_writers_room/writers_room.rb +2 -4
- data/examples/17_skills.rb +8 -17
- data/examples/18_rails/Gemfile +1 -0
- data/examples/19_token_tracking.rb +9 -15
- data/examples/20_circuit_breaker.rb +10 -19
- data/examples/21_learning_loop.rb +11 -20
- data/examples/22_context_compression.rb +6 -13
- data/examples/23_convergence.rb +6 -17
- data/examples/24_structured_delegation.rb +11 -15
- data/examples/25_history_search.rb +5 -12
- data/examples/26_document_store.rb +6 -13
- data/examples/27_incident_response/incident_response.rb +4 -5
- data/examples/28_mcp_discovery.rb +8 -11
- data/examples/29_ractor_tools.rb +4 -9
- data/examples/30_ractor_network.rb +10 -19
- data/examples/31_launch_assessment.rb +10 -23
- data/examples/32_newsletter_reader.rb +188 -0
- data/examples/33_stock_generator.rb +80 -0
- data/examples/33_stock_predictor.rb +306 -0
- data/examples/34_agentskills.rb +72 -0
- data/examples/README.md +1 -1
- data/examples/common.rb +76 -0
- data/examples/ruboruby.md +423 -0
- data/examples/temp.md +51 -0
- data/lib/robot_lab/agent_skill.rb +63 -0
- data/lib/robot_lab/agent_skill_catalog.rb +74 -0
- data/lib/robot_lab/ask_user.rb +2 -2
- data/lib/robot_lab/bus_poller.rb +12 -5
- data/lib/robot_lab/config.rb +1 -12
- data/lib/robot_lab/delegation_future.rb +1 -1
- data/lib/robot_lab/doom_loop_detector.rb +98 -0
- data/lib/robot_lab/history_compressor.rb +4 -10
- data/lib/robot_lab/mcp/client.rb +1 -2
- data/lib/robot_lab/mcp/connection_poller.rb +3 -3
- data/lib/robot_lab/mcp/server.rb +1 -1
- data/lib/robot_lab/mcp/server_discovery.rb +0 -2
- data/lib/robot_lab/memory.rb +32 -27
- data/lib/robot_lab/memory_change.rb +2 -2
- data/lib/robot_lab/message.rb +4 -4
- data/lib/robot_lab/network.rb +11 -6
- data/lib/robot_lab/robot/agent_skill_matching.rb +99 -0
- data/lib/robot_lab/robot/bus_messaging.rb +9 -27
- data/lib/robot_lab/robot/history_search.rb +4 -1
- data/lib/robot_lab/robot/mcp_management.rb +5 -11
- data/lib/robot_lab/robot/template_rendering.rb +60 -40
- data/lib/robot_lab/robot.rb +323 -206
- data/lib/robot_lab/robot_result.rb +6 -5
- data/lib/robot_lab/run_config.rb +5 -11
- data/lib/robot_lab/script_tool.rb +76 -0
- data/lib/robot_lab/state_proxy.rb +7 -5
- data/lib/robot_lab/tool.rb +3 -3
- data/lib/robot_lab/tool_config.rb +1 -1
- data/lib/robot_lab/tool_manifest.rb +5 -7
- data/lib/robot_lab/user_message.rb +2 -2
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab/waiter.rb +1 -1
- data/lib/robot_lab.rb +41 -52
- data/logfile +8 -0
- data/mkdocs.yml +2 -3
- data/robot_concurrency.md +38 -0
- data/simple_acp_review.md +298 -0
- data/site/404.html +2300 -0
- data/site/api/core/index.html +2706 -0
- data/site/api/core/memory/index.html +3793 -0
- data/site/api/core/network/index.html +3500 -0
- data/site/api/core/robot/index.html +4566 -0
- data/site/api/core/state/index.html +3390 -0
- data/site/api/core/tool/index.html +3843 -0
- data/site/api/index.html +2635 -0
- data/site/api/mcp/client/index.html +3435 -0
- data/site/api/mcp/index.html +2783 -0
- data/site/api/mcp/server/index.html +3252 -0
- data/site/api/mcp/transports/index.html +3352 -0
- data/site/api/messages/index.html +2641 -0
- data/site/api/messages/text-message/index.html +3087 -0
- data/site/api/messages/tool-call-message/index.html +3159 -0
- data/site/api/messages/tool-result-message/index.html +3252 -0
- data/site/api/messages/user-message/index.html +3212 -0
- data/site/api/streaming/context/index.html +3282 -0
- data/site/api/streaming/events/index.html +3347 -0
- data/site/api/streaming/index.html +2738 -0
- data/site/architecture/core-concepts/index.html +3757 -0
- data/site/architecture/index.html +2797 -0
- data/site/architecture/message-flow/index.html +3238 -0
- data/site/architecture/network-orchestration/index.html +3433 -0
- data/site/architecture/robot-execution/index.html +3140 -0
- data/site/architecture/state-management/index.html +3498 -0
- data/site/assets/css/custom.css +56 -0
- data/site/assets/images/favicon.png +0 -0
- data/site/assets/images/robot_lab.jpg +0 -0
- data/site/assets/javascripts/bundle.79ae519e.min.js +16 -0
- data/site/assets/javascripts/bundle.79ae519e.min.js.map +7 -0
- data/site/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
- data/site/assets/javascripts/lunr/tinyseg.js +206 -0
- data/site/assets/javascripts/lunr/wordcut.js +6708 -0
- data/site/assets/javascripts/workers/search.2c215733.min.js +42 -0
- data/site/assets/javascripts/workers/search.2c215733.min.js.map +7 -0
- data/site/assets/stylesheets/main.484c7ddc.min.css +1 -0
- data/site/assets/stylesheets/main.484c7ddc.min.css.map +1 -0
- data/site/assets/stylesheets/palette.ab4e12ef.min.css +1 -0
- data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +1 -0
- data/site/concepts/index.html +3455 -0
- data/site/examples/basic-chat/index.html +2880 -0
- data/site/examples/index.html +2907 -0
- data/site/examples/mcp-server/index.html +3018 -0
- data/site/examples/multi-robot-network/index.html +3131 -0
- data/site/examples/rails-application/index.html +3329 -0
- data/site/examples/tool-usage/index.html +3085 -0
- data/site/getting-started/configuration/index.html +3745 -0
- data/site/getting-started/index.html +2572 -0
- data/site/getting-started/installation/index.html +2981 -0
- data/site/getting-started/quick-start/index.html +2942 -0
- data/site/guides/building-robots/index.html +4290 -0
- data/site/guides/creating-networks/index.html +3858 -0
- data/site/guides/index.html +2586 -0
- data/site/guides/mcp-integration/index.html +3581 -0
- data/site/guides/memory/index.html +3586 -0
- data/site/guides/rails-integration/index.html +4019 -0
- data/site/guides/streaming/index.html +3157 -0
- data/site/guides/using-tools/index.html +3802 -0
- data/site/index.html +2671 -0
- data/site/search/search_index.json +1 -0
- data/site/sitemap.xml +183 -0
- data/site/sitemap.xml.gz +0 -0
- data/site/tags.json +1 -0
- data/temp.md +6 -0
- data/tool_manifest_plan.md +155 -0
- metadata +154 -92
- data/docs/examples/rails-application.md +0 -419
- data/docs/guides/ractor-parallelism.md +0 -364
- data/docs/guides/rails-integration.md +0 -681
- data/docs/superpowers/plans/2026-04-14-ractor-integration.md +0 -1538
- data/docs/superpowers/specs/2026-04-14-ractor-integration-design.md +0 -258
- data/lib/generators/robot_lab/install_generator.rb +0 -90
- data/lib/generators/robot_lab/job_generator.rb +0 -40
- data/lib/generators/robot_lab/robot_generator.rb +0 -55
- data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -42
- data/lib/generators/robot_lab/templates/job.rb.tt +0 -21
- data/lib/generators/robot_lab/templates/migration.rb.tt +0 -32
- data/lib/generators/robot_lab/templates/result_model.rb.tt +0 -52
- data/lib/generators/robot_lab/templates/robot.rb.tt +0 -31
- data/lib/generators/robot_lab/templates/robot_job.rb.tt +0 -18
- data/lib/generators/robot_lab/templates/robot_test.rb.tt +0 -34
- data/lib/generators/robot_lab/templates/routing_robot.rb.tt +0 -59
- data/lib/generators/robot_lab/templates/thread_model.rb.tt +0 -40
- data/lib/robot_lab/document_store.rb +0 -155
- data/lib/robot_lab/ractor_boundary.rb +0 -42
- data/lib/robot_lab/ractor_job.rb +0 -37
- data/lib/robot_lab/ractor_memory_proxy.rb +0 -85
- data/lib/robot_lab/ractor_network_scheduler.rb +0 -154
- data/lib/robot_lab/ractor_worker_pool.rb +0 -117
- data/lib/robot_lab/rails_integration/engine.rb +0 -29
- data/lib/robot_lab/rails_integration/job.rb +0 -158
- data/lib/robot_lab/rails_integration/railtie.rb +0 -51
- data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +0 -72
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,78 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.2.1] - 2026-05-19
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`examples/common.rb`** — shared setup file required by all numbered examples. Defines:
|
|
16
|
+
- `LlmConfig = Data.define(:provider, :model)` and a frozen `LLM` hash with `:default` (OpenAI/gpt-5.4), `:local` (Ollama/llama3.2), and `:anthropic` (claude-opus-4-7) entries — access as `LLM[:default].model`
|
|
17
|
+
- `RubyLLM.configure` with null logger and `LLM[:default].model` as `default_model`
|
|
18
|
+
- `RobotLab.configure` with null logger
|
|
19
|
+
- Output helpers: `banner(title)`, `section(title)`, `hr`, `show_code(ruby_string, label:)` using `rouge` for syntax highlighting
|
|
20
|
+
- **`rouge` gem** added to development group for syntax-highlighted example output
|
|
21
|
+
- **`.envrc` files** in `examples/`, `examples/14_rusty_circuit/`, `examples/15_memory_network_and_bus/`, and `examples/16_writers_room/` — each exports `ROBOT_LAB_TEMPLATE_PATH` pointing at the local `prompts/` directory for use with [direnv](https://direnv.net/)
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **All 27+ numbered examples** refactored to `require_relative "common"` instead of requiring the gem directly, and to use `LLM[:default].model` instead of the hardcoded string `"gpt-5.4"`
|
|
26
|
+
- **Example 04 (MCP)** now calls `robot.connect_mcp!` before inspecting MCP attributes, fixing a lazy-initialization issue where `mcp_clients` and `mcp_tools` were empty until the first `run()` call
|
|
27
|
+
|
|
28
|
+
## [0.2.1] - 2026-05-11 (unreleased)
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **`flog` complexity gate** — `flog_check` Rake task enforces method-level complexity limits (warn ≥20, fail ≥50); `quality` task runs tests, RuboCop, and Flog in sequence with a unified pass/fail summary
|
|
33
|
+
- `flog` gem added to development/test group
|
|
34
|
+
- Branch coverage enabled unconditionally (previously CI-only) with minimum thresholds: line 95%, branch 75%
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- Bumped version to 0.2.1
|
|
39
|
+
- **`Robot#initialize` decomposed** into focused private methods: `assign_identity_ivars`, `build_effective_config`, `extract_config_ivars`, `initialize_runtime_state`, `initialize_memory`, `configure_learning`, `apply_template`, `apply_system_prompt`, `apply_chat_params`, `register_chat_callbacks`
|
|
40
|
+
- **`Robot#run` decomposed** into: `resolve_run_memory`, `prepare_tools`, `invoke_ask`, `enforce_token_budget!`
|
|
41
|
+
- **`BusPoller#process_and_drain`** split into `drain_queued_deliveries` and `release_robot` for independent testability
|
|
42
|
+
- **`TemplateRendering#apply_skills_and_template_to_chat`** split into `collect_prompt_content` (pure computation) and `apply_prompt_to_chat` (pure mutation)
|
|
43
|
+
- Removed `.serena/` project configuration files from version control
|
|
44
|
+
- Removed `.claude/memory.sqlite3` database files from version control
|
|
45
|
+
|
|
46
|
+
## [0.2.0] - 2026-05-07
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- **`Durable::Learning`** — cross-session and within-session learning capability for robots. Robots accept `learn: true` and `learn_domain:` constructor params to persist knowledge across sessions via `~/.robot_lab/durable/` YAML files.
|
|
51
|
+
- **`Durable::Store`** — YAML-backed knowledge store with file locking, keyword recall, and confidence tracking.
|
|
52
|
+
- **`Durable::Entry`** — immutable value object for knowledge records with confidence progression.
|
|
53
|
+
- **`Durable::Reflector`** — promotes session learnings to durable storage at end of each run.
|
|
54
|
+
- **`RecallKnowledge` tool** — robots query past knowledge before uncertain decisions.
|
|
55
|
+
- **`RecordKnowledge` tool** — robots persist new knowledge learned during a session.
|
|
56
|
+
- **`AgentSkill`** — value object for discoverable skills defined by `SKILL.md` files with YAML front matter (name, description, version, dependencies, parameters)
|
|
57
|
+
- **`AgentSkillCatalog`** — service for locating and indexing `AgentSkills/` directories at runtime
|
|
58
|
+
- **`AgentSkillMatching`** — `Robot` mixin enabling runtime embedding-based skill injection: the robot selects the most relevant skills from a catalog based on semantic similarity before each run
|
|
59
|
+
- **`ScriptTool`** — factory that wraps shell scripts as robot tools; auto-generates JSON schema from script `--help` output
|
|
60
|
+
- **`DoomLoopDetector`** — detects consecutive and cyclic tool-call repetition; wired into `Robot#run` via singleton `execute_tool` override
|
|
61
|
+
- **`doom_loop_threshold`** field on `RunConfig` — configures the repetition threshold before a `ToolLoopError` is raised
|
|
62
|
+
- **`auto_compact` and `compact_threshold`** fields on `RunConfig` — `:context_window` mode estimates token usage before each run and calls `compress_history` when usage exceeds the threshold (default 80%); a `Proc` value delegates the decision and strategy entirely to the caller
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
|
|
66
|
+
- Bumped version to 0.2.0
|
|
67
|
+
- **Extension gems extracted** — `robot_lab-document_store`, `robot_lab-durable`, `robot_lab-ractor`, and `robot_lab-rails` are now separate gems distributed via rubygems.org; the core gemspec drops `fastembed`, `ractor_queue`, and `ractor-wrapper` as hard dependencies
|
|
68
|
+
- **`Durable::Learning` inclusion** is now conditional on the `robot_lab-durable` gem being loaded
|
|
69
|
+
- **Tool Ractor routing** guarded on `RobotLab.respond_to?(:ractor_pool)` so the core gem runs without the ractor extension
|
|
70
|
+
- **Memory drainer scheduling** — `@drainer_scheduled` remains `true` when rescheduling to prevent concurrent writers from spawning competing drain fibers
|
|
71
|
+
- `@chat` state reset now uses `reset_messages!` / `add_message` public API instead of internal instance variable manipulation
|
|
72
|
+
- Rails generators moved to `robot_lab-rails` extension gem
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
|
|
76
|
+
- Memory drainer double-schedule race when concurrent writers triggered overlapping drain cycles
|
|
77
|
+
- `DocumentStore` instantiation guarded with a `LoadError` message when the extension gem is absent
|
|
78
|
+
- `AgentSkill` YAML parsing hardened against empty strings and non-Hash front matter
|
|
79
|
+
- Missing requires added to examples after gem extraction
|
|
80
|
+
- `doom_loop_threshold` and `auto_compact` documented in README and guides
|
|
81
|
+
- `learn:` parameter and `robot_lab-acp` documented in README
|
|
82
|
+
|
|
11
83
|
## [0.1.0] - 2026-04-29
|
|
12
84
|
|
|
13
85
|
### Added
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
RobotLab is a Ruby framework for building and orchestrating multi-robot LLM workflows. It provides:
|
|
8
|
+
- **Robots**: LLM agents with tools, templates, and memory
|
|
9
|
+
- **Networks**: Orchestration of multiple robots with routing logic
|
|
10
|
+
- **MCP Integration**: Model Context Protocol for external tool servers
|
|
11
|
+
- **Rails Integration**: Generators and ActiveRecord support for conversation history
|
|
12
|
+
|
|
13
|
+
Built on top of [ruby_llm](https://rubyllm.com) and uses Zeitwerk for autoloading.
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Run all tests
|
|
19
|
+
bundle exec rake test
|
|
20
|
+
|
|
21
|
+
# Run a single test file
|
|
22
|
+
bundle exec rake test_file[robot_lab/robot_test.rb]
|
|
23
|
+
|
|
24
|
+
# Run tests with verbose output
|
|
25
|
+
bundle exec rake test_verbose
|
|
26
|
+
|
|
27
|
+
# Run integration tests only
|
|
28
|
+
bundle exec rake integration
|
|
29
|
+
|
|
30
|
+
# Lint with RuboCop
|
|
31
|
+
bundle exec rubocop
|
|
32
|
+
|
|
33
|
+
# Auto-fix RuboCop offenses
|
|
34
|
+
bundle exec rubocop -a
|
|
35
|
+
|
|
36
|
+
# Run all examples
|
|
37
|
+
bundle exec rake examples:all
|
|
38
|
+
|
|
39
|
+
# Run specific example by number (e.g., 01, 02)
|
|
40
|
+
bundle exec rake examples:run[1]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Architecture
|
|
44
|
+
|
|
45
|
+
### Core Classes
|
|
46
|
+
|
|
47
|
+
- **`RobotLab`** (`lib/robot_lab.rb`): Module entry point with factory methods `build()` for robots and `create_network()` for networks
|
|
48
|
+
- **`Robot`** (`lib/robot_lab/robot.rb`): Subclass of `RubyLLM::Agent` with template-based prompts, tools, MCP clients, and memory. Creates a persistent chat on initialization. Use `robot.run("...")` to interact. When standalone uses its own memory; when in a network uses shared network memory
|
|
49
|
+
- **`Network`** (`lib/robot_lab/network.rb`): Orchestrates multiple robots with routing logic. Robots execute sequentially sharing memory. Router is a lambda that returns robot names
|
|
50
|
+
- **`NetworkRun`** (`lib/robot_lab/network_run.rb`): Stateful execution of a network run with isolated memory clone
|
|
51
|
+
|
|
52
|
+
### RunConfig
|
|
53
|
+
|
|
54
|
+
- **`RunConfig`** (`lib/robot_lab/run_config.rb`): Shared configuration object for LLM, tools, callbacks, and infrastructure settings. Flows through the hierarchy: `RobotLab.config -> Network -> Robot -> Template front matter -> Task -> Runtime`. Supports keyword construction, block DSL, merge semantics (more-specific wins), and `apply_to(chat)` for LLM field application. Both Robot and Network accept `config:` parameter. Infrastructure fields include: `bus`, `enable_cache`, `max_tool_rounds`, `token_budget`, `ractor_pool_size`, `max_concurrent_robots`, `doom_loop_threshold`, `auto_compact`, `compact_threshold`.
|
|
55
|
+
|
|
56
|
+
### Memory System
|
|
57
|
+
|
|
58
|
+
- **`Memory`** (`lib/robot_lab/memory.rb`): Key-value store with reserved keys (`:data`, `:results`, `:messages`, `:session_id`, `:cache`). Supports Redis backend. Includes semantic caching via RubyLLM::SemanticCache
|
|
59
|
+
|
|
60
|
+
### MCP (Model Context Protocol)
|
|
61
|
+
|
|
62
|
+
- **`MCP::Client`** (`lib/robot_lab/mcp/client.rb`): Connects to MCP servers
|
|
63
|
+
- **Transports** (`lib/robot_lab/mcp/transports/`): stdio, websocket, SSE, streamable HTTP
|
|
64
|
+
|
|
65
|
+
### Built-in Tools
|
|
66
|
+
|
|
67
|
+
- **`AskUser`** (`lib/robot_lab/ask_user.rb`): Tool that lets a robot ask the user a question via the terminal. Supports open-ended text, multiple choice, and default values. IO sourced from `robot.input`/`robot.output` (defaults to `$stdin`/`$stdout`)
|
|
68
|
+
|
|
69
|
+
### Adapters
|
|
70
|
+
|
|
71
|
+
Provider adapters in `lib/robot_lab/adapters/`: Anthropic, OpenAI, Gemini (for provider-specific formatting)
|
|
72
|
+
|
|
73
|
+
### Configuration Hierarchy
|
|
74
|
+
|
|
75
|
+
Tools and MCP servers use hierarchical configuration: `runtime > robot > network > global config`. Values can be `:none`, `:inherit`, or explicit arrays.
|
|
76
|
+
|
|
77
|
+
## Key Patterns
|
|
78
|
+
|
|
79
|
+
### Creating Robots
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
# Bare robot (no template or prompt)
|
|
83
|
+
robot = RobotLab.build
|
|
84
|
+
robot.with_instructions("You are helpful.").run("Hello!")
|
|
85
|
+
|
|
86
|
+
# With template (.md file in prompts directory with YAML front matter)
|
|
87
|
+
robot = RobotLab.build(name: "helper", template: :helper, context: { key: "value" })
|
|
88
|
+
result = robot.run("Hello!")
|
|
89
|
+
|
|
90
|
+
# With inline system prompt
|
|
91
|
+
robot = RobotLab.build(name: "bot", system_prompt: "You are helpful.")
|
|
92
|
+
result = robot.run("What can you do?")
|
|
93
|
+
|
|
94
|
+
# With tools
|
|
95
|
+
robot = RobotLab.build(name: "bot", system_prompt: "...", local_tools: [my_tool])
|
|
96
|
+
|
|
97
|
+
# Chaining with_* methods
|
|
98
|
+
robot.with_temperature(0.9).with_model("claude-sonnet-4").run("Be creative!")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Creating Networks
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
router = ->(args) { args.call_count.zero? ? ["classifier"] : nil }
|
|
105
|
+
network = RobotLab.create_network(name: "support", robots: [robot1, robot2], router: router)
|
|
106
|
+
result = network.run(message: "Hello")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Router Args
|
|
110
|
+
|
|
111
|
+
Router receives `Router::Args` with: `context`, `network`, `stack`, `call_count`, `last_result`
|
|
112
|
+
|
|
113
|
+
## Testing
|
|
114
|
+
|
|
115
|
+
- Uses Minitest with SimpleCov for coverage
|
|
116
|
+
- Test helper at `test/test_helper.rb` provides `build_robot`, `build_network`, `build_tool` helpers
|
|
117
|
+
- Templates for tests are in `examples/prompts/`
|
|
118
|
+
- VCR and WebMock for HTTP stubbing
|
|
119
|
+
|
|
120
|
+
## Dependencies
|
|
121
|
+
|
|
122
|
+
Core: zeitwerk, ruby_llm (~> 1.12), ruby_llm-mcp, prompt_manager, ruby_llm-schema, ruby_llm-semantic_cache, async, simple_flow, state_machines
|
|
123
|
+
|
|
124
|
+
### Templates
|
|
125
|
+
|
|
126
|
+
Templates use prompt_manager format: single `.md` files with YAML front matter in the configured prompts directory.
|
|
127
|
+
|
|
128
|
+
```markdown
|
|
129
|
+
---
|
|
130
|
+
description: A helpful assistant
|
|
131
|
+
parameters:
|
|
132
|
+
company_name: null
|
|
133
|
+
tone: friendly
|
|
134
|
+
---
|
|
135
|
+
You are a helpful assistant for <%= company_name %>.
|
|
136
|
+
Respond in a <%= tone %> manner.
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Front matter supports: `description`, `parameters` (null = required), LLM config keys (`model`, `temperature`, `top_p`, `top_k`, `max_tokens`, etc.), and robot extras (`robot_name`, `tools`, `mcp`). Constructor-provided values always override front matter.
|
data/README.md
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
- <strong>Message Bus</strong> - Bidirectional robot communication via TypedBus<br>
|
|
27
27
|
- <strong>Dynamic Spawning</strong> - Robots create new robots at runtime<br>
|
|
28
28
|
- <strong>Layered Configuration</strong> - Cascading YAML, env vars, and RunConfig<br>
|
|
29
|
-
- <strong>Rails Integration</strong> - Generators, background jobs, Turbo Stream broadcasting<br>
|
|
29
|
+
- <strong>Rails Integration</strong> - Generators, background jobs, Turbo Stream broadcasting (via <a href="https://github.com/MadBomber/robot_lab-rails">robot_lab-rails</a>)<br>
|
|
30
30
|
- <strong>Token & Cost Tracking</strong> - Per-run and cumulative token counts on every robot<br>
|
|
31
31
|
- <strong>Tool Loop Circuit Breaker</strong> - <code>max_tool_rounds:</code> guards against runaway tool call loops<br>
|
|
32
32
|
- <strong>Learning Accumulation</strong> - <code>robot.learn()</code> builds up cross-run observations with deduplication<br>
|
|
@@ -93,7 +93,7 @@ robot = RobotLab.build(
|
|
|
93
93
|
|
|
94
94
|
### Configuration
|
|
95
95
|
|
|
96
|
-
RobotLab uses [MywayConfig](https://github.com/MadBomber/myway_config) for layered configuration.
|
|
96
|
+
RobotLab uses [MywayConfig](https://github.com/MadBomber/myway_config) for layered configuration. Configuration is loaded automatically from multiple sources in priority order:
|
|
97
97
|
|
|
98
98
|
1. Bundled defaults (`lib/robot_lab/config/defaults.yml`)
|
|
99
99
|
2. Environment-specific overrides (development, test, production)
|
|
@@ -124,6 +124,14 @@ ruby_llm:
|
|
|
124
124
|
request_timeout: 180
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
+
Runtime-only attributes (such as the logger) can be set with a `configure` block:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
RobotLab.configure do |c|
|
|
131
|
+
c.logger = Logger.new(File::NULL) # silence logging
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
127
135
|
### Using Templates
|
|
128
136
|
|
|
129
137
|
For production applications, RobotLab supports a template system built on [PromptManager](https://github.com/MadBomber/prompt_manager). Templates allow you to:
|
|
@@ -677,6 +685,67 @@ robot.clear_messages # flushes broken history; system prompt is kept
|
|
|
677
685
|
result = robot.run("Something new.") # robot is healthy again
|
|
678
686
|
```
|
|
679
687
|
|
|
688
|
+
## Doom Loop Detection
|
|
689
|
+
|
|
690
|
+
Doom loop detection catches the subtler failure mode where a robot repeats the same tool call pattern indefinitely — not hitting `max_tool_rounds`, but cycling through the same sequence over and over. Set `doom_loop_threshold:` to enable it:
|
|
691
|
+
|
|
692
|
+
```ruby
|
|
693
|
+
robot = RobotLab.build(
|
|
694
|
+
name: "runner",
|
|
695
|
+
system_prompt: "Execute steps.",
|
|
696
|
+
local_tools: [StepTool],
|
|
697
|
+
doom_loop_threshold: 3 # alert after 3 identical consecutive or cyclic sequences
|
|
698
|
+
)
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
When a doom loop is detected, a warning is embedded directly into the tool result, prompting the LLM to try a different approach. Detection covers both consecutive repetition (`A,A,A`) and cyclic patterns (`A,B,C,A,B,C`). Via `RunConfig`:
|
|
702
|
+
|
|
703
|
+
```ruby
|
|
704
|
+
config = RobotLab::RunConfig.new(doom_loop_threshold: 3)
|
|
705
|
+
robot = RobotLab.build(name: "runner", system_prompt: "...", config: config)
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
## Automatic Context Compaction
|
|
709
|
+
|
|
710
|
+
`auto_compact` triggers context window compression automatically before each `run()`, preventing context overflow without manual intervention.
|
|
711
|
+
|
|
712
|
+
```ruby
|
|
713
|
+
# Built-in trigger: compact when estimated token usage exceeds 80% of context window
|
|
714
|
+
robot = RobotLab.build(
|
|
715
|
+
name: "analyst",
|
|
716
|
+
system_prompt: "You are a research analyst.",
|
|
717
|
+
auto_compact: :context_window
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
# Tune the threshold (here: compact at 70%)
|
|
721
|
+
robot = RobotLab.build(
|
|
722
|
+
name: "analyst",
|
|
723
|
+
system_prompt: "You are a research analyst.",
|
|
724
|
+
auto_compact: :context_window,
|
|
725
|
+
compact_threshold: 0.70
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
# Application-owned compaction: full control over when and how
|
|
729
|
+
robot = RobotLab.build(
|
|
730
|
+
name: "analyst",
|
|
731
|
+
system_prompt: "You are a research analyst.",
|
|
732
|
+
auto_compact: ->(r) { r.compress_history(recent_turns: 5) if r.chat.messages.size > 40 }
|
|
733
|
+
)
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
| Value | Behaviour |
|
|
737
|
+
|-------|-----------|
|
|
738
|
+
| `nil` / `:none` | No automatic compaction (default) |
|
|
739
|
+
| `:context_window` | Compact when estimated token usage exceeds `compact_threshold` fraction of model's context window |
|
|
740
|
+
| `Proc` | Called with the robot before each `run()`; application decides when and how to compact |
|
|
741
|
+
|
|
742
|
+
`compact_threshold` defaults to `0.80` (80%). Requires the `classifier` gem when using the built-in `:context_window` strategy. Via `RunConfig`:
|
|
743
|
+
|
|
744
|
+
```ruby
|
|
745
|
+
config = RobotLab::RunConfig.new(auto_compact: :context_window, compact_threshold: 0.75)
|
|
746
|
+
robot = RobotLab.build(name: "analyst", system_prompt: "...", config: config)
|
|
747
|
+
```
|
|
748
|
+
|
|
680
749
|
## Learning Accumulation
|
|
681
750
|
|
|
682
751
|
`robot.learn(text)` records a cross-run observation. On each subsequent `run()`, active learnings are automatically prepended to the user message as a `LEARNINGS FROM PREVIOUS RUNS:` block so the LLM can incorporate prior context without needing a persistent chat:
|
|
@@ -693,6 +762,17 @@ reviewer.learn("This codebase prefers map/collect over manual array accumulation
|
|
|
693
762
|
reviewer.run("Review snippet B") # learning is injected automatically
|
|
694
763
|
```
|
|
695
764
|
|
|
765
|
+
Pass `learn: true` in the constructor to enable automatic end-of-session learning promotion via the `robot_lab-durable` gem:
|
|
766
|
+
|
|
767
|
+
```ruby
|
|
768
|
+
reviewer = RobotLab.build(
|
|
769
|
+
name: "reviewer",
|
|
770
|
+
system_prompt: "You are a Ruby code reviewer.",
|
|
771
|
+
learn: true,
|
|
772
|
+
learn_domain: "ruby_review"
|
|
773
|
+
)
|
|
774
|
+
```
|
|
775
|
+
|
|
696
776
|
Learnings deduplicate bidirectionally: if a broader learning is added that contains an existing narrower one, the narrower one is dropped. Learnings are persisted to the robot's `Memory` and survive a robot rebuild when the same `Memory` object is reused.
|
|
697
777
|
|
|
698
778
|
```ruby
|
|
@@ -796,101 +876,17 @@ future.robot_name # => "analyst"
|
|
|
796
876
|
future.delegated_by # => "manager"
|
|
797
877
|
```
|
|
798
878
|
|
|
799
|
-
##
|
|
800
|
-
|
|
801
|
-
RobotLab supports true CPU parallelism via Ruby Ractors — isolated execution contexts that bypass the GVL. Two modes are available:
|
|
802
|
-
|
|
803
|
-
**CPU-bound tools** — mark a tool `ractor_safe true` and RobotLab automatically routes its calls through a global `RactorWorkerPool` instead of running inline:
|
|
804
|
-
|
|
805
|
-
```ruby
|
|
806
|
-
class TranscribeAudio < RubyLLM::Tool
|
|
807
|
-
ractor_safe true
|
|
808
|
-
description "Transcribe an audio file"
|
|
809
|
-
param :path, type: :string, desc: "Path to audio file"
|
|
810
|
-
|
|
811
|
-
def execute(path:)
|
|
812
|
-
AudioTranscriber.run(path) # pure computation, no shared mutable state
|
|
813
|
-
end
|
|
814
|
-
end
|
|
815
|
-
```
|
|
816
|
-
|
|
817
|
-
**Parallel robot networks** — pass `parallel_mode: :ractor` when creating a network to dispatch independent robots across hardware threads simultaneously:
|
|
818
|
-
|
|
819
|
-
```ruby
|
|
820
|
-
network = RobotLab.create_network(name: "analysis", parallel_mode: :ractor) do
|
|
821
|
-
task :fetch, fetcher_robot, depends_on: :none
|
|
822
|
-
task :sentiment, sentiment_robot, depends_on: [:fetch]
|
|
823
|
-
task :entities, entity_robot, depends_on: [:fetch] # runs in parallel with sentiment
|
|
824
|
-
task :summarize, summary_robot, depends_on: [:sentiment, :entities]
|
|
825
|
-
end
|
|
826
|
-
|
|
827
|
-
results = network.run(message: "Analyze customer feedback")
|
|
828
|
-
# => { "fetch" => "...", "sentiment" => "positive", "entities" => "...", "summarize" => "..." }
|
|
829
|
-
```
|
|
830
|
-
|
|
831
|
-
See the [Ractor Parallelism guide](https://madbomber.github.io/robot_lab/guides/ractor-parallelism) for constraints, the frozen-data contract, and `RactorMemoryProxy` for shared state.
|
|
832
|
-
|
|
833
|
-
## Rails Integration
|
|
834
|
-
|
|
835
|
-
```bash
|
|
836
|
-
rails generate robot_lab:install
|
|
837
|
-
rails db:migrate
|
|
838
|
-
```
|
|
839
|
-
|
|
840
|
-
This creates:
|
|
841
|
-
- `config/initializers/robot_lab.rb` - Configuration
|
|
842
|
-
- `app/robots/` - Directory for your robots
|
|
843
|
-
- Database tables for conversation history
|
|
844
|
-
|
|
845
|
-
### Background Jobs
|
|
846
|
-
|
|
847
|
-
RobotLab ships with `RobotLab::Job`, an `ActiveJob::Base` subclass that handles the full robot-run lifecycle: robot class resolution, Turbo Stream wiring, thread-record persistence, and completion/error broadcasting.
|
|
848
|
-
|
|
849
|
-
**Generic job** (robot class supplied at enqueue time):
|
|
850
|
-
|
|
851
|
-
```bash
|
|
852
|
-
rails generate robot_lab:install # creates app/jobs/robot_run_job.rb
|
|
853
|
-
```
|
|
854
|
-
|
|
855
|
-
```ruby
|
|
856
|
-
# app/jobs/robot_run_job.rb (generated)
|
|
857
|
-
class RobotRunJob < RobotLab::Job
|
|
858
|
-
queue_as :default
|
|
859
|
-
end
|
|
860
|
-
|
|
861
|
-
# Enqueue from a controller:
|
|
862
|
-
RobotRunJob.perform_later(
|
|
863
|
-
robot_class: "SupportRobot",
|
|
864
|
-
message: params[:message],
|
|
865
|
-
thread_id: session_id
|
|
866
|
-
)
|
|
867
|
-
```
|
|
868
|
-
|
|
869
|
-
**Dedicated job** (robot class bound at the class level via DSL):
|
|
870
|
-
|
|
871
|
-
```bash
|
|
872
|
-
rails generate robot_lab:job Support # binds to SupportRobot, queue: default
|
|
873
|
-
rails generate robot_lab:job Support --queue ai # custom queue
|
|
874
|
-
```
|
|
875
|
-
|
|
876
|
-
```ruby
|
|
877
|
-
# app/jobs/support_job.rb (generated)
|
|
878
|
-
class SupportJob < RobotLab::Job
|
|
879
|
-
queue_as :default
|
|
880
|
-
robot_class SupportRobot
|
|
881
|
-
end
|
|
882
|
-
|
|
883
|
-
# Enqueue (no robot_class: needed):
|
|
884
|
-
SupportJob.perform_later(message: params[:message], thread_id: session_id)
|
|
885
|
-
```
|
|
886
|
-
|
|
887
|
-
When `thread_id` is provided and [turbo-rails](https://github.com/hotwired/turbo-rails) is installed, `RobotLab::Job` automatically:
|
|
879
|
+
## Extension Gems
|
|
888
880
|
|
|
889
|
-
|
|
890
|
-
- Broadcasts a **completion** event to `"robot_lab_thread_#{thread_id}"` when the run finishes
|
|
891
|
-
- Broadcasts an **error** event (HTML-escaped) if the job raises
|
|
881
|
+
RobotLab's optional capabilities are packaged as separate gems:
|
|
892
882
|
|
|
893
|
-
|
|
883
|
+
| Gem | Description |
|
|
884
|
+
|-----|-------------|
|
|
885
|
+
| [robot_lab-ractor](https://github.com/MadBomber/robot_lab-ractor) | CPU parallelism via Ruby Ractors — `ractor_safe` tools and DAG-scheduled parallel networks |
|
|
886
|
+
| [robot_lab-rails](https://github.com/MadBomber/robot_lab-rails) | Rails Engine, generators, `RobotLab::Job` ActiveJob base with Turbo Stream broadcasting |
|
|
887
|
+
| [robot_lab-durable](https://github.com/MadBomber/robot_lab-durable) | Cross-session knowledge persistence via YAML-backed durable store |
|
|
888
|
+
| [robot_lab-document_store](https://github.com/MadBomber/robot_lab-document_store) | In-memory vector store with fastembed embeddings for semantic search / RAG |
|
|
889
|
+
| [robot_lab-acp](https://github.com/MadBomber/robot_lab-acp) | Expose robots and networks as ACP (Agent Communication Protocol) HTTP+SSE services |
|
|
894
890
|
|
|
895
891
|
## Documentation
|
|
896
892
|
|
data/Rakefile
CHANGED
|
@@ -44,6 +44,76 @@ task :rubocop_fix do
|
|
|
44
44
|
sh "bundle exec rubocop -a"
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
desc "Check code complexity with Flog (warn ≥20, fail ≥50)"
|
|
48
|
+
task :flog_check do
|
|
49
|
+
require 'flog'
|
|
50
|
+
|
|
51
|
+
# Target to work toward; methods above this are warned but don't fail the gate.
|
|
52
|
+
METHOD_WARN = 20.0
|
|
53
|
+
# Current baseline floor — established from first run. Reduce incrementally.
|
|
54
|
+
METHOD_FAIL = 50.0
|
|
55
|
+
|
|
56
|
+
flogger = Flog.new(all: true)
|
|
57
|
+
flogger.flog(*Dir.glob('lib/**/*.rb'))
|
|
58
|
+
|
|
59
|
+
warnings = []
|
|
60
|
+
failures = []
|
|
61
|
+
|
|
62
|
+
flogger.each_by_score do |method, score|
|
|
63
|
+
next if method.end_with?('#none') # skip file-level non-method code
|
|
64
|
+
if score > METHOD_FAIL
|
|
65
|
+
failures << "#{'%.1f' % score}: #{method}"
|
|
66
|
+
elsif score > METHOD_WARN
|
|
67
|
+
warnings << "#{'%.1f' % score}: #{method}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
unless warnings.empty?
|
|
72
|
+
puts "\nFlog warnings (#{METHOD_WARN}–#{METHOD_FAIL}) — target for future refactoring:"
|
|
73
|
+
warnings.each { |v| puts " #{v}" }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if failures.empty?
|
|
77
|
+
puts "\nFlog: no methods exceed the failure threshold (≥#{METHOD_FAIL})"
|
|
78
|
+
else
|
|
79
|
+
puts "\nFlog failures (≥#{METHOD_FAIL}) — must be refactored:"
|
|
80
|
+
failures.each { |v| puts " #{v}" }
|
|
81
|
+
abort "\nFlog quality gate failed: #{failures.size} method(s) exceed #{METHOD_FAIL}"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
desc "Run all quality checks: tests (with coverage), RuboCop, and Flog"
|
|
86
|
+
task :quality do
|
|
87
|
+
results = {}
|
|
88
|
+
|
|
89
|
+
puts "\n#{'=' * 60}"
|
|
90
|
+
puts "Quality Gate: Tests + Coverage"
|
|
91
|
+
puts '=' * 60
|
|
92
|
+
results[:tests] = system("bundle exec rake test") ? :pass : :fail
|
|
93
|
+
|
|
94
|
+
puts "\n#{'=' * 60}"
|
|
95
|
+
puts "Quality Gate: RuboCop"
|
|
96
|
+
puts '=' * 60
|
|
97
|
+
results[:rubocop] = system("bundle exec rubocop") ? :pass : :fail
|
|
98
|
+
|
|
99
|
+
puts "\n#{'=' * 60}"
|
|
100
|
+
puts "Quality Gate: Flog Complexity"
|
|
101
|
+
puts '=' * 60
|
|
102
|
+
results[:flog] = system("bundle exec rake flog_check") ? :pass : :fail
|
|
103
|
+
|
|
104
|
+
puts "\n#{'=' * 60}"
|
|
105
|
+
puts "Quality Summary"
|
|
106
|
+
puts '=' * 60
|
|
107
|
+
results.each do |gate, status|
|
|
108
|
+
icon = status == :pass ? 'PASS' : 'FAIL'
|
|
109
|
+
puts " [#{icon}] #{gate}"
|
|
110
|
+
end
|
|
111
|
+
puts '=' * 60
|
|
112
|
+
|
|
113
|
+
abort "\nQuality gate failed" if results.values.any?(:fail)
|
|
114
|
+
puts "\nAll quality gates passed."
|
|
115
|
+
end
|
|
116
|
+
|
|
47
117
|
namespace :examples do
|
|
48
118
|
# Map of subdirectory-based demos to their entry point scripts
|
|
49
119
|
SUBDIR_ENTRY_POINTS = {
|
|
@@ -58,14 +128,37 @@ namespace :examples do
|
|
|
58
128
|
"18_rails" => { setup: "bin/setup", run: "bin/dev" }
|
|
59
129
|
}.freeze
|
|
60
130
|
|
|
131
|
+
# Examples that require external services or user setup not guaranteed to be present
|
|
132
|
+
EXTERNAL_SERVICE_EXAMPLES = {
|
|
133
|
+
"33_stock_generator.rb" => "Redis server on localhost:6379",
|
|
134
|
+
"33_stock_predictor.rb" => "Redis server on localhost:6379 + running 33_stock_generator",
|
|
135
|
+
"34_agentskills.rb" => "AgentSkills skill file at ~/.prompts/skills/code_reviewer/SKILL.md"
|
|
136
|
+
}.freeze
|
|
137
|
+
|
|
61
138
|
desc "Run all examples (excludes standalone apps like 18_rails)"
|
|
62
139
|
task :all do
|
|
140
|
+
failed = []
|
|
141
|
+
|
|
63
142
|
# Single-file examples
|
|
64
|
-
Dir.glob("examples/*.rb").
|
|
143
|
+
Dir.glob("examples/*.rb").each do |example|
|
|
144
|
+
base = File.basename(example)
|
|
145
|
+
|
|
146
|
+
if EXTERNAL_SERVICE_EXAMPLES.key?(base)
|
|
147
|
+
puts "\n#{'=' * 60}"
|
|
148
|
+
puts "Skipped: #{example} (requires #{EXTERNAL_SERVICE_EXAMPLES[base]})"
|
|
149
|
+
puts '=' * 60
|
|
150
|
+
next
|
|
151
|
+
end
|
|
152
|
+
|
|
65
153
|
puts "\n#{'=' * 60}"
|
|
66
154
|
puts "Running: #{example}"
|
|
67
155
|
puts '=' * 60
|
|
68
|
-
|
|
156
|
+
begin
|
|
157
|
+
ruby example
|
|
158
|
+
rescue RuntimeError => e
|
|
159
|
+
puts "FAILED: #{example} — #{e.message}"
|
|
160
|
+
failed << example
|
|
161
|
+
end
|
|
69
162
|
end
|
|
70
163
|
|
|
71
164
|
# Subdirectory-based demos
|
|
@@ -76,7 +169,12 @@ namespace :examples do
|
|
|
76
169
|
puts "\n#{'=' * 60}"
|
|
77
170
|
puts "Running: #{path}"
|
|
78
171
|
puts '=' * 60
|
|
79
|
-
|
|
172
|
+
begin
|
|
173
|
+
ruby path
|
|
174
|
+
rescue RuntimeError => e
|
|
175
|
+
puts "FAILED: #{path} — #{e.message}"
|
|
176
|
+
failed << path
|
|
177
|
+
end
|
|
80
178
|
end
|
|
81
179
|
|
|
82
180
|
# Remind about standalone apps
|
|
@@ -87,6 +185,14 @@ namespace :examples do
|
|
|
87
185
|
puts " Run: cd examples/#{dir} && #{commands[:run]}"
|
|
88
186
|
puts '=' * 60
|
|
89
187
|
end
|
|
188
|
+
|
|
189
|
+
if failed.any?
|
|
190
|
+
puts "\n#{'=' * 60}"
|
|
191
|
+
puts "#{failed.size} example(s) failed:"
|
|
192
|
+
failed.each { |f| puts " #{f}" }
|
|
193
|
+
puts '=' * 60
|
|
194
|
+
exit 1
|
|
195
|
+
end
|
|
90
196
|
end
|
|
91
197
|
|
|
92
198
|
desc "Run a specific example by number (e.g., rake examples:run[1])"
|