os-sdlc: crc-jobs, crc-inward-delta, crc-ledger-commits, crc-shares enter through Thor; one Thor class per concept; Thor generates help #1676
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#1676
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
Four commands still enter through
OsSdlc::Boot.run:crc-jobs,crc-inward-delta,crc-ledger-commits, andcrc-shares.Bootparses their argv by hand inBoot::Inputs, binds the pipeline database, and callsRun.new(io, project, request).call. None of the four reaches a Sequel model, so the database bind is unnecessary work and it creates.sdlc/pipeline.db. EachRunpasses the fullioobject toCrc::Emitter, which reads onlyio.stdoutandio.stderr;Crc::Ledger::Commits::Runalso readsio.git.crc-sweepmoved to Thor in #1675, but its classCrc::CLIholds a hand-writtenUSAGEtext and intercepts-hand--helpitself, so Thor does not generate the help. One Thor class cannot hold all elevencrc-*commands under theMetrics/ClassLengthlimit of 100 lines. The owner ruled on 2026-11-10 that Thor is the entry for os-sdlc commands, that a command and the classes below it take aProject, thatproject.repois lazy, and that aRunobject returns a result that the command prints. Rulings 121 and 122 (docs/decisions/rulings.tsv) set the shape: one Thor class per concept over a small base class, and the help that Thor generates.Approach
The language is Ruby. The suite command is
ruby test/all.rb, run fromplugins/os-sdlc. Readlib/os_sdlc/crc/cli.rb,lib/os_sdlc/crc/sweep/run.rb,lib/os_sdlc/crc/sweep/result.rb, andtest/crc/cli_test.rbfirst: they are the pattern from #1675.OsSdlc::Crc::CLI(lib/os_sdlc/crc/cli.rb) becomes a small Thor base class with no command of its own. It holds only what everycrc-*command shares: the guarded start that returns the exit status, the Thor error to stderr with exit 2, the project lookup from the working directory thatprocessholds (exit 3 withno .sdlc/project.yaml at or above <dir>when there is none), the project load for--target, and the printing of a result. Do not move option declarations into a module to pass the class-length cop.Crc::Sweep::CLI(crc/sweep/cli.rb),Crc::Jobs::CLI(crc/jobs/cli.rb),Crc::InwardDelta::CLI(crc/inward_delta/cli.rb),Crc::Ledger::Commits::CLI(crc/ledger/commits/cli.rb), andCrc::Shares::CLI(crc/shares/cli.rb). Each declares one command with its arguments and options through Thor (desc,method_option). The command takes no raw argv and calls noBoot::Inputs,Boot::Inputs::CrcArgv, orBoot::ArgvOptionparser.bin/script calls its own class with the command name first, in the formbin/crc-sweephas today:exit OsSdlc::Crc::Jobs::CLI.start(["jobs", *ARGV], process: OsSdlc::ProcessIO.from_process). The command names aresweep,jobs,inward-delta(or the Thor method name for it),ledger-commits, andshares.jobs RUN_DIR;inward-delta RUN_DIR BEFORE AFTER;ledger-commits RUN_DIR SHA;shares SUBCOMMAND RUN_DIRwith--max-bytes N(default 45000) and--ticket N. Every command has--target NAME(required). Options can come before or after the positional arguments (skills/crc-refactor/SKILL.mdwritescrc-shares --target X --ticket N rows-merge DIR).SUBCOMMANDmust be one ofCrc::Shares::SUBCOMMANDS;--max-bytesmust be a positive Integer;--ticketis required forrows-mergeand refused for every other subcommand;BEFOREandAFTERare expanded against the working directory thatprocessholds and each must be a file.USAGEconstant and the-h/--helpintercept fromcrc/cli.rb.--helpand-hprint the help that Thor generates for the command to the stdout thatprocessholds and return 0. Thor writes through its shell to the global$stdoutby default, so the base class gives Thor a shell that writes to the streams ofprocess; nothing goes to the global$stdoutor$stderr. The usage line of the help names the script (crc-jobs, not the test program name).Runtakes the project and its request, and noio.Run#callreturns a result that holds the exit status, the report (or nil), and the error line (or nil), and writes to no stream. The command prints the report to stdout as pretty JSON, prints the error line to stderr, and returns the status. The four commands share one result shape withcrc-sweep; make one shared result value for the five (Crc::Sweep::Resultis the present form) and remove the stream writes fromCrc::Emitter, or removeCrc::Emitterwhen nothing else needs it.Crc::Ledger::Commits::Runreads the repository fromproject.repo. WhileProcessIOstill holdsgit, the base class gives the project its repository fromprocess.git, ascrc/cli.rbdoes today.Boot::Commands::TABLE;Boot::Inputs.crc_jobs,crc_inward_delta,crc_ledger_commits, andcrc_sharesgo; theCrcArgvmethods andUsageTextentries that only these four read go.Boot::Inputs.crc_run_dir_inputstays whilecrc-scorereads it.test/support/in_process_dispatch_runner.rb) starts the Thor class for each of the five names; replace the singlecommand == "crc-sweep"branch with one lookup from name to class and command.Kept behavior. The existing suite pins these items, so they are not red-round criteria. The change must keep each one.
crc-<name>: no ledger.tsv in <run>and the like), and the same exit codes 0, 1, 2, and 3. Two things change. First, for a bad argument the exit code stays 2 and stdout stays empty, but the stderr text becomes the error text of Thor or the one-line error of the rule in code; a test that assertsusage: crc-<name>on a bad argument (test/crc/jobs_bin_test.rb,inward_delta_bin_test.rb,ledger_commits_bin_test.rb,shares_bin_test.rb,shares_row_merge_bin_test.rb) is updated to assert exit 2, empty stdout, and non-empty stderr. Second, the help format becomes the format of Thor; the smoke tests of the five commands (test/crc/*_bin_smoke_test.rb,test/support/crc_help_assertions.rb) are updated to assert exit 0, the script name, each positional name, and each option name in the Thor help. The smoke tests of the six commands that stay onBoot.runkeep their present assertions.test/detector/entry_points_test.rband any other test that maps a movedbin/crc-*script toOsSdlc::Bootis updated to the new entry class..claude/rules/ruby-external-calls.md).ProcessStartGuardenforces this.crc-census,crc-records,crc-tickets,crc-score,crc-placement,crc-file, and every other command stay onBoot.runin this ticket.Excludelist, no inlinerubocop:disable.Required work:
Crc::Sweep::CLI.startinplugins/os-sdlc/bin/crc-sweepCrc::Jobs::CLI.startinplugins/os-sdlc/bin/crc-jobsCrc::InwardDelta::CLI.startinplugins/os-sdlc/bin/crc-inward-deltaCrc::Ledger::Commits::CLI.startinplugins/os-sdlc/bin/crc-ledger-commitsCrc::Shares::CLI.startinplugins/os-sdlc/bin/crc-sharesBoot.runinplugins/os-sdlc/bin/crc-jobsBoot.runinplugins/os-sdlc/bin/crc-inward-deltaBoot.runinplugins/os-sdlc/bin/crc-ledger-commitsBoot.runinplugins/os-sdlc/bin/crc-shares"crc-jobs" =>inplugins/os-sdlc/lib/os_sdlc/boot/commands.rb"crc-inward-delta" =>inplugins/os-sdlc/lib/os_sdlc/boot/commands.rb"crc-ledger-commits" =>inplugins/os-sdlc/lib/os_sdlc/boot/commands.rb"crc-shares" =>inplugins/os-sdlc/lib/os_sdlc/boot/commands.rbUSAGEinplugins/os-sdlc/lib/os_sdlc/crc/cli.rbmethod_optioninplugins/os-sdlc/lib/os_sdlc/crc/cli.rbshares_requestinplugins/os-sdlc/lib/os_sdlc/boot/inputs/crc_argv.rbusage: crc-sharesinplugins/os-sdlc/lib/os_sdlc/boot/inputs/usage_text.rbAcceptance criteria
OsSdlc::Crc::Jobs::CLI.start(["jobs", "--target", target, run_dir], process: process)prints the jobs report as pretty JSON to the stdout ofprocessand returns the status of the report (0 for an ok report).OsSdlc::Crc::InwardDelta::CLI.start(["inward-delta", "--target", target, run_dir, before, after], process: process)prints the delta report as pretty JSON to the stdout ofprocessand returns its status.OsSdlc::Crc::Ledger::Commits::CLI.start(["ledger-commits", "--target", target, run_dir, sha], process: process)reads the commit through the repository thatprocess.gitsupplies to the project, prints the report as pretty JSON to the stdout ofprocess, and returns its status.OsSdlc::Crc::Shares::CLI.start(["shares", "--target", target, "verdicts-split", run_dir], process: process)prints the shares report as pretty JSON to the stdout ofprocessand returns its status.OsSdlc::Crc::Sweep::CLI.start(["sweep", "--target", target, run_dir], process: process)with a ledger that has one live hit prints the sweep report as JSON and returns 1, andOsSdlc::Crc::CLIhas nosweepcommand.Crc::Jobs::Run.new(project, request).callreturns a result whosestatusis an Integer and whosereportholds the report, and it takes no stream argument. The same holds forCrc::InwardDelta::Run,Crc::Ledger::Commits::Run(with a fake repository assigned toproject.repo), andCrc::Shares::Run.ledger.tsvin the run directory,Crc::Jobs::Run.new(project, request).callreturns a result withstatus2, a nilreport, and the error linecrc-jobs: no ledger.tsv in <run>.Db.connect_project!and withSequel::Model.dbunchanged.startwith--helpafter the command name returns 0, prints help text to the stdout ofprocessthat names the script (crc-jobsforCrc::Jobs::CLI), each positional argument, and each option, and prints nothing to the global$stdout.startwith no--targetreturns 2, prints nothing to the stdout ofprocess, and prints a line that names--targetto the stderr ofprocess.Crc::Shares::CLI.start(["shares", "--target", target, "--max-bytes", "0", "verdicts-split", run_dir], process: process)returns 2 with empty stdout and a non-empty stderr line.Crc::Shares::CLI.startwith the subcommandrows-mergeand no--ticketreturns 2 with empty stdout and a non-empty stderr line; with--ticket 5and the subcommandverdicts-splitit also returns 2; with an unknown subcommand it also returns 2.Crc::Shares::CLI.start(["shares", "--target", target, "--ticket", "5", "rows-merge", run_dir], process: process)(options before the positional arguments) reachesCrc::Shares::Runwith the subcommandrows-merge, the ticket 5, and the run directory.Crc::InwardDelta::CLI.startwith aBEFOREpath that names no file returns 2 with empty stdout and a non-empty stderr line; a relativeBEFOREpath is read against the working directory thatprocessholds.Owner sketch
OsSdlc::Crc::CLI(Thor base class) owns what everycrc-*entry shares: the guarded start, the streams ofprocessfor Thor output, the project lookup and load, and the printing of a result. EachCrc::<Concept>::CLIowns the command name, the arguments, and the options of its one command; it builds the request, calls the run object, and returns the status. EachCrc::<Concept>::Runowns the work for one request and returns the result; it writes nothing. One shared result value owns the exit status, the report, and the error line for the five commands.OsSdlc::Projectowns its repository:Crc::Ledger::Commits::Runreadsproject.repo. The error-to-exit-code wrapper stays withProcessIO::Outcome(guarded).Out of scope
crc-census,crc-records,crc-tickets,crc-score,crc-placement, andcrc-file(the next tickets of the same change),project.connect(:db), the removal ofBoot, the removal ofgitfromProcessIO, a new name forBoot::Inputs::CrcArgv, and cop changes.Reproduce
n/a: feature ticket.
Origin
Trigger: n/a: design ticket named by the user, not a run that showed a fault
Improvised this session: none
Chain: four
crc-*commands bind a database they never use and pass aniothey mostly ignore ← every command enters throughBoot.runand one command table ← ADR-0250 (retired habit) put all entries behindBootSession: a48658d5-b566-40ce-8257-ebeabf56f7e1
Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os-plugins-os-sdlc/a48658d5-b566-40ce-8257-ebeabf56f7e1.jsonl
Where: plugins/os-sdlc/lib/os_sdlc/crc/cli.rb (OsSdlc::Crc::CLI), plugins/os-sdlc/lib/os_sdlc/crc/emitter.rb (OsSdlc::Crc::Emitter), plugins/os-sdlc/lib/os_sdlc/boot/commands.rb
Root candidate: #1646
Approved by: Jared named and approved this change in session on 2026-11-10 ("List the changes we are making. Then implement them."). Shape per rulings 121 to 123 (predicted).
VERDICT: CREATE
REASON: Jared named and approved this slice. The backlog search found no open ticket that moves crc-jobs, crc-inward-delta, crc-ledger-commits and crc-shares from Boot to Thor. #1646 is the parent decision ticket, not this slice.
Work started on branch ticket-1676 through /os-sdlc:implement.
Resolution
Done: crc-jobs, crc-inward-delta, crc-ledger-commits and crc-shares enter through Thor. One Thor class per concept inherits from the base Crc::CLI, which holds no command; crc-sweep moved to Crc::Sweep::CLI. Thor generates the help and writes it to the streams of process. Each Run takes the project and its request, receives no streams, and returns Crc::CommandResult; Crc::Emitter is deleted. No command of the five binds the pipeline database. The four entries left Boot::Commands::TABLE, Boot::Inputs, CrcArgv and UsageText.
Evidence: Implementation 274 ended at merge-ready (15 dispatches, one reroute after a test-defect escalation). Commits
d2e97d81eandb8704b72bon branch ticket-1676. Full suite: 3966 runs, 0 failures, 5.9 s. Suite gate, mutation survivor gate, behavior-verifier and contract audit passed. Implementations 272 and 273 failed at change-auditor on an evidence-tool fault before any code existed.Follow-ups: None from this ticket. The six crc-* commands that stay on Boot (census, records, tickets, score, placement, file) get the next ticket of the plan through /os-backlog:capture. One lens proposal (A18, class-level factory on a Data value) and one tool defect (evidence collect with general names) go through /os-backlog:capture.