Dissolve RunnerCLI into one bin script per live command; delete the four uncalled subcommands (supersedes ADR-0123 for os-sdlc) #768

Closed
opened 2026-09-27 13:11:24 +00:00 by jared · 4 comments
Owner

Problem

RunnerCLI is a Thor CLI with subcommands for every dispatch command. A 2026-09-27 caller inventory shows every live caller is a map file or an agent prompt, never a human typing the command, so Thor's subcommand map and --help serve no reader. Four subcommands have no caller anywhere; brief looked uncalled but is named at runtime by the hook-cap message.

Observed

Live subcommands and callers:

  • gate — 12 map gate lines in maps/*.yaml, 5 agent prompts, 3 skill docs; no options.
  • subagent-stop — 10 agent prompts; see #766 for the double-path question this raises.
  • brief — no prompt line names it, but lib/os_sdlc/presenter/hook_payload.rb:32 prints "Brief exceeds the hook cap. Run: os-sdlc-runner brief --dispatch-id N" and six agent prompts tell the agent to run that command once; live.
  • implementation-open — skills/implement/SKILL.md:52; --target.
  • next — implement and pipeline-state-investigate skills; no options.
  • implementation-report — implement and responsibility-refactor skills; --sweep-only.
  • subagent-start — implement skill manual note.

No caller anywhere: retired-tests-check, implementation-close, violations, durations.

Reproduce

n/a: design and refactor ticket, not a reproducible bug.

Approach

VERDICT: CREATE (user-approved 2026-09-27)

One bin script per live command, stdlib option parsing, no Thor. Shape:

#!/usr/bin/env ruby
require_relative "../lib/os_sdlc"
puts JSON.generate(OsSdlc::DispatchCommands.gate(Dir.pwd, ARGV))

Flags such as --target and --sweep-only go through stdlib OptionParser.

Required work:

  1. Write one bin script per live command (gate, subagent-stop, brief, implementation-open, next, implementation-report, subagent-start).
  2. Confirm no test depends on the four uncalled subcommands (retired-tests-check, implementation-close, violations, durations); delete each uncalled subcommand and its tests if the tests exercise only the CLI layer.
  3. Rename roughly 20 prose and map call sites from os-sdlc-runner <sub> to the new script names, and add a test over maps/*.yaml that checks the new names resolve. Include the ten agent-prompt subagent-stop lines, the seven gate lines, and the hook-cap message in lib/os_sdlc/presenter/hook_payload.rb:32; all renames land in the same commit as the script that replaces them, so no agent ever reads a command name that does not resolve.
  4. Delete bin/os-sdlc-runner and the RunnerCLI class.
  5. Update the rubocop bin byte-contract rule from #423 at .rubocop.yml:305 to match the new one-script-per-command layout.
  6. Write an ADR superseding ADR-0123 for os-sdlc: "Thor for plugin bin CLIs" becomes "one script per command, stdlib option parsing" for os-sdlc.

Rejected: keep Thor and prune only the five uncalled subcommands — rejected because the caller inventory shows no human reader for any subcommand, so the subcommand map itself, not just the dead ones, has no purpose.

Acceptance criteria

  1. Given each live command, when the old os-sdlc-runner subcommand and the new bin script run on the same stdin and arguments, then their stdout is byte-identical (check: a diff per command, recorded in the PR), because agents and hooks read that stdout as their next instruction.
  2. Given the four uncalled subcommands, when this ticket is done, then neither their code nor their bin entry points exist in the repository.
  3. Given maps/*.yaml and the agent prompts, when this ticket is done, then none reference os-sdlc-runner.
  4. Given the full test suite, when it runs after this change, then it is green.

Owner sketch

# plugins/os-sdlc/bin/gate
#!/usr/bin/env ruby
require_relative "../lib/os_sdlc"
puts JSON.generate(OsSdlc::DispatchCommands.gate(Dir.pwd, ARGV))

Non-goals

lsp-probe and the evidence Thor CLIs — left for a later ticket, not touched here.
Changing which path settles a dispatch. The prompt-driven dispatch-id keyed subagent-stop stays authoritative; the native hook stays the safety net (#766).

Origin

  • Trigger: user question about why hooks route through RunnerCLI (Thor) in-process, during session 9 CRC.
  • Improvised this session: none.
  • Chain: unread Thor subcommand map with five dead entries ← ADR-0123 mandated Thor for plugin bin CLIs before the caller population was known to be exclusively maps and prompts ← DESIGN, ADR-0123.
  • Root candidate: this ticket.
  • Where: plugins/os-sdlc/bin/, plugins/os-sdlc/lib/os_sdlc/cli/runner_cli.rb, plugins/os-sdlc/maps/, plugins/os-sdlc/agents/.
  • Session: 8e4aac4f-9878-45da-ac68-c7d55261a73f
  • Transcript: /home/jared/dev/cc-os/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-crc-pass-os-sdlc/8e4aac4f-9878-45da-ac68-c7d55261a73f.jsonl
## Problem `RunnerCLI` is a Thor CLI with subcommands for every dispatch command. A 2026-09-27 caller inventory shows every live caller is a map file or an agent prompt, never a human typing the command, so Thor's subcommand map and `--help` serve no reader. Four subcommands have no caller anywhere; brief looked uncalled but is named at runtime by the hook-cap message. ## Observed Live subcommands and callers: - `gate` — 12 map gate lines in `maps/*.yaml`, 5 agent prompts, 3 skill docs; no options. - `subagent-stop` — 10 agent prompts; see #766 for the double-path question this raises. - brief — no prompt line names it, but `lib/os_sdlc/presenter/hook_payload.rb:32` prints "Brief exceeds the hook cap. Run: os-sdlc-runner brief --dispatch-id N" and six agent prompts tell the agent to run that command once; live. - `implementation-open` — `skills/implement/SKILL.md:52`; `--target`. - `next` — `implement` and `pipeline-state-investigate` skills; no options. - `implementation-report` — `implement` and `responsibility-refactor` skills; `--sweep-only`. - `subagent-start` — `implement` skill manual note. No caller anywhere: `retired-tests-check`, `implementation-close`, `violations`, `durations`. ## Reproduce n/a: design and refactor ticket, not a reproducible bug. ## Approach VERDICT: CREATE (user-approved 2026-09-27) One bin script per live command, stdlib option parsing, no Thor. Shape: ```ruby #!/usr/bin/env ruby require_relative "../lib/os_sdlc" puts JSON.generate(OsSdlc::DispatchCommands.gate(Dir.pwd, ARGV)) ``` Flags such as `--target` and `--sweep-only` go through stdlib `OptionParser`. Required work: 1. Write one bin script per live command (`gate`, `subagent-stop`, `brief`, `implementation-open`, `next`, `implementation-report`, `subagent-start`). 2. Confirm no test depends on the four uncalled subcommands (`retired-tests-check`, `implementation-close`, `violations`, `durations`); delete each uncalled subcommand and its tests if the tests exercise only the CLI layer. 3. Rename roughly 20 prose and map call sites from `os-sdlc-runner <sub>` to the new script names, and add a test over `maps/*.yaml` that checks the new names resolve. Include the ten agent-prompt subagent-stop lines, the seven gate lines, and the hook-cap message in `lib/os_sdlc/presenter/hook_payload.rb:32`; all renames land in the same commit as the script that replaces them, so no agent ever reads a command name that does not resolve. 4. Delete `bin/os-sdlc-runner` and the `RunnerCLI` class. 5. Update the rubocop bin byte-contract rule from #423 at `.rubocop.yml:305` to match the new one-script-per-command layout. 6. Write an ADR superseding ADR-0123 for os-sdlc: "Thor for plugin bin CLIs" becomes "one script per command, stdlib option parsing" for os-sdlc. Rejected: keep Thor and prune only the five uncalled subcommands — rejected because the caller inventory shows no human reader for any subcommand, so the subcommand map itself, not just the dead ones, has no purpose. ## Acceptance criteria 1. Given each live command, when the old os-sdlc-runner subcommand and the new bin script run on the same stdin and arguments, then their stdout is byte-identical (check: a diff per command, recorded in the PR), because agents and hooks read that stdout as their next instruction. 2. Given the four uncalled subcommands, when this ticket is done, then neither their code nor their bin entry points exist in the repository. 3. Given `maps/*.yaml` and the agent prompts, when this ticket is done, then none reference `os-sdlc-runner`. 4. Given the full test suite, when it runs after this change, then it is green. ## Owner sketch ```ruby # plugins/os-sdlc/bin/gate #!/usr/bin/env ruby require_relative "../lib/os_sdlc" puts JSON.generate(OsSdlc::DispatchCommands.gate(Dir.pwd, ARGV)) ``` ## Non-goals `lsp-probe` and the evidence Thor CLIs — left for a later ticket, not touched here. Changing which path settles a dispatch. The prompt-driven dispatch-id keyed subagent-stop stays authoritative; the native hook stays the safety net (#766). ## Origin - Trigger: user question about why hooks route through RunnerCLI (Thor) in-process, during session 9 CRC. - Improvised this session: none. - Chain: unread Thor subcommand map with five dead entries ← ADR-0123 mandated Thor for plugin bin CLIs before the caller population was known to be exclusively maps and prompts ← DESIGN, ADR-0123. - Root candidate: this ticket. - Where: plugins/os-sdlc/bin/, plugins/os-sdlc/lib/os_sdlc/cli/runner_cli.rb, plugins/os-sdlc/maps/, plugins/os-sdlc/agents/. - Session: 8e4aac4f-9878-45da-ac68-c7d55261a73f - Transcript: /home/jared/dev/cc-os/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-crc-pass-os-sdlc/8e4aac4f-9878-45da-ac68-c7d55261a73f.jsonl
Author
Owner

Depends on #767 (Hooks call DispatchCommands directly). Investigated after #766.

Depends on #767 (Hooks call DispatchCommands directly). Investigated after #766.
jared changed title from Dissolve RunnerCLI into one bin script per live command; delete the five uncalled subcommands (supersedes ADR-0123 for os-sdlc) to Dissolve RunnerCLI into one bin script per live command; delete the four uncalled subcommands (supersedes ADR-0123 for os-sdlc) 2026-09-27 13:46:23 +00:00
Author
Owner

Amended after #766 (2026-09-27): brief is live via the hook-cap message; uncalled set is four; prompt lines and hook_payload.rb:32 rename in the same commit; acceptance is a stdout diff per command.

Amended after #766 (2026-09-27): brief is live via the hook-cap message; uncalled set is four; prompt lines and hook_payload.rb:32 rename in the same commit; acceptance is a stdout diff per command.
Author
Owner

Caller inventory corrected again on 2026-09-27 before implementation: retired-tests-check is live (lib/os_sdlc/query/gate/commands.rb:39 emits it as a gate command whenever a ticket has a Retired tests section), so the dead set is three: implementation-close, violations, durations. Live set is eight. plugins/os/lib/worktree/usage_sweep.rb:8 hardcodes the path to bin/os-sdlc-runner for implementation-report --sweep-only; it and its five finish_command tests change in the same commit. Script names: bin/os-sdlc-, prefixed to avoid PATH collisions for bare words such as next and brief. .rubocop.yml:305 names tests/bin_thor_migration_test.rb which does not exist; the stale exclude goes.

Caller inventory corrected again on 2026-09-27 before implementation: retired-tests-check is live (lib/os_sdlc/query/gate/commands.rb:39 emits it as a gate command whenever a ticket has a Retired tests section), so the dead set is three: implementation-close, violations, durations. Live set is eight. plugins/os/lib/worktree/usage_sweep.rb:8 hardcodes the path to bin/os-sdlc-runner for implementation-report --sweep-only; it and its five finish_command tests change in the same commit. Script names: bin/os-sdlc-<command>, prefixed to avoid PATH collisions for bare words such as next and brief. .rubocop.yml:305 names tests/bin_thor_migration_test.rb which does not exist; the stale exclude goes.
Author
Owner

Resolution

Done: RunnerCLI dissolved. Eight scripts bin/os-sdlc- (gate, subagent-stop, brief, implementation-open, next, implementation-report, subagent-start, retired-tests-check) over one thin DispatchScripts class with stdlib OptionParser. Deleted bin/os-sdlc-runner, cli/runner_cli.rb, and the three uncalled commands implementation-close, violations, durations with their report chains. Every caller renamed in the same commit: ten agent prompts, four skill files, three maps, the hook-cap message in presenter/hook_payload.rb, the retired-tests gate command in query/gate/commands.rb, plugins/os usage_sweep and its five finish tests, CONTEXT.md, deterministic-gates.md, CLAUDE.md. Stale .rubocop.yml exclude removed.

Evidence: Commit 9efa4ef0 on main (fast-forwarded 2026-09-27). Stdout byte-identical per command against the old runner on the same fixture stdin, recorded in tests/fixtures/command_scripts/stdout.yaml and pinned by tests/cli/command_scripts_stdout_test.rb; tests/cli/command_script_names_resolve_test.rb checks every command name in maps and agent prompts resolves to a script. Suites: os-sdlc 1310 runs, 2868 assertions, 0 failures; os 27 runs, 0 failures. rubocop 71 offenses vs 72 baseline. grep os-sdlc-runner under plugins/ returns nothing. Live smoke after refresh: which os-sdlc-gate resolves via ~/.claude/plugins/os-sdlc/bin; os-sdlc-brief --dispatch-id 99999 prints 'no dispatch 99999' exit 3. ADR-0232; ADR-0123 stays Accepted with a partial-supersession note.

Follow-ups: Not ticketed by user choice (noted in HANDOFF.md Session 10): Implementation#closed? and CorrectionRound.for_implementation have no production callers; tests/support/dispatch_240_fixture.rb is dead; the retired-tests gate command resolves through PATH rather than an absolute path. Operational: Claude sessions open during the merge hold old prompts naming os-sdlc-runner and must be restarted before /os-sdlc:implement.

## Resolution **Done:** RunnerCLI dissolved. Eight scripts bin/os-sdlc-<command> (gate, subagent-stop, brief, implementation-open, next, implementation-report, subagent-start, retired-tests-check) over one thin DispatchScripts class with stdlib OptionParser. Deleted bin/os-sdlc-runner, cli/runner_cli.rb, and the three uncalled commands implementation-close, violations, durations with their report chains. Every caller renamed in the same commit: ten agent prompts, four skill files, three maps, the hook-cap message in presenter/hook_payload.rb, the retired-tests gate command in query/gate/commands.rb, plugins/os usage_sweep and its five finish tests, CONTEXT.md, deterministic-gates.md, CLAUDE.md. Stale .rubocop.yml exclude removed. **Evidence:** Commit 9efa4ef0 on main (fast-forwarded 2026-09-27). Stdout byte-identical per command against the old runner on the same fixture stdin, recorded in tests/fixtures/command_scripts/stdout.yaml and pinned by tests/cli/command_scripts_stdout_test.rb; tests/cli/command_script_names_resolve_test.rb checks every command name in maps and agent prompts resolves to a script. Suites: os-sdlc 1310 runs, 2868 assertions, 0 failures; os 27 runs, 0 failures. rubocop 71 offenses vs 72 baseline. grep os-sdlc-runner under plugins/ returns nothing. Live smoke after refresh: which os-sdlc-gate resolves via ~/.claude/plugins/os-sdlc/bin; os-sdlc-brief --dispatch-id 99999 prints 'no dispatch 99999' exit 3. ADR-0232; ADR-0123 stays Accepted with a partial-supersession note. **Follow-ups:** Not ticketed by user choice (noted in HANDOFF.md Session 10): Implementation#closed? and CorrectionRound.for_implementation have no production callers; tests/support/dispatch_240_fixture.rb is dead; the retired-tests gate command resolves through PATH rather than an absolute path. Operational: Claude sessions open during the merge hold old prompts naming os-sdlc-runner and must be restarted before /os-sdlc:implement.
jared closed this issue 2026-09-27 14:28:13 +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#768
No description provided.