Intent
Make Runner.Shutdown iterate r.runs and close scoped MCP registries so a wedged run's MCP subprocesses are torn down even when execute() never reaches a terminal-cleanup path.
Plan reference
Source finding
ScopedMCP subprocesses are not killed if a run is wedged at Shutdown (runner.go:3067-3091, mcp.go:662-665). closeScopedMCP only runs from the four terminal-cleanup paths. If execute() is blocked (e.g. a deadlock, or an MCP CallTool ignoring ctx), Runner.Shutdown only cancels cancelFuncs and does not iterate r.runs to close scoped registries.
Scope
- Allowed files (test):
internal/harness/runner_shutdown_mcp_test.go (new)
- Allowed files (impl):
internal/harness/runner.go, internal/harness/scoped_mcp.go
- Dependencies: none
- Complexity: low — Risk: low (idempotent
Close)
Acceptance criteria
Use slice T05 in the plan as detailed acceptance criteria. Summary:
- Failing test written first, compiles and fails on
main.
- Stub a wedged scoped MCP registry whose
ClientManager.Close blocks until called. Start a run, wedge its execute goroutine by injecting a tool that blocks on a channel. Call Runner.Shutdown; assert ClientManager.Close was called and the underlying MCP subprocess (a fake os/exec recording KILL via cmd.Process.Kill) was signalled.
- Impl:
closeScopedMCP is already idempotent — set state.scopedMCPRegistry = nil at the end so re-entry is a no-op. In Shutdown, snapshot state.scopedMCPRegistry for every live *runState, then _ = reg.Close(shutdownCtx) after cancelling cancelFuncs. Add defer r.closeScopedMCP(runID) inside execute() so a panic still tears down MCP.
go test ./internal/harness/... -race green; ./scripts/test-regression.sh green.
Guardrails
- Strict TDD; failing regression test must compile and fail on
main first.
- Worktree flow; own branch, merge via
verify-and-merge.sh.
- Update
.context/harness-reliability/tracker.md on pick-up and merge.
- Deterministic tests only — fake MCP registry + fake exec.
- Append engineering-log entry on merge.
Intent
Make
Runner.Shutdowniterater.runsand close scoped MCP registries so a wedged run's MCP subprocesses are torn down even whenexecute()never reaches a terminal-cleanup path.Plan reference
docs/plans/2026-06-24-harness-reliability-plan.md— slice T05Source finding
ScopedMCP subprocesses are not killed if a run is wedged at Shutdown (
runner.go:3067-3091,mcp.go:662-665).closeScopedMCPonly runs from the four terminal-cleanup paths. Ifexecute()is blocked (e.g. a deadlock, or an MCPCallToolignoring ctx),Runner.Shutdownonly cancelscancelFuncsand does not iterater.runsto close scoped registries.Scope
internal/harness/runner_shutdown_mcp_test.go(new)internal/harness/runner.go,internal/harness/scoped_mcp.goClose)Acceptance criteria
Use slice T05 in the plan as detailed acceptance criteria. Summary:
main.ClientManager.Closeblocks until called. Start a run, wedge itsexecutegoroutine by injecting a tool that blocks on a channel. CallRunner.Shutdown; assertClientManager.Closewas called and the underlying MCP subprocess (a fakeos/execrecording KILL viacmd.Process.Kill) was signalled.closeScopedMCPis already idempotent — setstate.scopedMCPRegistry = nilat the end so re-entry is a no-op. InShutdown, snapshotstate.scopedMCPRegistryfor every live*runState, then_ = reg.Close(shutdownCtx)after cancellingcancelFuncs. Adddefer r.closeScopedMCP(runID)insideexecute()so a panic still tears down MCP.go test ./internal/harness/... -racegreen;./scripts/test-regression.shgreen.Guardrails
mainfirst.verify-and-merge.sh..context/harness-reliability/tracker.mdon pick-up and merge.