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.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +182 -11
  3. data/db/migrations/014_multi_agent_upgrade.rb +79 -0
  4. data/lib/rubyn_code/agent/conversation.rb +89 -3
  5. data/lib/rubyn_code/agent/llm_caller.rb +2 -2
  6. data/lib/rubyn_code/agent/loop.rb +49 -9
  7. data/lib/rubyn_code/agent/system_prompt_builder.rb +37 -2
  8. data/lib/rubyn_code/agent/tool_processor.rb +3 -1
  9. data/lib/rubyn_code/auth/oauth.rb +1 -1
  10. data/lib/rubyn_code/auth/token_store.rb +49 -4
  11. data/lib/rubyn_code/checkpoint/hook.rb +26 -0
  12. data/lib/rubyn_code/checkpoint/manager.rb +109 -0
  13. data/lib/rubyn_code/chisel/debt.rb +65 -0
  14. data/lib/rubyn_code/chisel/inspection.rb +93 -0
  15. data/lib/rubyn_code/chisel.rb +127 -0
  16. data/lib/rubyn_code/cli/app.rb +2 -2
  17. data/lib/rubyn_code/cli/commands/agents.rb +31 -0
  18. data/lib/rubyn_code/cli/commands/chisel.rb +52 -0
  19. data/lib/rubyn_code/cli/commands/chisel_audit.rb +19 -0
  20. data/lib/rubyn_code/cli/commands/chisel_debt.rb +28 -0
  21. data/lib/rubyn_code/cli/commands/chisel_gain.rb +30 -0
  22. data/lib/rubyn_code/cli/commands/chisel_review.rb +19 -0
  23. data/lib/rubyn_code/cli/commands/command_template.rb +50 -0
  24. data/lib/rubyn_code/cli/commands/context.rb +3 -1
  25. data/lib/rubyn_code/cli/commands/custom_command.rb +42 -0
  26. data/lib/rubyn_code/cli/commands/custom_loader.rb +69 -0
  27. data/lib/rubyn_code/cli/commands/goal.rb +87 -0
  28. data/lib/rubyn_code/cli/commands/learning.rb +62 -0
  29. data/lib/rubyn_code/cli/commands/loop.rb +58 -0
  30. data/lib/rubyn_code/cli/commands/mcp.rb +18 -5
  31. data/lib/rubyn_code/cli/commands/megaplan.rb +50 -0
  32. data/lib/rubyn_code/cli/commands/registry.rb +14 -9
  33. data/lib/rubyn_code/cli/commands/rewind.rb +65 -0
  34. data/lib/rubyn_code/cli/first_run.rb +1 -1
  35. data/lib/rubyn_code/cli/loop_runner.rb +98 -0
  36. data/lib/rubyn_code/cli/mention_expander.rb +92 -0
  37. data/lib/rubyn_code/cli/renderer.rb +3 -2
  38. data/lib/rubyn_code/cli/repl.rb +37 -14
  39. data/lib/rubyn_code/cli/repl_commands.rb +77 -2
  40. data/lib/rubyn_code/cli/repl_setup.rb +9 -1
  41. data/lib/rubyn_code/cli/setup.rb +13 -0
  42. data/lib/rubyn_code/cli/stream_formatter.rb +3 -2
  43. data/lib/rubyn_code/cli/version_check.rb +10 -3
  44. data/lib/rubyn_code/config/defaults.rb +13 -1
  45. data/lib/rubyn_code/config/schema.json +4 -0
  46. data/lib/rubyn_code/config/settings.rb +17 -2
  47. data/lib/rubyn_code/context/manager.rb +29 -12
  48. data/lib/rubyn_code/debug.rb +11 -5
  49. data/lib/rubyn_code/goal/evaluator.rb +95 -0
  50. data/lib/rubyn_code/hooks/event_map.rb +56 -0
  51. data/lib/rubyn_code/hooks/external_dispatcher.rb +199 -0
  52. data/lib/rubyn_code/hooks/goal_hook.rb +88 -0
  53. data/lib/rubyn_code/hooks/response.rb +83 -0
  54. data/lib/rubyn_code/hooks/runner.rb +61 -3
  55. data/lib/rubyn_code/hooks/settings_json_loader.rb +109 -0
  56. data/lib/rubyn_code/hooks/subprocess_executor.rb +116 -0
  57. data/lib/rubyn_code/ide/handlers/plan_interview_answer_handler.rb +65 -0
  58. data/lib/rubyn_code/ide/handlers/plan_interview_cancel_handler.rb +22 -0
  59. data/lib/rubyn_code/ide/handlers/plan_interview_start_handler.rb +53 -0
  60. data/lib/rubyn_code/ide/handlers/plan_propose_handler.rb +41 -0
  61. data/lib/rubyn_code/ide/handlers/prompt_handler.rb +9 -1
  62. data/lib/rubyn_code/ide/handlers/recover_ci_handler.rb +143 -0
  63. data/lib/rubyn_code/ide/handlers/session_resume_handler.rb +1 -1
  64. data/lib/rubyn_code/ide/handlers.rb +17 -2
  65. data/lib/rubyn_code/ide/protocol.rb +15 -0
  66. data/lib/rubyn_code/ide/server.rb +39 -1
  67. data/lib/rubyn_code/index/codebase_index.rb +39 -1
  68. data/lib/rubyn_code/learning/porter.rb +129 -0
  69. data/lib/rubyn_code/llm/adapters/anthropic.rb +65 -16
  70. data/lib/rubyn_code/llm/adapters/openai.rb +1 -1
  71. data/lib/rubyn_code/llm/adapters/prompt_caching.rb +5 -1
  72. data/lib/rubyn_code/llm/adapters/token_caching.rb +54 -0
  73. data/lib/rubyn_code/llm/model_router.rb +2 -2
  74. data/lib/rubyn_code/mcp/client.rb +59 -0
  75. data/lib/rubyn_code/mcp/server_extras_bridge.rb +110 -0
  76. data/lib/rubyn_code/mcp/sse_transport.rb +2 -1
  77. data/lib/rubyn_code/mcp/tool_bridge.rb +16 -14
  78. data/lib/rubyn_code/megaplan/ci_recovery.rb +104 -0
  79. data/lib/rubyn_code/megaplan/interview_session.rb +250 -0
  80. data/lib/rubyn_code/megaplan/plan_proposer.rb +153 -0
  81. data/lib/rubyn_code/memory/search.rb +9 -5
  82. data/lib/rubyn_code/memory/session_persistence.rb +159 -21
  83. data/lib/rubyn_code/observability/cost_calculator.rb +3 -1
  84. data/lib/rubyn_code/output/diff_renderer.rb +62 -7
  85. data/lib/rubyn_code/skills/auto_suggest.rb +70 -2
  86. data/lib/rubyn_code/skills/registry_client.rb +4 -3
  87. data/lib/rubyn_code/sub_agents/agent_type.rb +17 -0
  88. data/lib/rubyn_code/sub_agents/catalog.rb +124 -0
  89. data/lib/rubyn_code/teams/agent_registry.rb +120 -0
  90. data/lib/rubyn_code/teams/mailbox.rb +99 -10
  91. data/lib/rubyn_code/teams/manager.rb +83 -5
  92. data/lib/rubyn_code/teams/teammate.rb +5 -1
  93. data/lib/rubyn_code/tools/ask_user.rb +15 -1
  94. data/lib/rubyn_code/tools/executor.rb +5 -3
  95. data/lib/rubyn_code/tools/spawn_agent.rb +47 -62
  96. data/lib/rubyn_code/tools/spawn_teammate.rb +7 -2
  97. data/lib/rubyn_code/tools/web_fetch.rb +1 -1
  98. data/lib/rubyn_code/tools/web_search.rb +4 -1
  99. data/lib/rubyn_code/version.rb +1 -1
  100. data/lib/rubyn_code.rb +53 -2
  101. data/skills/megaplan/megaplan.md +156 -0
  102. data/skills/rubyn_self_test.md +322 -14
  103. data/skills/self_test/chisel_smoke.rb +84 -0
  104. data/skills/self_test/fixtures/chisel_sample.rb +64 -0
  105. metadata +49 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 342d51b0944ce35908b8fd56f52b0321cf698ea01ec3cc8fa572d92b9c332b22
4
- data.tar.gz: '033868c98fedc4814cb9e8c94eb8f013b15641d5a3e7cecf7e819d3d3b57f771'
3
+ metadata.gz: f5cbf6f4737790408893a904d64412c2dd695e81a2cd2e5960b4da3196e48f4f
4
+ data.tar.gz: f0e9b80f10c77d6eb6c357db7518851344670653546d5eca2784d017d490608c
5
5
  SHA512:
6
- metadata.gz: 283cff44827418497c9ed9d27e4331d8fdf24c04825ee3f28e46a33da28bf61f65d0a2b85173187947393856053fad5466dce5782fcef3d0e1dcce68d37e00eb
7
- data.tar.gz: 6c96e7beba37f74c0fe947470a1a37d50b521087b573407d6ba7d0d0649d71385b263b70b126fe9c2b7da3c910a233862fdbfb8c1d7c534db6f6cc1d813ec305
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=4.0.2 gem install rubyn-code
90
- RBENV_VERSION=4.0.2 rubyn-code --setup
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 4.0.2
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 14 RPC methods: `initialize`, `prompt`, `cancel`, `review`, `approveToolUse`, `acceptEdit`, `session/*`, `config/*`, `models/list`, and `shutdown`.
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
- @messages << { role: 'user', content: [result_block] }
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
- orphaned = collect_tool_use_ids(formatted) - collect_tool_result_ids(formatted)
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.messages)
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.messages)
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)"