decision-system S8: decision-model gem adapters #1250

Closed
opened 2026-10-23 15:17:28 +00:00 by jared · 4 comments
Owner

Problem

os-me asks its model questions through the in-plugin port OsMe::DecisionModel (S2a). The port has one adapter, HeadlessClaude, and a Question that holds only text and options. The shared gem decision-model (module Decision::Model, email-triage #9) is now available. It gives Client#ask(state, questions) -> Reply, typed Question objects, one confidence formula for all adapters, and the CloudflareJev and SystemOne (Ollama) adapters. The gem has no Claude adapter. Thus os-me must keep a HeadlessClaude adapter that obeys the gem adapter contract: name and call(state:, questions:) -> Decision::Model::RawReply. This ticket is the first of two S8 tickets. It adds the gem dependency and the new adapter. S8b moves the callers to the gem and deletes the port.

Approach

O-7 is settled: the ADR challenge on 2026-10-23 rejected an ADR. One commit can reverse the dependency, and ADR-0166 and ADR-0257 already decide a private Forgejo gem over SSH. Name both ADRs in the commit message.

Add to plugins/os-me/Gemfile, after gem 'zeitwerk':

gem 'decision-model', git: 'ssh://git@forgejo.swansoncloud.com:2222/jared/decision-model.git'

Run bundle lock in plugins/os-me and commit Gemfile.lock. Add require 'decision/model' to plugins/os-me/lib/os_me.rb after require 'bundler/setup'.

Add OsMe::HeadlessClaude in plugins/os-me/lib/os_me/headless_claude.rb. It is a gem adapter. It does not replace OsMe::DecisionModel::HeadlessClaude in this ticket; S8b deletes the old class.

Item Exact value
name `"headless:#{model
Runner call @runner.structured(prompt:, cwd: Dir.tmpdir, model: @model, schema:)
prompt INSTRUCTION, a blank line, then JSON.generate(state: state, questions: Decision::Model::Wire::QuestionEncoder.call(questions))
INSTRUCTION Answer each question about the state. For a choice question, pick one option and give a probability from 0 to 1 for each option. For a noul question, give the probability from 0 to 1 that the answer is yes.
Return `Decision::Model::RawReply.new(answers: structured_output['answers'], usage: Decision::Model::Usage.new(input_tokens: result.input_tokens, output_tokens: result.output_tokens), model: "claude-#{model
No answers Hash in structured_output raise Decision::Model::ResponseError with the message headless claude gave no structured answers: #{result.text}

The schema (keys as strings after JSON.generate):

PROBABILITY = { type: 'number', minimum: 0, maximum: 1 }
{ type: 'object', required: ['answers'],
  properties: { answers: { type: 'object', required: questions.map(&:wire_key),
                           properties: questions.to_h { [it.wire_key, answer_schema(it)] } } } }
# choice question, with opts = question.options.map(&:to_s):
{ type: 'object', required: %w[choice probabilities],
  properties: { choice: { type: 'string', enum: opts },
                probabilities: { type: 'object', required: opts, properties: opts.to_h { [it, PROBABILITY] } } } }
# noul question:
{ type: 'object', required: ['noul'], properties: { noul: PROBABILITY } }

Required work:

  1. Add the decision-model git gem to the os-me Gemfile — check: present gem 'decision-model', git: 'ssh://git@forgejo.swansoncloud.com:2222/jared/decision-model.git' in plugins/os-me/Gemfile
  2. Lock the gem in the os-me lockfile — check: present decision-model in plugins/os-me/Gemfile.lock
  3. Load the gem at boot — check: present require 'decision/model' in plugins/os-me/lib/os_me.rb
  4. Add the gem adapter OsMe::HeadlessClaude — check: present def call(state:, questions:) in plugins/os-me/lib/os_me/headless_claude.rb
  5. Return a gem RawReply from the adapter — check: present Decision::Model::RawReply.new in plugins/os-me/lib/os_me/headless_claude.rb

Acceptance criteria

In criteria 3 to 9, runner is a fake with structured(**kwargs) that records kwargs and returns OsMe::HeadlessResult.new(text: 'TEXT', input_tokens: 120, output_tokens: 30, structured_output: OUT). shape is Decision::Model::Question.choice(:shape, criteria: { 'accept' => 'The reply approves it.', 'modify' => 'The reply changes it.' }, instructions: 'Classify the reply.'). set is Decision::Model::QuestionSet.new([shape]).

  1. Given plugins/os-me/lib/os_me.rb is required, when Decision::Model::Client and Decision::Model::Adapters::Fixed are referenced, then both are defined.
  2. Given OsMe::HeadlessClaude.new(runner:, model: 'sonnet'), when name runs, then it returns "headless:sonnet"; with no model:, it returns "headless:default".
  3. Given set and state { 'memo' => 'M', 'reply' => 'R' }, when call(state:, questions: set) runs, then the runner gets one call with cwd: Dir.tmpdir and model: 'sonnet', and kwargs[:prompt] equals "#{OsMe::HeadlessClaude::INSTRUCTION}\n\n#{JSON.generate(state: { 'memo' => 'M', 'reply' => 'R' }, questions: { 'shape' => { 'type' => 'choice', 'instructions' => 'Classify the reply.', 'criteria' => { 'accept' => 'The reply approves it.', 'modify' => 'The reply changes it.' } } })}".
  4. Given criterion 3, when the call runs, then JSON.parse(JSON.generate(kwargs[:schema])) equals {"type"=>"object","required"=>["answers"],"properties"=>{"answers"=>{"type"=>"object","required"=>["shape"],"properties"=>{"shape"=>{"type"=>"object","required"=>["choice","probabilities"],"properties"=>{"choice"=>{"type"=>"string","enum"=>["accept","modify"]},"probabilities"=>{"type"=>"object","required"=>["accept","modify"],"properties"=>{"accept"=>{"type"=>"number","minimum"=>0,"maximum"=>1},"modify"=>{"type"=>"number","minimum"=>0,"maximum"=>1}}}}}}}}}.
  5. Given OUT = { 'answers' => { 'shape' => { 'choice' => 'modify', 'probabilities' => { 'accept' => 0.2, 'modify' => 0.8 } } } }, when call(state: 'S', questions: set) runs, then it returns Decision::Model::RawReply.new(answers: OUT['answers'], usage: Decision::Model::Usage.new(input_tokens: 120, output_tokens: 30), model: 'claude-sonnet').
  6. Given the OUT of criterion 5, when Decision::Model::Client.new(OsMe::HeadlessClaude.new(runner:, model: 'sonnet')).ask('S', [shape]) runs, then the reply has adapter "headless:sonnet", and reply.fetch(:shape) has choice "modify", probabilities { 'accept' => 0.2, 'modify' => 0.8 }, confidence within 1e-6 of 0.6, and model "claude-sonnet".
  7. Given three noul questions Decision::Model::Question.noul(:hard_to_undo, instructions: 'A'), (:compounds, instructions: 'B'), (:stays_hidden, instructions: 'C') and OUT = { 'answers' => { 'hard_to_undo' => { 'noul' => 0.9 }, 'compounds' => { 'noul' => 0.2 }, 'stays_hidden' => { 'noul' => 0.5 } } }, when the client of criterion 6 asks them with state 'row', then the schema property of each key is {"type"=>"object","required"=>["noul"],"properties"=>{"noul"=>{"type"=>"number","minimum"=>0,"maximum"=>1}}}, and the answer choices in key order are [true, false, true], and reply.fetch(:hard_to_undo).value is 0.9.
  8. Given OUT = nil and result text '**none**', when call(state: 'S', questions: set) runs, then it raises Decision::Model::ResponseError whose message is headless claude gave no structured answers: **none**.
  9. Given OUT = { 'answers' => { 'other' => { 'noul' => 0.5 } } }, when the client of criterion 6 asks [shape], then it raises Decision::Model::ResponseError whose message starts with headless:sonnet: no answer for question :shape.
  10. Given a fake claude executable on PATH (the FakeClaudeFixture in plugins/os-me/test/fake_claude_fixture.rb) that replies with structured_output { answers: { shape: { choice: 'accept', probabilities: { accept: 0.9, modify: 0.1 } } } }, when OsMe::HeadlessClaude.new(runner: OsMe::HeadlessRunner.new) is called with set, then the argv holds --json-schema with the schema of criterion 4, --setting-sources with '', and no --model flag.

Owner sketch

module OsMe
  class HeadlessClaude
    INSTRUCTION = 'Answer each question about the state. ...'
    PROBABILITY = { type: 'number', minimum: 0, maximum: 1 }.freeze

    def initialize(runner:, model: nil); end
    def name; end
    def call(state:, questions:); end

    private

    def prompt(state, questions); end
    def schema(questions); end
    def answer_schema(question); end
    def raw_reply(result); end
  end
end

Non-goals

  • Moving ReplyShape, AnswerSource, WrongCost::Assessor, RowLabels, and MomentExtractor to the gem, and deleting OsMe::DecisionModel (S8b).
  • score questions through OsMe::HeadlessClaude. No caller asks one.
  • A check in the adapter that a choice is one of the options. The --json-schema enum limits the choice; the gem passes an unknown choice through to the caller.
  • os-me wrappers for Adapters::CloudflareJev and Adapters::SystemOne. They are in the gem.
  • The adapter comparison on the frozen 50-point sample and the S8 gate (agreement not worse than HeadlessClaude, lower cost per run). A later ticket does it after S8b.
  • Changes to the gem. The gem repo is read-only for this ticket.

Origin

  • Trigger: Rewritten 2026-10-23 with the gem's real contract (email-triage session 0daf3e86) and exact criteria.
  • Improvised this session: none
  • Chain: off-design S3 build ← acceptance criteria too loose ← first ticket draft. Stop at DESIGN (docs/decision-system/build-plan.md S8; gem docs/design/adapters.md).
  • Root candidate: none found; searched decision-model gem, HeadlessClaude, RawReply
  • Where: docs/decision-system/build-plan.md S8; docs/decision-system/decision-model.md; plugins/os-me/Gemfile; plugins/os-me/lib/os_me.rb; plugins/os-me/lib/os_me/headless_claude.rb; /home/jared/dev/ruby-gems/decision-model/docs/design/adapters.md
  • Session: 4f3f95ea-3c6f-4d23-a0ea-e349d6ab02a5
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/4f3f95ea-3c6f-4d23-a0ea-e349d6ab02a5.jsonl

VERDICT: CREATE (user-approved; backlog search only)

## Problem os-me asks its model questions through the in-plugin port `OsMe::DecisionModel` (S2a). The port has one adapter, `HeadlessClaude`, and a `Question` that holds only `text` and `options`. The shared gem `decision-model` (module `Decision::Model`, email-triage #9) is now available. It gives `Client#ask(state, questions)` -> `Reply`, typed `Question` objects, one confidence formula for all adapters, and the `CloudflareJev` and `SystemOne` (Ollama) adapters. The gem has no Claude adapter. Thus os-me must keep a `HeadlessClaude` adapter that obeys the gem adapter contract: `name` and `call(state:, questions:)` -> `Decision::Model::RawReply`. This ticket is the first of two S8 tickets. It adds the gem dependency and the new adapter. S8b moves the callers to the gem and deletes the port. ## Approach O-7 is settled: the ADR challenge on 2026-10-23 rejected an ADR. One commit can reverse the dependency, and ADR-0166 and ADR-0257 already decide a private Forgejo gem over SSH. Name both ADRs in the commit message. Add to `plugins/os-me/Gemfile`, after `gem 'zeitwerk'`: ```ruby gem 'decision-model', git: 'ssh://git@forgejo.swansoncloud.com:2222/jared/decision-model.git' ``` Run `bundle lock` in `plugins/os-me` and commit `Gemfile.lock`. Add `require 'decision/model'` to `plugins/os-me/lib/os_me.rb` after `require 'bundler/setup'`. Add `OsMe::HeadlessClaude` in `plugins/os-me/lib/os_me/headless_claude.rb`. It is a gem adapter. It does not replace `OsMe::DecisionModel::HeadlessClaude` in this ticket; S8b deletes the old class. | Item | Exact value | |---|---| | `name` | `"headless:#{model || 'default'}"` | | Runner call | `@runner.structured(prompt:, cwd: Dir.tmpdir, model: @model, schema:)` | | `prompt` | `INSTRUCTION`, a blank line, then `JSON.generate(state: state, questions: Decision::Model::Wire::QuestionEncoder.call(questions))` | | `INSTRUCTION` | `Answer each question about the state. For a choice question, pick one option and give a probability from 0 to 1 for each option. For a noul question, give the probability from 0 to 1 that the answer is yes.` | | Return | `Decision::Model::RawReply.new(answers: structured_output['answers'], usage: Decision::Model::Usage.new(input_tokens: result.input_tokens, output_tokens: result.output_tokens), model: "claude-#{model || 'default'}")` | | No `answers` Hash in `structured_output` | raise `Decision::Model::ResponseError` with the message `headless claude gave no structured answers: #{result.text}` | The schema (keys as strings after `JSON.generate`): ```ruby PROBABILITY = { type: 'number', minimum: 0, maximum: 1 } { type: 'object', required: ['answers'], properties: { answers: { type: 'object', required: questions.map(&:wire_key), properties: questions.to_h { [it.wire_key, answer_schema(it)] } } } } # choice question, with opts = question.options.map(&:to_s): { type: 'object', required: %w[choice probabilities], properties: { choice: { type: 'string', enum: opts }, probabilities: { type: 'object', required: opts, properties: opts.to_h { [it, PROBABILITY] } } } } # noul question: { type: 'object', required: ['noul'], properties: { noul: PROBABILITY } } ``` Required work: 1. Add the `decision-model` git gem to the os-me Gemfile — check: present `gem 'decision-model', git: 'ssh://git@forgejo.swansoncloud.com:2222/jared/decision-model.git'` in `plugins/os-me/Gemfile` 2. Lock the gem in the os-me lockfile — check: present `decision-model` in `plugins/os-me/Gemfile.lock` 3. Load the gem at boot — check: present `require 'decision/model'` in `plugins/os-me/lib/os_me.rb` 4. Add the gem adapter `OsMe::HeadlessClaude` — check: present `def call(state:, questions:)` in `plugins/os-me/lib/os_me/headless_claude.rb` 5. Return a gem `RawReply` from the adapter — check: present `Decision::Model::RawReply.new` in `plugins/os-me/lib/os_me/headless_claude.rb` ## Acceptance criteria In criteria 3 to 9, `runner` is a fake with `structured(**kwargs)` that records `kwargs` and returns `OsMe::HeadlessResult.new(text: 'TEXT', input_tokens: 120, output_tokens: 30, structured_output: OUT)`. `shape` is `Decision::Model::Question.choice(:shape, criteria: { 'accept' => 'The reply approves it.', 'modify' => 'The reply changes it.' }, instructions: 'Classify the reply.')`. `set` is `Decision::Model::QuestionSet.new([shape])`. 1. Given `plugins/os-me/lib/os_me.rb` is required, when `Decision::Model::Client` and `Decision::Model::Adapters::Fixed` are referenced, then both are defined. 2. Given `OsMe::HeadlessClaude.new(runner:, model: 'sonnet')`, when `name` runs, then it returns `"headless:sonnet"`; with no `model:`, it returns `"headless:default"`. 3. Given `set` and state `{ 'memo' => 'M', 'reply' => 'R' }`, when `call(state:, questions: set)` runs, then the runner gets one call with `cwd: Dir.tmpdir` and `model: 'sonnet'`, and `kwargs[:prompt]` equals `"#{OsMe::HeadlessClaude::INSTRUCTION}\n\n#{JSON.generate(state: { 'memo' => 'M', 'reply' => 'R' }, questions: { 'shape' => { 'type' => 'choice', 'instructions' => 'Classify the reply.', 'criteria' => { 'accept' => 'The reply approves it.', 'modify' => 'The reply changes it.' } } })}"`. 4. Given criterion 3, when the call runs, then `JSON.parse(JSON.generate(kwargs[:schema]))` equals `{"type"=>"object","required"=>["answers"],"properties"=>{"answers"=>{"type"=>"object","required"=>["shape"],"properties"=>{"shape"=>{"type"=>"object","required"=>["choice","probabilities"],"properties"=>{"choice"=>{"type"=>"string","enum"=>["accept","modify"]},"probabilities"=>{"type"=>"object","required"=>["accept","modify"],"properties"=>{"accept"=>{"type"=>"number","minimum"=>0,"maximum"=>1},"modify"=>{"type"=>"number","minimum"=>0,"maximum"=>1}}}}}}}}}`. 5. Given `OUT = { 'answers' => { 'shape' => { 'choice' => 'modify', 'probabilities' => { 'accept' => 0.2, 'modify' => 0.8 } } } }`, when `call(state: 'S', questions: set)` runs, then it returns `Decision::Model::RawReply.new(answers: OUT['answers'], usage: Decision::Model::Usage.new(input_tokens: 120, output_tokens: 30), model: 'claude-sonnet')`. 6. Given the `OUT` of criterion 5, when `Decision::Model::Client.new(OsMe::HeadlessClaude.new(runner:, model: 'sonnet')).ask('S', [shape])` runs, then the reply has `adapter` `"headless:sonnet"`, and `reply.fetch(:shape)` has `choice` `"modify"`, `probabilities` `{ 'accept' => 0.2, 'modify' => 0.8 }`, `confidence` within 1e-6 of `0.6`, and `model` `"claude-sonnet"`. 7. Given three noul questions `Decision::Model::Question.noul(:hard_to_undo, instructions: 'A')`, `(:compounds, instructions: 'B')`, `(:stays_hidden, instructions: 'C')` and `OUT = { 'answers' => { 'hard_to_undo' => { 'noul' => 0.9 }, 'compounds' => { 'noul' => 0.2 }, 'stays_hidden' => { 'noul' => 0.5 } } }`, when the client of criterion 6 asks them with state `'row'`, then the schema property of each key is `{"type"=>"object","required"=>["noul"],"properties"=>{"noul"=>{"type"=>"number","minimum"=>0,"maximum"=>1}}}`, and the answer choices in key order are `[true, false, true]`, and `reply.fetch(:hard_to_undo).value` is `0.9`. 8. Given `OUT = nil` and result text `'**none**'`, when `call(state: 'S', questions: set)` runs, then it raises `Decision::Model::ResponseError` whose message is `headless claude gave no structured answers: **none**`. 9. Given `OUT = { 'answers' => { 'other' => { 'noul' => 0.5 } } }`, when the client of criterion 6 asks `[shape]`, then it raises `Decision::Model::ResponseError` whose message starts with `headless:sonnet: no answer for question :shape`. 10. Given a fake `claude` executable on `PATH` (the `FakeClaudeFixture` in `plugins/os-me/test/fake_claude_fixture.rb`) that replies with `structured_output` `{ answers: { shape: { choice: 'accept', probabilities: { accept: 0.9, modify: 0.1 } } } }`, when `OsMe::HeadlessClaude.new(runner: OsMe::HeadlessRunner.new)` is called with `set`, then the argv holds `--json-schema` with the schema of criterion 4, `--setting-sources` with `''`, and no `--model` flag. ## Owner sketch ```ruby module OsMe class HeadlessClaude INSTRUCTION = 'Answer each question about the state. ...' PROBABILITY = { type: 'number', minimum: 0, maximum: 1 }.freeze def initialize(runner:, model: nil); end def name; end def call(state:, questions:); end private def prompt(state, questions); end def schema(questions); end def answer_schema(question); end def raw_reply(result); end end end ``` ## Non-goals - Moving `ReplyShape`, `AnswerSource`, `WrongCost::Assessor`, `RowLabels`, and `MomentExtractor` to the gem, and deleting `OsMe::DecisionModel` (S8b). - `score` questions through `OsMe::HeadlessClaude`. No caller asks one. - A check in the adapter that a choice is one of the options. The `--json-schema` enum limits the choice; the gem passes an unknown choice through to the caller. - os-me wrappers for `Adapters::CloudflareJev` and `Adapters::SystemOne`. They are in the gem. - The adapter comparison on the frozen 50-point sample and the S8 gate (agreement not worse than `HeadlessClaude`, lower cost per run). A later ticket does it after S8b. - Changes to the gem. The gem repo is read-only for this ticket. ## Origin - Trigger: Rewritten 2026-10-23 with the gem's real contract (email-triage session 0daf3e86) and exact criteria. - Improvised this session: none - Chain: off-design S3 build ← acceptance criteria too loose ← first ticket draft. Stop at `DESIGN` (`docs/decision-system/build-plan.md` S8; gem `docs/design/adapters.md`). - Root candidate: none found; searched decision-model gem, HeadlessClaude, RawReply - Where: docs/decision-system/build-plan.md S8; docs/decision-system/decision-model.md; plugins/os-me/Gemfile; plugins/os-me/lib/os_me.rb; plugins/os-me/lib/os_me/headless_claude.rb; /home/jared/dev/ruby-gems/decision-model/docs/design/adapters.md - Session: 4f3f95ea-3c6f-4d23-a0ea-e349d6ab02a5 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/4f3f95ea-3c6f-4d23-a0ea-e349d6ab02a5.jsonl VERDICT: CREATE (user-approved; backlog search only)
Author
Owner

Unblocked 2026-10-23 (message from email-triage session 0daf3e86). Gem decision-model (module Decision::Model), repo jared/decision-model, local /home/jared/dev/ruby-gems/decision-model; Gemfile: gem "decision-model", git: "ssh://git@forgejo.swansoncloud.com:2222/jared/decision-model.git". Read docs/design/core.md and adapters.md in the gem first. Contract changes against docs/decision-system/decision-model.md: (1) an adapter's call(state:, questions:) returns a RawReply; Client#ask(state, questions) returns a Reply (read-only hash by key, plus usage, latency, model, adapter); HeadlessClaude must return a RawReply. (2) Question.new(key:, type:, criteria:, instructions:), no options:; options come from criteria keys; types choice, noul, score. (3) Answer fields: key, type, choice, probabilities, confidence (margin, D-11), entropy_confidence (D-13), vendor_confidence, value, model; answer.probability(option = true). (4) CriteriaRules runs by default in QuestionSet. (5) Adapters::Fixed for caller tests; Price + Usage#+ give cost per run. O-8 done: Ollama 0.40.9 at http://localhost:11434, models tev1:4b (gem default), d3:4b, d3-4b-16k, nimble-v3 (CC BY-NC, personal only). Compare confidence/entropy_confidence, not vendor_confidence. Gem S4/S5 (building blocks, comparison) still in progress.

Unblocked 2026-10-23 (message from email-triage session 0daf3e86). Gem `decision-model` (module `Decision::Model`), repo jared/decision-model, local /home/jared/dev/ruby-gems/decision-model; Gemfile: `gem "decision-model", git: "ssh://git@forgejo.swansoncloud.com:2222/jared/decision-model.git"`. Read `docs/design/core.md` and `adapters.md` in the gem first. Contract changes against docs/decision-system/decision-model.md: (1) an adapter's `call(state:, questions:)` returns a `RawReply`; `Client#ask(state, questions)` returns a `Reply` (read-only hash by key, plus usage, latency, model, adapter); HeadlessClaude must return a RawReply. (2) `Question.new(key:, type:, criteria:, instructions:)`, no `options:`; options come from criteria keys; types choice, noul, score. (3) Answer fields: key, type, choice, probabilities, confidence (margin, D-11), entropy_confidence (D-13), vendor_confidence, value, model; `answer.probability(option = true)`. (4) CriteriaRules runs by default in QuestionSet. (5) `Adapters::Fixed` for caller tests; `Price` + `Usage#+` give cost per run. O-8 done: Ollama 0.40.9 at http://localhost:11434, models tev1:4b (gem default), d3:4b, d3-4b-16k, nimble-v3 (CC BY-NC, personal only). Compare `confidence`/`entropy_confidence`, not `vendor_confidence`. Gem S4/S5 (building blocks, comparison) still in progress.
Author
Owner

Contract changes from the shared gem (email-triage #9), for this ticket. Gem: decision-model, module Decision::Model, https://forgejo.swansoncloud.com/jared/decision-model (gem "decision-model", git: "ssh://git@forgejo.swansoncloud.com:2222/jared/decision-model.git"). Design: docs/design/core.md, adapters.md in the gem repo.

Item cc-os draft (docs/decision-system/decision-model.md) Gem
Adapter return {key => Answer} RawReply (wire answers, usage, model)
Client#ask(state, questions) {key => Answer} Reply: read-only hash by key plus usage, latency, model, adapter
Question (key, type, options, criteria, instructions) Question.new(key:, type:, criteria:, instructions:); options come from criteria keys; Ollama requires instructions ("" accepted)
Answer (key, choice, probabilities, confidence, model) adds type, entropy_confidence, vendor_confidence, value (noul p, score); confidence = jev margin formula on every adapter
Cloudflare CloudflareJev CloudflareJev.build(account: CloudflareAccount.new(...)); caller passes credentials
Tests — Adapters::Fixed for caller tests; Price and Usage#+ for cost per run

HeadlessClaude stays in cc-os and must return a RawReply. Ollama 0.40.9 with tev1:4b, d3:4b, d3-4b-16k is installed (O-8 done); nimble-v3 is pulling.

Contract changes from the shared gem (email-triage #9), for this ticket. Gem: `decision-model`, module `Decision::Model`, https://forgejo.swansoncloud.com/jared/decision-model (`gem "decision-model", git: "ssh://git@forgejo.swansoncloud.com:2222/jared/decision-model.git"`). Design: `docs/design/core.md`, `adapters.md` in the gem repo. | Item | cc-os draft (`docs/decision-system/decision-model.md`) | Gem | |---|---|---| | Adapter return | `{key => Answer}` | `RawReply` (wire answers, usage, model) | | `Client#ask(state, questions)` | `{key => Answer}` | `Reply`: read-only hash by key plus `usage`, `latency`, `model`, `adapter` | | `Question` | `(key, type, options, criteria, instructions)` | `Question.new(key:, type:, criteria:, instructions:)`; options come from `criteria` keys; Ollama requires `instructions` ("" accepted) | | `Answer` | `(key, choice, probabilities, confidence, model)` | adds `type`, `entropy_confidence`, `vendor_confidence`, `value` (noul p, score); `confidence` = jev margin formula on every adapter | | Cloudflare | `CloudflareJev` | `CloudflareJev.build(account: CloudflareAccount.new(...))`; caller passes credentials | | Tests | — | `Adapters::Fixed` for caller tests; `Price` and `Usage#+` for cost per run | `HeadlessClaude` stays in cc-os and must return a `RawReply`. Ollama 0.40.9 with `tev1:4b`, `d3:4b`, `d3-4b-16k` is installed (O-8 done); `nimble-v3` is pulling.
Author
Owner

Work started on branch ticket-1250 (/os-sdlc:implement, S8).

Work started on branch ticket-1250 (/os-sdlc:implement, S8).
Author
Owner

Resolution

Done: OsMe::HeadlessClaude adapter added; decision-model gem in os-me Gemfile

Evidence: implementation 133 merge-ready; suite and lint gates pass; commit on ticket-1250 touches headless_claude.rb, headless_claude_test.rb, Gemfile, Gemfile.lock, os_me.rb

Follow-ups: #1266 (S8b) moves the callers to the gem with CriteriaRules::Off; adapter comparison and the S8 gate get a ticket after S8b

## Resolution **Done:** OsMe::HeadlessClaude adapter added; decision-model gem in os-me Gemfile **Evidence:** implementation 133 merge-ready; suite and lint gates pass; commit on ticket-1250 touches headless_claude.rb, headless_claude_test.rb, Gemfile, Gemfile.lock, os_me.rb **Follow-ups:** #1266 (S8b) moves the callers to the gem with CriteriaRules::Off; adapter comparison and the S8 gate get a ticket after S8b
jared closed this issue 2026-10-23 18:21:44 +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#1250
No description provided.