decision-system S5: route judgment rows in os-sdlc flows #1247
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#1247
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
os-sdlc flows (
fast-batch,crc-refactor) put each row in a tier by the table inplugins/os-sdlc/references/decision-tiers.md. No object computes the cheapest route of a row from the levers, and no record shows where that route and the tier disagree. Stage S11 must compare the two on outcomes, so it needs one log line for each judgment row. The design is indocs/decision-system/levers.md(CostModelandRouteSelector) anddocs/decision-system/operations.md(RowRouter, "What operations writes"). This ticket is the first of two S5 tickets. S5b addsReviewOrderandRandomSample.Approach
All new classes live in os-me (
plugins/os-me/lib/os_me/). O-9 is ruled: the levers live in os-me, and os-sdlc calls the os-me CLIs as a subprocess. os-sdlc gets no new code. The flows are LLM orchestrators that run the CLI from their skill text.CalibrationPriorgivesp_wrong(route, source)from a constant table. It has the interface thatRouteSelectorwill later get from calibration data (S10, S11). Values::alone:predictRouteSelector#choose(wrong_cost, source)returns aRouteSelector::Route(name,expected_minutes). It compares three routes in this order and returns the first with the lowestexpected_minutes:decide_alone:p_wrong(:alone, source) × cost.wrong_minutes(wrong_cost)predict:cost.predict_minutes + p_wrong(:predict, source) × cost.wrong_minutes(wrong_cost)defer(oraskwhenask_allowed: true):cost.ping_minutessourceis aSymbol(AnswerSource::Result#source). The design sketch takes arowargument too;choosedoes not, because no route uses it.RouteSelectorruns in shadow mode only (O-4 AFK default). It never changes the tier.RowRouter.build(store_path)usesCostModel.new(minutes_per_dollar: 1.0, predict_dollars: 0.5)(sopredict_minutesis 0.5, andping_minutesis 10 by O-6),CalibrationPrior.new,ask_allowed: false, andRouteLog.new(File.join(store_path, 'moments', 'route-log.jsonl')).RowRouter#route(labels, row_id: nil, tier: nil, flow: nil)returnslabels.to_hplus the keyshadow_route(a String) at the end. Whenrow_idis given, it also appends oneRouteLogline.RowLabelsgets readerssourceandwrong_cost, so thatRowRoutercan give them toRouteSelector.RouteLog#append(row_id:, flow:, tier:, shadow_route:, source:, level:)creates the parent directory if it is absent and appends one JSON line with the keys in this order:row_id,flow,tier,shadow_route,source,level,recorded_at(UTC ISO 8601, for example2026-10-23T14:05:09Z). It appends each call; it does not remove duplicates.os-me-route ROW_FILEgets three options:--row-id ID,--tier TIER(one ofmechanical,low-stakes,judgment), and--flow NAME. With no options, it prints the S4 JSON plusshadow_routeand writes no log line. Theos-mepredict skill calls it this way today, so this form must stay. With--row-id, the command also needs--tierand--flow. It checks this before it asks the model. Update the expected lines inplugins/os-me/test/route_cli_test.rbfor the new key.Orchestrator work (after this ticket merges, fast path, not in the pipeline):
decision-tiers.md"Final judgment round", step 1: for each open row, write the row text to a file and runruby ~/.claude/plugins/os-me/bin/os-me-route FILE --row-id <id> --tier judgment --flow <flow>. Put the JSON in the memo of the row.fast-batchandcrc-refactorSKILL.mdat that step, with the textos-me-route.os-me-routeas today.bin/refresh-plugins.Required work:
CalibrationPriorwith thep_wrongtable — check: presentclass CalibrationPriorinplugins/os-me/lib/os_me/calibration_prior.rbRouteSelectorandRouteSelector::Route— check: presentdef choose(wrong_cost, source)inplugins/os-me/lib/os_me/route_selector.rbRouteLog— check: presentclass RouteLoginplugins/os-me/lib/os_me/route_log.rbRowRouterwithbuildandroute— check: presentclass RowRouterinplugins/os-me/lib/os_me/row_router.rbsourceandwrong_costtoRowLabels— check: presentattr_reader :source, :wrong_costinplugins/os-me/lib/os_me/row_labels.rb--row-id,--tier, and--flowto theroutecommand — check: present:row_idinplugins/os-me/lib/os_me/cli.rbAcceptance criteria
CostModel.new(minutes_per_dollar: 1.0, predict_dollars: 0.5),CalibrationPrior.new,ask_allowed: false,WrongCost.new(hard_to_undo: false, compounds: true, stays_hidden: false), and source:facts, whenRouteSelector#chooseruns, then the route name is:predictandexpected_minutesis within 0.001 of2.0.WrongCost.new(hard_to_undo: true, compounds: true, stays_hidden: true), and source:taste, whenRouteSelector#chooseruns withask_allowed: false, then the route is:deferwithexpected_minutes10; withask_allowed: true, the route is:askwithexpected_minutes10.WrongCost.new(hard_to_undo: false, compounds: false, stays_hidden: false), and source:rule, whenRouteSelector#chooseruns, then the route is:decide_aloneandexpected_minutesis within 0.001 of0.1.CalibrationPrior.new, whenp_wrong(:alone, :taste)andp_wrong(:predict, :facts)run, then they return0.5and0.05;p_wrong(:ask, :facts)raisesKeyError.DIR/moments/route-log.jsonlwhosemomentsdirectory does not exist, whenRouteLog#append(row_id: "b1/F3", flow: "fast-batch", tier: "judgment", shadow_route: "predict", source: "facts", level: "medium")runs, then the file has one line; its keys are, in order,row_id,flow,tier,shadow_route,source,level,recorded_at; the first six values are the given ones;recorded_atmatches/\A\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ\z/.RowLabels.new(source: AnswerSource::Result.new(source: :facts), wrong_cost: WrongCost.new(hard_to_undo: false, compounds: true, stays_hidden: false))andRowRouter.build(DIR), whenroute(labels, row_id: "b1/F3", tier: "judgment", flow: "fast-batch")runs, then it returns{ source: :facts, max_rung: 2, novel: false, wrong_cost: { hard_to_undo: false, compounds: true, stays_hidden: false }, level: :medium, required_band: 70, shadow_route: "predict" }, andDIR/moments/route-log.jsonlhas one line with"shadow_route":"predict".Move Gate::StepOutcome to OsSdlc::StepOutcome.,--store DIR, and a fakeclaudethat answersfacts,no,yes,no, whenos-me-route ROW_FILE --store DIRruns with no other option, then stdout is exactly{"source":"facts","max_rung":2,"novel":false,"wrong_cost":{"hard_to_undo":false,"compounds":true,"stays_hidden":false},"level":"medium","required_band":70,"shadow_route":"predict"}plus a newline, the exit status is 0, andDIR/moments/route-log.jsonldoes not exist.claudeof criterion 7, whenos-me-route ROW_FILE --store DIR --row-id b1/F3 --tier judgment --flow fast-batchruns, then stdout is the line of criterion 7, andDIR/moments/route-log.jsonlhas one line whose first six values are"b1/F3","fast-batch","judgment","predict","facts","medium".os-me-route ROW_FILE --store DIR --row-id b1/F3runs with no--tier, then the exit status is not 0, stdout is empty, and the fakeclaudegets no call.claudewhose first answer iswhim, whenos-me-route ROW_FILE --store DIR --row-id b1/F3 --tier judgment --flow fast-batchruns, then the exit status is not 0, stdout is empty, andDIR/moments/route-log.jsonldoes not exist.Owner sketch
Non-goals
RouteSelector(O-4). The shadow route only goes to the log and the JSON.p_wrongfrom calibration data (S10, S11).--ask-allowedoption. AFK flows always usedefer.ReviewOrderandRandomSample(S5b).decision-tiers.mdchange as orchestrator work.Origin
DESIGN(docs/decision-system/levers.md,docs/decision-system/operations.md).VERDICT: CREATE (user-approved; backlog search only)
Work started on branch ticket-1247 (/os-sdlc:implement, S5).
Resolution
Done: S5 shadow route built: CalibrationPrior#p_wrong, RouteSelector (+Route), RouteLog, RowRouter, RowLabels readers, os-me-route --row-id/--tier/--flow writing route-log.jsonl; implementation 131 complete.
Evidence: Branch ticket-1247, Runner commit; one test-review round, one required-work repair, one refactor round (RowRouter takes labels, CLI owns options and file read); suite and lint green; 16 dispatches, 13 min.
Follow-ups: S5b #1265 (review order, 5% sample). Skill text for decision-tiers.md, fast-batch and crc-refactor to call os-me-route is orchestrator work after #1265.