os-me-extract: structured DecisionModel answers, per-session cursor, Haiku model #1256

Closed
opened 2026-10-23 16:00:58 +00:00 by jared · 4 comments
Owner

Problem

os-me-extract crashed on its first session. ReplyShape sends the model a bare JSON blob with no instruction, no choice key and no list of allowed choices. HeadlessClaude runs JSON.parse on the free-text reply. The model replied **none**, and the parse error killed the run:

JSON::Ext::Parser.parse: unexpected character: '**none**' at line 1 column 1 (JSON::ParserError)

Two more defects sit on the same path. MomentExtractor#run marks sessions read only after the whole loop, so a crash after some appends makes a rerun duplicate those moments. MomentExtractor.build sets no model, so the CLI default runs instead of Haiku (docs/decision-system/decision-model.md:50).

Approach

Required work:

  1. Question carries options; ReplyShape sets the four shapes as options and puts an instruction before the memo and reply. check: present options in plugins/os-me/lib/os_me/decision_model/question.rb
  2. HeadlessRunner passes --json-schema (one required choice key, enum of the options), a system prompt, and --setting-sources '', following plugins/os-adr/lib/adr/judge/claude_command.rb. check: present --json-schema in plugins/os-me/lib/**/*.rb
  3. HeadlessClaude reads structured_output, validates choice against the options, and raises DecisionModel::UnreadableAnswer with the raw text otherwise. check: present UnreadableAnswer in plugins/os-me/lib/**/*.rb
  4. MomentExtractor marks each session read right after its moments are appended.
  5. MomentExtractor.build passes the Haiku model to HeadlessClaude. check: present haiku in plugins/os-me/lib/os_me/moment_extractor.rb

Acceptance criteria

  1. Given an injected runner, when ReplyShape classifies a pair, then the prompt holds an instruction and all four options.
  2. Given a runner whose command is captured, when it builds argv, then argv holds --json-schema with the four-value enum and --setting-sources.
  3. Given structured_output {"choice":"modify"}, when HeadlessClaude is called, then the answer choice is modify.
  4. Given result text **none** and no structured_output, when HeadlessClaude is called, then it raises UnreadableAnswer that carries the text.
  5. Given a choice outside the options, when HeadlessClaude is called, then it raises UnreadableAnswer.
  6. Given a second session that raises UnreadableAnswer, when MomentExtractor runs, then the cursor marks the first session read and not the second.
  7. Given MomentExtractor.build, when it builds the adapter, then the adapter uses the Haiku model.

Owner sketch

# HeadlessRunner owns argv and the schema flags.
# DecisionModel::HeadlessClaude owns reading and validating the answer.
# ReplyShape owns the prompt and the options.
# DecisionModel::UnreadableAnswer < OsMe::Error

Blocks: #1245 (S3 needs the extracted moments). Found while running the #1244 orchestrator step.

Origin

  • Session: 4f3f95ea-3c6f-4d23-a0ea-e349d6ab02a5
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/4f3f95ea-3c6f-4d23-a0ea-e349d6ab02a5.jsonl
  • Where: plugins/os-me/lib/os_me/decision_model/headless_claude.rb:14, plugins/os-me/lib/os_me/reply_shape.rb:70, plugins/os-me/lib/os_me/moment_extractor.rb:27-33
  • Root candidate: none found; searched os-me-extract, HeadlessClaude, HeadlessRunner, ReplyShape, UnreadableAnswer, json-schema, structured_output, MomentExtractor, extract cursor

VERDICT: CREATE

## Problem `os-me-extract` crashed on its first session. `ReplyShape` sends the model a bare JSON blob with no instruction, no `choice` key and no list of allowed choices. `HeadlessClaude` runs `JSON.parse` on the free-text reply. The model replied `**none**`, and the parse error killed the run: ``` JSON::Ext::Parser.parse: unexpected character: '**none**' at line 1 column 1 (JSON::ParserError) ``` Two more defects sit on the same path. `MomentExtractor#run` marks sessions read only after the whole loop, so a crash after some appends makes a rerun duplicate those moments. `MomentExtractor.build` sets no model, so the CLI default runs instead of Haiku (`docs/decision-system/decision-model.md:50`). ## Approach Required work: 1. `Question` carries `options`; `ReplyShape` sets the four shapes as options and puts an instruction before the memo and reply. check: present `options` in `plugins/os-me/lib/os_me/decision_model/question.rb` 2. `HeadlessRunner` passes `--json-schema` (one required `choice` key, enum of the options), a system prompt, and `--setting-sources ''`, following `plugins/os-adr/lib/adr/judge/claude_command.rb`. check: present `--json-schema` in `plugins/os-me/lib/**/*.rb` 3. `HeadlessClaude` reads `structured_output`, validates `choice` against the options, and raises `DecisionModel::UnreadableAnswer` with the raw text otherwise. check: present `UnreadableAnswer` in `plugins/os-me/lib/**/*.rb` 4. `MomentExtractor` marks each session read right after its moments are appended. 5. `MomentExtractor.build` passes the Haiku model to `HeadlessClaude`. check: present `haiku` in `plugins/os-me/lib/os_me/moment_extractor.rb` ## Acceptance criteria 1. Given an injected runner, when `ReplyShape` classifies a pair, then the prompt holds an instruction and all four options. 2. Given a runner whose command is captured, when it builds argv, then argv holds `--json-schema` with the four-value enum and `--setting-sources`. 3. Given `structured_output` `{"choice":"modify"}`, when `HeadlessClaude` is called, then the answer choice is `modify`. 4. Given result text `**none**` and no `structured_output`, when `HeadlessClaude` is called, then it raises `UnreadableAnswer` that carries the text. 5. Given a choice outside the options, when `HeadlessClaude` is called, then it raises `UnreadableAnswer`. 6. Given a second session that raises `UnreadableAnswer`, when `MomentExtractor` runs, then the cursor marks the first session read and not the second. 7. Given `MomentExtractor.build`, when it builds the adapter, then the adapter uses the Haiku model. ## Owner sketch ```ruby # HeadlessRunner owns argv and the schema flags. # DecisionModel::HeadlessClaude owns reading and validating the answer. # ReplyShape owns the prompt and the options. # DecisionModel::UnreadableAnswer < OsMe::Error ``` Blocks: #1245 (S3 needs the extracted moments). Found while running the #1244 orchestrator step. ## Origin - Session: 4f3f95ea-3c6f-4d23-a0ea-e349d6ab02a5 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/4f3f95ea-3c6f-4d23-a0ea-e349d6ab02a5.jsonl - Where: `plugins/os-me/lib/os_me/decision_model/headless_claude.rb:14`, `plugins/os-me/lib/os_me/reply_shape.rb:70`, `plugins/os-me/lib/os_me/moment_extractor.rb:27-33` - Root candidate: none found; searched os-me-extract, HeadlessClaude, HeadlessRunner, ReplyShape, UnreadableAnswer, json-schema, structured_output, MomentExtractor, extract cursor VERDICT: CREATE
Author
Owner

Work started on branch ticket-1256 (worktree). Found during the #1244 orchestrator extraction run.

Work started on branch ticket-1256 (worktree). Found during the #1244 orchestrator extraction run.
Author
Owner

Run 122 failed at test-reviewer (verdict:error, brief test_changes null): the orchestrator session cwd was the main checkout at dispatch time, so the brief diffed the wrong tree. Red tests from run 122 are in the worktree. Reopening with the session inside the worktree.

Run 122 failed at test-reviewer (verdict:error, brief test_changes null): the orchestrator session cwd was the main checkout at dispatch time, so the brief diffed the wrong tree. Red tests from run 122 are in the worktree. Reopening with the session inside the worktree.
Author
Owner

Hook note (ticket-skeptic DROP): implementation 121 stalled in await_completion after a fast-batch finding-builder and a SendMessage resume were dispatched while it was open. That was an out-of-policy, hand-orchestrated run; no hook source was read to prove the SubagentStart hook records foreign dispatches. Workaround used: no other Agent dispatch while an implementation is open.

Hook note (ticket-skeptic DROP): implementation 121 stalled in await_completion after a fast-batch finding-builder and a SendMessage resume were dispatched while it was open. That was an out-of-policy, hand-orchestrated run; no hook source was read to prove the SubagentStart hook records foreign dispatches. Workaround used: no other Agent dispatch while an implementation is open.
Author
Owner

Resolution

Done: HeadlessClaude asks through --json-schema with an enum of Question#options and reads structured_output; any other reply raises UnreadableAnswer. ReplyShape sends an instruction plus the four options. MomentExtractor marks each session read after its moments are appended, runs ReplyShape on Haiku, and stamps the labeler model on each moment. Hand-finished after escalation 65 (two pre-ticket tests pinned the free-text parse; fixed their fakes).

Evidence: Commits 76ce0add and 4d6508d7 on ticket-1256. ruby plugins/os-me/test/all.rb: 160 runs, 0 failures. rubocop plugins/os-me: no offenses. Design audit (opus): F1-F5 fixed, F6 kept (require 'time' load fix), F7 kept (only test that pins the question text reaching the runner).

Follow-ups: none

## Resolution **Done:** HeadlessClaude asks through --json-schema with an enum of Question#options and reads structured_output; any other reply raises UnreadableAnswer. ReplyShape sends an instruction plus the four options. MomentExtractor marks each session read after its moments are appended, runs ReplyShape on Haiku, and stamps the labeler model on each moment. Hand-finished after escalation 65 (two pre-ticket tests pinned the free-text parse; fixed their fakes). **Evidence:** Commits 76ce0add and 4d6508d7 on ticket-1256. ruby plugins/os-me/test/all.rb: 160 runs, 0 failures. rubocop plugins/os-me: no offenses. Design audit (opus): F1-F5 fixed, F6 kept (require 'time' load fix), F7 kept (only test that pins the question text reaching the runner). **Follow-ups:** none
jared closed this issue 2026-10-23 16:21:33 +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#1256
No description provided.