decision-system S4c: lever fields in PredictionParser and PredictionLog, and the os-me-route CLI #1263

Closed
opened 2026-10-23 17:10:39 +00:00 by jared · 2 comments
Owner

Problem

The prediction log does not record the lever labels of a prediction. PredictionParser reads only the ruling text and the Confidence: line. os-sdlc flows (S5) must get the labels of a row without a gem dependency between plugins, and no command prints them. The design is in docs/decision-system/operations.md (PredictionParser extension) and docs/decision-system/build-plan.md S4. This ticket is the third of three S4 tickets. It uses Row, AnswerSource, WrongCost::Assessor, and RequiredBand from S4a.

Approach

PredictionParser#parse reads four more lines from the prediction text. It adds their values to each ruling hash. It removes these lines from the ruling text, as it removes the Confidence: line today. A field whose line is absent is not in the hash.

Line Pattern (as in operations.md) Hash keys and types
Source: precedent /^Source:\s*(\w+)/ source: String
Cost: medium /^Cost:\s*(\w+)/ cost: String
Band: 80 /^Band:\s*(\d+)/ band: Integer
Evidence: 55% -> 82% /^Evidence:\s*(\d+)%\s*->\s*(\d+)%/ evidence_before: Integer, evidence_after: Integer

PredictionLog::FIELDS gains source cost band evidence_before evidence_after, after beat_note. Old records read these fields as nil.

The command os-me-route ROW_FILE reads the whole file as the row text. It asks the model through DecisionModel::Client with the HeadlessClaude adapter and no model flag. It asks four questions in this order: the AnswerSource question, then the WrongCost::Assessor questions hard_to_undo, compounds, stays_hidden. It prints one line of JSON (JSON.generate) and exits 0. The keys are in this order:

{"source":"facts","max_rung":2,"novel":false,"wrong_cost":{"hard_to_undo":false,"compounds":true,"stays_hidden":false},"level":"medium","required_band":70}

required_band is null for the level low. O-9 is settled: os-sdlc calls this CLI; the levers stay in os-me. The ADR challenge rejected an ADR.

Required work:

  1. Read the Source:, Cost:, Band:, and Evidence: lines in PredictionParser — check: present Evidence: in plugins/os-me/lib/os_me/prediction_parser.rb
  2. Add source cost band evidence_before evidence_after to PredictionLog::FIELDS — check: present evidence_before in plugins/os-me/lib/os_me/prediction_log.rb
  3. Add RowLabels, which builds the labels of one row and gives the hash for the JSON line — check: present class RowLabels in plugins/os-me/lib/os_me/row_labels.rb
  4. Add the route ROW_FILE command to OsMe::CLI — check: present def route(row_file) in plugins/os-me/lib/os_me/cli.rb
  5. Add the executable bin/os-me-route, which starts OsMe::CLI with route — check: present ['route', *ARGV] in plugins/os-me/bin/os-me-route

Acceptance criteria

  1. Given the text a. Keep StepOutcome in Gate.\nConfidence: 80%\nSource: precedent\nCost: medium\nBand: 80\nEvidence: 55% -> 82%, when PredictionParser#parse runs, then it returns [{ id: "session:a", text: "Keep StepOutcome in Gate.", confidence: 80, triggers: [], source: "precedent", cost: "medium", band: 80, evidence_before: 55, evidence_after: 82 }].
  2. Given the text Jared would accept the plan.\nConfidence: 60%, when PredictionParser#parse runs, then it returns [{ id: "session:1", text: "Jared would accept the plan.", confidence: 60, triggers: [] }].
  3. Given the text a. Split it.\nb. Defer it.\nConfidence: 65%\nBand: 65\nEvidence: 50%->65%, when PredictionParser#parse runs, then both rulings have band: 65, evidence_before: 50, and evidence_after: 65, and their texts are Split it. and Defer it.
  4. Given a log line {"id":"session:a","session_id":"s1","confidence":80}, when PredictionLog#records runs, then the record has "source", "cost", "band", "evidence_before", and "evidence_after", each nil.
  5. Given a transcript whose last assistant message is the text of criterion 1, when os-me-eval append TRANSCRIPT runs, then the log record has "source" => "precedent", "cost" => "medium", "band" => 80, "evidence_before" => 55, and "evidence_after" => 82.
  6. Given a row file with the text Move Gate::StepOutcome to OsSdlc::StepOutcome. and a fake claude that answers facts, no, yes, no in turn, when os-me-route ROW_FILE runs, then stdout is exactly the JSON line in Approach plus a newline, and the exit status is 0.
  7. Given a row file and a fake claude that answers taste, no, no, no, when os-me-route ROW_FILE runs, then stdout is {"source":"taste","max_rung":0,"novel":true,"wrong_cost":{"hard_to_undo":false,"compounds":false,"stays_hidden":false},"level":"low","required_band":null} plus a newline.
  8. Given a row file and a fake claude that answers facts, yes, yes, yes, when os-me-route ROW_FILE runs, then the JSON has "level":"high" and "required_band":85.
  9. Given the row file of criterion 6, when os-me-route ROW_FILE runs, then the fake claude gets four calls, and each prompt includes Move Gate::StepOutcome to OsSdlc::StepOutcome.
  10. Given a fake claude whose first answer is not one of the five sources, when os-me-route ROW_FILE runs, then the exit status is not 0 and stdout is empty.

Owner sketch

class OsMe::PredictionParser
  LEVER_FIELDS = {
    source: /^Source:\s*(\w+)/, cost: /^Cost:\s*(\w+)/, band: /^Band:\s*(\d+)/,
    evidence: /^Evidence:\s*(\d+)%\s*->\s*(\d+)%/
  }.freeze
  def parse(text); end
end

class OsMe::RowLabels
  def self.of(row, client:); end
  def to_h; end
end

class OsMe::CLI < Thor
  desc 'route ROW_FILE', 'print the lever labels of one row as JSON'
  def route(row_file); end
end

Non-goals

  • The PredictLoop text in plugins/os-me/skills/predict/SKILL.md. The orchestrator writes it through the fast path.
  • The Store SUGGESTIONS.md line for brief.md. The orchestrator writes it.
  • The stage gate (five real predictions carry all four fields). The orchestrator checks it after the skill text ships.
  • Bold markdown forms such as **Band:** 80. The parser reads the plain forms in the table only.
  • Routing in os-sdlc flows and RowRouter (S5).

Blocked by: #1246 (Row, AnswerSource, WrongCost::Assessor, RequiredBand).

Origin

  • Trigger: User asked for the decision-system build tickets this session. Rewritten 2026-10-23: split #1246 with exact criteria after the #1245 lesson.
  • Improvised this session: none
  • Chain: off-design S3 build ← acceptance criteria too loose ← first ticket draft. Stop at DESIGN (docs/decision-system/operations.md).
  • Root candidate: none found; searched WrongCost, levers, os-me, predict
  • Where: docs/decision-system/build-plan.md S4; docs/decision-system/operations.md PredictionParser; plugins/os-me/lib/os_me/prediction_parser.rb; plugins/os-me/lib/os_me/prediction_log.rb; plugins/os-me/lib/os_me/cli.rb; plugins/os-me/bin/os-me-route
  • 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 stage split of #1246; backlog search only)

## Problem The prediction log does not record the lever labels of a prediction. `PredictionParser` reads only the ruling text and the `Confidence:` line. os-sdlc flows (S5) must get the labels of a row without a gem dependency between plugins, and no command prints them. The design is in `docs/decision-system/operations.md` (`PredictionParser` extension) and `docs/decision-system/build-plan.md` S4. This ticket is the third of three S4 tickets. It uses `Row`, `AnswerSource`, `WrongCost::Assessor`, and `RequiredBand` from S4a. ## Approach `PredictionParser#parse` reads four more lines from the prediction text. It adds their values to each ruling hash. It removes these lines from the ruling text, as it removes the `Confidence:` line today. A field whose line is absent is not in the hash. | Line | Pattern (as in `operations.md`) | Hash keys and types | |---|---|---| | `Source: precedent` | `/^Source:\s*(\w+)/` | `source:` String | | `Cost: medium` | `/^Cost:\s*(\w+)/` | `cost:` String | | `Band: 80` | `/^Band:\s*(\d+)/` | `band:` Integer | | `Evidence: 55% -> 82%` | `/^Evidence:\s*(\d+)%\s*->\s*(\d+)%/` | `evidence_before:` Integer, `evidence_after:` Integer | `PredictionLog::FIELDS` gains `source cost band evidence_before evidence_after`, after `beat_note`. Old records read these fields as `nil`. The command `os-me-route ROW_FILE` reads the whole file as the row text. It asks the model through `DecisionModel::Client` with the `HeadlessClaude` adapter and no model flag. It asks four questions in this order: the `AnswerSource` question, then the `WrongCost::Assessor` questions `hard_to_undo`, `compounds`, `stays_hidden`. It prints one line of JSON (`JSON.generate`) and exits 0. The keys are in this order: ```json {"source":"facts","max_rung":2,"novel":false,"wrong_cost":{"hard_to_undo":false,"compounds":true,"stays_hidden":false},"level":"medium","required_band":70} ``` `required_band` is `null` for the level `low`. O-9 is settled: os-sdlc calls this CLI; the levers stay in os-me. The ADR challenge rejected an ADR. Required work: 1. Read the `Source:`, `Cost:`, `Band:`, and `Evidence:` lines in `PredictionParser` — check: present `Evidence:` in `plugins/os-me/lib/os_me/prediction_parser.rb` 2. Add `source cost band evidence_before evidence_after` to `PredictionLog::FIELDS` — check: present `evidence_before` in `plugins/os-me/lib/os_me/prediction_log.rb` 3. Add `RowLabels`, which builds the labels of one row and gives the hash for the JSON line — check: present `class RowLabels` in `plugins/os-me/lib/os_me/row_labels.rb` 4. Add the `route ROW_FILE` command to `OsMe::CLI` — check: present `def route(row_file)` in `plugins/os-me/lib/os_me/cli.rb` 5. Add the executable `bin/os-me-route`, which starts `OsMe::CLI` with `route` — check: present `['route', *ARGV]` in `plugins/os-me/bin/os-me-route` ## Acceptance criteria 1. Given the text `a. Keep StepOutcome in Gate.\nConfidence: 80%\nSource: precedent\nCost: medium\nBand: 80\nEvidence: 55% -> 82%`, when `PredictionParser#parse` runs, then it returns `[{ id: "session:a", text: "Keep StepOutcome in Gate.", confidence: 80, triggers: [], source: "precedent", cost: "medium", band: 80, evidence_before: 55, evidence_after: 82 }]`. 2. Given the text `Jared would accept the plan.\nConfidence: 60%`, when `PredictionParser#parse` runs, then it returns `[{ id: "session:1", text: "Jared would accept the plan.", confidence: 60, triggers: [] }]`. 3. Given the text `a. Split it.\nb. Defer it.\nConfidence: 65%\nBand: 65\nEvidence: 50%->65%`, when `PredictionParser#parse` runs, then both rulings have `band: 65`, `evidence_before: 50`, and `evidence_after: 65`, and their texts are `Split it.` and `Defer it.` 4. Given a log line `{"id":"session:a","session_id":"s1","confidence":80}`, when `PredictionLog#records` runs, then the record has `"source"`, `"cost"`, `"band"`, `"evidence_before"`, and `"evidence_after"`, each `nil`. 5. Given a transcript whose last assistant message is the text of criterion 1, when `os-me-eval append TRANSCRIPT` runs, then the log record has `"source" => "precedent"`, `"cost" => "medium"`, `"band" => 80`, `"evidence_before" => 55`, and `"evidence_after" => 82`. 6. Given a row file with the text `Move Gate::StepOutcome to OsSdlc::StepOutcome.` and a fake `claude` that answers `facts`, `no`, `yes`, `no` in turn, when `os-me-route ROW_FILE` runs, then stdout is exactly the JSON line in Approach plus a newline, and the exit status is 0. 7. Given a row file and a fake `claude` that answers `taste`, `no`, `no`, `no`, when `os-me-route ROW_FILE` runs, then stdout is `{"source":"taste","max_rung":0,"novel":true,"wrong_cost":{"hard_to_undo":false,"compounds":false,"stays_hidden":false},"level":"low","required_band":null}` plus a newline. 8. Given a row file and a fake `claude` that answers `facts`, `yes`, `yes`, `yes`, when `os-me-route ROW_FILE` runs, then the JSON has `"level":"high"` and `"required_band":85`. 9. Given the row file of criterion 6, when `os-me-route ROW_FILE` runs, then the fake `claude` gets four calls, and each prompt includes `Move Gate::StepOutcome to OsSdlc::StepOutcome.` 10. Given a fake `claude` whose first answer is not one of the five sources, when `os-me-route ROW_FILE` runs, then the exit status is not 0 and stdout is empty. ## Owner sketch ```ruby class OsMe::PredictionParser LEVER_FIELDS = { source: /^Source:\s*(\w+)/, cost: /^Cost:\s*(\w+)/, band: /^Band:\s*(\d+)/, evidence: /^Evidence:\s*(\d+)%\s*->\s*(\d+)%/ }.freeze def parse(text); end end class OsMe::RowLabels def self.of(row, client:); end def to_h; end end class OsMe::CLI < Thor desc 'route ROW_FILE', 'print the lever labels of one row as JSON' def route(row_file); end end ``` ## Non-goals - The `PredictLoop` text in `plugins/os-me/skills/predict/SKILL.md`. The orchestrator writes it through the fast path. - The Store `SUGGESTIONS.md` line for `brief.md`. The orchestrator writes it. - The stage gate (five real predictions carry all four fields). The orchestrator checks it after the skill text ships. - Bold markdown forms such as `**Band:** 80`. The parser reads the plain forms in the table only. - Routing in os-sdlc flows and `RowRouter` (S5). Blocked by: #1246 (`Row`, `AnswerSource`, `WrongCost::Assessor`, `RequiredBand`). ## Origin - Trigger: User asked for the decision-system build tickets this session. Rewritten 2026-10-23: split #1246 with exact criteria after the #1245 lesson. - Improvised this session: none - Chain: off-design S3 build ← acceptance criteria too loose ← first ticket draft. Stop at `DESIGN` (`docs/decision-system/operations.md`). - Root candidate: none found; searched WrongCost, levers, os-me, predict - Where: docs/decision-system/build-plan.md S4; docs/decision-system/operations.md PredictionParser; plugins/os-me/lib/os_me/prediction_parser.rb; plugins/os-me/lib/os_me/prediction_log.rb; plugins/os-me/lib/os_me/cli.rb; plugins/os-me/bin/os-me-route - 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 stage split of #1246; backlog search only)
Author
Owner

Work started on branch ticket-1263 (/os-sdlc:implement, S4c).

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

Resolution

Done: S4c built: Source/Cost/Band/Evidence fields in PredictionParser and PredictionLog::FIELDS, RowLabels, and the os-me-route ROW_FILE CLI that prints the labels as JSON; implementation 130 complete.

Evidence: Branch ticket-1263, Runner commit; one test-review round; suite, lint and required-work gates green; contract audit passed first time; lever_fields_test.rb and route_cli_test.rb.

Follow-ups: The predict SKILL.md PredictLoop text and the Store SUGGESTIONS.md brief line are orchestrator work for S4 (fast path), not tickets. S5 #1247 wires os-sdlc flows to os-me-route.

## Resolution **Done:** S4c built: Source/Cost/Band/Evidence fields in PredictionParser and PredictionLog::FIELDS, RowLabels, and the os-me-route ROW_FILE CLI that prints the labels as JSON; implementation 130 complete. **Evidence:** Branch ticket-1263, Runner commit; one test-review round; suite, lint and required-work gates green; contract audit passed first time; lever_fields_test.rb and route_cli_test.rb. **Follow-ups:** The predict SKILL.md PredictLoop text and the Store SUGGESTIONS.md brief line are orchestrator work for S4 (fast path), not tickets. S5 #1247 wires os-sdlc flows to os-me-route.
jared closed this issue 2026-10-23 17:39:45 +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#1263
No description provided.