Migrate cc-os plugins to the RubyLLM-native llm-stack (llm-stack ADR 0004) #1502
Labels
No labels
P0
P1
P2
P3
bug
category/code-change
category/cross-repo
category/eval-run
category/investigation
category/no-test-target
category/owner-ruling
category/prose-artifact
category/recurring-chore
category/structure
category/umbrella
crc-refactor
create
delete
enhancement
filed-by/agent
filed-by/user
frozen
lint-rule
needs-info
needs-triage
next
plugin/cc-architect
plugin/os
plugin/os-adr
plugin/os-aidd-lint
plugin/os-backlog
plugin/os-context
plugin/os-doc-hygiene
plugin/os-sdlc
plugin/os-vault
project/cc-os
ready-for-agent
ready-for-human
recurring
review
rule-proposal
update
waiting
wayfinder:grilling
wayfinder:map
wayfinder:map
wayfinder:research
wayfinder:task
wayfinder:task
wontfix
worklist/deviations
worklist/lint-rule
worklist/new-implement-build
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jared/cc-os#1502
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
llm-stack ticket #19 rebuilds llm-stack as a RubyLLM extension (ADR 0004).
The rebuild is on llm-stack branch
rebuild/rubyllm-extension. The deletion commit is c8891f0.The rebuild removes LLM::Client, LLM::Response, LLM::Decision, LLM::Usage, LLM::Embedding, LLM::PriceTable, LLM::TimedOut, LLM::ProviderError and LLM::Http::Errors.
Three cc-os plugins call these classes: os-me, os-adr and os-sdlc (ticket-takedown).
The code breaks when it loads the rebuilt gem.
A profile now builds a
RubyLLM::Contextwithprofile.context(credentials). The caller gets RubyLLM types and RubyLLM errors.LLM::Profile.findandfrom_hkeep their names.require "llm"keeps working.Claude Code is now a RubyLLM provider (
claude_code). Profile settings (cwd,tools,executable,timeout) go into the context through the profile.The llm-stack branch is pushed to origin. The pin style stays a bundler
path:source; do not usegit: ..., ref:. See the Migration brief comment.llm-stack merges to
mainonly after both consumers are green. The decision-model ticket is the other consumer. The two tickets do not depend on each other.An open decision on llm-stack #19 may tighten the ruby_llm pin.
Some
Gemfile.lockfiles in cc-os have uncommitted changes. They are not ours to touch. Do not stage, revert or regenerate them for this work. Use a worktree or stage by explicit path.Approach
Open a migration branch in cc-os. In the three plugin Gemfiles, change the
path:of llm-stack fromdev/ruby-gems/llm-stacktodev/ruby-gems/llm-stack-rebuild.After llm-stack merges to
main, changepath:back todev/ruby-gems/llm-stack, run each suite, and merge. Do not merge the migration branch before then.Source: llm-stack
docs/parity/llm-stack.md, section "Consumer call sites". Line numbers are stale (taken from cc-os496c2184); usergfor exact lines.plugins/os-me/Gemfile:12;plugins/os-adr/Gemfile:9;plugins/os-sdlc/Gemfile:14gem 'llm-stack', path: File.expand_path('dev/ruby-gems/llm-stack', Etc.getpwuid.dir), require: 'llm'path:todev/ruby-gems/llm-stack-rebuildduring migration, then back todev/ruby-gems/llm-stackplugins/os-me/lib/os_me.rb:6;plugins/os-adr/lib/adr.rb:5;plugins/os-sdlc/eval/ticket-takedown/lib/takedown.rb:5require "llm"plugins/os-me/lib/os_me/headless_runner.rb:13,20-21Client.new(profile: Profile.from_h(:os_me_headless, ...), credentials: {})thenchat(prompt:, system:, schema:)profile.context.chat(**profile.keywords).with_instructions(system).with_schema(schema).ask(prompt)plugins/os-me/lib/os_me/headless_runner.rb:26-28response => LLM::Response[text:, usage: {input:, output:, cache_read:, cache_write:}, meta:];meta.fetch('reported_cost')message.content,message.tokens(fields may be nil) andmessage.cost.total(the CLIreported_cost).metais gone.plugins/os-me/lib/os_me/headless_request.rb:11-14provider: 'claude_code',model,settings: {cwd, tools, allowed_tools, read_roots, executable, timeout}LLM::Profile.from_h. Unknown keys now raiseLLM::InvalidProfile. Per-callcwd,tools,allowed_toolscan usewith_provider_options.plugins/os-me/lib/os_me/ollama_embedder.rb:6,14Client.new(profile: Profile.find(provider: 'ollama', model: 'nomic-embed-text'));embed(texts:).vectorsprofile.context.embed(texts, **profile.keywords, provider_options: profile.provider_options).vectorsplugins/os-adr/lib/adr/judge/model_chat.rb:15,19-24,29-31Client.new(profile:, credentials: {});chat(prompt:, system:, schema:).text;rescue LLM::TimedOut;rescue LLM::ProviderError => eprofile.context.chat(...).ask(prompt).content;rescue RubyLLM::Error => e(coversLLM::TimedOut,LLM::ProcessFailed,LLM::ReplyFailed; these are newRubyLLM::Errorsubclasses). Keep the 200 character cut of the message.plugins/os-sdlc/eval/ticket-takedown/lib/takedown/adapter/claude_executor.rb:7,16-19,26,30-32LLM::PriceTable.new({})asREPORTED_COSTS;Client.new(profile:, credentials: {}, prices: REPORTED_COSTS);rescue LLM::TimedOut, LLM::ProviderErrorREPORTED_COSTSandprices:. Usemessage.cost.total, which is the CLIreported_cost.rescue RubyLLM::Error.plugins/os-sdlc/eval/ticket-takedown/lib/takedown/parser/claude_reply.rb:13-17,31response => LLM::Response;response.text,response.cost,meta["raw"],["num_turns"],["models"],["session_id"];usage.to_hwith keysinput, cache_write, cache_read, outputmessage.content,message.cost.total. Readsession_id,num_turnsandmodelsfrommessage.raw. Build the usage hash frommessage.tokens(fields may be nil).plugins/os-me/test/headless_runner_chat_test.rb:29,35assert_raises(LLM::TimedOut),assert_raises(LLM::ProviderError)assert_raises(LLM::TimedOut),assert_raises(LLM::ProcessFailed)(the newRubyLLM::Errorsubclasses)plugins/os-me/test/headless_runner_cost_test.rb:16-20LLM::Client.newto addprices: LLM::PriceTable.defaultmessage.cost.total.plugins/os-me/test/llm_embed_stub.rb:5,21-25;plugins/os-me/test/ollama_embedder_test.rb:26LLM::Embedding.new(vectors:, usage: LLM::Usage.new, cost: 0.0); stubsClient.newRubyLLM::Embeddingand stubContext#embed.Profile.findstays.docs/llm-stack/spec.mddocs/adr/0301-*.md,docs/adr/0303-*.mdRequired work: none beyond the acceptance criteria.
Acceptance criteria
path:pointed atdev/ruby-gems/llm-stack-rebuildin os-me, os-adr and os-sdlc, when each plugin test suite runs, then every suite is green.LLM::TimedOutis a newRubyLLM::Errorsubclass in the rebuild, so a hit that rescues or asserts the new class is allowed.RubyLLM::Errorand handles it as it did before.claude_reply.rbparses it, then it reports the CLI cost frommessage.cost.totaland reads session id, turn count and models frommessage.raw.RubyLLM::Embedding#vectorsin input order.docs/llm-stack/spec.mdand ADRs 0301 and 0303, when a reader opens them, then they describe the RubyLLM-native API and point to llm-stack ADR 0004.main, when each Gemfilepath:points back atdev/ruby-gems/llm-stack, then every suite is still green.Gemfile.lockedits that exist in cc-os today, and no change outside cc-os.Discoverer: /home/jared/dev/ruby-gems/llm-stack, session 4ee5ef17-d652-472d-853c-91dd2af699ea, 2026-11-01. llm-stack removed LLM::Client and its wrappers (ADR 0004); this repo calls them.
Migration brief (2026-11-02)
Self-contained start sheet for cc-os #1502. Read this, then the body.
Body edits made today
git: ..., ref: <sha>pinning with the bundlerpath:flow (user decision). The Gemfile row now names the real current lines:plugins/os-me/Gemfile:12,plugins/os-adr/Gemfile:9,plugins/os-sdlc/Gemfile:14(the body said 10, 7, 12), and the realpath:text.LLM::ClaudeCode::TimedOutandProcessFailed. The rebuild has top-levelLLM::TimedOut,LLM::ProcessFailed,LLM::ReplyFailed(all< RubyLLM::Error). There is noLLM::ClaudeCode::error namespace.LLM::TimedOutis a NEW rebuild class with an old name, so a use of the new class is allowed.path:flow.State of llm-stack
Branch
rebuild/rubyllm-extension, pushed to origin (forgejo jared/llm-stack). Local worktree:~/dev/ruby-gems/llm-stack-rebuild. Its tests pass. Live smokes passed on 2026-11-02 (claude_codesubscription, Cloudflare jev judge, Ollama).Flow
Gemfile.lockfiles have uncommitted edits; do not stage, revert or regenerate them; stage by explicit path). Changepath:inplugins/os-me/Gemfile:12,plugins/os-adr/Gemfile:9andplugins/os-sdlc/Gemfile:14fromdev/ruby-gems/llm-stacktodev/ruby-gems/llm-stack-rebuild.path:values back todev/ruby-gems/llm-stack, run the suites again, then merge.API reference (rebuild worktree; confirm names there)
README.md,docs/design.md,docs/adr/0004*,docs/parity/llm-stack.md("Consumer call sites"),docs/parity/rebuild-plan.mdsection 4.LLM::Client,LLM::Http::Errors::*,LLM::PriceTable(usemessage.cost.total),LLM::Response,LLM::Decision,LLM::Usage,LLM::Embedding,LLM::ProviderError.LLM::Profile.find/from_h,LLM::InvalidProfile,LLM::Error(StandardError; gem configuration errors only).< RubyLLM::Error:LLM::InvalidJudgment,LLM::BadReply,LLM::TimedOut,LLM::ProcessFailed,LLM::ReplyFailed. Callers rescueRubyLLM::Error(plusFaraday::TimeoutError,Faraday::ConnectionFailed,LLM::Errorwhere needed).ruby_llmpinned exactly at 2.1.1.claude_codemodels:haiku,sonnet,opus.cwdis required forclaude_code.Call sites that still use the removed API (rg, 2026-11-02, cc-os
268d6a9e; paths verified)os-me:
plugins/os-me/lib/os_me/headless_runner.rb:20(LLM::Client.new),:26(LLM::Response)plugins/os-me/lib/os_me/ollama_embedder.rb:6plugins/os-me/test/llm_embed_stub.rb:5(LLM::Embedding, LLM::Usage),:21-25(LLM::Client)plugins/os-me/test/headless_runner_cost_test.rb:16-20(LLM::Client, PriceTable)plugins/os-me/test/headless_runner_chat_test.rb:29(LLM::TimedOut: old class),:35(LLM::ProviderError)os-adr:
plugins/os-adr/lib/adr/judge/model_chat.rb:15(LLM::Client),:20(LLM::TimedOut),:22(LLM::ProviderError)os-sdlc:
plugins/os-sdlc/eval/ticket-takedown/lib/takedown/adapter/claude_executor.rb:7(PriceTable),:19(TimedOut, ProviderError),:26(Client)plugins/os-sdlc/eval/ticket-takedown/lib/takedown/parser/claude_reply.rb:13(LLM::Response)Docs:
docs/llm-stack/spec.md,docs/adr/0301-llm-stack-gem-is-the-shared-llm-layer-pinned-per-plugin-from-forgejo.md,docs/adr/0303-llm-stack-gem-is-the-shared-llm-layer-pinned-per-plugin-from-forgejo.md(both ADR titles are the same; read both). Alsodocs/implementation-status/os-me.mdandos-sdlc.mdmention the old API.Note:
plugins/os-me/test/headless_claude_test.rbandwrong_cost_test.rbuseDecision::Model::Client, notLLM::Client; leave them (they belong to #1478 / #1488). Body line numbers are stale; userg.Out of scope: no change outside cc-os, no
Gemfile.lockedits, no labels.Work started 2026-11-02 on branch
migrate/llm-stack-rebuild(worktree .claude/worktrees/agent-af87e80dcb876e85c), commits85a6dde8,21dd9a64,f050bf5b. Not merged. os-adr, os-sdlc and takedown suites are green. os-me has 1 failure: a request with no model now sends--model gpt-5.6(RubyLLM default) to the claude CLI; the old client sent no--model. AC7 waits for the llm-stack merge.Blocked on llm-stack #21 (jared/llm-stack#21): a no-model claude_code call sends
--model gpt-5.6. Ruling 2026-11-02: fix in llm-stack, not in os-me. After #21 lands on the rebuild branch, re-run the os-me suite onmigrate/llm-stack-rebuild.Green 2026-11-02 against llm-stack rebuild 7c80f38 (fix for llm-stack #21). Branch
migrate/llm-stack-rebuild: os-me 447 runs 0 failures; os-adr 144/0; os-sdlc 3140/0; takedown 608/0. Remaining: AC7 after the llm-stack merge to main.Resolution
Done: Migrated os-me, os-adr and the os-sdlc takedown to the RubyLLM-native llm-stack (llm-stack ADR 0004): profile.context chats and embeds, message.cost.total, message.raw, rescue RubyLLM::Error. Amended docs/llm-stack/spec.md and ADR-0303. Gemfiles point at dev/ruby-gems/llm-stack again after the llm-stack merge (29fe64b).
Evidence: main
00b91803(commits85a6dde8,21dd9a64,f050bf5b,68548df0). Against llm-stack main 29fe64b: os-me 447 runs, os-adr 144, os-sdlc 3140, takedown 608, 0 failures, 0 errors. No-model regression fixed upstream in llm-stack #21 (7c80f38). Lock diffs are only the llm-stack ruby_llm pin line.Follow-ups: none. ADR-0301 left unchanged on purpose: it is superseded by 0303 and the amend gate rejects pointers on superseded history. ADR-0303 decision 4 wording about path: as a local override is a doc nit, dropped.