Move PostToolUse pending-dispatch filter into DispatchCommands; exit_loudly raises #779

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

Problem

The PostToolUse hook decides whether a next instruction is due. It checks the agent type, the async flag, that the database exists, and queries for a pending dispatch before it calls the core. The argv path bin/os-sdlc-next makes none of these checks. A domain rule lives in the boundary and only one of two entry paths applies it. Separately, DispatchCommands.exit_loudly calls warn and exit 3 from inside the core, so it bypasses the boundary rescue and the injected stderr in CLI::Invocation.

Reproduce

n/a: design defect; the pinning tests in tests/hooks/post_tool_use_*_test.rb show the filter living in the hook.

Approach

Move the filter into the core as one query method, and make the core raise instead of exiting.

module OsSdlc::DispatchCommands
  def self.next_instruction_after_async_launch(event)
    return nil unless AsyncLaunch.new(event).pending_dispatch?
    next_instruction_from_event(event)
  end

  def self.exit_loudly(message)
    raise HarnessError, "os-sdlc-#{message}"
  end
end

PostToolUse#call becomes parse, call next_instruction_after_async_launch, render. DispatchScripts#guarded and the CLI rescue map HarnessError to exit 3 through invocation.stderr.

Required work:

  1. Extract the PostToolUse filter (agent type, async_launched, db exists, pending dispatch) into DispatchCommands.next_instruction_after_async_launch; the hook keeps no Db.connect call.
  2. Replace exit_loudly's warn plus exit with a raised OsSdlc::HarnessError; DispatchScripts#guarded rescues it, writes to invocation.stderr, returns 3.
  3. Hook output stays byte-identical for every existing fixture in tests/hooks/.

Rejected: keep the filter in the hook and add it to bin/os-sdlc-next too. Two copies of one rule.

Acceptance criteria

  1. Given a PostToolUse event for a non-Agent tool, when the hook runs, then it prints nothing and exits 0, as today.
  2. Given a PostToolUse event with a pending dispatch, when the hook runs, then it prints the same hookSpecificOutput envelope as today.
  3. Given an argv call that hits exit_loudly, when run through DispatchScripts, then the message appears on invocation.stderr and the return code is 3.
  4. Given the whole suite, when run, then it is green and no file under hook/ opens the database.

Owner sketch

module OsSdlc::DispatchCommands
  def self.next_instruction_after_async_launch(event)
end
class OsSdlc::HarnessError < StandardError; end

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/hook/post_tool_use.rb, command/dispatch_commands.rb
  • 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 The PostToolUse hook decides whether a next instruction is due. It checks the agent type, the async flag, that the database exists, and queries for a pending dispatch before it calls the core. The argv path `bin/os-sdlc-next` makes none of these checks. A domain rule lives in the boundary and only one of two entry paths applies it. Separately, `DispatchCommands.exit_loudly` calls `warn` and `exit 3` from inside the core, so it bypasses the boundary rescue and the injected stderr in `CLI::Invocation`. ## Reproduce n/a: design defect; the pinning tests in tests/hooks/post_tool_use_*_test.rb show the filter living in the hook. ## Approach Move the filter into the core as one query method, and make the core raise instead of exiting. ```ruby module OsSdlc::DispatchCommands def self.next_instruction_after_async_launch(event) return nil unless AsyncLaunch.new(event).pending_dispatch? next_instruction_from_event(event) end def self.exit_loudly(message) raise HarnessError, "os-sdlc-#{message}" end end ``` `PostToolUse#call` becomes parse, call `next_instruction_after_async_launch`, render. `DispatchScripts#guarded` and the CLI rescue map `HarnessError` to exit 3 through `invocation.stderr`. Required work: 1. Extract the PostToolUse filter (agent type, async_launched, db exists, pending dispatch) into DispatchCommands.next_instruction_after_async_launch; the hook keeps no Db.connect call. 2. Replace exit_loudly's warn plus exit with a raised OsSdlc::HarnessError; DispatchScripts#guarded rescues it, writes to invocation.stderr, returns 3. 3. Hook output stays byte-identical for every existing fixture in tests/hooks/. Rejected: keep the filter in the hook and add it to bin/os-sdlc-next too. Two copies of one rule. ## Acceptance criteria 1. Given a PostToolUse event for a non-Agent tool, when the hook runs, then it prints nothing and exits 0, as today. 2. Given a PostToolUse event with a pending dispatch, when the hook runs, then it prints the same hookSpecificOutput envelope as today. 3. Given an argv call that hits exit_loudly, when run through DispatchScripts, then the message appears on invocation.stderr and the return code is 3. 4. Given the whole suite, when run, then it is green and no file under hook/ opens the database. ## Owner sketch ```ruby module OsSdlc::DispatchCommands def self.next_instruction_after_async_launch(event) end class OsSdlc::HarnessError < StandardError; end ``` ## 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/hook/post_tool_use.rb, command/dispatch_commands.rb - 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: Dispatch::AsyncLaunch#due? (query/dispatch/) owns the PostToolUse filter; DispatchCommands.next_instruction_after_async_launch is the one entry; exit_loudly raises LoudFailure < HarnessError, DispatchScripts#guarded and the subagent hooks map it to exit 3 with unchanged stderr text. ADR-0235 amends ADR-0231; plugins/os-sdlc/CLAUDE.md hook contract updated.

Evidence: commits bc3fcb02, 991363c9 on crc-pass-os-sdlc; suite 1314 runs 0 failures; tests/hooks byte-identity green; bin/lint-inward-dependencies 64 before and after; new tests tests/command/dispatch_commands_async_launch_test.rb and cli_invocation_test#test_a_loud_refusal_reaches_the_invocation_stderr_and_returns_3

Follow-ups: Two warn+exit 3 calls remain in the core (parser/dispatch_commands/args_holder.rb#shift_or_fail, command/start_subagent.rb#abort_missing_implementation!) and tests/support/in_process_dispatch_script.rb still rescues SystemExit: folded into #781's DRY step, not a separate ticket. The duplicated LoudFailure rescue in the two subagent hooks collapses in #781 (ProcessIO).

## Resolution **Done:** Dispatch::AsyncLaunch#due? (query/dispatch/) owns the PostToolUse filter; DispatchCommands.next_instruction_after_async_launch is the one entry; exit_loudly raises LoudFailure < HarnessError, DispatchScripts#guarded and the subagent hooks map it to exit 3 with unchanged stderr text. ADR-0235 amends ADR-0231; plugins/os-sdlc/CLAUDE.md hook contract updated. **Evidence:** commits bc3fcb02, 991363c9 on crc-pass-os-sdlc; suite 1314 runs 0 failures; tests/hooks byte-identity green; bin/lint-inward-dependencies 64 before and after; new tests tests/command/dispatch_commands_async_launch_test.rb and cli_invocation_test#test_a_loud_refusal_reaches_the_invocation_stderr_and_returns_3 **Follow-ups:** Two warn+exit 3 calls remain in the core (parser/dispatch_commands/args_holder.rb#shift_or_fail, command/start_subagent.rb#abort_missing_implementation!) and tests/support/in_process_dispatch_script.rb still rescues SystemExit: folded into #781's DRY step, not a separate ticket. The duplicated LoudFailure rescue in the two subagent hooks collapses in #781 (ProcessIO).
jared closed this issue 2026-09-27 21:07:44 +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#779
No description provided.