<!-- task=m-iop-agent-cli-runtime/07+06_target_policy plan=1 tag=REVIEW_API -->

# Harden Project-Aware Durable Target Policy

## For the Implementing Agent

Implement only the fixes and tests in this plan. Run every verification command, paste actual stdout/stderr into `CODE_REVIEW-cloud-G10.md`, keep both active files in place, and report ready for review. Final verdicts, log renames, `complete.log`, task archival, and next-state classification belong only to the review agent. If blocked, record the exact blocker, attempted commands/output, and resume condition in the implementation-owned evidence fields; do not ask the user, call user-input tools, create control-plane stop files, or archive artifacts.

## Background

The first implementation established ordered matching but did not satisfy the S06 durable-route and effective project-policy contract. Persisted payloads can be altered without rejection, unresolved profiles can yield empty provider/model identities, project overrides are ignored, and no resume path preserves an existing route without reselection. The required `agenttask` package verification is also nondeterministic because one lease test observes `pending_integration` before the lease claim.

## Archive Evidence Snapshot

- Prior plan: `agent-task/m-iop-agent-cli-runtime/07+06_target_policy/plan_local_G07_0.log`
- Prior review: `agent-task/m-iop-agent-cli-runtime/07+06_target_policy/code_review_cloud_G07_0.log`
- Verdict: `FAIL`
- Findings: 5 Required, 0 Suggested, 0 Nit.
- Affected files: `packages/go/agentpolicy/decision.go`, `packages/go/agentpolicy/evaluator.go`, `packages/go/agentpolicy/evaluator_test.go`, `packages/go/agenttask/ports.go`, `packages/go/agenttask/manager_test.go`.
- Verification evidence: the existing `agentpolicy` suite passed; reviewer-only payload and unresolved-profile reproducers failed as expected; the required combined package command failed; `go test -count=20 -run '^TestIntegrationLeaseRenewsAndFencesLateResult$' ./packages/go/agenttask` also failed.
- Roadmap carryover: `target-policy` remains incomplete until S06 project-aware selection, immutable route replay/history, corruption rejection, and deterministic verification all pass.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md`
- Milestone link: [Milestone document](agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md)
- Task ids:
  - `target-policy`: ordered evaluator and durable route history
- Completion mode: check-on-pass

## Analysis

### Files Read

- `packages/go/agentpolicy/decision.go`
- `packages/go/agentpolicy/evaluator.go`
- `packages/go/agentpolicy/evaluator_test.go`
- `packages/go/agenttask/ports.go`
- `packages/go/agenttask/manager_test.go`
- `packages/go/agenttask/test_support_test.go`
- `packages/go/agentconfig/runtime_config.go`
- `packages/go/agentconfig/catalog.go`
- `agent-contract/index.md`
- `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-test/local/rules.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-task/archive/2026/07/m-iop-agent-cli-runtime/06+05_config_registry/complete.log`
- `agent-task/m-iop-agent-cli-runtime/07+06_target_policy/plan_local_G07_0.log`
- `agent-task/m-iop-agent-cli-runtime/07+06_target_policy/code_review_cloud_G07_0.log`

### SDD Criteria

- SDD: `agent-roadmap/sdd/automation-runtime-bridge/iop-agent-cli-runtime/SDD.md`; status approved, lock released.
- Target: S06 / `target-policy`.
- Acceptance: overlapping time/quota/stage/grade policy returns the first matching provider/model, retains decision history, and rejects damaged persisted state without silent reselection.
- Evidence Map: ordered selector plus persisted-route/tamper matrix, with selected rule, reason, retained route history, and `target-policy` Roadmap Completion evidence.
- Consequence: the checklist requires effective project policy resolution, complete ordered candidate evidence, integrity-sealed persistence, exact replay without reevaluation, mutation tests, and fresh deterministic package verification.

### Test Environment Rules

- `test_env=local`.
- Read `agent-test/local/rules.md`; the host `go` binary, resolved path, version, and `GOROOT` must be recorded before tests.
- Read matched profile `agent-test/local/platform-common-smoke.md` because the change affects `packages/go/**` runtime/config contracts.
- Apply `make proto`, the Edge/Node transport unit command, target package tests, `git diff --check`, and formatting/vet checks. No external service, credential, Docker, device, or non-local runner is required.
- Fresh execution is required; Go test cache output is not accepted. Repeated execution is required for the previously flaky integration-lease test.

### Test Coverage Gaps

- Ordered first-match, basic time boundaries, grade/stage, capability, default, and basic JSON round-trip already have tests.
- Missing: effective project override selection and project-specific selection revision.
- Missing: no-catalog/unknown-profile rejection and non-empty provider/model/profile output enforcement.
- Missing: payload-integrity mutations, envelope/payload revision disagreement, unknown-field rejection, and selection-revision mismatch.
- Missing: exact persisted-route resume without reevaluation, selected reason/eligibility, ordered candidates, and used-route history.
- Missing: direct quota/min-token coverage required by S06.
- Existing integration-lease fencing test is flaky because its wait condition admits a pre-claim state.

### Symbol References

- `SelectPolicy`, `SelectionContext`, `RouteDecision`, `RouteHistory`, `EncodeDecision`, and `DecodeDecision` are referenced only in `packages/go/agentpolicy/*.go`, `packages/go/agentpolicy/evaluator_test.go`, and `packages/go/agenttask/ports.go`.
- No production caller currently consumes `PolicySelector`; preserve the port while extending its context and document the effective-project behavior.
- Any decision codec signature or history type change must update every reference returned by `rg --sort path -n 'SelectPolicy|SelectionContext|RouteDecision|RouteHistory|EncodeDecision|DecodeDecision' packages/go --glob '*.go'`.

### Split Judgment

- Keep one plan. Effective policy resolution, persisted decision validation/replay, and the S06 evidence matrix form one completion invariant; the task cannot PASS while the required package verification remains flaky.
- Predecessor `06` is satisfied by `agent-task/archive/2026/07/m-iop-agent-cli-runtime/06+05_config_registry/complete.log`.

### Scope Rationale

- Do not wire the new policy selector into `agenttask.Manager`; the original task and modified-files boundary introduced the common evaluator and port only.
- Do not implement S07 quota observation, retry, or failover state transitions. This plan evaluates quota predicates and records route evidence only.
- Do not modify `agentconfig` merge semantics, catalog schema, agentstate persistence, workspace isolation, Node transport, protobuf, or roadmap state.
- `make proto` and transport tests are verification-only profile requirements; no generated-file change is expected.

### Final Routing

- `evaluation_mode=isolated-reassessment`; `finalizer=finalize-task-policy.sh`, mode `pair`.
- Build closures: scope/context/verification/evidence/ownership/decision all closed; no capability gap.
- Build scores: scope coupling 2, state/concurrency 2, blast/irreversibility 2, evidence/diagnosis 2, verification complexity 1; grade `G09`.
- Build signals: `large_indivisible_context=false`; matched loop risks `temporal_state`, `concurrent_consistency`, `boundary_contract`, `structured_interpretation`, `variant_product`; count 5; `review_rework_count=1`; `evidence_integrity_failure=true`.
- Build route: base/final basis `grade-boundary`, cloud, `PLAN-cloud-G09.md`. Risk and recovery boundaries also matched but do not replace the grade basis.
- Review closures: all closed; scores 2/2/2/2/2; official review, cloud `G10`, `CODE_REVIEW-cloud-G10.md`, Codex `gpt-5.6-sol` xhigh.

## Implementation Checklist

- [ ] Resolve the effective project selection policy and require one catalog-backed provider/model/profile identity with immutable revisions.
- [ ] Integrity-seal the complete route decision, preserve ordered candidate/selected reason/used history, and resume a valid persisted route without reevaluation.
- [ ] Add the S06 regression matrix for project overrides, unresolved profiles, quota predicates, payload mutations, and stable persisted-route replay.
- [ ] Stabilize the integration-lease fencing test and prove it passes repeatedly before the full package verification.
- [ ] Run the local platform-common profile and all fresh target verification commands without generated-file drift.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [REVIEW_API-1] Resolve Effective Project Policy and Target Identity

**Problem:** `packages/go/agentpolicy/evaluator.go:65` always reads the global `snapshot.Config().Selection`, so `RuntimeSnapshot.Project(projectID).Selection` and its project-specific revision are unreachable. At `packages/go/agentpolicy/evaluator.go:250`, a failed profile lookup returns an error only when the profile ID is empty; a non-empty unknown profile succeeds with empty provider/model IDs.

**Solution:** Add `ProjectID` to `SelectionContext`. Resolve the effective project registration when it is present, return a typed error for an unknown project, and use its `Selection` plus the snapshot config/catalog. Require `ResolveProfile` to succeed for every selected default or rule target, reject any declared provider/model mismatch defensively, populate all provider/model/profile IDs, and pin `ProfileRevision` to an immutable catalog/config-derived revision.

Before (`packages/go/agentpolicy/evaluator.go:65`):

```go
config := snapshot.Config()
selection := config.Selection
```

After:

```go
config := snapshot.Config()
selection := config.Selection
if selCtx.ProjectID != "" {
	project, ok := snapshot.Project(selCtx.ProjectID)
	if !ok {
		return RouteDecision{}, fmt.Errorf("%w: %s", ErrUnknownProject, selCtx.ProjectID)
	}
	selection = project.Selection
}
```

Before (`packages/go/agentpolicy/evaluator.go:250`):

```go
if resolved, ok := config.Catalog.ResolveProfile(target.Profile); ok {
	// fill omitted provider/model
} else if target.Profile == "" {
	return RouteDecision{}, ErrUnknownProfile
}
```

After:

```go
resolved, ok := config.Catalog.ResolveProfile(target.Profile)
if !ok {
	return RouteDecision{}, fmt.Errorf("%w: %s", ErrUnknownProfile, target.Profile)
}
// Validate or fill provider/model, and pin the resolved target revisions.
```

**Modified Files and Checklist:**

- [ ] `packages/go/agentpolicy/evaluator.go`: add project context, effective policy resolution, typed unknown-project/profile errors, and complete target identities.
- [ ] `packages/go/agenttask/ports.go`: document that `PolicySelector` consumes the effective project selected by `SelectionContext.ProjectID`.
- [ ] `packages/go/agentpolicy/evaluator_test.go`: add global/project override and unresolved-profile regression cases.

**Test Strategy:** Add `TestEvaluatorUsesProjectSelectionOverride`, `TestEvaluatorRejectsUnknownProject`, `TestEvaluatorRejectsUnresolvedProfile`, and a target identity/revision assertion. Fixtures must use `LoadRuntimeConfigBytes` so config composition and project revisions are exercised.

**Verification:**

```bash
go test -count=1 -run 'TestEvaluator(UsesProjectSelectionOverride|RejectsUnknownProject|RejectsUnresolvedProfile|FirstMatchWins)$' -v ./packages/go/agentpolicy
```

### [REVIEW_API-2] Seal and Replay Durable Route Evidence

**Problem:** `packages/go/agentpolicy/decision.go:90` verifies only the envelope config revision and returns an unchecked payload. The evaluator at `packages/go/agentpolicy/evaluator.go:60` has no persisted-route input; it always evaluates current rules and creates `PreviousRuleID` from the current rule at line 269. The result does not contain the S06 ordered candidate eligibility, selected reason, or used-route history.

**Solution:** Define a stable JSON schema with explicit tags and an integrity field computed over length-prefixed version, config revision, selection revision, and canonical decision bytes. Strictly decode one document, reject unknown fields, verify the integrity value, cross-check envelope/payload revisions and required target/history fields, and validate an expectation containing both config and effective selection revisions. Replace the ambiguous history shape with ordered candidate evaluations and used-route entries; the selected candidate records `eligible=true`, `reason=matched|default`, and `used=true`, while evaluated rejections retain their reasons and later rules are explicitly unevaluated after first match. Add `ResumePolicy` (or an equivalent API) that resolves the effective project revision, decodes and validates the persisted decision against the pinned snapshot/catalog, and returns it byte-for-byte semantically without evaluating current rules.

Before (`packages/go/agentpolicy/decision.go:90`):

```go
func DecodeDecision(data []byte, expectedConfigRevision string) (RouteDecision, error) {
	var envelope decisionEnvelope
	if err := json.Unmarshal(data, &envelope); err != nil {
		return RouteDecision{}, fmt.Errorf("%w: %v", ErrCorruptDecision, err)
	}
	if envelope.Version != decisionVersion {
		return RouteDecision{}, fmt.Errorf("%w: version %q", ErrCorruptDecision, envelope.Version)
	}
	if expectedConfigRevision != "" && envelope.ConfigRevision != expectedConfigRevision {
		return RouteDecision{}, fmt.Errorf("%w: ...", ErrRevisionMismatch)
	}
	return envelope.Decision, nil
}
```

After:

```go
func DecodeDecision(data []byte, expected DecisionExpectation) (RouteDecision, error) {
	envelope, err := decodeStrictEnvelope(data)
	if err != nil {
		return RouteDecision{}, err
	}
	if err := verifyDecisionIntegrity(envelope); err != nil {
		return RouteDecision{}, err
	}
	if err := validateDecision(envelope, expected); err != nil {
		return RouteDecision{}, err
	}
	return envelope.Decision, nil
}
```

**Modified Files and Checklist:**

- [ ] `packages/go/agentpolicy/decision.go`: add stable JSON fields, decision expectations, complete integrity verification, typed validation, ordered candidate evidence, and used-route history.
- [ ] `packages/go/agentpolicy/evaluator.go`: populate complete candidate/history evidence and add exact persisted-route resume without reselection.
- [ ] `packages/go/agentpolicy/evaluator_test.go`: add strict decode, mutation matrix, selected/rejected/unevaluated candidate assertions, quota/min-token cases, and exact replay tests.

**Test Strategy:** Add table-driven mutation tests for envelope revision, payload config/selection revisions, provider/model/profile identity, history, checksum, unknown fields, malformed JSON, and unsupported version. Add `TestResumePolicyReturnsPinnedDecisionWithoutReselection` by encoding a decision, changing rule inputs in a different snapshot, and proving revision mismatch blocks rather than selecting a replacement. Add direct quota/min-token overlap cases and assert ordered candidate evidence.

**Verification:**

```bash
go test -count=1 -run 'Test(EncodeDecodeDecision|DecodeDecision|ResumePolicy|EvaluatorQuota|EvaluatorOrderedCandidate)' -v ./packages/go/agentpolicy
```

### [REVIEW_API-3] Make Required Lease Verification Deterministic

**Problem:** `packages/go/agenttask/manager_test.go:983` exits its wait loop for either `integrating` or `pending_integration`. The latter can be observed before `claimIntegration` persists the lease, so line 996 races and the required package test fails intermittently.

**Solution:** Wait on one snapshot that proves both the external integration phase and the manager-owned integration lease are present before replacing the token. Keep the integrator blocked until the successor lease is installed, then retain all late-result fencing assertions.

Before (`packages/go/agenttask/manager_test.go:981`):

```go
work := state.Projects["project"].Works["work"]
if work.State == WorkStateIntegrating || work.State == WorkStatePendingIntegration {
	break
}
```

After:

```go
work := state.Projects["project"].Works["work"]
lease, claimed := state.IntegrationLeases["workspace"]
if work.State == WorkStateIntegrating && claimed &&
	lease.OwnerID == harness.manager.config.OwnerID {
	break
}
```

**Modified Files and Checklist:**

- [ ] `packages/go/agenttask/manager_test.go`: wait for the claimed manager-owned integration lease before the fencing mutation.

**Test Strategy:** Keep the existing behavior assertions and run the focused test 20 fresh times. No production change is planned; if the corrected synchronization still exposes a production lease-loss defect, record the evidence and fix only that directly proven path.

**Verification:**

```bash
go test -count=20 -run '^TestIntegrationLeaseRenewsAndFencesLateResult$' ./packages/go/agenttask
```

## Modified Files Summary

| File | Item |
|------|------|
| `packages/go/agentpolicy/evaluator.go` | REVIEW_API-1, REVIEW_API-2 |
| `packages/go/agentpolicy/decision.go` | REVIEW_API-2 |
| `packages/go/agentpolicy/evaluator_test.go` | REVIEW_API-1, REVIEW_API-2 |
| `packages/go/agenttask/ports.go` | REVIEW_API-1 |
| `packages/go/agenttask/manager_test.go` | REVIEW_API-3 |

## Dependencies and Execution Order

1. `06+05_config_registry` is complete at `agent-task/archive/2026/07/m-iop-agent-cli-runtime/06+05_config_registry/complete.log`.
2. Implement REVIEW_API-1 before final decision validation so REVIEW_API-2 can validate the exact effective selection revision and catalog target.
3. REVIEW_API-3 may be edited independently, but the subtask is complete only after all final verification passes.

## Final Verification

```bash
command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT
make proto
go test -count=1 ./apps/edge/internal/transport ./apps/node/internal/transport
go test -count=1 -run 'TestEvaluator(UsesProjectSelectionOverride|RejectsUnknownProject|RejectsUnresolvedProfile|FirstMatchWins)$' -v ./packages/go/agentpolicy
go test -count=1 -run 'Test(EncodeDecodeDecision|DecodeDecision|ResumePolicy|EvaluatorQuota|EvaluatorOrderedCandidate)' -v ./packages/go/agentpolicy
go test -count=20 -run '^TestIntegrationLeaseRenewsAndFencesLateResult$' ./packages/go/agenttask
go test -count=1 ./packages/go/agentpolicy ./packages/go/agenttask
go test -count=1 -race ./packages/go/agentpolicy ./packages/go/agenttask
go vet ./packages/go/agentpolicy ./packages/go/agenttask
gofmt -d packages/go/agentpolicy packages/go/agenttask/ports.go packages/go/agenttask/manager_test.go
git diff --check
```

Expected: all commands exit 0; `gofmt -d` and `git diff --check` print no output; `make proto` creates no unintended generated diff.

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