<!-- task=m-iop-agent-cli-runtime/20+13,17_project_log_journal plan=3 tag=REVIEW_API -->

# Fence Archive Ordinal Materialization and Type Intent Corruption

## For the Implementing Agent

Filling every implementation-owned section in `CODE_REVIEW-*-G??.md` is mandatory. Implement the checklist, run every verification command, paste actual notes and stdout/stderr into the review file, keep both active files in place, and report ready for review; finalization is owned by the code-review skill. If blocked, record only 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 third review confirmed the prior persisted-record and pre-prune verification fixes, but found two remaining archive admission defects. Concurrent `Archive` calls can pass the fresh-ordinal check independently and overwrite a committed artifact set after one caller has pruned the journal, while syntactically malformed intent content escapes the typed conflict contract.

## Archive Evidence Snapshot

- Prior task: `agent-task/m-iop-agent-cli-runtime/20+13,17_project_log_journal`
- Predicted current-loop evidence: `agent-task/m-iop-agent-cli-runtime/20+13,17_project_log_journal/plan_cloud_G09_2.log` and `agent-task/m-iop-agent-cli-runtime/20+13,17_project_log_journal/code_review_cloud_G09_2.log`
- Verdict: FAIL
- Findings: two Required findings covering non-exclusive archive ordinal materialization and untyped malformed-intent rejection; no Suggested or Nit findings
- Affected files: `apps/agent/internal/projectlog/store.go`, `apps/agent/internal/projectlog/store_test.go`
- Verification evidence: all declared focused, package, race, vet, formatting, and diff commands passed; reviewer-only reproducers showed malformed intent does not satisfy `errors.Is(err, ErrArchiveConflict)` and a concurrent writer can corrupt an ordinal after another writer prunes the journal
- Roadmap carryover: this remains an S12 `project-logs` journal subset and does not claim the Milestone Task on PASS; EventSink mapping, WORK_LOG projection, and the complete dynamic frontier remain in later children

## Analysis

### Files Read

- `agent-task/m-iop-agent-cli-runtime/20+13,17_project_log_journal/PLAN-cloud-G09.md`
- `agent-task/m-iop-agent-cli-runtime/20+13,17_project_log_journal/CODE_REVIEW-cloud-G09.md`
- `agent-task/m-iop-agent-cli-runtime/20+13,17_project_log_journal/plan_local_G08_1.log`
- `agent-task/m-iop-agent-cli-runtime/20+13,17_project_log_journal/code_review_cloud_G09_1.log`
- `apps/agent/internal/projectlog/store.go`
- `apps/agent/internal/projectlog/store_test.go`
- `apps/agent/internal/projectlog/record.go`
- `packages/go/agentstate/store.go`
- `agent-contract/index.md`
- `agent-contract/inner/iop-agent-cli-runtime.md`
- `agent-roadmap/phase/automation-runtime-bridge/PHASE.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-ops/rules/project/domain/agent/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-task/archive/2026/07/m-iop-agent-cli-runtime/13_agent_domain/complete.log`
- `agent-task/archive/2026/07/m-iop-agent-cli-runtime/17+13_project_log_records/complete.log`

### SDD Criteria

- SDD: `agent-roadmap/sdd/automation-runtime-bridge/iop-agent-cli-runtime/SDD.md`
- Status: approved; SDD lock released; no active SDD `USER_REVIEW.md`
- Targeted scenario: S12, Milestone Task `project-logs`
- Acceptance: stable task loop/attempt/locator identity and exactly-once archive/cleanup only after terminal closure
- Evidence Map: S12 requires an archive reconciliation fixture and exactly-once archive evidence
- Plan effect: the checklist requires exclusive ordinal ownership across concurrent callers, immutable committed artifact bytes, typed corruption rejection, deterministic overlap/corruption tests, and race execution. PASS remains child-level evidence and does not claim `project-logs`.

### Verification Context

- Supplied handoff: none
- Repository-native sources: `agent-test/local/rules.md`, the active and archived loop evidence, project-log store tests, `agentstate` CAS/locking code, the standalone runtime contract, and SDD S12
- Preflight: `/config/.local/bin/go` resolves to `/config/opt/go/bin/go`; `go version go1.26.2 linux/arm64`; `GOROOT=/config/opt/go`
- Current checkout: intentionally dirty shared checkout with unrelated sibling task artifacts; this child owns only `store.go`, `store_test.go`, and its active review evidence
- Fresh reviewer results: both focused commands, the final focused matrix, full package suite, project-log/agentstate race suite, vet, formatting, and `git diff --check` passed
- Reviewer reproducers: malformed intent returned an untyped decode error; two coordinated store instances left an archive ordinal unverifiable after one pruned the journal and the other overwrote its manifest
- External verification: not applicable; deterministic filesystem/CAS tests require no service, credential, remote runner, port, or OS-specific device
- Gap: existing tests cover sequential retry and mutation before verification, but not concurrent fresh-ordinal ownership or syntactically malformed intent classification
- Confidence: high; both failures have deterministic local filesystem/CAS oracles and direct no-corruption/no-mutation assertions

### Test Coverage Gaps

- Concurrent ordinal ownership: absent. `TestArchiveExactlyOnce` is sequential and cannot detect two writers crossing the fresh-artifact preflight.
- Immutable post-prune artifacts: absent. Current retry tests begin only after a complete artifact set already exists.
- Malformed intent classification: absent. Existing intent cases mutate a valid schema field and do not exercise JSON syntax failure.
- Persisted record/sequence drift and normal pre-prune content verification: covered and retained as regression guards.

### Symbol References

None. No exported symbol is renamed or removed. Archive lock helpers and failure-hook phases remain package-internal.

### Split Judgment

Keep one plan. Exclusive ordinal admission and typed artifact verification are one fail-closed archive transaction: cleanup is safe only while the same owner prevents another writer from replacing the verified set. Predecessor `13_agent_domain` is satisfied by `agent-task/archive/2026/07/m-iop-agent-cli-runtime/13_agent_domain/complete.log`; predecessor `17+13_project_log_records` is satisfied by `agent-task/archive/2026/07/m-iop-agent-cli-runtime/17+13_project_log_records/complete.log`.

### Scope Rationale

Modify only `store.go`, `store_test.go`, and the active review evidence file. Do not change `ProjectLogRecord` or `ArchiveManifest` schemas, `agentstate.Store`, EventSink mapping, WORK_LOG rendering, CLI wiring, contracts, SDD, roadmap state, or the complete S12 scenario.

### Final Routing

- evaluation_mode: `isolated-reassessment`
- finalizer: `finalize-task-policy.sh pair`
- build closures: scope/context/verification/evidence/ownership/decision all closed; no capability gap
- build grade scores: scope `2`, state `2`, blast `2`, evidence `2`, verification `1` -> `G09`
- build route: base `grade-boundary`, lane `cloud`, filename `PLAN-cloud-G09.md`
- review closures: scope/context/verification/evidence/ownership/decision all closed; no capability gap
- review grade scores: scope `2`, state `2`, blast `2`, evidence `2`, verification `1` -> `G09`
- review route: `official-review`, lane `cloud`, filename `CODE_REVIEW-cloud-G09.md`
- large_indivisible_context: `false`
- matched loop risks: `temporal_state`, `concurrent_consistency`, `boundary_contract` (count `3`)
- recovery signals: `review_rework_count=3`, `evidence_integrity_failure=true`; recovery boundary matched without replacing the grade-boundary basis
- capability-gap evidence: none

## Implementation Checklist

- [ ] Serialize or fence archive ordinal materialization and reconciliation across store instances so concurrent callers reuse one immutable committed set, with a deterministic overlap regression.
- [ ] Return typed archive conflict for syntactically malformed persisted intent and prove journal/artifact no-mutation behavior.
- [ ] Run fresh focused, package, race, vet, formatting, and diff verification.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [REVIEW_API-1] Own One Archive Ordinal Exclusively

#### Problem

`apps/agent/internal/projectlog/store.go:280-385` checks whether ordinal artifacts exist before fresh materialization, but `writeAtomically` and the manifest rename overwrite final paths without an ordinal owner spanning verification and journal prune. Two store instances can therefore cross the preflight, and a late writer can replace an already verified archive after the winning writer removed the only journal copy.

#### Solution

Add one archive-directory advisory lock shared by every store instance for the same project/workspace and hold it across `Archive` load/admission/materialization/verification/prune. Hold the same lock across `Reconcile` scan/verification/prune so recovery cannot race fresh materialization. Keep the lock file outside the ordinal artifact set, use restrictive permissions and the existing Unix advisory-lock pattern already used by `agentstate`, and preserve context/error propagation. Add a deterministic pre-lock failure-hook phase only if required to coordinate the test without timing sleeps.

Before (`store.go:268-280`):

```go
func (s *Store) Archive(ctx context.Context) error {
	if err := ctx.Err(); err != nil {
		return err
	}
	j, _, found, err := s.loadJournal(ctx)
	// ...
	artifactsExist, err := s.archiveArtifactsExist(ordinal)
```

After:

```go
func (s *Store) Archive(ctx context.Context) error {
	if err := s.failAt("archive_before_lock"); err != nil {
		return err
	}
	return s.withArchiveLock(ctx, func() error {
		return s.archiveLocked(ctx)
	})
}

func (s *Store) Reconcile(ctx context.Context) error {
	return s.withArchiveLock(ctx, func() error {
		return s.reconcileLocked(ctx)
	})
}
```

#### Modified Files and Checklist

- [ ] `apps/agent/internal/projectlog/store.go` — add shared archive locking and keep admission, materialization, verification, reconstruction, and prune inside the lock.
- [ ] `apps/agent/internal/projectlog/store_test.go` — add deterministic two-store concurrent archive coverage with byte verification and journal exactly-once assertions.

#### Test Strategy

Add `TestArchiveConcurrentCallsShareImmutableOrdinal` in `store_test.go`. Use two `Store` instances sharing one `agentstate.Store` and archive root; pause the first at `archive_before_write`, prove the second has entered its pre-lock phase, release the first, and require both calls to return nil, one verified artifact set to remain byte-stable, the journal to be empty, and a later `Reconcile` to stay idempotent. The test must not use timing sleeps.

#### Verification

```bash
go test -count=1 ./apps/agent/internal/projectlog -run 'TestArchiveConcurrentCallsShareImmutableOrdinal|TestArchiveRetryReusesCommittedArtifacts|TestReconcilePreservesConcurrentAppend'
```

Expected: concurrent callers converge on one immutable ordinal, sequential retry bytes remain unchanged, and concurrent append cleanup still preserves only the suffix.

### [REVIEW_API-2] Type Malformed Intent as Archive Conflict

#### Problem

`apps/agent/internal/projectlog/store.go:462-465` returns a raw JSON decode error for syntactically malformed intent, although the archive verifier contract and follow-up plan require every conflicting persisted artifact to return `ErrArchiveConflict` or `ErrArchiveIncomplete`.

#### Solution

Wrap intent syntax failure with `ErrArchiveConflict` while retaining the decode detail. Keep missing intent classified as `ErrArchiveIncomplete`, and do not alter the valid schema/identity/range/checksum checks.

Before (`store.go:462-465`):

```go
var intent archiveIntent
if err := json.Unmarshal(intentData, &intent); err != nil {
	return archiveIntent{}, nil, fmt.Errorf("projectlog: decode intent: %w", err)
}
```

After:

```go
var intent archiveIntent
if err := json.Unmarshal(intentData, &intent); err != nil {
	return archiveIntent{}, nil, fmt.Errorf("%w: decode intent: %v", ErrArchiveConflict, err)
}
```

#### Modified Files and Checklist

- [ ] `apps/agent/internal/projectlog/store.go` — preserve decode context while exposing the typed archive conflict sentinel.
- [ ] `apps/agent/internal/projectlog/store_test.go` — add malformed-intent cases that assert the typed error and exact journal/artifact retention.

#### Test Strategy

Extend `TestArchiveRejectsPreCleanupArtifactDrift` with a `malformed intent` case that writes invalid JSON at the existing hook, requires `errors.Is(err, ErrArchiveConflict)`, and reuses the table's exact journal no-mutation assertion. Add or extend the reconcile drift matrix to assert the malformed bytes are not rewritten.

#### Verification

```bash
go test -count=1 ./apps/agent/internal/projectlog -run 'TestArchiveRejectsPreCleanupArtifactDrift|TestReconcileRejectsCommittedArtifactDrift'
```

Expected: malformed existing intent is a typed conflict and neither Archive nor Reconcile rewrites evidence or prunes the journal.

## Dependencies and Execution Order

- `13_agent_domain` is satisfied by `agent-task/archive/2026/07/m-iop-agent-cli-runtime/13_agent_domain/complete.log`.
- `17+13_project_log_records` is satisfied by `agent-task/archive/2026/07/m-iop-agent-cli-runtime/17+13_project_log_records/complete.log`.
- Implement REVIEW_API-1 before REVIEW_API-2 so every normal and recovery verification path runs under the same ordinal owner, then run both focused matrices before the final package/race suite.

## Modified Files Summary

| File | Item |
|------|------|
| `apps/agent/internal/projectlog/store.go` | REVIEW_API-1, REVIEW_API-2 |
| `apps/agent/internal/projectlog/store_test.go` | REVIEW_API-1, REVIEW_API-2 |
| `agent-task/m-iop-agent-cli-runtime/20+13,17_project_log_journal/CODE_REVIEW-cloud-G09.md` | implementation evidence |

## Final Verification

```bash
gofmt -w apps/agent/internal/projectlog/store.go apps/agent/internal/projectlog/store_test.go
go test -count=1 ./apps/agent/internal/projectlog -run 'TestStore|TestArchive|TestReconcile'
go test -count=1 ./apps/agent/internal/projectlog
go test -count=1 -race ./apps/agent/internal/projectlog ./packages/go/agentstate
go vet ./apps/agent/internal/projectlog
test -z "$(gofmt -d apps/agent/internal/projectlog/store.go apps/agent/internal/projectlog/store_test.go)"
git diff --check
```

Expected: every fresh focused/package/race test passes; vet, formatting, and diff checks are clean; concurrent archive callers cannot replace a committed ordinal, and malformed archive intent never escapes typed fail-closed handling. Cached Go test output is not acceptable.

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