os-me: move HeadlessRunner onto llm-stack #1438

Closed
opened 2026-10-27 18:11:57 +00:00 by jared · 2 comments
Owner

Problem

OsMe::HeadlessRunner in plugins/os-me/lib/os_me/headless_runner.rb builds a claude -p argv, runs it with Open3, and parses the JSON reply for text, tokens, and cost.
It has no timeout today.
ADR-0301 puts this work in the llm-stack gem.
Was blocked by #1437, merged 2026-10-27 at 0faed2ff.

Reproduce

n/a: this is a feature, not a defect.

Approach

Required work:

  1. Add gem "llm-stack", git: "ssh://git@forgejo.swansoncloud.com:2222/jared/llm-stack.git", require: "llm" to plugins/os-me/Gemfile and commit plugins/os-me/Gemfile.lock (ADR-0166) — check: present llm-stack in plugins/os-me/Gemfile.lock
  2. Replace the Open3 run in HeadlessRunner#run with LLM::Client#chat on a claude_code profile; build the profile settings from the HeadlessRequest fields cwd, model, tools, and read_roots (gem settings cwd, allowed_tools, read_roots), and set a timeout, which the runner lacks today — check: absent Open3 in plugins/os-me/lib/os_me/headless_runner.rb
  3. Build HeadlessResult from the gem Response: text, usage for the token counts, and cost for cost_usd; keep structured_output for the schema path — check: present Response in plugins/os-me/lib/os_me/headless_runner.rb
  4. Remove HeadlessRunner#parse and HeadlessRunner#input_tokens, and the flag builders in HeadlessRequest#argv (allowed_flags, model_flags, structured_flags, root_flags), because the gem now owns the CLI flags and the reply parse — check: absent structured_flags in plugins/os-me/lib/os_me/headless_request.rb
  5. Keep the narrow permission rule: HeadlessRequest#allow_rules still builds the Tool(/root/**) rules, and the profile's allowed_tools setting carries them — check: present allow_rules in plugins/os-me/lib/os_me/headless_request.rb
  6. Update plugins/os-me/test/headless_runner_test.rb and plugins/os-me/test/test_helper.rb to use a fake claude script through the profile executable setting; no test runs the real binary — check: absent command(prompt: in plugins/os-me/test/headless_runner_test.rb

Orchestrator work: run bin/refresh-plugins and add one History row to the os-me leaf under docs/implementation-status/.

Acceptance criteria

  1. Given a fake claude that returns text, usage, and cost, when HeadlessRunner#call runs, then the HeadlessResult holds the same text, token counts, and cost.
  2. Given a schema: argument, when HeadlessRunner#structured runs against a fake claude that returns structured output, then the result holds that structured output.
  3. Given a fake claude that never answers, when HeadlessRunner#call runs, then it raises after the timeout.
  4. Given a fake claude that reports is_error, when HeadlessRunner#call runs, then it raises with the reported message.
  5. Given tools and read_roots, when HeadlessRunner#call runs, then the fake claude records the same allow rules and --add-dir values that the old command built.

Owner sketch

module OsMe
  class HeadlessRunner
    def initialize(executable: 'claude', timeout: 900)
    end

    def call(prompt:, cwd:, model: nil, tools: [], read_roots: [])
    end

    def structured(prompt:, cwd:, schema:, model: nil)
    end
  end
end

Non-goals

  • plugins/os-me/lib/os_me/headless_claude.rb: it moves in spec section 6 step 5.
  • The Ollama embedder and the Jev decision callers.
  • Change to HeadlessResult fields or to the callers of HeadlessRunner.
  • A live call to the real claude binary in any test.

Origin

  • Trigger: Spec docs/llm-stack/spec.md section 6 step 3 lists os-me HeadlessRunner.
  • Improvised this session: none
  • Chain: HeadlessRunner owns its own claude -p argv, process run, and reply parse ← ADR-0166 and ADR-0279 forbid a shared model layer outside a pinned gem ← DESIGN (docs/adr/0301-llm-stack-gem-is-the-shared-llm-layer-pinned-per-plugin-from-forgejo.md)
  • Root candidate: none found; searched HeadlessRunner, headless_runner, llm-stack, llm
  • Where: plugins/os-me/lib/os_me/headless_runner.rb
  • Session: 375f16d0-a830-4297-b9f5-3acb6090a73f
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/375f16d0-a830-4297-b9f5-3acb6090a73f.jsonl
    VERDICT: CREATE
    REASON: Human-ruled work from ADR-0301 and spec section 3.4; no open ticket with the same mechanism.
## Problem `OsMe::HeadlessRunner` in `plugins/os-me/lib/os_me/headless_runner.rb` builds a `claude -p` argv, runs it with Open3, and parses the JSON reply for text, tokens, and cost. It has no timeout today. ADR-0301 puts this work in the `llm-stack` gem. Was blocked by #1437, merged 2026-10-27 at 0faed2ff. ## Reproduce n/a: this is a feature, not a defect. ## Approach Required work: 1. Add `gem "llm-stack", git: "ssh://git@forgejo.swansoncloud.com:2222/jared/llm-stack.git", require: "llm"` to `plugins/os-me/Gemfile` and commit `plugins/os-me/Gemfile.lock` (ADR-0166) — check: present `llm-stack` in `plugins/os-me/Gemfile.lock` 2. Replace the Open3 run in `HeadlessRunner#run` with `LLM::Client#chat` on a `claude_code` profile; build the profile settings from the `HeadlessRequest` fields `cwd`, `model`, `tools`, and `read_roots` (gem settings `cwd`, `allowed_tools`, `read_roots`), and set a timeout, which the runner lacks today — check: absent `Open3` in `plugins/os-me/lib/os_me/headless_runner.rb` 3. Build `HeadlessResult` from the gem `Response`: `text`, `usage` for the token counts, and `cost` for `cost_usd`; keep `structured_output` for the schema path — check: present `Response` in `plugins/os-me/lib/os_me/headless_runner.rb` 4. Remove `HeadlessRunner#parse` and `HeadlessRunner#input_tokens`, and the flag builders in `HeadlessRequest#argv` (`allowed_flags`, `model_flags`, `structured_flags`, `root_flags`), because the gem now owns the CLI flags and the reply parse — check: absent `structured_flags` in `plugins/os-me/lib/os_me/headless_request.rb` 5. Keep the narrow permission rule: `HeadlessRequest#allow_rules` still builds the `Tool(/root/**)` rules, and the profile's `allowed_tools` setting carries them — check: present `allow_rules` in `plugins/os-me/lib/os_me/headless_request.rb` 6. Update `plugins/os-me/test/headless_runner_test.rb` and `plugins/os-me/test/test_helper.rb` to use a fake `claude` script through the profile `executable` setting; no test runs the real binary — check: absent `command(prompt:` in `plugins/os-me/test/headless_runner_test.rb` Orchestrator work: run `bin/refresh-plugins` and add one History row to the os-me leaf under `docs/implementation-status/`. ## Acceptance criteria 1. Given a fake `claude` that returns text, usage, and cost, when `HeadlessRunner#call` runs, then the `HeadlessResult` holds the same text, token counts, and cost. 2. Given a `schema:` argument, when `HeadlessRunner#structured` runs against a fake `claude` that returns structured output, then the result holds that structured output. 3. Given a fake `claude` that never answers, when `HeadlessRunner#call` runs, then it raises after the timeout. 4. Given a fake `claude` that reports `is_error`, when `HeadlessRunner#call` runs, then it raises with the reported message. 5. Given `tools` and `read_roots`, when `HeadlessRunner#call` runs, then the fake `claude` records the same allow rules and `--add-dir` values that the old command built. ## Owner sketch ```ruby module OsMe class HeadlessRunner def initialize(executable: 'claude', timeout: 900) end def call(prompt:, cwd:, model: nil, tools: [], read_roots: []) end def structured(prompt:, cwd:, schema:, model: nil) end end end ``` ## Non-goals - `plugins/os-me/lib/os_me/headless_claude.rb`: it moves in spec section 6 step 5. - The Ollama embedder and the Jev decision callers. - Change to `HeadlessResult` fields or to the callers of `HeadlessRunner`. - A live call to the real `claude` binary in any test. ## Origin - Trigger: Spec docs/llm-stack/spec.md section 6 step 3 lists os-me HeadlessRunner. - Improvised this session: none - Chain: HeadlessRunner owns its own claude -p argv, process run, and reply parse ← ADR-0166 and ADR-0279 forbid a shared model layer outside a pinned gem ← DESIGN (docs/adr/0301-llm-stack-gem-is-the-shared-llm-layer-pinned-per-plugin-from-forgejo.md) - Root candidate: none found; searched HeadlessRunner, headless_runner, llm-stack, llm - Where: plugins/os-me/lib/os_me/headless_runner.rb - Session: 375f16d0-a830-4297-b9f5-3acb6090a73f - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/375f16d0-a830-4297-b9f5-3acb6090a73f.jsonl VERDICT: CREATE REASON: Human-ruled work from ADR-0301 and spec section 3.4; no open ticket with the same mechanism.
Author
Owner

Work started on branch ticket-1438 via /os-sdlc:implement (session 375f16d0). Body checks corrected to the current HeadlessRequest/HeadlessRunner split before open.

Work started on branch ticket-1438 via /os-sdlc:implement (session 375f16d0). Body checks corrected to the current HeadlessRequest/HeadlessRunner split before open.
Author
Owner

Resolution

Done: HeadlessRunner calls LLM::Client#chat on a claude_code profile built by HeadlessRequest#profile; HeadlessResult comes from the gem Response; Open3 and the argv builders are gone; plugins/os-me Gemfile and lockfile pin llm-stack 87fc9a4.

Evidence: Commit 271555af merged to main at f5c043a5. os-me suite 442 runs, 808 assertions, 0 failures. Root rubocop clean on changed files. Design audit (contract-auditor, opus): no fix-required findings. Gem change 87fc9a4 omits --model when the profile has none.

Follow-ups: none

## Resolution **Done:** HeadlessRunner calls LLM::Client#chat on a claude_code profile built by HeadlessRequest#profile; HeadlessResult comes from the gem Response; Open3 and the argv builders are gone; plugins/os-me Gemfile and lockfile pin llm-stack 87fc9a4. **Evidence:** Commit 271555af merged to main at f5c043a5. os-me suite 442 runs, 808 assertions, 0 failures. Root rubocop clean on changed files. Design audit (contract-auditor, opus): no fix-required findings. Gem change 87fc9a4 omits --model when the profile has none. **Follow-ups:** none
jared closed this issue 2026-10-27 19:07:49 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#1438
No description provided.