Hook dispatch in-process (supersede ADR-0172 item 4) #632
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#632
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
plugins/os-sdlc/lib/os_sdlc/hook/pipe.rb:41runsOpen3.capture3("ruby", runner_bin, @subcommand, chdir: @event.cwd, stdin_data: @raw). This boots a second Ruby process for every SubagentStart, SubagentStop, and PostToolUse hook event. The hooks inplugins/os-sdlc/hooks/*.rbalreadyrequire_relative "../lib/os_sdlc"— the same boot filebin/os-sdlc-runneruses — so the second boot is redundant work paid on every hook firing, not just in tests.Observed
plugins/os-sdlc/lib/os_sdlc/hook/pipe.rb:41forksruby <runner_bin> <subcommand>per event.os_sdlcbefore Pipe runs.Pipeline::CLI.startrescuesStandardErrorinternally and callsexit 3; Thor itself may also raiseSystemExit. Both must be mapped to the same stderr-plus-nonzero-status contract the subprocess path gives today.hookSpecificOutput.additionalContext.Reproduce
n/a: design change. Latency is measured as part of this ticket's own acceptance criteria, not reproduced as a defect.
Approach
Pipe builds an
OsSdlc::CLI::Invocation.new(cwd: @event.cwd, stdin: StringIO.new(@raw), stdout: StringIO.new, stderr: StringIO.new, env: ENV.to_h)and callsOsSdlc::Pipeline::CLI.start([@subcommand], invocation: ...)in-process instead of forking. NoDir.chdir: every command reads@invocation.cwd.surface_failuremust rescue bothStandardErrorandSystemExitfrom the in-process call and map each to the same stderr text and nonzero exit status the subprocess path produces today.Required work:
Open3.capture3fork with the in-processInvocation/Pipeline::CLI.startcall — check:grep -n "Open3.capture3" plugins/os-sdlc/lib/os_sdlc/hook/pipe.rbreturns nothing.SystemExitandStandardErrorfrom the in-process call to the existing stderr-plus-status contract — check:plugins/os-sdlc/tests/hook/pipe_missing_cwd_test.rbstays green.time; post both numbers as a ticket comment./os-adr:create, superseding ADR-0172 item 4. Cite ADR-0128 as still satisfied (Pipe adds no decision logic, only relays). Note ADR-0156 is itself superseded by ADR-0182; cite ADR-0182 as the precedent for the pre_tool_use in-process pattern instead.bin/refresh-pluginsso the change reaches active sessions.Rejected: a long-lived daemon. That is a different shape (persistent process, IPC, lifecycle management) and out of scope for this ticket.
Condition that would make this plan wrong: class-level memoized state in
Pipeline::CLIor its dispatch commands that assumes one command per process. If such state exists, the in-process call would leak state across hook events and the approach needs revisiting.VERDICT: CREATE
Acceptance criteria
ruby <runner_bin>subprocess is forked — the runner subcommand runs in-process.StandardErrororSystemExit, when Pipe rescues it, then the hook process does not crash, and the emitted stderr and exit status match what the subprocess path produced for the same failure today.bin/refresh-pluginshas been run.Owner sketch
Pinning test:
plugins/os-sdlc/tests/hook/pipe_missing_cwd_test.rbstays green.Non-goals
pre_tool_usehook path (already in-process via ADR-0182).Origin
Work started in session 082c3e8b on branch ticket-632 via /os-sdlc:implement.
Latency, SubagentStop hook with a probe payload, 6 timed runs per side after one warm-up, ruby 4.0.2: before (Open3 fork of bin/os-sdlc-runner) median 0.315 s, min 0.31 s; after (in-process via Invocation) median 0.17 s, min 0.17 s; delta -0.145 s, about -46%. Both sides took the same code path (identical harness error, exit 3).
Resolution
Done: OsSdlc::Hook::Pipe#relay calls OsSdlc::Pipeline::CLI.start in-process through an Invocation (cwd from the event, stdin from the raw payload, StringIO stdout/stderr). SystemExit passes its status through; StandardError is written to stderr and exits 3; stdout still becomes hookSpecificOutput. The runner is injectable via runner: for tests. ADR-0217 amends ADR-0172 item 4.
Evidence: Pipeline implementation 35 (escalated at programmer-repair bound: the test file used process-global stubs under parallelize_me!; the test seam was hand-fixed to inject the runner, no production change of substance; #709 captures the routing gap). Latency posted on the ticket: median 0.315 s to 0.17 s (-46%). Tests: tests/hook/pipe_in_process_test.rb (3 cases, green across seeds 1-5), pipe_missing_cwd_test.rb; os-sdlc suite 1449 runs 0 failures; rubocop clean. Commit
26c4eccdon branch ticket-632.Follow-ups: #709 captured (post-green test defect has no route to test-writer, VERDICT: CREATE). Dropped: env: ENV.to_h in Hook::Pipe is the hook entry point's own environment, the same read bin/os-sdlc-runner performs; not a new ENV fallback under ADR-0213.