<!-- task=m-node-provider-execution-liveness-recovery/03+02_health_probe_contract plan=3 tag=REVIEW_API milestone-task=health-classification -->

# PLAN — Enforce the Independent Health Probe Ceiling

## For the Implementing Agent

Implement only this follow-up checklist, preserve unrelated worktree changes, run every verification command, and fill the implementation-owned sections of `CODE_REVIEW-cloud-G05.md` with exact output. Keep the active PLAN/review pair in place and report ready for official review. If blocked, record the exact blocker, attempted commands/output, and resume condition only 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, write `complete.log`, commit, push, or modify roadmap state.

## Background

The health probe contract correctly classifies returned results, but the coordinator invokes the prober synchronously. A prober that ignores context cancellation and does not return can therefore hold `ProbeHealth` beyond its promised independent ceiling. This follow-up makes the ceiling control coordinator return time and adds deterministic evidence for the blocked-hook boundary without changing adapter, identity, progress, fence, terminal, retry, or Edge behavior.

## Archive Evidence Snapshot

- Closing pair: `agent-task/m-node-provider-execution-liveness-recovery/03+02_health_probe_contract/plan_local_G07_2.log` and `agent-task/m-node-provider-execution-liveness-recovery/03+02_health_probe_contract/code_review_cloud_G07_2.log`.
- Verdict: FAIL with 1 Required, 0 Suggested, and 0 Nit findings.
- Required finding: `apps/node/internal/node/health_probe.go:71` blocks synchronously inside `probe(...)`; the independent deadline is checked only after the hook returns.
- Fresh reviewer evidence: a 10ms ceiling with a permanently blocking hook remained blocked after 100ms. Focused Node unit, race, vet, format, and diff checks otherwise passed.
- Roadmap carryover: preserve `milestone-task=health-classification`; this follow-up closes the bounded-probe portion of approved SDD scenario S03 only.

## Dependencies and Execution Order

- Runtime predecessor index `02` is satisfied by `agent-task/archive/2026/08/m-node-provider-execution-liveness-recovery/02+01_stall_watchdog/complete.log`.
- Keep the existing `03+02_health_probe_contract` task path. The sibling `04+03_health_evidence` remains dependent on this task's future PASS `complete.log`.

## Analysis

### Files Read

- `agent-task/m-node-provider-execution-liveness-recovery/03+02_health_probe_contract/PLAN-local-G07.md`
- `agent-task/m-node-provider-execution-liveness-recovery/03+02_health_probe_contract/CODE_REVIEW-cloud-G07.md`
- `agent-task/m-node-provider-execution-liveness-recovery/03+02_health_probe_contract/plan_cloud_G08_0.log`
- `agent-task/m-node-provider-execution-liveness-recovery/03+02_health_probe_contract/plan_cloud_G08_1.log`
- `agent-task/m-node-provider-execution-liveness-recovery/03+02_health_probe_contract/code_review_cloud_G08_0.log`
- `agent-task/m-node-provider-execution-liveness-recovery/03+02_health_probe_contract/code_review_cloud_G08_1.log`
- `agent-task/archive/2026/08/m-node-provider-execution-liveness-recovery/02+01_stall_watchdog/complete.log`
- `agent-task/m-node-provider-execution-liveness-recovery/04+03_health_evidence/PLAN-cloud-G08.md`
- `apps/node/internal/node/health_probe.go`
- `apps/node/internal/node/health_probe_test.go`
- `packages/go/execution/liveness.go`
- `packages/go/execution/liveness_test.go`
- `packages/go/execution/types.go`
- `apps/node/internal/node/command_handler.go`
- `apps/node/internal/adapters/ollama/provider.go`
- `apps/node/internal/adapters/vllm/provider.go`
- `apps/node/internal/adapters/openai_compat/provider.go`
- `agent-contract/inner/execution-runtime.md`
- `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
- `agent-roadmap/phase/operational-observability-provider-management/milestones/node-provider-execution-liveness-recovery.md`
- `agent-roadmap/sdd/operational-observability-provider-management/node-provider-execution-liveness-recovery/SDD.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-test/local/testing-smoke.md`

### SDD Criteria

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/node-provider-execution-liveness-recovery/SDD.md`; status `[승인됨]`, lock released, no user review.
- First-line scope remains `milestone-task=health-classification`.
- Targeted Acceptance Scenario: S03. Its target probe must be bounded and must map timeout/inconclusive outcomes to unknown without resetting request progress.
- Evidence Map row S03 requires deterministic timeout target-prober evidence. The checklist therefore pairs the coordinator timeout selection with a channel-controlled blocked-hook regression and retains the existing available/unavailable/identity/error assertions.
- Adapter/target/observation-sequence terminal integration remains the already-planned `04+03_health_evidence` slice and is not re-owned here.

### Verification Context

- Handoff source: the verdict-appended current review and fresh reviewer reproducer; no separate external verification handoff was supplied.
- Repository-native profiles: `agent-test/local/node-smoke.md`, `platform-common-smoke.md`, and `testing-smoke.md` require fresh Node/shared tests, repository regression, and diff hygiene.
- Fresh reviewer evidence: the focused blocking-hook reproducer failed; `go test -count=1 ./packages/go/execution ./apps/node/...`, the focused `-race` command, `go vet`, `gofmt`, and target `git diff --check` passed.
- Preconditions: local Go module at `/config/workspace/iop-s1/go.mod`; no provider credential or external service is required.
- Real-process constraint: `/tmp` is mounted `noexec`; build the diagnostic Node binary under executable `/config/tmp` and set both `TMPDIR` and `IOP_NODE_BIN` there.
- Confidence: high. The blocking call is at one exact line and the correction has a deterministic result-versus-context selection oracle.

### Test Coverage Gaps

- Covered: returned available/unavailable/error/unsupported/identity-mismatch outcomes and post-return deadline recheck.
- Missing: a hook that remains blocked after its context is canceled. The current test at `apps/node/internal/node/health_probe_test.go:130-151` still returns from the hook, so it cannot prove coordinator return is bounded.
- Required regression: manually cancel a supplied probe context only after the hook signals that it started, prove the coordinator returns `health_unknown` before the hook is released, then release the hook so the test leaves no blocked goroutine.

### Symbol References

- No symbol is renamed or removed.
- `ProbeHealth` and `ResolveProbeFunc` currently have test call sites in `apps/node/internal/node/health_probe_test.go`; production integration is intentionally owned by `04+03_health_evidence`.

### Split Judgment

- Keep one compact plan. The asynchronous call, deadline/result selection, fail-closed result, and blocked-hook regression form one concurrency invariant and cannot independently PASS if split.
- Dependent predecessor index `02` is satisfied by the archived `complete.log` listed above.

### Scope Rationale

- In scope: `ProbeHealth` coordinator return bounding and its deterministic Node unit/race evidence.
- Excluded: shared classification values, adapter `ProbeProvider` semantics, command-handler mapping, watchdog progress/fence/terminal assembly, observation sequence, Edge overlay, retry, recovery, contract wording, config, and roadmap state. Their reviewed behavior remains unchanged.

### Final Routing

- `evaluation_mode=isolated-reassessment`; finalizer `finalize-task-policy.sh pair`.
- Build closures: scope/context/verification/evidence/ownership/decision are all closed; no capability gap.
- Build grade scores: `scope=0,state=2,blast=0,evidence=2,verification=1` → G05. Base basis `local-fit`; `review_rework_count=1` and `evidence_integrity_failure=true` trigger `recovery-boundary`, so the route is cloud `PLAN-cloud-G05.md`.
- Review closures are all closed; grade scores `scope=0,state=2,blast=0,evidence=2,verification=1` → official-review cloud G05 `CODE_REVIEW-cloud-G05.md`.
- `large_indivisible_context=false`; positive loop risks are `temporal_state` and `concurrent_consistency` (`count=2`); risk boundary is not matched.

## Implementation Checklist

- [ ] [REVIEW_API-1] Make `ProbeHealth` return fail-closed at its independent ceiling even when the prober ignores context, and add a deterministic channel-controlled blocking-hook regression.
- [ ] Run every command in Final Verification and record exact stdout/stderr and exit status in `CODE_REVIEW-cloud-G05.md`.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [REVIEW_API-1] Bound coordinator return independently of hook cooperation

**Problem:** `apps/node/internal/node/health_probe.go:71-84` invokes the hook synchronously and cannot observe `probeCtx.Done()` until the hook returns. The test at `apps/node/internal/node/health_probe_test.go:130-151` uses a hook that ignores an already-expired context but still returns immediately, leaving the actual non-returning boundary untested.

Before (`apps/node/internal/node/health_probe.go:71-84`):

```go
res, err := probe(probeCtx, target)
if err == nil && probeCtx.Err() != nil {
	err = probeCtx.Err()
}
outcome.AdapterName = res.AdapterName
outcome.InstanceKey = res.InstanceKey
outcome.Target = res.Target
outcome.Status = res.Status
outcome.Err = err
return finalizeHealthProbe(outcome)
```

**Solution:** keep public `ProbeHealth` responsible for the background five-second context, move result orchestration into an unexported context-taking helper, and invoke the hook in one goroutine that sends a typed result to a buffer of size one. Select the buffered result against `probeCtx.Done()`. On the deadline branch, return `context.Canceled`/`DeadlineExceeded` through the existing normalizer as `health_unknown`; on the result branch, retain the existing post-result `probeCtx.Err()` recheck so a simultaneously expired deadline wins fail-closed. The buffered channel must allow a late cooperating hook to finish after the coordinator has returned.

After shape:

```go
type probeCallResult struct {
	result runtime.ProviderProbeResult
	err    error
}

resultCh := make(chan probeCallResult, 1)
go func() {
	res, err := probe(probeCtx, target)
	resultCh <- probeCallResult{result: res, err: err}
}()

select {
case call := <-resultCh:
	// Preserve identity/status population and the deadline-wins recheck.
case <-probeCtx.Done():
	outcome.Err = probeCtx.Err()
}
```

**Modified Files and Checklist:**

- [ ] `apps/node/internal/node/health_probe.go` — add the buffered result/timeout selection without changing public signatures or classification semantics.
- [ ] `apps/node/internal/node/health_probe_test.go` — add `TestProbeHealthReturnsWhenBlockedHookOutlivesContext` with a manual cancel, `started`, `release`, and buffered `done` channels; assert `health_unknown`/`probe timed out` before releasing the hook, then release it to avoid a leaked test goroutine.
- [ ] `agent-task/m-node-provider-execution-liveness-recovery/03+02_health_probe_contract/CODE_REVIEW-cloud-G05.md` — record implementation notes and exact verification output.

**Test Strategy:** required concurrency regression. Use an unexported context-taking helper from the package-local test. Start the coordinator in a goroutine, wait for the hook's `started` signal, cancel the manual context, receive fail-closed evidence from `done`, and only then close `release`. Do not use `time.Sleep`, wall-clock polling, a live provider, or arbitrary provider metadata. Retain all existing normal, error, unsupported, identity, and deadline-recheck tests.

**Verification:**

- `go test -count=20 ./apps/node/internal/node -run '^TestProbeHealth(ReturnsWhenBlockedHookOutlivesContext|RechecksDeadlineWhenProbeIgnoresContext|ReceivesIndependentBoundedContext|RootsFromBackground)$'` must pass all 20 iterations without timeout or goroutine/race diagnostics.
- `go test -race -count=3 ./packages/go/execution ./apps/node/internal/node` must pass without race reports.

## Modified Files Summary

| Path | Action | Checklist |
|------|--------|-----------|
| `apps/node/internal/node/health_probe.go` | modify | REVIEW_API-1 |
| `apps/node/internal/node/health_probe_test.go` | modify | REVIEW_API-1 |
| `agent-task/m-node-provider-execution-liveness-recovery/03+02_health_probe_contract/CODE_REVIEW-cloud-G05.md` | create and fill evidence | all |

## Final Verification

1. `go version && go env GOMOD` — exit 0 and report the current toolchain plus `/config/workspace/iop-s1/go.mod`.
2. `go test -count=20 ./apps/node/internal/node -run '^TestProbeHealth(ReturnsWhenBlockedHookOutlivesContext|RechecksDeadlineWhenProbeIgnoresContext|ReceivesIndependentBoundedContext|RootsFromBackground)$'` — exit 0 across 20 fresh iterations.
3. `go test -count=1 ./packages/go/execution ./apps/node/...` — exit 0 for shared execution and all Node packages.
4. `go test -race -count=3 ./packages/go/execution ./apps/node/internal/node` — exit 0 with no race report.
5. `go vet ./packages/go/execution ./apps/node/internal/node` — exit 0 with no diagnostics.
6. `go test -count=1 ./...` — exit 0 for the full Go repository suite.
7. `./scripts/e2e-smoke.sh` — exit 0 and report the provider-only Edge-Node smoke PASS marker.
8. `mkdir -p /config/tmp && go build -o /config/tmp/iop-node ./apps/node/cmd/node && IOP_DEV_RECONNECT_BIND_TIMEOUT=45 TMPDIR=/config/tmp IOP_NODE_BIN=/config/tmp/iop-node ./scripts/dev/edge-node-reconnect-diagnostic.sh` — exit 0 with registration, ordered payload, terminal ordering, command, and reconnect PASS evidence.
9. `make readability-audit` — record exact exit/output; no new or increased violation may name `apps/node/internal/node/health_probe.go`, `apps/node/internal/node/health_probe_test.go`, or this task read set. Unchanged out-of-slice ratchet failures must be identified explicitly.
10. `test -z "$(gofmt -l apps/node/internal/node/health_probe.go apps/node/internal/node/health_probe_test.go)" && git diff --check -- apps/node/internal/node/health_probe.go apps/node/internal/node/health_probe_test.go agent-task/m-node-provider-execution-liveness-recovery/03+02_health_probe_contract/CODE_REVIEW-cloud-G05.md` — exit 0 with no formatting or whitespace diagnostics.

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