<!-- task=m-iop-agent-cli-runtime/15+13_host_lifecycle plan=1 tag=REVIEW_API -->

# Make Concurrent Host Stop Cancellation-Safe

## For the Implementing Agent

Filling implementation-owned sections in `CODE_REVIEW-cloud-G04.md` is mandatory. Run every verification command, paste actual notes and output, leave the active pair in place, and report ready for review. Finalization belongs only to the code-review skill. If blocked, record the exact blocker, attempted commands/output, and resume condition in implementation-owned evidence fields; do not ask the user, call user-input tools, create control-plane stop files, classify the next state, archive logs, or write `complete.log`.

## Background

The prior review found that `Host.Stop` cannot cancel a component that is still blocked in `Start`, because both operations wait on the same lifecycle mutex before `Stop` reaches the run cancel function. The fix must make the stop request visible before serialized teardown while preserving ordered rollback, error identity, idempotent repeated stop, and application-only lifecycle ownership.

## Archive Evidence Snapshot

- Prior plan: `agent-task/m-iop-agent-cli-runtime/15+13_host_lifecycle/plan_local_G06_0.log`
- Prior review: `agent-task/m-iop-agent-cli-runtime/15+13_host_lifecycle/code_review_cloud_G06_0.log`
- Verdict: FAIL
- Findings: Required 1, Suggested 0, Nit 0. `Start` holds `lifecycleMu` while components start, and `Stop` waits for that mutex before calling the stored cancel function, so a context-blocked component cannot be stopped.
- Affected files: `apps/agent/internal/host/host.go`, `apps/agent/internal/host/host_test.go`
- Verification evidence: fresh package, race, vet, formatting, and diff checks passed; a focused reviewer regression failed after 250 ms with `Stop() could not cancel an in-progress component Start`.
- Roadmap carryover: this is a compatibility prerequisite for the active IOP Agent CLI Runtime Milestone and does not complete a Roadmap Task id.

## Analysis

### Files Read

- `apps/agent/internal/host/host.go`
- `apps/agent/internal/host/ports.go`
- `apps/agent/internal/host/host_test.go`
- `packages/go/agenttask/ports.go`
- `packages/go/agenttask/types.go`
- `agent-contract/inner/agent-runtime.md`
- `agent-contract/inner/iop-agent-cli-runtime.md`
- `agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md`
- `agent-roadmap/sdd/automation-runtime-bridge/iop-agent-cli-runtime/SDD.md`
- `agent-task/archive/2026/07/m-iop-agent-cli-runtime/13_agent_domain/complete.log`
- `agent-task/m-iop-agent-cli-runtime/16+13,15_bootstrap_composition/PLAN-local-G05.md`
- `agent-test/local/rules.md`

### SDD Criteria

- SDD: `agent-roadmap/sdd/automation-runtime-bridge/iop-agent-cli-runtime/SDD.md`
- Status: `[승인됨]`; SDD lock: released; active `USER_REVIEW.md`: absent.
- Related scenarios: S10 / `cli-surface`, S11 / `local-control`, S12 / `project-logs`, and S15 / `client-process-manager` consume the standalone host lifecycle seam.
- Evidence Map effect: those rows require later CLI, socket, log, and client-process integration evidence. This packet only makes their shared host prerequisite cancellation-safe, so it adds a focused lifecycle regression and race verification without claiming any Evidence Map row or Roadmap Task id.

### Verification Context

- Supplied handoff: none.
- Repository-native sources: `agent-test/local/rules.md`, the Agent and testing domain rules, the two runtime contracts, existing host tests, and the prior review evidence.
- Local preflight: `/config/.local/bin/go`; `go version go1.26.2 linux/arm64`; `GOROOT=/config/opt/go`; repository root `/config/workspace/iop-s0`.
- Fresh evidence: `go test -count=1`, `go test -count=1 -race`, `go vet`, `gofmt -d`, and `git diff --check` passed for the current host source; the focused concurrent Stop reproducer failed deterministically.
- Preconditions: use the current intentional dirty checkout and do not modify sibling task files. No external runner, service, credential, or full-cycle user entrypoint is required because bootstrap and command composition remain out of scope.
- Constraint: fresh test output is required; cached Go test output is not acceptable.
- Gap: the existing suite has no concurrent Stop-during-Start case.
- Confidence: high; the lock/cancel cycle is directly visible in source and reproduced by one bounded test.

### Test Coverage Gaps

- Existing tests cover ordered start/reverse stop, startup rollback, cancellation after a component returns an error, error identity, defensive status copies, and repeated Stop.
- No test calls `Stop` while a component is blocked in `Start` waiting for the host-provided context.
- The regression must also prove that only the successfully started prefix is rolled back and that the race detector remains clean.

### Symbol References

None. No symbol is renamed or removed, and no production caller of `apps/agent/internal/host` exists yet.

### Split Judgment

Keep one compact packet. Stop-request publication, lifecycle serialization, and the concurrent regression test form one correctness invariant and cannot independently PASS.

The `13` predecessor encoded by `15+13_host_lifecycle` is satisfied by `agent-task/archive/2026/07/m-iop-agent-cli-runtime/13_agent_domain/complete.log`.

### Scope Rationale

Change only `apps/agent/internal/host/host.go` and `host_test.go`. Do not change `ports.go`, bootstrap composition, shared `agenttask` lifecycle algorithms, contracts, configuration, project logs, local control, client processes, or command surfaces.

### Final Routing

- evaluation_mode: `isolated-reassessment`
- finalizer: `finalize-task-policy.sh pair`
- build closures: all `true`; grade scores `1/2/0/0/1`; base and final route `local-fit`; lane/grade `local-G04`; filename `PLAN-local-G04.md`
- review closures: all `true`; grade scores `1/2/0/0/1`; route `official-review`; lane/grade `cloud-G04`; filename `CODE_REVIEW-cloud-G04.md`
- large_indivisible_context: `false`
- positive loop risks: `temporal_state`, `concurrent_consistency` (count `2`)
- recovery signals: `review_rework_count=1`, `evidence_integrity_failure=false`
- capability-gap evidence: none

## Implementation Checklist

- [x] Publish a concurrent stop request before lifecycle teardown waits, and add a deterministic regression proving prompt cancellation, started-prefix rollback, and safe repeated Stop.
- [x] Run fresh focused, full-package, race, vet, formatting, and diff verification.
- [x] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [REVIEW_API-1] Cancel an In-Progress Start Without Losing the Stop Request

#### Problem

At `apps/agent/internal/host/host.go:73-74`, `Start` holds `lifecycleMu` across every component callback. At `apps/agent/internal/host/host.go:144-145`, `Stop` waits for the same mutex before reading and calling `h.cancel` at lines 153-157. A component whose `Start` waits on `ctx.Done()` therefore prevents the only operation intended to cancel that context.

Before:

```go
// apps/agent/internal/host/host.go:72
func (h *Host) Start(ctx context.Context) error {
	h.lifecycleMu.Lock()
	defer h.lifecycleMu.Unlock()
	// Component Start calls run while lifecycleMu is held.
}

// apps/agent/internal/host/host.go:143
func (h *Host) Stop(ctx context.Context) error {
	h.lifecycleMu.Lock()
	defer h.lifecycleMu.Unlock()
	// h.cancel is reached only after Start returns.
}
```

#### Solution

Add an internal stop-request state protected by `mu`. `Stop` must publish that request and snapshot/call the current cancel function before waiting for `lifecycleMu`; `Start` must reject a request that was published before it creates the run context. This closes both races: Stop after cancel publication cancels the blocked component, and Stop in the small window before publication cannot be lost. Do not mark `Status.Stopped` until rollback or teardown has completed.

After:

```go
type Host struct {
	lifecycleMu  sync.Mutex
	mu           sync.RWMutex
	stopRequested bool
	// existing fields
}

func (h *Host) Start(ctx context.Context) error {
	h.lifecycleMu.Lock()
	defer h.lifecycleMu.Unlock()

	h.mu.Lock()
	if h.status.Stopped || h.stopRequested {
		h.mu.Unlock()
		return errors.New("host: cannot start after stop")
	}
	// Publish h.cancel while still holding mu before component callbacks.
}

func (h *Host) Stop(ctx context.Context) error {
	h.mu.Lock()
	if h.status.Stopped {
		h.mu.Unlock()
		return nil
	}
	h.stopRequested = true
	cancel := h.cancel
	h.mu.Unlock()
	if cancel != nil {
		cancel()
	}

	h.lifecycleMu.Lock()
	defer h.lifecycleMu.Unlock()
	// Recheck terminal state, then perform the existing reverse teardown.
}
```

Equivalent explicit lifecycle state is acceptable only if it proves the same no-lost-stop and no-premature-stopped invariants.

#### Modified Files and Checklist

- [x] `apps/agent/internal/host/host.go` — publish and observe stop requests without introducing lock-order inversion or premature terminal status.
- [x] `apps/agent/internal/host/host_test.go` — add `TestHostStopCancelsInProgressStart`.

#### Test Strategy

Extend the deterministic fake component with a start-entered signal and a mode that blocks until its context is cancelled. Start a host with one ready component followed by the blocking component, wait for the signal, call `Stop` concurrently, and assert within a bounded deadline that:

- `Stop` returns nil;
- `Start` returns an error matching `context.Canceled`;
- the blocking component is not stopped because its Start did not succeed;
- the ready prefix is stopped exactly once in reverse order;
- status is terminal only after cleanup, and another `Stop` returns nil.

Run the package under `-race` to validate the new state and signal synchronization.

#### Verification

```bash
gofmt -w apps/agent/internal/host/*.go
go test -count=1 -run 'TestHost(StartStopOrdering|StartRollback|StopIsIdempotent|StopCancelsInProgressStart)$' ./apps/agent/internal/host
```

```
gofmt: clean

ok  	iop/apps/agent/internal/host	0.003s
```

## Dependencies and Execution Order

Predecessor `13_agent_domain` is complete at `agent-task/archive/2026/07/m-iop-agent-cli-runtime/13_agent_domain/complete.log`. Keep the existing `15+13_host_lifecycle` task path and complete this follow-up before dependent bootstrap work consumes the lifecycle package.

## Modified Files Summary

| File | Item |
|------|------|
| `apps/agent/internal/host/host.go` | REVIEW_API-1 |
| `apps/agent/internal/host/host_test.go` | REVIEW_API-1 |

## Final Verification

```bash
gofmt -w apps/agent/internal/host/*.go
go test -count=1 -run 'TestHost(StartStopOrdering|StartRollback|StopIsIdempotent|StopCancelsInProgressStart)$' ./apps/agent/internal/host
go test -count=1 ./apps/agent/internal/host
go test -count=1 -race ./apps/agent/internal/host
go vet ./apps/agent/internal/host
git diff --check
```

```
gofmt: clean

ok  	iop/apps/agent/internal/host	0.003s

ok  	iop/apps/agent/internal/host	0.002s

ok  	iop/apps/agent/internal/host	1.007s

vet: clean

diff check: clean
```

After completing all code changes, fill implementation-owned sections in `CODE_REVIEW-*-G??.md`.
