<!-- task=m-openai-workspace-agent-execution-contract/07_nomad_metadata_shape plan=0 tag=NOMAD_METADATA -->

# Plan - NOMAD_METADATA

## 이 파일을 읽는 구현 에이전트에게

`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 필수 마지막 단계다. 검증을 실행하고 실제 출력과 구현 메모를 기록한 뒤 active 파일을 그대로 두고 리뷰 준비 상태로 보고한다. 사용자 결정, 사용자 소유 외부 환경, 또는 범위 충돌 없이는 진행할 수 없을 때만 review stub의 `사용자 리뷰 요청` 섹션을 증거와 함께 채우고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 증거 공백은 사용자 리뷰 요청이 아니다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용이다.

## 배경

IOP 계약은 NomadCode authoring handoff의 workspace를 flat `metadata.workspace` string으로 받는다. 현재 `../nomadcode`의 OpenAI client는 typed `WorkspaceMetadata` 전체 객체를 `metadata.workspace`에 넣는다. producer와 consumer fixture가 같은 contract를 쓰도록 정렬해야 IOP HTTP surface를 NomadCode가 직접 사용할 수 있다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active `CODE_REVIEW-cloud-G07.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식을 따른다. 구현 에이전트는 직접 사용자 프롬프트를 만들지 않고, code-review가 사용자 리뷰 필요성을 검증하고 실제 `USER_REVIEW.md` 작성을 소유한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md`
- Task ids:
  - `nomad-metadata-shape`: NomadCode Core가 workspace slot path를 flat `metadata.workspace`로, task/source context를 `metadata.task_id`/`metadata.source` 또는 `metadata.nomadcode.*`로 전달하는 호출 shape를 맞춘다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `AGENTS.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/testing-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/node/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-contract/index.md`
- `agent-contract/provided/openai-compatible-api.md`
- `apps/edge/internal/edgecmd/smoke_openai.go`
- `apps/edge/internal/openai/types.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/openai/server_test.go`
- `apps/edge/cmd/edge/main_test.go`
- `../nomadcode/AGENTS.md`
- `../nomadcode/agent-ops/rules/project/rules.md`
- `../nomadcode/agent-ops/rules/private/rules.md`
- `../nomadcode/agent-ops/rules/common/rules-roadmap.md`
- `../nomadcode/agent-roadmap/current.md`
- `../nomadcode/agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md`
- `../nomadcode/agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
- `../nomadcode/agent-ops/rules/project/domain/core/rules.md`
- `../nomadcode/services/core/internal/adapters/openai/client.go`
- `../nomadcode/services/core/internal/adapters/openai/client_test.go`
- `../nomadcode/services/core/internal/model/model.go`
- `../nomadcode/services/core/internal/authoring/request.go`
- `../nomadcode/services/core/internal/authoring/request_test.go`
- `../nomadcode/services/core/internal/scheduler/jobs.go`
- `../nomadcode/services/core/internal/scheduler/jobs_test.go`

### 테스트 환경 규칙

- test_env: `local` for IOP. `agent-test/local/rules.md`와 matched `edge-smoke`, `node-smoke`, `testing-smoke`, `platform-common-smoke`를 읽었다.
- IOP 적용 명령: OpenAI-compatible 경계는 `go test ./apps/edge/...` 및 smoke 동등 확인.
- NomadCode에는 `agent-contract/index.md`가 없었다. NomadCode project/core rules에 따라 `../nomadcode/services/core` 변경은 해당 module의 Go tests로 검증한다.
- 이 plan의 cross-repo command는 `cd ../nomadcode/services/core && go test ./...`다.
- `<확인 필요>` 값은 없었다. Go test cache output은 허용하지 않고 `-count=1`을 쓴다.

### 테스트 커버리지 공백

- NomadCode `../nomadcode/services/core/internal/adapters/openai/client_test.go:98-151`은 현재 `metadata.workspace` object를 기대한다. flat string expectation으로 바꿔야 한다.
- IOP `apps/edge/internal/openai/server_test.go:520-573`은 flat `metadata.workspace` 수신을 커버한다.
- IOP에는 old NomadCode object shape를 명시적으로 reject하는 test가 없다. 이 plan에서 추가해 contract drift를 막는다.
- `apps/edge/internal/edgecmd/smoke_openai.go`는 workspace flag가 아직 없어서 `06+05_authoring_http_smoke`와 충돌하지 않게 fixture 정렬만 확인한다.

### 심볼 참조

- renamed/removed symbols: none.
- `WorkspaceMetadata` type은 제거하지 않는다. producer input model로 유지하고 serialization만 flat path로 바꾼다.

### 분할 판단

- split decision policy를 plan 파일 선택 전에 평가했다.
- shared task group: `m-openai-workspace-agent-execution-contract`.
- sibling subtasks:
  - `05_failure_surface`: 독립.
  - `06+05_authoring_http_smoke`: `05` 완료 후 실행.
  - `07_nomad_metadata_shape`: 이 plan, 독립. cross-repo producer contract 정렬이다.
- 이 subtask는 predecessor가 없다. NomadCode active milestone에는 `plane-agent-authoring` lock이 있으나, 여기서는 OpenAI client metadata shape fixture만 맞추며 Plane authoring agent execution을 시작하지 않는다.

### 범위 결정 근거

- NomadCode Plane work item creation, MCP control plane, Todo projection 구현은 범위 밖이다.
- NomadCode `WorkspaceMetadata` struct 제거는 call-site churn이 커서 범위 밖이다. `Path`를 `metadata.workspace`로 serialize하는 최소 변경만 한다.
- provider, work item id, source branch를 IOP metadata root에 임의 추가하지 않는다. IOP contract가 요구하는 task/source metadata와 workspace path만 보장한다.
- IOP runtime behavior 변경은 하지 않는다. consumer fixture와 contract rejection test만 다룬다.

### 빌드 등급

- `cloud-G07`: sibling repo contract producer와 IOP consumer fixture를 동시에 다루는 cross-project protocol/schema 정렬이다.

## 구현 체크리스트

- [ ] [NOMAD_METADATA-1] NomadCode OpenAI Responses client가 `WorkspaceMetadata.Path`를 flat `metadata.workspace` string으로 보내도록 바꾸고 tests를 갱신한다.
- [ ] [NOMAD_METADATA-2] IOP consumer fixture가 old object shape를 거부하고 flat task/source/workspace contract를 유지함을 테스트한다.
- [ ] `cd ../nomadcode/services/core && go test -count=1 ./...`와 IOP Edge tests를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

이 subtask는 runtime predecessor가 없다. 구현 순서는 NomadCode producer 변경, NomadCode tests, IOP consumer rejection test, cross-repo verification 순서다.

### [NOMAD_METADATA-1] NomadCode Producer Shape

#### 문제

NomadCode client는 `WorkspaceMetadata` 전체 객체를 `metadata.workspace`에 넣는다. IOP contract는 flat path string을 요구한다.

```go
// ../nomadcode/services/core/internal/adapters/openai/client.go:252-265
// buildRequestMetadata merges flat string metadata with typed WorkspaceMetadata.
// WorkspaceMetadata wins on key collision ("workspace" key).
func buildRequestMetadata(input model.GenerateInput) map[string]any {
	if len(input.Metadata) == 0 && input.WorkspaceMetadata == nil {
		return nil
	}
	merged := make(map[string]any, len(input.Metadata)+1)
	for k, v := range input.Metadata {
		merged[k] = v
	}
	if input.WorkspaceMetadata != nil {
		merged["workspace"] = input.WorkspaceMetadata
	}
	return merged
}
```

`../nomadcode/services/core/internal/adapters/openai/client_test.go:98-151`도 object shape를 기대한다.

#### 해결 방법

`buildRequestMetadata`가 `WorkspaceMetadata.Path`만 `metadata.workspace` flat string으로 넣도록 바꾼다. `input.Metadata["workspace"]`와 typed path가 충돌하면 typed path가 계속 이긴다. `Path`가 blank인 경우는 workspace key를 넣지 않거나 기존 flat metadata만 유지한다. comments는 typed struct가 producer-side context이고 wire shape는 flat string임을 설명하게 고친다.

```go
// after
if input.WorkspaceMetadata != nil {
	if path := strings.TrimSpace(input.WorkspaceMetadata.Path); path != "" {
		merged["workspace"] = path
	}
}
```

#### 수정 파일 및 체크리스트

- [ ] `../nomadcode/services/core/internal/adapters/openai/client.go`: serialization 변경과 `strings` import 확인.
- [ ] `../nomadcode/services/core/internal/adapters/openai/client_test.go`: test 이름과 expectation을 flat string으로 변경.
- [ ] `../nomadcode/services/core/internal/model/model.go`: `WorkspaceMetadata` comment를 wire contract에 맞게 수정.

#### 테스트 작성

테스트 작성: yes. 기존 `TestGenerateWorkspaceMetadataIsObject`를 `TestGenerateWorkspaceMetadataUsesFlatWorkspacePath`로 바꾸고 `metadata.workspace`가 string path인지, `task_id`/`source` flat metadata가 유지되는지 검증한다. blank path boundary test는 helper가 작으면 같은 test table에 넣는다.

#### 중간 검증

```bash
cd ../nomadcode/services/core && go test -count=1 ./internal/adapters/openai ./internal/authoring ./internal/scheduler
```

기대 결과: OpenAI client, authoring request, scheduler fixtures가 모두 통과한다.

### [NOMAD_METADATA-2] IOP Consumer Fixture

#### 문제

IOP는 flat workspace를 이미 받지만 old NomadCode object shape rejection을 명시적으로 고정하지 않았다. contract drift가 재발하면 consumer 쪽에서 애매한 실패가 된다.

```go
// apps/edge/internal/openai/server_test.go:520-533
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
	"model":"client-model",
	"input":"test",
	"metadata":{
		"request_id":"req-001",
		"workspace":"/config/workspace/iop",
		"inference":{"target":"metadata-target"},
		"nomadcode":{"task_id":"task-123","source":"nomadcode"}
	}
}`))
```

#### 해결 방법

`apps/edge/internal/openai/server_test.go`에 `metadata.workspace` object를 보내는 `/v1/responses` request가 400 `invalid metadata format` 계열로 실패하는 test를 추가한다. flat `metadata.task_id`/`metadata.source` 또는 structured `metadata.nomadcode.*` precedence tests는 기존 coverage를 유지하되 필요한 경우 한 assertion만 추가한다.

#### 수정 파일 및 체크리스트

- [ ] `apps/edge/internal/openai/server_test.go`: object workspace rejection test 추가.
- [ ] `agent-contract/provided/openai-compatible-api.md`: 이미 작은 작업에서 갱신된 계약과 test가 어긋나면 문구만 최소 보정.
- [ ] `apps/edge/internal/edgecmd/smoke_openai.go`: `06+05`가 workspace flag를 추가한 뒤 conflict가 있으면 flat metadata 유지 확인만 한다.

#### 테스트 작성

테스트 작성: yes. Contract boundary regression이므로 IOP Edge test를 추가한다.

#### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/openai ./apps/edge/cmd/edge
```

기대 결과: old object workspace rejection과 existing flat workspace tests가 통과한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `../nomadcode/services/core/internal/adapters/openai/client.go` | NOMAD_METADATA-1 |
| `../nomadcode/services/core/internal/adapters/openai/client_test.go` | NOMAD_METADATA-1 |
| `../nomadcode/services/core/internal/model/model.go` | NOMAD_METADATA-1 |
| `apps/edge/internal/openai/server_test.go` | NOMAD_METADATA-2 |
| `agent-contract/provided/openai-compatible-api.md` | NOMAD_METADATA-2 only if contract wording conflicts with tests |
| `apps/edge/internal/edgecmd/smoke_openai.go` | NOMAD_METADATA-2 only if `06+05` landed and needs flat metadata assertion alignment |

## 최종 검증

```bash
cd ../nomadcode/services/core && go test -count=1 ./...
go test -count=1 ./apps/edge/internal/openai ./apps/edge/cmd/edge
git diff --check
```

기대 결과: 모든 명령이 0 exit. Go test cache output은 허용하지 않는다. `git diff --check` output은 없어야 한다.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
