os-sdlc: fast-gate and detector enter through Thor #1682
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#1682
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
bin/fast-gateandbin/detectorenter throughOsSdlc::Boot.run.Bootlooks each name up inBoot::Commands::TABLE, parses argv by hand (Boot::Inputs::FastGateArgv,Boot::Inputs::DetectorArgv,Boot::Inputs::CrcArgv, and the hand-written text inBoot::Inputs::UsageText), binds the pipeline database throughProjectBind, and then callsFastGate::Run.new(io, project, request).callorDetector::Run.new(io, project, request).call. Neither command reaches a Sequel model, so the database bind is unnecessary work. BothRunclasses take the fullioobject.Detector::Runuses onlyio.stdoutandio.stderr.FastGate::Runusesio.stdout,io.lint_process,io.git, andio.project_shell. 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, that collaborators onProjectare lazy, and that aRunobject returns a result that the Thor command prints. Elevencrc-*commands already follow this shape (#1675, #1676, #1677), and #1681 moved the commandless base toOsSdlc::CLI. This ticket movesfast-gateanddetectorto the same shape.Approach
The language is Ruby. The suite command is
ruby test/all.rb, run fromplugins/os-sdlc. Rulings 126 to 128 indocs/decisions/rulings.tsvdecided the shape. Follow the existing pattern:lib/os_sdlc/cli.rb,lib/os_sdlc/cli/invocation.rb,lib/os_sdlc/cli/shell.rb,lib/os_sdlc/command_result.rb,lib/os_sdlc/crc/census/cli.rb,bin/crc-census,test/crc/census_cli_test.rb,test/support/crc_thor_scenario.rb.OsSdlc::Projectgains two lazy readers with writers for fakes, in the same form asrepoandrepo=:shell(defaultProjectShell.new) andlint_process(defaultLint::RubocopProcess.new). They are not attributes: they never enterattributes, the snapshot, or equality. A copy made bywithorbound_tokeeps an assigned value.ProcessIOstill holdsgit,project_shell, andlint_process,OsSdlc::CLI::Invocationgives the project all three fromprocess. In-process tests that pass fakes on the process (test/support/fast_gate_scenario.rb) continue to work.OsSdlc::FastGate::CLI < OsSdlc::CLI(lib/os_sdlc/fast_gate/cli.rb) has one command,gate PATH....bin/fast-gatebecomesexit OsSdlc::FastGate::CLI.start(["gate", *ARGV], process: OsSdlc::ProcessIO.from_process). Thor declares--target NAME(required),--cop COP(repeatable, required), and--base REF. The command refuses an empty path list with aThor::Error. Paths are expanded against the working directory thatprocessholds, as today.OsSdlc::Detector::CLI < OsSdlc::CLI(lib/os_sdlc/detector/cli.rb) has ten real Thor subcommands, one per name inDetector::Run::NAMES:holders,reach-through,error-lineage,cycle-edges,inward,effects,params,calls,flay,reek.bin/detectorbecomesexit OsSdlc::Detector::CLI.start(ARGV, process: OsSdlc::ProcessIO.from_process), with no added first word. Each subcommand declares its own options:--target NAME(required) and--brieffor all;--ledger FILE(required) only forinwardandeffects;--flay-mass N(numeric, defaultDetector::Measures::FLAY_MASS) only forflay. Thor refuses an option that a subcommand does not declare. No custom unknown-command message. IfMetrics/ClassLength(Max 100) is exceeded, declare the subcommands from one data table; do not add a cop Exclude.FastGate::RunandDetector::Runtake(project, request)and noio.callreturnsOsSdlc::CommandResultand writes to no stream. The Thor command prints throughOsSdlc::CLI::Invocation.FastGate::Runreadsproject.repo,project.shell, andproject.lint_process."fast-gate"and"detector"entries leaveBoot::Commands::TABLE.Boot::Inputs.fast_gate,Boot::Inputs.detector,Boot::Inputs::FastGateArgv,Boot::Inputs::DetectorArgv, andBoot::Inputs::UsageTextare deleted.Boot::Inputs::CrcArgvstays, becausecollaborator-passandmutation-passstill use it; remove only its methods that no caller uses after this ticket.Kept behavior. The change must keep each item.
fast-gateexits 0 with onePASSline, 1 with the failure blocks on stdout;detectorprints Signal TSV or the--briefmarkdown with exit 0, and printsdetector NAME: <message>on stderr with exit 2 forProject::LibRootCountError. Exit 3 withno .sdlc/project.yaml at or above <cwd>when no project exists.--helpprints Thor help on stdout with exit 0..claude/rules/ruby-external-calls.md).Boot.runin this ticket.Test-writer work. These existing tests and helpers pin the old text or the old shape. The test-writer changes each one in the red round; the programmer never edits a test.
test/support/in_process_dispatch_runner.rb:17-36: addfast-gate([OsSdlc::FastGate::CLI, "gate"]) anddetector(no prepended word) toTHOR_ENTRIES;runmust accept an entry with no command word.test/support/crc_help_assertions.rb:4-10assert_help_describes: only detector tests (5 methods) and one fast-gate test use it, and it requires a-hrow that Thor help does not print. Change its callers toassert_thor_helpor direct assertions on the Thor help text, then delete it.test/detector/detector_bin_test.rb::12-16(unknown name listsNAMES) asserts exit 2, empty stdout, and a stderr line that names the bad word;:18-21and:23-29(help) assert Thor help;:43-48and:50-55(--ledgertexts built fromUsageText) assert exit 2, empty stdout, and a stderr line that names--ledger;:66usage(name)readsBoot::Inputs::UsageTextand goes.test/detector/holders_bin_test.rb:68-69,test/detector/cycle_edges_bin_test.rb:12-13,test/detector/error_lineage_bin_test.rb:27-28,test/detector/reach_through_bin_test.rb:11-13: help assertions follow Thor help.test/detector/reach_through_bin_test.rb:24-28: no--targetasserts exit 2, empty stdout, and a stderr line that names--target.test/fast_gate/fast_gate_bin_test.rb:21-26: no--copasserts exit 2 and an output line that names--cop(notBoot::Inputs::UsageText).test/fast_gate/fast_gate_bin_smoke_test.rb: help assertion follows Thor help.FastGate::Run.new(io, ...)orDetector::Run.new(io, ...)directly follows the new constructor and reads the returned result.test/os_sdlc/boot/crc_commands_test.rb:8: also assert that the keysfast-gateanddetectorare absent fromBoot::Commands::TABLE.test/forUsageText,FastGateArgv,DetectorArgv,Inputs.fast_gate, andInputs.detector; change every hit.Required work:
FastGate::CLI.startinplugins/os-sdlc/bin/fast-gateBoot.runinplugins/os-sdlc/bin/fast-gateDetector::CLI.startinplugins/os-sdlc/bin/detectorBoot.runinplugins/os-sdlc/bin/detectorclass CLI < OsSdlc::CLIinplugins/os-sdlc/lib/os_sdlc/fast_gate/cli.rbclass CLI < OsSdlc::CLIinplugins/os-sdlc/lib/os_sdlc/detector/cli.rb"fast-gate" =>inplugins/os-sdlc/lib/os_sdlc/boot/commands.rb"detector" =>inplugins/os-sdlc/lib/os_sdlc/boot/commands.rbdef self.fast_gateinplugins/os-sdlc/lib/os_sdlc/boot/inputs.rbdef self.detectorinplugins/os-sdlc/lib/os_sdlc/boot/inputs.rbUsageTextinplugins/os-sdlc/lib/os_sdlc/**/*.rbFastGateArgvinplugins/os-sdlc/lib/os_sdlc/**/*.rbDetectorArgvinplugins/os-sdlc/lib/os_sdlc/**/*.rb@ioinplugins/os-sdlc/lib/os_sdlc/fast_gate/run.rb@ioinplugins/os-sdlc/lib/os_sdlc/detector/run.rbassert_help_describesinplugins/os-sdlc/test/**/*.rbAcceptance criteria
Project.new(name: "p", root: root).shellreturns aProjectShell, and a second call returns the same object;lint_processreturns aLint::RubocopProcess, and a second call returns the same object.project.shell = fakeandproject.lint_process = other, the readers returnfakeandother, andproject.with(tracker: "t")returns a copy whose readers return the same two objects.==when only one of them has an assignedshellandlint_process, andproject.attributeshas no"shell"key and no"lint_process"key.OsSdlc::FastGate::CLI.start(["gate", "--target", target, "--cop", cop, path], process: process), with fakes onprocessthat report no offense and a green suite, prints onePASSline to the stdout ofprocessand returns 0.FastGate::Run.new(project, request).call, with fakes assigned toproject.repo,project.shell, andproject.lint_process, returns anOsSdlc::CommandResultwhosestatusis 0 and whosestdoutholds thePASSline; the constructor takes no stream argument.FastGate::CLIstart in a project directory finishes with no call toDb.connect_project!and withSequel::Model.dbunchanged.FastGate::CLI.start(["gate", "--target", target, path], process: process)with no--copreturns 2, prints nothing to the stdout ofprocess, and prints a line that names--copto the stderr ofprocess.FastGate::CLI.start(["gate", "--target", target, "--cop", cop], process: process)with no path returns 2, prints nothing to the stdout ofprocess, and prints a non-empty error to the stderr ofprocess.OsSdlc::Detector::CLI.start(["holders", "--target", target], process: process)prints the Signal TSV of the holders detector to the stdout ofprocessand returns 0.Detector::Run.new(project, request).callreturns anOsSdlc::CommandResultwhosestatusis 0 and whosestdoutholds the Signal TSV; the constructor takes no stream argument.Detector::CLI.start(["bogus", "--target", target], process: process)returns 2, prints nothing to the stdout ofprocess, and prints a line that namesbogusto the stderr ofprocess.Detector::CLI.start(["holders", "--target", target, "--ledger", "x.tsv"], process: process)returns 2, prints nothing to stdout, and prints a line that names--ledgerto stderr.Detector::CLI.start(["inward", "--target", target], process: process)with no--ledgerreturns 2, prints nothing to stdout, and prints a line that names--ledgerto stderr.Detector::CLI.start(["flay", "--target", target, "--flay-mass", "abc"], process: process)returns 2, prints nothing to stdout, and prints a non-empty error to stderr.Detector::CLI.start(["reach-through", "--target", target], process: process)runs thereach-throughdetector: it returns 0 and prints its Signal TSV.Detector::CLI.start(["holders", "--help"], process: process)returns 0 and prints Thor help that names--targetand--briefto the stdout ofprocess.Detector::CLI.start(["holders", "--target", target], process: process)in a project with no Zeitwerk loader file returns 2 and prints a line that starts withdetector holders:to the stderr ofprocess.Detector::CLIstart in a project directory finishes with no call toDb.connect_project!and withSequel::Model.dbunchanged.Owner sketch
OsSdlc::FastGate::CLIandOsSdlc::Detector::CLI(Thor) own the command names and option parsing of their commands.OsSdlc::CLI::Invocationfinds the project, gives it the collaborators thatprocessstill holds, calls the run object, prints the result, and returns the status.OsSdlc::FastGate::Runowns one gate run and returns the result;OsSdlc::Detector::Runowns one detector run and returns the result; neither writes to a stream.OsSdlc::Projectowns its collaborators: other objects readproject.repo,project.shell, andproject.lint_process.Out of scope
collaborator-pass,system-model, the mutation commands,project.connect(:db), removal ofBoot, removal ofgitfromProcessIO, and cop changes. These are later tickets of the same change.Origin
Trigger: n/a: design ticket named by the user, not a run that showed a fault
Improvised this session: none
Chain:
fast-gateanddetectorbind a database they never use and take 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--claude-worktrees-ticket-1677/a48658d5-b566-40ce-8257-ebeabf56f7e1.jsonl
Where: plugins/os-sdlc/bin/fast-gate, plugins/os-sdlc/bin/detector, plugins/os-sdlc/lib/os_sdlc/fast_gate/run.rb (OsSdlc::FastGate::Run), plugins/os-sdlc/lib/os_sdlc/detector/run.rb (OsSdlc::Detector::Run), 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.").
VERDICT: CREATE
REASON: No open ticket covers moving fast-gate and detector off Boot onto Thor. The only match is #1646, the parent decision ticket.
Takedown run docs/takedown/1682-20261110-162833 failed at final-red after 1131.8 s and $3.53: 'final design revise-2: failing test not red at base (plugins/os-sdlc/test/fast_gate/fast_gate_bin_smoke_test.rb names no new test method)'. The design edits an existing test that pins the old usage text, and the red check accepts only new test methods. Per ruling 125 the ticket goes to the default map; the body now has Approach, Acceptance criteria and Owner sketch. Work starts on branch ticket-1682.
Resolution
Done: fast-gate and detector enter through Thor (OsSdlc::FastGate::CLI, OsSdlc::Detector::CLI with ten subcommands). Both Run classes take (project, request), return OsSdlc::CommandResult, and bind no database. Project has lazy shell and lint_process readers. The two Boot table entries, FastGateArgv, DetectorArgv and UsageText are deleted. Cop AdapterBuiltOutsideEntry permits a seamed lazy reader (ruling 129).
Evidence: Commit
5ce7f5567, merged to main atfff600d2c. Suite on the merged code: 4213 runs, 8654 assertions, 0 failures. Installed commands checked after the merge: detector holders --target os-sdlc exit 0 with TSV; detector --help exit 0; detector with no argument exit 2; fast-gate without --cop exit 2. Run 277 escalated at the programmer (wrong expectations in new tests); the orchestrator finished by hand and abandoned the run.Follow-ups: The remaining commands (collaborator-pass, system-model, mutation commands, model commands, Boot removal) are built by hand in session a48658d5 under parent #1646, by owner order of 2026-11-10. Other follow-ups (takedown final-red rejects edited tests; fast-gate with a project root below the git top level) go through /os-backlog:capture in the same session.