rubyn-code 0.5.1 → 0.8.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 +137 -4
- data/db/migrations/014_multi_agent_upgrade.rb +79 -0
- data/lib/rubyn_code/agent/conversation.rb +100 -4
- data/lib/rubyn_code/agent/dynamic_tool_schema.rb +1 -1
- data/lib/rubyn_code/agent/llm_caller.rb +7 -3
- data/lib/rubyn_code/agent/loop.rb +106 -12
- data/lib/rubyn_code/agent/response_parser.rb +8 -0
- data/lib/rubyn_code/agent/system_prompt_builder.rb +40 -2
- data/lib/rubyn_code/agent/tool_processor.rb +13 -1
- data/lib/rubyn_code/auth/oauth.rb +1 -1
- data/lib/rubyn_code/auth/token_store.rb +49 -4
- data/lib/rubyn_code/autonomous/daemon.rb +1 -1
- 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/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 +30 -1
- data/lib/rubyn_code/cli/commands/custom_command.rb +84 -0
- data/lib/rubyn_code/cli/commands/custom_loader.rb +100 -0
- data/lib/rubyn_code/cli/commands/effort.rb +47 -0
- data/lib/rubyn_code/cli/commands/export.rb +174 -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 +49 -12
- data/lib/rubyn_code/cli/commands/megaplan.rb +1 -1
- data/lib/rubyn_code/cli/commands/registry.rb +14 -9
- data/lib/rubyn_code/cli/commands/resume.rb +97 -26
- data/lib/rubyn_code/cli/commands/rewind.rb +65 -0
- data/lib/rubyn_code/cli/commands/think.rb +47 -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 +111 -0
- data/lib/rubyn_code/cli/renderer.rb +3 -2
- data/lib/rubyn_code/cli/repl.rb +68 -15
- data/lib/rubyn_code/cli/repl_commands.rb +76 -2
- data/lib/rubyn_code/cli/repl_setup.rb +17 -7
- 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 +14 -1
- data/lib/rubyn_code/config/schema.json +9 -0
- data/lib/rubyn_code/config/settings.rb +19 -2
- data/lib/rubyn_code/context/auto_compact.rb +1 -1
- data/lib/rubyn_code/context/manager.rb +29 -12
- data/lib/rubyn_code/context/manual_compact.rb +1 -1
- 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 +13 -13
- data/lib/rubyn_code/ide/handlers/plan_interview_cancel_handler.rb +1 -1
- data/lib/rubyn_code/ide/handlers/plan_interview_start_handler.rb +10 -10
- data/lib/rubyn_code/ide/handlers/plan_propose_handler.rb +1 -1
- data/lib/rubyn_code/ide/handlers/prompt_handler.rb +9 -1
- data/lib/rubyn_code/ide/handlers/recover_ci_handler.rb +27 -16
- data/lib/rubyn_code/ide/handlers/session_resume_handler.rb +1 -1
- data/lib/rubyn_code/index/codebase_index.rb +103 -4
- data/lib/rubyn_code/index/prism_extractor.rb +82 -0
- data/lib/rubyn_code/learning/injector.rb +1 -2
- data/lib/rubyn_code/learning/porter.rb +129 -0
- data/lib/rubyn_code/llm/adapters/anthropic.rb +168 -29
- data/lib/rubyn_code/llm/adapters/anthropic_streaming.rb +13 -0
- data/lib/rubyn_code/llm/adapters/base.rb +2 -1
- data/lib/rubyn_code/llm/adapters/openai.rb +2 -2
- data/lib/rubyn_code/llm/adapters/openai_message_translator.rb +21 -0
- data/lib/rubyn_code/llm/adapters/prompt_caching.rb +5 -1
- data/lib/rubyn_code/llm/client.rb +16 -3
- data/lib/rubyn_code/llm/image_reader.rb +60 -0
- data/lib/rubyn_code/llm/message_builder.rb +21 -1
- data/lib/rubyn_code/llm/model_router.rb +4 -4
- data/lib/rubyn_code/mcp/client.rb +59 -0
- data/lib/rubyn_code/mcp/discovery.rb +93 -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 +3 -3
- data/lib/rubyn_code/megaplan/interview_session.rb +8 -3
- data/lib/rubyn_code/megaplan/plan_proposer.rb +3 -3
- 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 +8 -3
- data/lib/rubyn_code/output/diff_renderer.rb +62 -7
- data/lib/rubyn_code/protocols/RUBYN.md +0 -3
- 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/tasks/models.rb +0 -16
- 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 +3 -14
- data/lib/rubyn_code/tools/RUBYN.md +2 -2
- data/lib/rubyn_code/tools/ask_user.rb +15 -1
- data/lib/rubyn_code/tools/bash.rb +3 -3
- data/lib/rubyn_code/tools/code_graph.rb +134 -0
- data/lib/rubyn_code/tools/executor.rb +9 -4
- 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/todo_store.rb +55 -0
- data/lib/rubyn_code/tools/todo_write.rb +88 -0
- 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 +57 -9
- data/skills/rubyn_self_test.md +462 -14
- data/skills/self_test/chisel_smoke.rb +84 -0
- data/skills/self_test/fixtures/chisel_sample.rb +64 -0
- metadata +45 -6
- data/lib/rubyn_code/context/context_budget.rb +0 -183
- data/lib/rubyn_code/context/schema_filter.rb +0 -64
- data/lib/rubyn_code/learning/shortcut.rb +0 -95
- data/lib/rubyn_code/llm/streaming.rb +0 -10
- data/lib/rubyn_code/protocols/plan_approval.rb +0 -72
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7696c779b900fe4dca806aa73d7fcca79309ebe58a242d16e2d5309b43d44ec4
|
|
4
|
+
data.tar.gz: 2d8d4c8905255ce11e98569f86f4c2d788c620ea30c513fd6771b3c58bb98073
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5d2b5f15b21d7e02b2f6c2252248eb6e933337b3f4c491135f5453c1ddf4ce47c48605aaeb6e26180a38324c8effc7a149e528c794ca9391ec8c43febbc2bdb
|
|
7
|
+
data.tar.gz: 15c9aa06c02dd8c105116b403cba0f36cb5a2b91c8690b66b6320a289011cf6f2986a308fdaacad933fbd221646de84b7f774a47709a7c8409fdc5bddca5b671
|
data/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<a href="https://github.com/MatthewSuttles/rubyn-code/actions/workflows/ci.yml"><img src="https://github.com/MatthewSuttles/rubyn-code/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
|
-
Refactor controllers, generate idiomatic RSpec, catch N+1 queries, review code for anti-patterns, and build entire features — all context-aware with your schema, routes, and specs. Powered by Claude Opus 4.
|
|
18
|
+
Refactor controllers, generate idiomatic RSpec, catch N+1 queries, review code for anti-patterns, and build entire features — all context-aware with your schema, routes, and specs. Powered by Claude Opus 4.8, running on your existing Claude subscription.
|
|
19
19
|
|
|
20
20
|
<img width="1230" height="280" alt="image" src="https://github.com/user-attachments/assets/14e07ce8-def0-4a8f-ac89-46661361a4eb" />
|
|
21
21
|
|
|
@@ -69,6 +69,22 @@ Refactor controllers, generate idiomatic RSpec, catch N+1 queries, review code f
|
|
|
69
69
|
- **IDE-ready** — works in the terminal and inside VS Code with full bidirectional communication
|
|
70
70
|
- **Extensible** — connect external tool servers via MCP, add custom skills, or wire up your own providers
|
|
71
71
|
|
|
72
|
+
## Claude Code Feature Parity
|
|
73
|
+
|
|
74
|
+
Phase 4 ships nine parity features with Claude Code:
|
|
75
|
+
|
|
76
|
+
- **Extended thinking** — `/think <budget>` toggles per-session reasoning; the Anthropic adapter emits `thinking: {type: 'adaptive'}` on Claude 4.6+ models (`{type: 'enabled', budget_tokens}` on older ones).
|
|
77
|
+
- **Reasoning effort** — `/effort <low|medium|high|xhigh|max>` sets the request-level reasoning depth; the Anthropic adapter emits `output_config: {effort}` on the wire. GA on Claude 4.6+ (Opus 4.6/4.7/4.8, Sonnet 4.6/5, Fable 5); `xhigh` needs Opus 4.7+ / Sonnet 5 / Fable 5. Model default is `high`.
|
|
78
|
+
- **Task budgets (beta)** — the agent loop's remaining token budget is sent as `output_config: {task_budget: {type: 'tokens', total}}` on supported models (Fable 5, Sonnet 5, Opus 4.7/4.8), advisory pacing only — `max_tokens` stays the enforced per-response cap.
|
|
79
|
+
- **Image / vision input** — `@chart.png` (and `.jpg` / `.jpeg` / `.gif` / `.webp`) becomes a real image content block attached to the user turn; Anthropic and OpenAI each emit their native shape.
|
|
80
|
+
- **TodoWrite live checklist** — `TodoWrite` tool; the checklist refreshes above the spinner on every tool result so you see in-turn progress at a glance.
|
|
81
|
+
- **Custom-command frontmatter** — `argument-hint`, `allowed-tools`, and `model:` keys in `~/.rubyn-code/commands/*.md`; the loop honors the per-prompt tool restriction and model override.
|
|
82
|
+
- **`.mcp.json` auto-discovery** — project-root MCP servers flow into the REPL automatically; `/mcp` shows entries prefixed `[project]` or `[user]`. OAuth for URL transports is still deferred.
|
|
83
|
+
- **`/export` transcript** — `/export <path>` writes the conversation as Markdown (default) or `--jsonl` for tooling.
|
|
84
|
+
- **Fable 5 refusal handling** — a declined request (`stop_reason: "refusal"`) surfaces a clear message instead of an empty turn; `claude-fable-5` / `claude-mythos-5` requests opt into a server-side Opus 4.8 fallback by default, so most refusals are transparently re-served in the same call.
|
|
85
|
+
|
|
86
|
+
See `docs/04-feature-parity/` for per-feature design + tasks docs.
|
|
87
|
+
|
|
72
88
|
## Install
|
|
73
89
|
|
|
74
90
|
Requires **Ruby 4.0.2+**. Install with your latest Ruby, then pin it so it works in every project:
|
|
@@ -138,6 +154,8 @@ rubyn-code -p "Refactor app/controllers/orders_controller.rb into service object
|
|
|
138
154
|
rubyn-code --ide
|
|
139
155
|
```
|
|
140
156
|
|
|
157
|
+
**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.
|
|
158
|
+
|
|
141
159
|
## What Can Rubyn Do?
|
|
142
160
|
|
|
143
161
|
### Refactor code
|
|
@@ -227,7 +245,7 @@ The extension communicates over 19 RPC methods: `initialize`, `prompt`, `cancel`
|
|
|
227
245
|
|
|
228
246
|
## MCP — External Tool Servers
|
|
229
247
|
|
|
230
|
-
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.
|
|
248
|
+
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.
|
|
231
249
|
|
|
232
250
|
### Configuration
|
|
233
251
|
|
|
@@ -349,7 +367,7 @@ Rubyn automatically loads relevant context based on what you're working on:
|
|
|
349
367
|
- **Controllers** → includes models, routes, request specs, services
|
|
350
368
|
- **Models** → includes schema, associations, specs, factories
|
|
351
369
|
- **Service objects** → includes referenced models and their specs
|
|
352
|
-
- **Any file** → checks for `RUBYN.md`, `CLAUDE.md`, or `AGENT.md` instructions
|
|
370
|
+
- **Any file** → checks for `RUBYN.md`, `CLAUDE.md`, `AGENTS.md`, or `AGENT.md` instructions
|
|
353
371
|
|
|
354
372
|
The [codebase index](#codebase-indexing) enhances this with structural awareness — Rubyn knows which files depend on each other before it reads them.
|
|
355
373
|
|
|
@@ -367,7 +385,7 @@ Drop a `RUBYN.md` in your project root and Rubyn follows your conventions:
|
|
|
367
385
|
- Run rubocop before committing
|
|
368
386
|
```
|
|
369
387
|
|
|
370
|
-
Also reads `CLAUDE.md` and `AGENT.md` — no migration needed from other tools.
|
|
388
|
+
Also reads `CLAUDE.md`, `AGENTS.md`, and `AGENT.md` — no migration needed from other tools.
|
|
371
389
|
|
|
372
390
|
| Location | Scope |
|
|
373
391
|
|----------|-------|
|
|
@@ -390,6 +408,65 @@ Focus areas: `all`, `security`, `performance`, `style`, `testing`
|
|
|
390
408
|
|
|
391
409
|
Severity ratings: **[critical]** **[warning]** **[suggestion]** **[nitpick]**
|
|
392
410
|
|
|
411
|
+
## Chisel — Write the Minimum That Works
|
|
412
|
+
|
|
413
|
+
Chisel is an opt-in mode that makes Rubyn think like the laziest senior dev in
|
|
414
|
+
the room: the best code is the code you never wrote. It's **off by default** and
|
|
415
|
+
only changes the agent's behavior once you turn it on.
|
|
416
|
+
|
|
417
|
+
```
|
|
418
|
+
rubyn > /chisel # show current intensity
|
|
419
|
+
rubyn > /chisel full # turn it on
|
|
420
|
+
rubyn > /chisel off # back to normal
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Intensities: `off` (default) · `lite` · `full` · `ultra`. When on, Rubyn walks a
|
|
424
|
+
decision ladder before writing code — does this need to exist? does stdlib or an
|
|
425
|
+
installed gem already do it? is it one line? — and only then writes the smallest
|
|
426
|
+
change that solves the task. The safety floor (validation, error/data-loss
|
|
427
|
+
handling, security, accessibility) is never on the chopping block.
|
|
428
|
+
|
|
429
|
+
Set it permanently with `chisel_mode: full` in `~/.rubyn-code/config.yml`, or
|
|
430
|
+
per-shell with `RUBYN_CHISEL_MODE=full`.
|
|
431
|
+
|
|
432
|
+
**On-demand audits** (work whether or not the always-on mode is enabled):
|
|
433
|
+
|
|
434
|
+
```
|
|
435
|
+
rubyn > /chisel-review # over-engineering in your diff vs main
|
|
436
|
+
rubyn > /chisel-review develop # ...vs a different base
|
|
437
|
+
rubyn > /chisel-audit # sweep the whole repo
|
|
438
|
+
rubyn > /chisel-audit app/services # ...scoped to a path
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Both return a ranked deletion/simplification list — each item with a location, the
|
|
442
|
+
ladder rung it skipped, and the concrete simpler form — and stay read-only (they
|
|
443
|
+
report, they don't edit).
|
|
444
|
+
|
|
445
|
+
**Debt ledger & status.** Leave a `# chisel: …` comment when you consciously defer a
|
|
446
|
+
simplification, then collect them later:
|
|
447
|
+
|
|
448
|
+
```
|
|
449
|
+
rubyn > /chisel-debt # list every `# chisel:` deferral, with file:line and note
|
|
450
|
+
rubyn > /chisel-gain # current mode, outstanding debt count, and reference impact
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
**Verifying Chisel.** A standalone smoke test runs the whole layer against a
|
|
454
|
+
committed, deliberately over-engineered fixture and gives the same result every
|
|
455
|
+
time — no LLM, no network:
|
|
456
|
+
|
|
457
|
+
```
|
|
458
|
+
$ bundle exec ruby skills/self_test/chisel_smoke.rb
|
|
459
|
+
CHISEL debt: PASS # scans the fixture → its exact planted `# chisel:` markers
|
|
460
|
+
CHISEL engine: PASS # off injects nothing; lite/full/ultra keep the safety floor
|
|
461
|
+
CHISEL inspection: PASS # review/audit prompts assemble; bad scope raises
|
|
462
|
+
CHISEL commands: PASS # all five slash commands register
|
|
463
|
+
CHISEL: PASS
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
The fixture lives at `skills/self_test/fixtures/chisel_sample.rb`, the same check is
|
|
467
|
+
Section 18 of the `/skill self-test` scorecard, and `spec/rubyn_code/chisel/self_test_fixture_spec.rb`
|
|
468
|
+
guards the fixture's exact scan output so it can't silently drift.
|
|
469
|
+
|
|
393
470
|
## Megaplan — Phased Planning
|
|
394
471
|
|
|
395
472
|
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.
|
|
@@ -476,6 +553,19 @@ Rubyn gets smarter with every session:
|
|
|
476
553
|
3. **On next startup** — injects top instincts into the system prompt
|
|
477
554
|
4. **Over time** — reinforced instincts strengthen, unused ones decay and get pruned
|
|
478
555
|
|
|
556
|
+
### Take your learnings with you
|
|
557
|
+
|
|
558
|
+
Instincts live in `~/.rubyn-code`. Move them to another machine with `/learning`:
|
|
559
|
+
|
|
560
|
+
```
|
|
561
|
+
rubyn > /learning # how many instincts you've accumulated
|
|
562
|
+
rubyn > /learning export learnings.json # write them to a portable file
|
|
563
|
+
# ...on the new machine...
|
|
564
|
+
rubyn > /learning import learnings.json --here # load them (--here = apply to this project)
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
Import regenerates ids and skips duplicates, so it's safe to run repeatedly.
|
|
568
|
+
|
|
479
569
|
## Streaming Output
|
|
480
570
|
|
|
481
571
|
Real-time streaming with live syntax highlighting via Rouge/Monokai. Code blocks are buffered and highlighted when complete. No waiting for full responses.
|
|
@@ -537,6 +627,8 @@ rubyn-code daemon [OPTIONS] # Run GOLEM autonomous daemon
|
|
|
537
627
|
| `/new` | Save session and start a fresh conversation |
|
|
538
628
|
| `/review [base]` | PR review against best practices |
|
|
539
629
|
| `/spawn name role` | Spawn a persistent teammate |
|
|
630
|
+
| `/goal <condition>` | Set a goal Rubyn works toward until met (`/goal clear` to cancel) |
|
|
631
|
+
| `/loop [xN] [interval] <prompt-or-/cmd>` | Repeat a prompt or command on an interval (Ctrl-C to stop) |
|
|
540
632
|
| `/compact` | Compress conversation context |
|
|
541
633
|
| `/cost` | Show token usage and costs |
|
|
542
634
|
| `/tasks` | List all tasks |
|
|
@@ -547,6 +639,47 @@ rubyn-code daemon [OPTIONS] # Run GOLEM autonomous daemon
|
|
|
547
639
|
| `/model` | Show/switch model and provider |
|
|
548
640
|
| `/doctor` | Run environment health checks |
|
|
549
641
|
| `/mcp` | MCP server documentation and status |
|
|
642
|
+
| `/agents` | List sub-agent types (built-in + custom) |
|
|
643
|
+
| `/rewind [id] [code\|chat]` | Rewind code and/or conversation to a checkpoint |
|
|
644
|
+
|
|
645
|
+
### Custom Sub-Agents
|
|
646
|
+
|
|
647
|
+
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.
|
|
648
|
+
|
|
649
|
+
```markdown
|
|
650
|
+
---
|
|
651
|
+
description: Reviews a diff for bugs
|
|
652
|
+
tools: read_file, grep, glob, bash # optional — omit for the access default
|
|
653
|
+
access: read # read | write (default: write)
|
|
654
|
+
---
|
|
655
|
+
You are a meticulous code reviewer. Find correctness bugs only.
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
### Custom Slash Commands
|
|
659
|
+
|
|
660
|
+
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.
|
|
661
|
+
|
|
662
|
+
```markdown
|
|
663
|
+
---
|
|
664
|
+
description: Open a PR for the current branch
|
|
665
|
+
---
|
|
666
|
+
Open a pull request for the current branch.
|
|
667
|
+
Title: $ARGUMENTS
|
|
668
|
+
Current diff:
|
|
669
|
+
!`git diff main --stat`
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
Templating in the body:
|
|
673
|
+
|
|
674
|
+
| Token | Expands to |
|
|
675
|
+
|-------|-----------|
|
|
676
|
+
| `$ARGUMENTS` | everything typed after the command |
|
|
677
|
+
| `$1` … `$9` | individual positional arguments |
|
|
678
|
+
| `` !`shell cmd` `` | the command's output, inlined |
|
|
679
|
+
|
|
680
|
+
### Checkpoints & Rewind
|
|
681
|
+
|
|
682
|
+
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.
|
|
550
683
|
|
|
551
684
|
## Authentication
|
|
552
685
|
|
|
@@ -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,21 +1,28 @@
|
|
|
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.
|
|
13
16
|
#
|
|
14
|
-
# @param content [String]
|
|
17
|
+
# @param content [String, Array<Hash>] either a plain string or an
|
|
18
|
+
# array of content blocks (text, image, etc.). Strings get wrapped
|
|
19
|
+
# as `[{ type: 'text', text: content }]` only if there's a planned
|
|
20
|
+
# Array; we keep raw String for backward compatibility with the API.
|
|
15
21
|
# @return [Hash] the appended message
|
|
16
22
|
def add_user_message(content)
|
|
17
23
|
message = { role: 'user', content: content }
|
|
18
24
|
@messages << message
|
|
25
|
+
track_added_message(message)
|
|
19
26
|
message
|
|
20
27
|
end
|
|
21
28
|
|
|
@@ -28,6 +35,7 @@ module RubynCode
|
|
|
28
35
|
blocks = normalize_content(content, tool_calls)
|
|
29
36
|
message = { role: 'assistant', content: blocks }
|
|
30
37
|
@messages << message
|
|
38
|
+
track_added_message(message)
|
|
31
39
|
message
|
|
32
40
|
end
|
|
33
41
|
|
|
@@ -52,8 +60,11 @@ module RubynCode
|
|
|
52
60
|
# tool results for the same assistant turn are batched together.
|
|
53
61
|
if @messages.last && @messages.last[:role] == 'user' && tool_result_message?(@messages.last)
|
|
54
62
|
@messages.last[:content] << result_block
|
|
63
|
+
track_appended_block(result_block)
|
|
55
64
|
else
|
|
56
|
-
|
|
65
|
+
message = { role: 'user', content: [result_block] }
|
|
66
|
+
@messages << message
|
|
67
|
+
track_added_message(message)
|
|
57
68
|
end
|
|
58
69
|
|
|
59
70
|
result_block
|
|
@@ -79,6 +90,29 @@ module RubynCode
|
|
|
79
90
|
# @return [void]
|
|
80
91
|
def clear!
|
|
81
92
|
@messages.clear
|
|
93
|
+
reset_derived_state!
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Character length of the JSON-serialized messages array, maintained
|
|
97
|
+
# incrementally on append so per-turn token estimation doesn't have to
|
|
98
|
+
# re-serialize the whole history. Matches JSON.generate(messages).length.
|
|
99
|
+
#
|
|
100
|
+
# @return [Integer]
|
|
101
|
+
def estimated_json_chars
|
|
102
|
+
@json_chars ||= @messages.sum { |msg| JSON.generate(msg).length }
|
|
103
|
+
return 2 if @messages.empty?
|
|
104
|
+
|
|
105
|
+
# "[" + per-message JSON joined by "," + "]"
|
|
106
|
+
@json_chars + @messages.length + 1
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Drops cached serialization/tool-ID bookkeeping. Must be called after
|
|
110
|
+
# messages are mutated in place from outside this class (e.g. by
|
|
111
|
+
# Context::MicroCompact); the caches rebuild lazily on next access.
|
|
112
|
+
#
|
|
113
|
+
# @return [void]
|
|
114
|
+
def refresh_derived_state!
|
|
115
|
+
reset_derived_state!
|
|
82
116
|
end
|
|
83
117
|
|
|
84
118
|
# Return the messages array formatted for the Claude API.
|
|
@@ -115,21 +149,83 @@ module RubynCode
|
|
|
115
149
|
@messages.pop
|
|
116
150
|
removed += 1
|
|
117
151
|
end
|
|
152
|
+
reset_derived_state!
|
|
118
153
|
end
|
|
119
154
|
|
|
120
155
|
# Replace messages with a new array (used after compaction).
|
|
121
156
|
def replace!(new_messages)
|
|
122
157
|
@messages.replace(new_messages)
|
|
158
|
+
reset_derived_state!
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Alias for `messages`. Common shorthand for callers that want to
|
|
162
|
+
# treat the conversation as an Array of message hashes.
|
|
163
|
+
def to_a
|
|
164
|
+
@messages
|
|
123
165
|
end
|
|
166
|
+
alias to_ary to_a
|
|
124
167
|
|
|
125
168
|
private
|
|
126
169
|
|
|
170
|
+
# Derived bookkeeping kept in sync with @messages so hot paths stay
|
|
171
|
+
# cheap: per-message JSON char total (token estimation) and
|
|
172
|
+
# tool_use/tool_result ID sets (orphan repair). nil means "rebuild
|
|
173
|
+
# lazily from @messages on next access".
|
|
174
|
+
def reset_derived_state!
|
|
175
|
+
@json_chars = nil
|
|
176
|
+
@tool_use_ids = nil
|
|
177
|
+
@tool_result_ids = nil
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def track_added_message(message)
|
|
181
|
+
register_tool_ids(message)
|
|
182
|
+
return if @json_chars.nil?
|
|
183
|
+
|
|
184
|
+
@json_chars += JSON.generate(message).length
|
|
185
|
+
rescue StandardError
|
|
186
|
+
@json_chars = nil
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# A tool_result block appended to an existing user message grows that
|
|
190
|
+
# message's JSON by the block plus one separator comma.
|
|
191
|
+
def track_appended_block(result_block)
|
|
192
|
+
@tool_result_ids << result_block[:tool_use_id] if @tool_result_ids
|
|
193
|
+
return if @json_chars.nil?
|
|
194
|
+
|
|
195
|
+
@json_chars += JSON.generate(result_block).length + 1
|
|
196
|
+
rescue StandardError
|
|
197
|
+
@json_chars = nil
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def register_tool_ids(message)
|
|
201
|
+
return unless @tool_use_ids && message[:content].is_a?(Array)
|
|
202
|
+
|
|
203
|
+
message[:content].each do |block|
|
|
204
|
+
next unless block.is_a?(Hash)
|
|
205
|
+
|
|
206
|
+
if message[:role] == 'assistant' && block_matches_type?(block, 'tool_use')
|
|
207
|
+
@tool_use_ids << (block[:id] || block['id'])
|
|
208
|
+
elsif message[:role] == 'user' && block_matches_type?(block, 'tool_result')
|
|
209
|
+
@tool_result_ids << (block[:tool_use_id] || block['tool_use_id'])
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def rebuild_tool_id_sets!
|
|
215
|
+
@tool_use_ids = collect_tool_use_ids(@messages)
|
|
216
|
+
@tool_result_ids = collect_tool_result_ids(@messages)
|
|
217
|
+
end
|
|
218
|
+
|
|
127
219
|
# Ensure every tool_use block has a matching tool_result.
|
|
128
220
|
# If a tool_use is orphaned (e.g. from Ctrl-C interruption),
|
|
129
221
|
# inject a synthetic tool_result immediately after the assistant
|
|
130
222
|
# message that contains the orphaned tool_use.
|
|
223
|
+
#
|
|
224
|
+
# The ID sets are tracked incrementally as messages are appended, so
|
|
225
|
+
# the common no-orphan case skips the full-history scans.
|
|
131
226
|
def repair_orphaned_tool_uses(formatted)
|
|
132
|
-
|
|
227
|
+
rebuild_tool_id_sets! if @tool_use_ids.nil?
|
|
228
|
+
orphaned = @tool_use_ids - @tool_result_ids
|
|
133
229
|
return formatted if orphaned.empty?
|
|
134
230
|
|
|
135
231
|
insert_orphan_results(formatted, orphaned)
|
|
@@ -25,7 +25,7 @@ module RubynCode
|
|
|
25
25
|
def build_llm_opts
|
|
26
26
|
opts = {
|
|
27
27
|
messages: @conversation.to_api_format,
|
|
28
|
-
tools:
|
|
28
|
+
tools: filtered_tool_definitions,
|
|
29
29
|
system: build_system_prompt,
|
|
30
30
|
on_text: @on_text
|
|
31
31
|
}
|
|
@@ -36,6 +36,8 @@ module RubynCode
|
|
|
36
36
|
total: UsageTracker::TASK_BUDGET_TOTAL, remaining: @task_budget_remaining
|
|
37
37
|
}
|
|
38
38
|
end
|
|
39
|
+
budget = @llm_client.respond_to?(:thinking_budget_tokens) ? @llm_client.thinking_budget_tokens.to_i : 0
|
|
40
|
+
opts[:thinking] = { budget_tokens: budget } if budget.positive?
|
|
39
41
|
opts
|
|
40
42
|
end
|
|
41
43
|
|
|
@@ -45,6 +47,8 @@ module RubynCode
|
|
|
45
47
|
# Falls back to nil (use client's default) if routing fails.
|
|
46
48
|
# -- guard clauses for provider/mode checks
|
|
47
49
|
def routed_model
|
|
50
|
+
return @model_override if @model_override
|
|
51
|
+
|
|
48
52
|
return nil if manual_model_mode?
|
|
49
53
|
|
|
50
54
|
last_user = last_user_message_text
|
|
@@ -114,7 +118,7 @@ module RubynCode
|
|
|
114
118
|
|
|
115
119
|
def run_compaction
|
|
116
120
|
before = @conversation.length
|
|
117
|
-
est = @context_manager.estimated_tokens(@conversation
|
|
121
|
+
est = @context_manager.estimated_tokens(@conversation)
|
|
118
122
|
RubynCode::Debug.token(
|
|
119
123
|
"context=#{est} tokens (~#{before} messages, " \
|
|
120
124
|
"threshold=#{Config::Defaults::CONTEXT_THRESHOLD_TOKENS})"
|
|
@@ -130,7 +134,7 @@ module RubynCode
|
|
|
130
134
|
after = @conversation.length
|
|
131
135
|
return unless after < before
|
|
132
136
|
|
|
133
|
-
new_est = @context_manager.estimated_tokens(@conversation
|
|
137
|
+
new_est = @context_manager.estimated_tokens(@conversation)
|
|
134
138
|
RubynCode::Debug.loop_tick(
|
|
135
139
|
"Compacted: #{before} -> #{after} messages " \
|
|
136
140
|
"(#{est} -> #{new_est} tokens)"
|