rubyn-code 0.5.0 → 0.7.0
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/README.md +182 -11
- data/db/migrations/014_multi_agent_upgrade.rb +79 -0
- data/lib/rubyn_code/agent/conversation.rb +89 -3
- data/lib/rubyn_code/agent/llm_caller.rb +2 -2
- data/lib/rubyn_code/agent/loop.rb +49 -9
- data/lib/rubyn_code/agent/system_prompt_builder.rb +37 -2
- data/lib/rubyn_code/agent/tool_processor.rb +3 -1
- data/lib/rubyn_code/auth/oauth.rb +1 -1
- data/lib/rubyn_code/auth/token_store.rb +49 -4
- data/lib/rubyn_code/checkpoint/hook.rb +26 -0
- data/lib/rubyn_code/checkpoint/manager.rb +109 -0
- data/lib/rubyn_code/chisel/debt.rb +65 -0
- data/lib/rubyn_code/chisel/inspection.rb +93 -0
- data/lib/rubyn_code/chisel.rb +127 -0
- data/lib/rubyn_code/cli/app.rb +2 -2
- data/lib/rubyn_code/cli/commands/agents.rb +31 -0
- data/lib/rubyn_code/cli/commands/chisel.rb +52 -0
- data/lib/rubyn_code/cli/commands/chisel_audit.rb +19 -0
- data/lib/rubyn_code/cli/commands/chisel_debt.rb +28 -0
- data/lib/rubyn_code/cli/commands/chisel_gain.rb +30 -0
- data/lib/rubyn_code/cli/commands/chisel_review.rb +19 -0
- data/lib/rubyn_code/cli/commands/command_template.rb +50 -0
- data/lib/rubyn_code/cli/commands/context.rb +3 -1
- data/lib/rubyn_code/cli/commands/custom_command.rb +42 -0
- data/lib/rubyn_code/cli/commands/custom_loader.rb +69 -0
- data/lib/rubyn_code/cli/commands/goal.rb +87 -0
- data/lib/rubyn_code/cli/commands/learning.rb +62 -0
- data/lib/rubyn_code/cli/commands/loop.rb +58 -0
- data/lib/rubyn_code/cli/commands/mcp.rb +18 -5
- data/lib/rubyn_code/cli/commands/megaplan.rb +50 -0
- data/lib/rubyn_code/cli/commands/registry.rb +14 -9
- data/lib/rubyn_code/cli/commands/rewind.rb +65 -0
- data/lib/rubyn_code/cli/first_run.rb +1 -1
- data/lib/rubyn_code/cli/loop_runner.rb +98 -0
- data/lib/rubyn_code/cli/mention_expander.rb +92 -0
- data/lib/rubyn_code/cli/renderer.rb +3 -2
- data/lib/rubyn_code/cli/repl.rb +37 -14
- data/lib/rubyn_code/cli/repl_commands.rb +77 -2
- data/lib/rubyn_code/cli/repl_setup.rb +9 -1
- data/lib/rubyn_code/cli/setup.rb +13 -0
- data/lib/rubyn_code/cli/stream_formatter.rb +3 -2
- data/lib/rubyn_code/cli/version_check.rb +10 -3
- data/lib/rubyn_code/config/defaults.rb +13 -1
- data/lib/rubyn_code/config/schema.json +4 -0
- data/lib/rubyn_code/config/settings.rb +17 -2
- data/lib/rubyn_code/context/manager.rb +29 -12
- data/lib/rubyn_code/debug.rb +11 -5
- data/lib/rubyn_code/goal/evaluator.rb +95 -0
- data/lib/rubyn_code/hooks/event_map.rb +56 -0
- data/lib/rubyn_code/hooks/external_dispatcher.rb +199 -0
- data/lib/rubyn_code/hooks/goal_hook.rb +88 -0
- data/lib/rubyn_code/hooks/response.rb +83 -0
- data/lib/rubyn_code/hooks/runner.rb +61 -3
- data/lib/rubyn_code/hooks/settings_json_loader.rb +109 -0
- data/lib/rubyn_code/hooks/subprocess_executor.rb +116 -0
- data/lib/rubyn_code/ide/handlers/plan_interview_answer_handler.rb +65 -0
- data/lib/rubyn_code/ide/handlers/plan_interview_cancel_handler.rb +22 -0
- data/lib/rubyn_code/ide/handlers/plan_interview_start_handler.rb +53 -0
- data/lib/rubyn_code/ide/handlers/plan_propose_handler.rb +41 -0
- data/lib/rubyn_code/ide/handlers/prompt_handler.rb +9 -1
- data/lib/rubyn_code/ide/handlers/recover_ci_handler.rb +143 -0
- data/lib/rubyn_code/ide/handlers/session_resume_handler.rb +1 -1
- data/lib/rubyn_code/ide/handlers.rb +17 -2
- data/lib/rubyn_code/ide/protocol.rb +15 -0
- data/lib/rubyn_code/ide/server.rb +39 -1
- data/lib/rubyn_code/index/codebase_index.rb +39 -1
- data/lib/rubyn_code/learning/porter.rb +129 -0
- data/lib/rubyn_code/llm/adapters/anthropic.rb +65 -16
- data/lib/rubyn_code/llm/adapters/openai.rb +1 -1
- data/lib/rubyn_code/llm/adapters/prompt_caching.rb +5 -1
- data/lib/rubyn_code/llm/adapters/token_caching.rb +54 -0
- data/lib/rubyn_code/llm/model_router.rb +2 -2
- data/lib/rubyn_code/mcp/client.rb +59 -0
- data/lib/rubyn_code/mcp/server_extras_bridge.rb +110 -0
- data/lib/rubyn_code/mcp/sse_transport.rb +2 -1
- data/lib/rubyn_code/mcp/tool_bridge.rb +16 -14
- data/lib/rubyn_code/megaplan/ci_recovery.rb +104 -0
- data/lib/rubyn_code/megaplan/interview_session.rb +250 -0
- data/lib/rubyn_code/megaplan/plan_proposer.rb +153 -0
- data/lib/rubyn_code/memory/search.rb +9 -5
- data/lib/rubyn_code/memory/session_persistence.rb +159 -21
- data/lib/rubyn_code/observability/cost_calculator.rb +3 -1
- data/lib/rubyn_code/output/diff_renderer.rb +62 -7
- data/lib/rubyn_code/skills/auto_suggest.rb +70 -2
- data/lib/rubyn_code/skills/registry_client.rb +4 -3
- data/lib/rubyn_code/sub_agents/agent_type.rb +17 -0
- data/lib/rubyn_code/sub_agents/catalog.rb +124 -0
- data/lib/rubyn_code/teams/agent_registry.rb +120 -0
- data/lib/rubyn_code/teams/mailbox.rb +99 -10
- data/lib/rubyn_code/teams/manager.rb +83 -5
- data/lib/rubyn_code/teams/teammate.rb +5 -1
- data/lib/rubyn_code/tools/ask_user.rb +15 -1
- data/lib/rubyn_code/tools/executor.rb +5 -3
- data/lib/rubyn_code/tools/spawn_agent.rb +47 -62
- data/lib/rubyn_code/tools/spawn_teammate.rb +7 -2
- data/lib/rubyn_code/tools/web_fetch.rb +1 -1
- data/lib/rubyn_code/tools/web_search.rb +4 -1
- data/lib/rubyn_code/version.rb +1 -1
- data/lib/rubyn_code.rb +53 -2
- data/skills/megaplan/megaplan.md +156 -0
- data/skills/rubyn_self_test.md +322 -14
- data/skills/self_test/chisel_smoke.rb +84 -0
- data/skills/self_test/fixtures/chisel_sample.rb +64 -0
- metadata +49 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5cbf6f4737790408893a904d64412c2dd695e81a2cd2e5960b4da3196e48f4f
|
|
4
|
+
data.tar.gz: f0e9b80f10c77d6eb6c357db7518851344670653546d5eca2784d017d490608c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bc4553f04fbac43903e1515cec43cf595245d36223bca70342ddafb2a2920f38f502b356eb353b4b6e5284d128688e07c015e0a3ebb4aa06f7029fac0ba42a37
|
|
7
|
+
data.tar.gz: b9f8ab09a40e073bff1657ef9d829b7551a5ca56ce9f77b65bd59b835d079665d3663faf92ccf44fd84d4ff3674f835c09d102b4f449c175c77bcafb466dd934
|
data/README.md
CHANGED
|
@@ -35,9 +35,11 @@ Refactor controllers, generate idiomatic RSpec, catch N+1 queries, review code f
|
|
|
35
35
|
- [MCP — External Tool Servers](#mcp--external-tool-servers)
|
|
36
36
|
- [Codebase Indexing](#codebase-indexing)
|
|
37
37
|
- [112 Best Practice Skills](#112-best-practice-skills)
|
|
38
|
+
- [Skill Packs — Registry-Backed Extensions](#skill-packs--registry-backed-extensions)
|
|
38
39
|
- [Context Architecture](#context-architecture)
|
|
39
40
|
- [RUBYN.md — Project Instructions](#rubynmd--project-instructions)
|
|
40
41
|
- [PR Review](#pr-review)
|
|
42
|
+
- [Megaplan — Phased Planning](#megaplan--phased-planning)
|
|
41
43
|
- [Sub-Agents & Teams](#sub-agents--teams)
|
|
42
44
|
- [GOLEM — Autonomous Daemon](#golem--autonomous-daemon)
|
|
43
45
|
- [Continuous Learning](#continuous-learning)
|
|
@@ -61,14 +63,15 @@ Refactor controllers, generate idiomatic RSpec, catch N+1 queries, review code f
|
|
|
61
63
|
|
|
62
64
|
- **Rails-native** — understands service object extraction, RSpec conventions, ActiveRecord patterns, and Hotwire
|
|
63
65
|
- **Context-aware** — automatically incorporates schema, routes, specs, factories, and models
|
|
64
|
-
- **Best practices built in** — ships with 112 curated Ruby and Rails guidelines that load on demand
|
|
66
|
+
- **Best practices built in** — ships with 112 curated Ruby and Rails guidelines that load on demand, plus registry-backed [skill packs](#skill-packs--registry-backed-extensions) that autoload as you need them
|
|
67
|
+
- **Plans big work in phases** — [`/megaplan`](#megaplan--phased-planning) runs a read-only interview, then breaks rewrites and migrations into vertical-slice phases that ship one at a time
|
|
65
68
|
- **Agentic** — doesn't just answer questions. Reads files, writes code, runs specs, commits, reviews PRs, spawns sub-agents, and remembers what it learns
|
|
66
69
|
- **IDE-ready** — works in the terminal and inside VS Code with full bidirectional communication
|
|
67
70
|
- **Extensible** — connect external tool servers via MCP, add custom skills, or wire up your own providers
|
|
68
71
|
|
|
69
72
|
## Install
|
|
70
73
|
|
|
71
|
-
Requires **Ruby 4.0+**. Install with your latest Ruby, then pin it so it works in every project:
|
|
74
|
+
Requires **Ruby 4.0.2+**. Install with your latest Ruby, then pin it so it works in every project:
|
|
72
75
|
|
|
73
76
|
```bash
|
|
74
77
|
# Install the gem
|
|
@@ -83,11 +86,11 @@ That's it. `rubyn-code` now works in any project regardless of `.ruby-version`.
|
|
|
83
86
|
<details>
|
|
84
87
|
<summary>Using rbenv?</summary>
|
|
85
88
|
|
|
86
|
-
If you manage multiple Rubies with rbenv, install on your latest:
|
|
89
|
+
If you manage multiple Rubies with rbenv, install on your latest (run `rbenv versions` to list what you have):
|
|
87
90
|
|
|
88
91
|
```bash
|
|
89
|
-
RBENV_VERSION
|
|
90
|
-
RBENV_VERSION
|
|
92
|
+
RBENV_VERSION=<your-ruby-version> gem install rubyn-code
|
|
93
|
+
RBENV_VERSION=<your-ruby-version> rubyn-code --setup
|
|
91
94
|
```
|
|
92
95
|
|
|
93
96
|
The `--setup` command creates a launcher in `~/.local/bin` that calls the gem wrapper directly, skipping rbenv's shim. As long as `~/.local/bin` is in your PATH before `~/.rbenv/shims`, you're good.
|
|
@@ -98,7 +101,7 @@ The `--setup` command creates a launcher in `~/.local/bin` that calls the gem wr
|
|
|
98
101
|
<summary>Using rvm?</summary>
|
|
99
102
|
|
|
100
103
|
```bash
|
|
101
|
-
rvm use
|
|
104
|
+
rvm use <your-ruby-version>
|
|
102
105
|
gem install rubyn-code
|
|
103
106
|
rubyn-code --setup
|
|
104
107
|
```
|
|
@@ -135,6 +138,8 @@ rubyn-code -p "Refactor app/controllers/orders_controller.rb into service object
|
|
|
135
138
|
rubyn-code --ide
|
|
136
139
|
```
|
|
137
140
|
|
|
141
|
+
**Tip:** Reference files inline with `@` — `rubyn > explain @lib/foo.rb and @config/routes.rb`. Rubyn inlines their contents so you don't have to wait for it to read them.
|
|
142
|
+
|
|
138
143
|
## What Can Rubyn Do?
|
|
139
144
|
|
|
140
145
|
### Refactor code
|
|
@@ -203,7 +208,7 @@ Rubyn Code includes a VS Code extension that provides a full IDE experience with
|
|
|
203
208
|
| `default` | Per-tool approval required |
|
|
204
209
|
| `bypass` | YOLO — skip all approval prompts |
|
|
205
210
|
|
|
206
|
-
The extension communicates over
|
|
211
|
+
The extension communicates over 19 RPC methods: `initialize`, `prompt`, `cancel`, `review`, `approveToolUse`, `acceptEdit`, `session/*`, `config/*`, `models/list`, `plan/propose`, `plan/interview/*` (chat-resident [megaplan](#megaplan--phased-planning)), `recover_ci`, and `shutdown`.
|
|
207
212
|
|
|
208
213
|
## 29 Built-in Tools
|
|
209
214
|
|
|
@@ -224,7 +229,7 @@ The extension communicates over 14 RPC methods: `initialize`, `prompt`, `cancel`
|
|
|
224
229
|
|
|
225
230
|
## MCP — External Tool Servers
|
|
226
231
|
|
|
227
|
-
Connect external tool servers via the [Model Context Protocol](https://modelcontextprotocol.io). MCP tools are dynamically discovered and registered as native Rubyn tools, available in the REPL, IDE, and daemon.
|
|
232
|
+
Connect external tool servers via the [Model Context Protocol](https://modelcontextprotocol.io). MCP tools are dynamically discovered and registered as native Rubyn tools, available in the REPL, IDE, and daemon. Servers that expose **resources** and **prompts** are also bridged — Rubyn registers per-server `read_resource` and `get_prompt` tools, and `/mcp` reports the counts.
|
|
228
233
|
|
|
229
234
|
### Configuration
|
|
230
235
|
|
|
@@ -316,6 +321,29 @@ mkdir -p ~/.rubyn-code/skills
|
|
|
316
321
|
echo "# Use double quotes for strings" > ~/.rubyn-code/skills/my_style.md
|
|
317
322
|
```
|
|
318
323
|
|
|
324
|
+
## Skill Packs — Registry-Backed Extensions
|
|
325
|
+
|
|
326
|
+
Beyond the 112 built-in skills, Rubyn can pull additional skill packs from the [rubyn.ai](https://rubyn.ai) registry. Packs are bundles of related skills published by the community or by Rubyn itself.
|
|
327
|
+
|
|
328
|
+
```
|
|
329
|
+
rubyn > /skills # list installed packs and browse the registry
|
|
330
|
+
rubyn > /install-skills sidekiq # install a pack by name
|
|
331
|
+
rubyn > /install-skills graphql viewcomponent # install multiple at once
|
|
332
|
+
rubyn > /remove-skills sidekiq # uninstall
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Installed packs live at `~/.rubyn-code/skill-packs/<pack-name>/` and load alongside the built-in catalog.
|
|
336
|
+
|
|
337
|
+
### Auto-suggest from your Gemfile
|
|
338
|
+
|
|
339
|
+
On session start, Rubyn parses your `Gemfile` and quietly suggests matching packs the first time it sees a gem (e.g. detects `sidekiq` → suggests the sidekiq pack). Suggestions are recorded in `.rubyn-code/suggested.json` so you only see each one once.
|
|
340
|
+
|
|
341
|
+
### Trigger-based autoload
|
|
342
|
+
|
|
343
|
+
If your message mentions a topic that matches an uninstalled pack's name or tags, Rubyn fetches the pack from the registry on the fly, installs it, and feeds the relevant skills into the same turn. Registry failures are silent — the conversation continues as if the autoload weren't there.
|
|
344
|
+
|
|
345
|
+
Point at a custom registry with `RUBYN_REGISTRY_URL=https://your-registry.example.com`.
|
|
346
|
+
|
|
319
347
|
## Context Architecture
|
|
320
348
|
|
|
321
349
|
Rubyn automatically loads relevant context based on what you're working on:
|
|
@@ -323,7 +351,7 @@ Rubyn automatically loads relevant context based on what you're working on:
|
|
|
323
351
|
- **Controllers** → includes models, routes, request specs, services
|
|
324
352
|
- **Models** → includes schema, associations, specs, factories
|
|
325
353
|
- **Service objects** → includes referenced models and their specs
|
|
326
|
-
- **Any file** → checks for `RUBYN.md`, `CLAUDE.md`, or `AGENT.md` instructions
|
|
354
|
+
- **Any file** → checks for `RUBYN.md`, `CLAUDE.md`, `AGENTS.md`, or `AGENT.md` instructions
|
|
327
355
|
|
|
328
356
|
The [codebase index](#codebase-indexing) enhances this with structural awareness — Rubyn knows which files depend on each other before it reads them.
|
|
329
357
|
|
|
@@ -341,7 +369,7 @@ Drop a `RUBYN.md` in your project root and Rubyn follows your conventions:
|
|
|
341
369
|
- Run rubocop before committing
|
|
342
370
|
```
|
|
343
371
|
|
|
344
|
-
Also reads `CLAUDE.md` and `AGENT.md` — no migration needed from other tools.
|
|
372
|
+
Also reads `CLAUDE.md`, `AGENTS.md`, and `AGENT.md` — no migration needed from other tools.
|
|
345
373
|
|
|
346
374
|
| Location | Scope |
|
|
347
375
|
|----------|-------|
|
|
@@ -364,6 +392,93 @@ Focus areas: `all`, `security`, `performance`, `style`, `testing`
|
|
|
364
392
|
|
|
365
393
|
Severity ratings: **[critical]** **[warning]** **[suggestion]** **[nitpick]**
|
|
366
394
|
|
|
395
|
+
## Chisel — Write the Minimum That Works
|
|
396
|
+
|
|
397
|
+
Chisel is an opt-in mode that makes Rubyn think like the laziest senior dev in
|
|
398
|
+
the room: the best code is the code you never wrote. It's **off by default** and
|
|
399
|
+
only changes the agent's behavior once you turn it on.
|
|
400
|
+
|
|
401
|
+
```
|
|
402
|
+
rubyn > /chisel # show current intensity
|
|
403
|
+
rubyn > /chisel full # turn it on
|
|
404
|
+
rubyn > /chisel off # back to normal
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Intensities: `off` (default) · `lite` · `full` · `ultra`. When on, Rubyn walks a
|
|
408
|
+
decision ladder before writing code — does this need to exist? does stdlib or an
|
|
409
|
+
installed gem already do it? is it one line? — and only then writes the smallest
|
|
410
|
+
change that solves the task. The safety floor (validation, error/data-loss
|
|
411
|
+
handling, security, accessibility) is never on the chopping block.
|
|
412
|
+
|
|
413
|
+
Set it permanently with `chisel_mode: full` in `~/.rubyn-code/config.yml`, or
|
|
414
|
+
per-shell with `RUBYN_CHISEL_MODE=full`.
|
|
415
|
+
|
|
416
|
+
**On-demand audits** (work whether or not the always-on mode is enabled):
|
|
417
|
+
|
|
418
|
+
```
|
|
419
|
+
rubyn > /chisel-review # over-engineering in your diff vs main
|
|
420
|
+
rubyn > /chisel-review develop # ...vs a different base
|
|
421
|
+
rubyn > /chisel-audit # sweep the whole repo
|
|
422
|
+
rubyn > /chisel-audit app/services # ...scoped to a path
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Both return a ranked deletion/simplification list — each item with a location, the
|
|
426
|
+
ladder rung it skipped, and the concrete simpler form — and stay read-only (they
|
|
427
|
+
report, they don't edit).
|
|
428
|
+
|
|
429
|
+
**Debt ledger & status.** Leave a `# chisel: …` comment when you consciously defer a
|
|
430
|
+
simplification, then collect them later:
|
|
431
|
+
|
|
432
|
+
```
|
|
433
|
+
rubyn > /chisel-debt # list every `# chisel:` deferral, with file:line and note
|
|
434
|
+
rubyn > /chisel-gain # current mode, outstanding debt count, and reference impact
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
**Verifying Chisel.** A standalone smoke test runs the whole layer against a
|
|
438
|
+
committed, deliberately over-engineered fixture and gives the same result every
|
|
439
|
+
time — no LLM, no network:
|
|
440
|
+
|
|
441
|
+
```
|
|
442
|
+
$ bundle exec ruby skills/self_test/chisel_smoke.rb
|
|
443
|
+
CHISEL debt: PASS # scans the fixture → its exact planted `# chisel:` markers
|
|
444
|
+
CHISEL engine: PASS # off injects nothing; lite/full/ultra keep the safety floor
|
|
445
|
+
CHISEL inspection: PASS # review/audit prompts assemble; bad scope raises
|
|
446
|
+
CHISEL commands: PASS # all five slash commands register
|
|
447
|
+
CHISEL: PASS
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
The fixture lives at `skills/self_test/fixtures/chisel_sample.rb`, the same check is
|
|
451
|
+
Section 18 of the `/skill self-test` scorecard, and `spec/rubyn_code/chisel/self_test_fixture_spec.rb`
|
|
452
|
+
guards the fixture's exact scan output so it can't silently drift.
|
|
453
|
+
|
|
454
|
+
## Megaplan — Phased Planning
|
|
455
|
+
|
|
456
|
+
For work too big for a single PR — rewrites, migrations, multi-feature initiatives — Rubyn ships a planning workflow that breaks the feature into vertical-slice phases before any code gets written.
|
|
457
|
+
|
|
458
|
+
```
|
|
459
|
+
rubyn > /megaplan extract billing into its own service
|
|
460
|
+
Megaplan mode — interviewer with read-only tools
|
|
461
|
+
|
|
462
|
+
Decisions so far: (none yet)
|
|
463
|
+
|
|
464
|
+
Q1. What triggers the extraction now — a scaling issue, a team boundary,
|
|
465
|
+
or a compliance constraint?
|
|
466
|
+
1. Scaling (recommended — billing is the hottest table)
|
|
467
|
+
2. Team boundary
|
|
468
|
+
3. Compliance
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
What happens when you run `/megaplan`:
|
|
472
|
+
|
|
473
|
+
- Loads the **megaplan** skill into context.
|
|
474
|
+
- Flips the agent into **plan mode** — only read-only tools (file reads, search, git status) are available. No edits, no shell mutations.
|
|
475
|
+
- Conducts a one-question-at-a-time interview to lock down scope, constraints, and risk before proposing phases.
|
|
476
|
+
- Outputs a numbered phase breakdown, each phase shippable on its own with the trunk staying green.
|
|
477
|
+
|
|
478
|
+
Trigger phrases like "megaplan", "mega plan", "plan phases", or "phase this out" in normal conversation will surface the skill via [trigger-based autoload](#skill-packs--registry-backed-extensions) too.
|
|
479
|
+
|
|
480
|
+
In the VS Code extension the same workflow runs as a chat-resident interview with structured question cards instead of free-text Q&A. Same skill driving both surfaces.
|
|
481
|
+
|
|
367
482
|
## Sub-Agents & Teams
|
|
368
483
|
|
|
369
484
|
### Sub-Agents (disposable)
|
|
@@ -422,6 +537,19 @@ Rubyn gets smarter with every session:
|
|
|
422
537
|
3. **On next startup** — injects top instincts into the system prompt
|
|
423
538
|
4. **Over time** — reinforced instincts strengthen, unused ones decay and get pruned
|
|
424
539
|
|
|
540
|
+
### Take your learnings with you
|
|
541
|
+
|
|
542
|
+
Instincts live in `~/.rubyn-code`. Move them to another machine with `/learning`:
|
|
543
|
+
|
|
544
|
+
```
|
|
545
|
+
rubyn > /learning # how many instincts you've accumulated
|
|
546
|
+
rubyn > /learning export learnings.json # write them to a portable file
|
|
547
|
+
# ...on the new machine...
|
|
548
|
+
rubyn > /learning import learnings.json --here # load them (--here = apply to this project)
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Import regenerates ids and skips duplicates, so it's safe to run repeatedly.
|
|
552
|
+
|
|
425
553
|
## Streaming Output
|
|
426
554
|
|
|
427
555
|
Real-time streaming with live syntax highlighting via Rouge/Monokai. Code blocks are buffered and highlighted when complete. No waiting for full responses.
|
|
@@ -483,6 +611,8 @@ rubyn-code daemon [OPTIONS] # Run GOLEM autonomous daemon
|
|
|
483
611
|
| `/new` | Save session and start a fresh conversation |
|
|
484
612
|
| `/review [base]` | PR review against best practices |
|
|
485
613
|
| `/spawn name role` | Spawn a persistent teammate |
|
|
614
|
+
| `/goal <condition>` | Set a goal Rubyn works toward until met (`/goal clear` to cancel) |
|
|
615
|
+
| `/loop [xN] [interval] <prompt-or-/cmd>` | Repeat a prompt or command on an interval (Ctrl-C to stop) |
|
|
486
616
|
| `/compact` | Compress conversation context |
|
|
487
617
|
| `/cost` | Show token usage and costs |
|
|
488
618
|
| `/tasks` | List all tasks |
|
|
@@ -493,6 +623,47 @@ rubyn-code daemon [OPTIONS] # Run GOLEM autonomous daemon
|
|
|
493
623
|
| `/model` | Show/switch model and provider |
|
|
494
624
|
| `/doctor` | Run environment health checks |
|
|
495
625
|
| `/mcp` | MCP server documentation and status |
|
|
626
|
+
| `/agents` | List sub-agent types (built-in + custom) |
|
|
627
|
+
| `/rewind [id] [code\|chat]` | Rewind code and/or conversation to a checkpoint |
|
|
628
|
+
|
|
629
|
+
### Custom Sub-Agents
|
|
630
|
+
|
|
631
|
+
Beyond the built-in `explore` (read-only) and `worker` (read/write) sub-agents, define your own in `.rubyn-code/agents/<name>.md` (project) or `~/.rubyn-code/agents/<name>.md` (global). `spawn_agent` can then target them by name, and `/agents` lists them.
|
|
632
|
+
|
|
633
|
+
```markdown
|
|
634
|
+
---
|
|
635
|
+
description: Reviews a diff for bugs
|
|
636
|
+
tools: read_file, grep, glob, bash # optional — omit for the access default
|
|
637
|
+
access: read # read | write (default: write)
|
|
638
|
+
---
|
|
639
|
+
You are a meticulous code reviewer. Find correctness bugs only.
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### Custom Slash Commands
|
|
643
|
+
|
|
644
|
+
Drop a markdown file in `.rubyn-code/commands/` (project) or `~/.rubyn-code/commands/` (global) and it becomes a slash command — `deploy.md` → `/deploy`. Project commands override global ones; built-ins always win.
|
|
645
|
+
|
|
646
|
+
```markdown
|
|
647
|
+
---
|
|
648
|
+
description: Open a PR for the current branch
|
|
649
|
+
---
|
|
650
|
+
Open a pull request for the current branch.
|
|
651
|
+
Title: $ARGUMENTS
|
|
652
|
+
Current diff:
|
|
653
|
+
!`git diff main --stat`
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
Templating in the body:
|
|
657
|
+
|
|
658
|
+
| Token | Expands to |
|
|
659
|
+
|-------|-----------|
|
|
660
|
+
| `$ARGUMENTS` | everything typed after the command |
|
|
661
|
+
| `$1` … `$9` | individual positional arguments |
|
|
662
|
+
| `` !`shell cmd` `` | the command's output, inlined |
|
|
663
|
+
|
|
664
|
+
### Checkpoints & Rewind
|
|
665
|
+
|
|
666
|
+
Rubyn snapshots a checkpoint at the start of every turn — capturing the conversation and the original contents of any files it changes that turn. `/rewind` lists them; `/rewind <id>` rolls back both code and conversation (or just one with `code`/`chat`). Note: rewind restores files edited via Rubyn's `write_file`/`edit_file`; it does not touch your git history.
|
|
496
667
|
|
|
497
668
|
## Authentication
|
|
498
669
|
|
|
@@ -707,7 +878,7 @@ Checks Ruby version, bundler, database state, authentication, skills, project ty
|
|
|
707
878
|
|
|
708
879
|
## Development
|
|
709
880
|
|
|
710
|
-
Requires Ruby 4.0+.
|
|
881
|
+
Requires Ruby 4.0.2+.
|
|
711
882
|
|
|
712
883
|
```bash
|
|
713
884
|
git clone https://github.com/MatthewSuttles/rubyn-code.git
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Multi-agent upgrade: adds parent-child tracking to teammates and
|
|
4
|
+
# structured messaging to mailbox_messages.
|
|
5
|
+
#
|
|
6
|
+
# Teammates: adds parent_agent_id column, fixes status CHECK
|
|
7
|
+
# (DB had 'busy' but Ruby code uses 'active').
|
|
8
|
+
# Mailbox: adds correlation_id and data columns for structured messaging.
|
|
9
|
+
#
|
|
10
|
+
# Uses table-rebuild pattern because SQLite cannot ALTER CHECK constraints
|
|
11
|
+
# or ADD COLUMN with constraints reliably.
|
|
12
|
+
module Migration014MultiAgentUpgrade
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def up(db)
|
|
16
|
+
upgrade_teammates(db)
|
|
17
|
+
upgrade_mailbox(db)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def upgrade_teammates(db)
|
|
21
|
+
db.execute(<<~SQL)
|
|
22
|
+
CREATE TABLE teammates_new (
|
|
23
|
+
id TEXT PRIMARY KEY,
|
|
24
|
+
name TEXT NOT NULL UNIQUE,
|
|
25
|
+
role TEXT NOT NULL,
|
|
26
|
+
persona TEXT,
|
|
27
|
+
model TEXT NOT NULL DEFAULT 'claude-sonnet-4-20250514',
|
|
28
|
+
status TEXT NOT NULL DEFAULT 'idle' CHECK(status IN ('idle','active','offline')),
|
|
29
|
+
parent_agent_id TEXT,
|
|
30
|
+
metadata TEXT DEFAULT '{}',
|
|
31
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
|
32
|
+
)
|
|
33
|
+
SQL
|
|
34
|
+
|
|
35
|
+
db.execute(<<~SQL)
|
|
36
|
+
INSERT INTO teammates_new (id, name, role, persona, model, status, metadata, created_at)
|
|
37
|
+
SELECT id, name, role, persona, model,
|
|
38
|
+
CASE WHEN status = 'busy' THEN 'active' ELSE status END,
|
|
39
|
+
metadata, created_at
|
|
40
|
+
FROM teammates
|
|
41
|
+
SQL
|
|
42
|
+
|
|
43
|
+
db.execute('DROP TABLE teammates')
|
|
44
|
+
db.execute('ALTER TABLE teammates_new RENAME TO teammates')
|
|
45
|
+
db.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_teammates_name ON teammates(name)')
|
|
46
|
+
db.execute('CREATE INDEX IF NOT EXISTS idx_teammates_status ON teammates(status)')
|
|
47
|
+
db.execute('CREATE INDEX IF NOT EXISTS idx_teammates_parent ON teammates(parent_agent_id)')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def upgrade_mailbox(db)
|
|
51
|
+
db.execute(<<~SQL)
|
|
52
|
+
CREATE TABLE mailbox_messages_new (
|
|
53
|
+
id TEXT PRIMARY KEY,
|
|
54
|
+
sender TEXT NOT NULL,
|
|
55
|
+
recipient TEXT NOT NULL,
|
|
56
|
+
message_type TEXT NOT NULL DEFAULT 'message'
|
|
57
|
+
CHECK(message_type IN ('message','task','result','error','broadcast','shutdown_request','shutdown_response','status_change')),
|
|
58
|
+
payload TEXT NOT NULL,
|
|
59
|
+
correlation_id TEXT,
|
|
60
|
+
data TEXT,
|
|
61
|
+
read INTEGER NOT NULL DEFAULT 0,
|
|
62
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
|
63
|
+
)
|
|
64
|
+
SQL
|
|
65
|
+
|
|
66
|
+
db.execute(<<~SQL)
|
|
67
|
+
INSERT INTO mailbox_messages_new (id, sender, recipient, message_type, payload, read, created_at)
|
|
68
|
+
SELECT id, sender, recipient, message_type, payload, read, created_at
|
|
69
|
+
FROM mailbox_messages
|
|
70
|
+
SQL
|
|
71
|
+
|
|
72
|
+
db.execute('DROP TABLE mailbox_messages')
|
|
73
|
+
db.execute('ALTER TABLE mailbox_messages_new RENAME TO mailbox_messages')
|
|
74
|
+
db.execute('CREATE INDEX IF NOT EXISTS idx_mailbox_recipient_read ON mailbox_messages(recipient, read)')
|
|
75
|
+
db.execute('CREATE INDEX IF NOT EXISTS idx_mailbox_sender ON mailbox_messages(sender)')
|
|
76
|
+
db.execute('CREATE INDEX IF NOT EXISTS idx_mailbox_created ON mailbox_messages(created_at)')
|
|
77
|
+
db.execute('CREATE INDEX IF NOT EXISTS idx_mailbox_correlation ON mailbox_messages(correlation_id)')
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
3
5
|
module RubynCode
|
|
4
6
|
module Agent
|
|
5
|
-
class Conversation
|
|
7
|
+
class Conversation # rubocop:disable Metrics/ClassLength -- message log + incremental token/tool-ID bookkeeping
|
|
6
8
|
attr_reader :messages
|
|
7
9
|
|
|
8
10
|
def initialize
|
|
9
11
|
@messages = []
|
|
12
|
+
reset_derived_state!
|
|
10
13
|
end
|
|
11
14
|
|
|
12
15
|
# Append a user turn to the conversation.
|
|
@@ -16,6 +19,7 @@ module RubynCode
|
|
|
16
19
|
def add_user_message(content)
|
|
17
20
|
message = { role: 'user', content: content }
|
|
18
21
|
@messages << message
|
|
22
|
+
track_added_message(message)
|
|
19
23
|
message
|
|
20
24
|
end
|
|
21
25
|
|
|
@@ -28,6 +32,7 @@ module RubynCode
|
|
|
28
32
|
blocks = normalize_content(content, tool_calls)
|
|
29
33
|
message = { role: 'assistant', content: blocks }
|
|
30
34
|
@messages << message
|
|
35
|
+
track_added_message(message)
|
|
31
36
|
message
|
|
32
37
|
end
|
|
33
38
|
|
|
@@ -52,8 +57,11 @@ module RubynCode
|
|
|
52
57
|
# tool results for the same assistant turn are batched together.
|
|
53
58
|
if @messages.last && @messages.last[:role] == 'user' && tool_result_message?(@messages.last)
|
|
54
59
|
@messages.last[:content] << result_block
|
|
60
|
+
track_appended_block(result_block)
|
|
55
61
|
else
|
|
56
|
-
|
|
62
|
+
message = { role: 'user', content: [result_block] }
|
|
63
|
+
@messages << message
|
|
64
|
+
track_added_message(message)
|
|
57
65
|
end
|
|
58
66
|
|
|
59
67
|
result_block
|
|
@@ -79,6 +87,29 @@ module RubynCode
|
|
|
79
87
|
# @return [void]
|
|
80
88
|
def clear!
|
|
81
89
|
@messages.clear
|
|
90
|
+
reset_derived_state!
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Character length of the JSON-serialized messages array, maintained
|
|
94
|
+
# incrementally on append so per-turn token estimation doesn't have to
|
|
95
|
+
# re-serialize the whole history. Matches JSON.generate(messages).length.
|
|
96
|
+
#
|
|
97
|
+
# @return [Integer]
|
|
98
|
+
def estimated_json_chars
|
|
99
|
+
@json_chars ||= @messages.sum { |msg| JSON.generate(msg).length }
|
|
100
|
+
return 2 if @messages.empty?
|
|
101
|
+
|
|
102
|
+
# "[" + per-message JSON joined by "," + "]"
|
|
103
|
+
@json_chars + @messages.length + 1
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Drops cached serialization/tool-ID bookkeeping. Must be called after
|
|
107
|
+
# messages are mutated in place from outside this class (e.g. by
|
|
108
|
+
# Context::MicroCompact); the caches rebuild lazily on next access.
|
|
109
|
+
#
|
|
110
|
+
# @return [void]
|
|
111
|
+
def refresh_derived_state!
|
|
112
|
+
reset_derived_state!
|
|
82
113
|
end
|
|
83
114
|
|
|
84
115
|
# Return the messages array formatted for the Claude API.
|
|
@@ -115,21 +146,76 @@ module RubynCode
|
|
|
115
146
|
@messages.pop
|
|
116
147
|
removed += 1
|
|
117
148
|
end
|
|
149
|
+
reset_derived_state!
|
|
118
150
|
end
|
|
119
151
|
|
|
120
152
|
# Replace messages with a new array (used after compaction).
|
|
121
153
|
def replace!(new_messages)
|
|
122
154
|
@messages.replace(new_messages)
|
|
155
|
+
reset_derived_state!
|
|
123
156
|
end
|
|
124
157
|
|
|
125
158
|
private
|
|
126
159
|
|
|
160
|
+
# Derived bookkeeping kept in sync with @messages so hot paths stay
|
|
161
|
+
# cheap: per-message JSON char total (token estimation) and
|
|
162
|
+
# tool_use/tool_result ID sets (orphan repair). nil means "rebuild
|
|
163
|
+
# lazily from @messages on next access".
|
|
164
|
+
def reset_derived_state!
|
|
165
|
+
@json_chars = nil
|
|
166
|
+
@tool_use_ids = nil
|
|
167
|
+
@tool_result_ids = nil
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def track_added_message(message)
|
|
171
|
+
register_tool_ids(message)
|
|
172
|
+
return if @json_chars.nil?
|
|
173
|
+
|
|
174
|
+
@json_chars += JSON.generate(message).length
|
|
175
|
+
rescue StandardError
|
|
176
|
+
@json_chars = nil
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# A tool_result block appended to an existing user message grows that
|
|
180
|
+
# message's JSON by the block plus one separator comma.
|
|
181
|
+
def track_appended_block(result_block)
|
|
182
|
+
@tool_result_ids << result_block[:tool_use_id] if @tool_result_ids
|
|
183
|
+
return if @json_chars.nil?
|
|
184
|
+
|
|
185
|
+
@json_chars += JSON.generate(result_block).length + 1
|
|
186
|
+
rescue StandardError
|
|
187
|
+
@json_chars = nil
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def register_tool_ids(message)
|
|
191
|
+
return unless @tool_use_ids && message[:content].is_a?(Array)
|
|
192
|
+
|
|
193
|
+
message[:content].each do |block|
|
|
194
|
+
next unless block.is_a?(Hash)
|
|
195
|
+
|
|
196
|
+
if message[:role] == 'assistant' && block_matches_type?(block, 'tool_use')
|
|
197
|
+
@tool_use_ids << (block[:id] || block['id'])
|
|
198
|
+
elsif message[:role] == 'user' && block_matches_type?(block, 'tool_result')
|
|
199
|
+
@tool_result_ids << (block[:tool_use_id] || block['tool_use_id'])
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def rebuild_tool_id_sets!
|
|
205
|
+
@tool_use_ids = collect_tool_use_ids(@messages)
|
|
206
|
+
@tool_result_ids = collect_tool_result_ids(@messages)
|
|
207
|
+
end
|
|
208
|
+
|
|
127
209
|
# Ensure every tool_use block has a matching tool_result.
|
|
128
210
|
# If a tool_use is orphaned (e.g. from Ctrl-C interruption),
|
|
129
211
|
# inject a synthetic tool_result immediately after the assistant
|
|
130
212
|
# message that contains the orphaned tool_use.
|
|
213
|
+
#
|
|
214
|
+
# The ID sets are tracked incrementally as messages are appended, so
|
|
215
|
+
# the common no-orphan case skips the full-history scans.
|
|
131
216
|
def repair_orphaned_tool_uses(formatted)
|
|
132
|
-
|
|
217
|
+
rebuild_tool_id_sets! if @tool_use_ids.nil?
|
|
218
|
+
orphaned = @tool_use_ids - @tool_result_ids
|
|
133
219
|
return formatted if orphaned.empty?
|
|
134
220
|
|
|
135
221
|
insert_orphan_results(formatted, orphaned)
|
|
@@ -114,7 +114,7 @@ module RubynCode
|
|
|
114
114
|
|
|
115
115
|
def run_compaction
|
|
116
116
|
before = @conversation.length
|
|
117
|
-
est = @context_manager.estimated_tokens(@conversation
|
|
117
|
+
est = @context_manager.estimated_tokens(@conversation)
|
|
118
118
|
RubynCode::Debug.token(
|
|
119
119
|
"context=#{est} tokens (~#{before} messages, " \
|
|
120
120
|
"threshold=#{Config::Defaults::CONTEXT_THRESHOLD_TOKENS})"
|
|
@@ -130,7 +130,7 @@ module RubynCode
|
|
|
130
130
|
after = @conversation.length
|
|
131
131
|
return unless after < before
|
|
132
132
|
|
|
133
|
-
new_est = @context_manager.estimated_tokens(@conversation
|
|
133
|
+
new_est = @context_manager.estimated_tokens(@conversation)
|
|
134
134
|
RubynCode::Debug.loop_tick(
|
|
135
135
|
"Compacted: #{before} -> #{after} messages " \
|
|
136
136
|
"(#{est} -> #{new_est} tokens)"
|