Replace os-sdlc cli/ and hook/ with one controller/ role directory nested by noun #781

Closed
opened 2026-09-27 20:56:53 +00:00 by jared · 1 comment
Owner

Problem

After #767/#768 every class in cli/ and hook/ runs one sequence: receive a process invocation, parse it, call a core command, render the result, set an exit code. The two directories name the input source, not the role. Class names (CLI, DispatchScripts, SubagentStart, PostToolUse) name transport or harness events, not the noun they act on. Three copies of the hook envelope, three spellings of the exit-3 policy, and four spellings of event parsing exist because no one object owns the boundary.

Reproduce

n/a: naming and duplication; see the shared-sequence table in the session transcript.

Approach

One role directory controller/, nested by noun per lens A1, with a single process-IO owner. Structure commit first, then the DRY commit.

lib/os_sdlc/controller/
  process_io.rb                 # Invocation (was CLI::Invocation), exit-3 policy, hook_envelope(name, payload)
  project/setup_cli.rb          # was cli/cli.rb
  implementation/scripts.rb     # implementation_open, next, implementation_report, retired_tests_check
  dispatch/scripts.rb           # subagent_start, subagent_stop, gate, brief
  dispatch/arrival_hook.rb      # was hook/subagent_start.rb
  dispatch/completion_hook.rb   # was hook/subagent_stop.rb
  dispatch/async_launch_hook.rb # was hook/post_tool_use.rb
  evidence/report_cli.rb        # was cli/evidence_cli.rb
  lsp/probe_cli.rb              # was cli/lsp_probe_cli.rb

Required work:

  1. Create controller/ and move the nine classes with the renames above; every bin/ and hooks/ entry script updated in the same commit.
  2. Collapse the three hookSpecificOutput copies into ProcessIO.hook_envelope and the three exit-3 spellings into one ProcessIO method.
  3. Hooks write through the injected stdout, not Kernel#puts.
  4. Delete cli/ and hook/ directories; update Zeitwerk inflections, inward-dependency baseline, plugins/os-sdlc/CLAUDE.md hook contract.
  5. ADR amending ADR-0222 (controller/ role) and ADR-0231 (hook class names) via /os-adr:create; ADR-0123 stays Accepted.

Rejected: group by transport (argv/, event/). Splits each script/hook twin across two directories.
Rejected: name the directory boundary/. Two-way door; controller is the common word for receive, route, render.

Acceptance criteria

  1. Given every bin/os-sdlc-* script and both hook entry scripts, when run against the recorded fixtures, then stdout is byte-identical to before the move.
  2. Given the suite, when run, then it is green and lib/os_sdlc/cli/ and lib/os_sdlc/hook/ do not exist.
  3. Given the hookSpecificOutput string, when grepped under lib/, then it appears in exactly one file.
  4. Given a hook test, when it injects a StringIO stdout, then the envelope lands in that StringIO.

Owner sketch

module OsSdlc::Controller
  ProcessIO = Data.define(:cwd, :stdin, :stdout, :stderr, :env)
  class Dispatch::ArrivalHook
  class Implementation::Scripts
end

Non-goals

Moving the PostToolUse decision into the core (its own ticket, must land first). Changing any Thor subclass to live in bin (ADR-0123 stays).

Origin

  • Trigger: session 12 design read of plugins/os-sdlc/lib/os_sdlc/cli/ and hook/ after the user challenged the value of the cli/ classes
  • Improvised this session: none
  • Chain: boundary classes named by transport ← RunnerCLI/Pipe removal (#767, #768) left cli/ and hook/ as one role with two names ← DESIGN (ADR-0222 role layout lists no boundary role)
  • Root candidate: this ticket
  • Where: plugins/os-sdlc/lib/os_sdlc/cli/, plugins/os-sdlc/lib/os_sdlc/hook/
  • Session: 363157fa-82c5-4379-aec1-7c12405e4075
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-crc-pass-os-sdlc/363157fa-82c5-4379-aec1-7c12405e4075.jsonl
## Problem After #767/#768 every class in `cli/` and `hook/` runs one sequence: receive a process invocation, parse it, call a core command, render the result, set an exit code. The two directories name the input source, not the role. Class names (`CLI`, `DispatchScripts`, `SubagentStart`, `PostToolUse`) name transport or harness events, not the noun they act on. Three copies of the hook envelope, three spellings of the exit-3 policy, and four spellings of event parsing exist because no one object owns the boundary. ## Reproduce n/a: naming and duplication; see the shared-sequence table in the session transcript. ## Approach One role directory `controller/`, nested by noun per lens A1, with a single process-IO owner. Structure commit first, then the DRY commit. ``` lib/os_sdlc/controller/ process_io.rb # Invocation (was CLI::Invocation), exit-3 policy, hook_envelope(name, payload) project/setup_cli.rb # was cli/cli.rb implementation/scripts.rb # implementation_open, next, implementation_report, retired_tests_check dispatch/scripts.rb # subagent_start, subagent_stop, gate, brief dispatch/arrival_hook.rb # was hook/subagent_start.rb dispatch/completion_hook.rb # was hook/subagent_stop.rb dispatch/async_launch_hook.rb # was hook/post_tool_use.rb evidence/report_cli.rb # was cli/evidence_cli.rb lsp/probe_cli.rb # was cli/lsp_probe_cli.rb ``` Required work: 1. Create controller/ and move the nine classes with the renames above; every bin/ and hooks/ entry script updated in the same commit. 2. Collapse the three hookSpecificOutput copies into ProcessIO.hook_envelope and the three exit-3 spellings into one ProcessIO method. 3. Hooks write through the injected stdout, not Kernel#puts. 4. Delete cli/ and hook/ directories; update Zeitwerk inflections, inward-dependency baseline, plugins/os-sdlc/CLAUDE.md hook contract. 5. ADR amending ADR-0222 (controller/ role) and ADR-0231 (hook class names) via /os-adr:create; ADR-0123 stays Accepted. Rejected: group by transport (argv/, event/). Splits each script/hook twin across two directories. Rejected: name the directory boundary/. Two-way door; controller is the common word for receive, route, render. ## Acceptance criteria 1. Given every bin/os-sdlc-* script and both hook entry scripts, when run against the recorded fixtures, then stdout is byte-identical to before the move. 2. Given the suite, when run, then it is green and lib/os_sdlc/cli/ and lib/os_sdlc/hook/ do not exist. 3. Given the hookSpecificOutput string, when grepped under lib/, then it appears in exactly one file. 4. Given a hook test, when it injects a StringIO stdout, then the envelope lands in that StringIO. ## Owner sketch ```ruby module OsSdlc::Controller ProcessIO = Data.define(:cwd, :stdin, :stdout, :stderr, :env) class Dispatch::ArrivalHook class Implementation::Scripts end ``` ## Non-goals Moving the PostToolUse decision into the core (its own ticket, must land first). Changing any Thor subclass to live in bin (ADR-0123 stays). ## Origin - Trigger: session 12 design read of plugins/os-sdlc/lib/os_sdlc/cli/ and hook/ after the user challenged the value of the cli/ classes - Improvised this session: none - Chain: boundary classes named by transport ← RunnerCLI/Pipe removal (#767, #768) left cli/ and hook/ as one role with two names ← DESIGN (ADR-0222 role layout lists no boundary role) - Root candidate: this ticket - Where: plugins/os-sdlc/lib/os_sdlc/cli/, plugins/os-sdlc/lib/os_sdlc/hook/ - Session: 363157fa-82c5-4379-aec1-7c12405e4075 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-crc-pass-os-sdlc/363157fa-82c5-4379-aec1-7c12405e4075.jsonl
Author
Owner

Resolution

Done: lib/os_sdlc/controller/ replaces cli/ and hook/, nested by noun: ProcessIO (sole envelope and exit-3 site), ArgvOption, Project::SetupCLI, Implementation::Scripts, Dispatch::Scripts, Dispatch::ArrivalHook/CompletionHook/AsyncLaunchHook, Evidence::ReportCLI, Lsp::ProbeCLI. Hook entry scripts are one exit line over ProcessIO.from_process; hooks write through injected IO. args_holder and start_subagent raise LoudFailure; SystemExit rescues removed from tests. ADR-0236 amends ADR-0222/0231/0232/0194 item 6; plugins/os-sdlc/CLAUDE.md hook contract, docs/architecture/os-sdlc-profile.md, lsp-navigate cheatsheet updated; bin/refresh-plugins run.

Evidence: commits 7d8d908f (structure), bbca07a0 (DRY), 84ca0618 (ADR) on crc-pass-os-sdlc; suite 1314 runs 0 failures after each commit; lint-inward-dependencies 64 unchanged; 25 bin/hook commands byte-identical on stdout/stderr/exit vs 132df5ae, plus uncovered refusal paths diffed in a scratch project; grep hookSpecificOutput hits one file

Follow-ups: Lens A2 root files missing for the implicit CLI namespace (TargetFlag, OptionParserBuilder, TargetSelectionError, ProjectConfigCommand, ErrorMessage should move under Project::), Evidence, and Lsp; remaining warn+exit 1 on the SetupCLI path (CommandSupport.fail_soft, ProjectConfigCommand, InitCommand, WorklistReport); dead evidence entries in the collapse list and ROLE_LAYER, InwardDependencies::INFLECTIONS never read. All three recorded in docs/crc/os-sdlc/HANDOFF.md next steps for the user to ticket or drop; not filed as tickets because they are AI-initiated follow-ups of an AI-shaped ticket.

## Resolution **Done:** lib/os_sdlc/controller/ replaces cli/ and hook/, nested by noun: ProcessIO (sole envelope and exit-3 site), ArgvOption, Project::SetupCLI, Implementation::Scripts, Dispatch::Scripts, Dispatch::ArrivalHook/CompletionHook/AsyncLaunchHook, Evidence::ReportCLI, Lsp::ProbeCLI. Hook entry scripts are one exit line over ProcessIO.from_process; hooks write through injected IO. args_holder and start_subagent raise LoudFailure; SystemExit rescues removed from tests. ADR-0236 amends ADR-0222/0231/0232/0194 item 6; plugins/os-sdlc/CLAUDE.md hook contract, docs/architecture/os-sdlc-profile.md, lsp-navigate cheatsheet updated; bin/refresh-plugins run. **Evidence:** commits 7d8d908f (structure), bbca07a0 (DRY), 84ca0618 (ADR) on crc-pass-os-sdlc; suite 1314 runs 0 failures after each commit; lint-inward-dependencies 64 unchanged; 25 bin/hook commands byte-identical on stdout/stderr/exit vs 132df5ae, plus uncovered refusal paths diffed in a scratch project; grep hookSpecificOutput hits one file **Follow-ups:** Lens A2 root files missing for the implicit CLI namespace (TargetFlag, OptionParserBuilder, TargetSelectionError, ProjectConfigCommand, ErrorMessage should move under Project::), Evidence, and Lsp; remaining warn+exit 1 on the SetupCLI path (CommandSupport.fail_soft, ProjectConfigCommand, InitCommand, WorklistReport); dead evidence entries in the collapse list and ROLE_LAYER, InwardDependencies::INFLECTIONS never read. All three recorded in docs/crc/os-sdlc/HANDOFF.md next steps for the user to ticket or drop; not filed as tickets because they are AI-initiated follow-ups of an AI-shaped ticket.
jared closed this issue 2026-09-27 21:28:06 +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#781
No description provided.