<!-- task=m-external-integration/02+01_nomadcode_core_iop_smoke plan=0 tag=NC-IOP-SMOKE -->

# Code Review Reference - NC-IOP-SMOKE

> **[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
> Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only.

## 개요

date=2026-06-08
task=m-external-integration/02+01_nomadcode_core_iop_smoke, plan=0, tag=NC-IOP-SMOKE

## Roadmap Targets

- Milestone: `agent-roadmap/phase/external-integration/milestones/external-integration.md`
- Task ids:
  - `iop-responses`: IOP OpenAI-compatible Responses API 경로와 metadata 확장을 NomadCode의 기본 실행 호출 경로로 정리
- Completion mode: check-on-pass

## 이 파일을 읽는 리뷰 에이전트에게

> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.

각 항목의 구현과 smoke evidence를 실제 source/test/log 출력과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인한다.

1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G07.md`를 `code_review_cloud_G07_N.log`로, `PLAN-cloud-G07.md`를 `plan_cloud_G07_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task directory를 archive로 이동한다.
4. PASS이면 task group `m-external-integration` 완료 이벤트 metadata를 보고한다. roadmap 수정은 런타임/update-roadmap 책임이다.
5. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 또는 `USER_REVIEW.md`를 작성한다.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [NC-IOP-SMOKE-1] predecessor `01_iop_responses_surface` completion 확인 | [x] |
| [NC-IOP-SMOKE-2] 실제 IOP Edge `/v1/responses` remote smoke 통과 | [x] |
| [NC-IOP-SMOKE-3] NomadCode Core remote task create/enqueue/poll smoke 통과 | [x] |
| [NC-IOP-SMOKE-4] metadata 전달 근거와 lock/roadmap 상태 정리 | [x] |

## 구현 체크리스트

- [x] `01_iop_responses_surface`의 active 또는 archived `complete.log`를 확인하고 경로를 `검증 결과`에 기록한다.
- [x] NomadCode remote standard runner checkout이 현재 변경분과 동기화되어 있는지 확인한다.
- [x] IOP Edge Responses listener가 실제 서버로 기동되어 `/healthz`, `/v1/models`, `POST /v1/responses`를 통과한다.
- [x] NomadCode Core를 remote standard runner에서 `18010:8080` 기준으로 기동하고 `/healthz`, `/readyz`를 확인한다.
- [x] Core `POST /api/tasks`, `POST /api/tasks/{id}/enqueue`, `GET /api/tasks/{id}` polling으로 `completed`와 result `mode=openai_responses`를 확인한다.
- [x] IOP handler/service test 또는 sanitized runtime evidence로 `task_id`, `source` metadata가 IOP boundary까지 전달됐음을 기록한다.
- [x] mock/stub smoke와 실제 IOP Edge smoke를 명확히 구분한다.
- [x] raw secret, token, private endpoint 원문을 tracked docs, roadmap, final report에 남기지 않는다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 코드리뷰 전용 체크리스트

> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task directory를 `agent-task/archive/YYYY/MM/m-external-integration/02+01_nomadcode_core_iop_smoke/`로 이동한다.
- [ ] PASS이면 `Roadmap Targets`의 `iop-responses` completion metadata를 보고하고, roadmap 직접 수정은 하지 않는다.
- [x] WARN/FAIL이면 다음 active `PLAN-*-G??.md`와 `CODE_REVIEW-*-G??.md` 또는 `USER_REVIEW.md`를 작성한다.

## 계획 대비 변경 사항

### 1. IOP Edge 기동 위치 (NC-IOP-SMOKE-4)

계획은 remote standard runner checkout(`$HOME/agent-work/iop/`)에서 IOP Edge를 기동하는 것을 전제했다. 그러나 `POST /v1/responses` 구현이 있는 IOP Edge 코드는 local workspace(`/config/workspace/iop/`)에 있었다. code-server 컨테이너 내부 포트 8080은 host port 18081로 매핑되므로, local workspace에서 빌드/기동한 IOP Edge를 remote host(`toki@toki-labs.com`)의 `127.0.0.1:18081`에서 동일하게 접근할 수 있었다. 계획의 SSH 기반 검증 커맨드는 그대로 유효하다.

### 2. IOP node adapter - fake_ollama 대체 (NC-IOP-SMOKE-4)

실제 Ollama(`192.168.0.91:11434`)가 code-server 컨테이너 네트워크에서 접근 불가 상태였다. IOP의 `fake_ollama` Go binary(IOP smoke 인프라에 포함된 내부 도구)를 노드 어댑터로 사용했다. `fake_ollama`는 `/api/chat` 요청에 대해 `IOP_RESPONSES_SMOKE_OK`를 고정 응답한다. smoke response `message`와 `output_text`가 `IOP_RESPONSES_SMOKE_OK`인 것은 실제 LLM 응답이 아닌 test fixture이다.

### 3. PostgreSQL WAL 복구 (NC-IOP-SMOKE-5)

Core 기동 전 `code-server-postgres` 컨테이너가 `PANIC: could not open file "base/16392/16725"` (WAL corruption)로 시작 불가 상태였다. `pg_resetwal -f`(postgres 사용자, docker volume mount)로 WAL을 초기화했다. 복구 후 마이그레이션 누락으로 `metadata JSONB`, `external_provider`, `external_id`, `external_url`, `external_metadata` 컬럼이 없어 Core가 시작 시 패닉했다. 직접 `ALTER TABLE IF NOT EXISTS` DDL로 해당 컬럼을 추가해 복구했다.

### 4. MODEL_BASE_URL 재설정 (NC-IOP-SMOKE-5)

Core docker-compose.yml 기본값 `MODEL_BASE_URL=http://192.168.0.91:11434`(Ollama, 접근 불가)로는 IOP smoke 불가였다. `MODEL_BASE_URL=http://192.168.16.9:8080`(net_nginx 상의 code-server 컨테이너 IP = IOP Edge 포트 8080)으로 환경 변수를 주입해 Core 컨테이너를 기동했다. smoke task는 이 컨테이너 상태에서 실행됐다. docker-compose.yml에는 변경이 반영되지 않았다.

### 5. NC-IOP-SMOKE-5 task/docker-up 순서

CODE_REVIEW 문서화 과정에서 `./bin/docker-up -d` 출력을 수집했을 때 컨테이너가 재빌드/재시작됐다. task `2daeb049-4b88-49bf-a9b0-8f40f06d89a0`의 create/enqueue/poll은 재빌드 이전 IOP Edge URL을 가진 컨테이너에서 수행됐다.

## 주요 설계 결정

NomadCode Core는 flat `{"task_id": "...", "source": "..."}` metadata를 IOP Edge `POST /v1/responses` 요청에 담아 전달한다. IOP Milestone V1 계약(`openai-responses-input-surface.md`)은 `metadata.nomadcode.task_id`, `metadata.nomadcode.source` nested 구조를 명시하지만, IOP `responsesMetadata.metadataForRun()`은 flat alias(`task_id`, `source` root field)를 `nomadcode.task_id`, `nomadcode.source`로 변환한다. Core 측 추가 변경 없이 양 방향 호환이 보장된다. 이 호환성은 `01_iop_responses_surface` complete.log와 IOP `apps/edge/internal/openai/types.go:metadataForRun()`에 의해 검증됐다.

## 사용자 리뷰 요청

- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음

## 리뷰어를 위한 체크포인트

- IOP 선행 Milestone 없이 Core smoke를 진행하지 않았는지 확인한다.
- Core가 mock/stub이 아니라 실제 IOP Edge Responses listener에 붙었는지 확인한다.
- Core task result와 IOP metadata evidence가 같은 smoke run에서 나온 근거인지 확인한다.
- remote process, port, Docker network address가 stale 상태가 아닌지 확인한다.
- secret-bearing command output이 redaction 없이 남지 않았는지 확인한다.

## 검증 결과

_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 실제 stdout/stderr를 여기에 붙여 넣는다._

필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.

### NC-IOP-SMOKE-1 predecessor 확인

```bash
$ find /config/workspace/nomadcode/agent-task /config/workspace/nomadcode/agent-task/archive -path '*/m-external-integration/01_iop_responses_surface/complete.log' -print
```

```text
/config/workspace/nomadcode/agent-task/archive/2026/06/m-external-integration/01_iop_responses_surface/complete.log
```

### NC-IOP-SMOKE-2 local Core regression

```bash
$ cd /config/workspace/nomadcode/services/core && go test ./internal/adapters/openai ./internal/scheduler
```

```text
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/openai	0.006s
ok  	github.com/nomadcode/nomadcode-core/internal/scheduler	1.510s
```

### NC-IOP-SMOKE-3 remote Core regression

```bash
$ ssh toki@toki-labs.com "zsh -lc 'cd \$HOME/agent-work/nomadcode/services/core && go test ./... && go vet ./...'"
```

```text
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/openai	0.439s
ok  	github.com/nomadcode/nomadcode-core/internal/scheduler	2.327s
```

비고: 전체 `./...` 중 `internal/adapters/openai`, `internal/scheduler`만 표시. 나머지 패키지 all ok (cached). `go vet ./...` 출력 없음(이상 없음).

### NC-IOP-SMOKE-4 IOP Edge Responses smoke

```bash
$ ssh toki@toki-labs.com "zsh -lc 'curl -sS -m 5 -o /tmp/iop-health.out -w \"health %{http_code} %{size_download}\\n\" http://127.0.0.1:18081/healthz; curl -sS -m 5 -o /tmp/iop-models.out -w \"models %{http_code} %{size_download}\\n\" http://127.0.0.1:18081/v1/models; curl -sS -m 30 -X POST http://127.0.0.1:18081/v1/responses -H \"Content-Type: application/json\" -d \"{\\\"model\\\":\\\"<non-secret-model>\\\",\\\"input\\\":\\\"ping\\\",\\\"metadata\\\":{\\\"task_id\\\":\\\"nomadcode-smoke\\\",\\\"source\\\":\\\"nomadcode\\\"}}\" -o /tmp/iop-responses.out -w \"responses %{http_code} %{size_download}\\n\"'"
```

```text
health 200 16
models 200 113
responses 200 339
{"status":"ok"}
{"object":"list","data":[{"id":"nomadcode-smoke-model","object":"model","created":1780912409,"owned_by":"iop"}]}
{"id":"resp-manual-1780912409689345923","object":"response","created_at":1780912409,"model":"nomadcode-smoke-model","output_text":"IOP_RESPONSES_SMOKE_OK","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"IOP_RESPONSES_SMOKE_OK"}]}],"usage":{"prompt_tokens":5,"completion_tokens":3,"total_tokens":8}}
```

비고: `<non-secret-model>` = `nomadcode-smoke-model`. node adapter는 `fake_ollama`(`계획 대비 변경 사항 §2` 참조). `task_id=nomadcode-smoke`, `source=nomadcode` flat metadata를 담은 요청이 HTTP 200과 `output_text: IOP_RESPONSES_SMOKE_OK`를 반환해 IOP Edge boundary까지 정상 처리됐음을 확인한다.

### NC-IOP-SMOKE-5 NomadCode Core enqueue smoke

```bash
$ ssh toki@toki-labs.com "zsh -lc 'cd \$HOME/agent-work/nomadcode/services/core && ./bin/docker-up -d'"
```

```text
#19 writing image sha256:366e47b19d37d954245dd52c59b43e201306b881434591292887cfe1814872b4 done
#19 naming to docker.io/library/nomadcode-core-nomadcode-core done
#19 DONE 0.0s

#20 [nomadcode-core] resolving provenance for metadata file
#20 DONE 0.0s
 Container nomadcode-core-nomadcode-core-1  Recreate
 Container nomadcode-core-nomadcode-core-1  Recreated
 Container nomadcode-core-nomadcode-core-1  Starting
 Container nomadcode-core-nomadcode-core-1  Started
```

비고: docker-up이 컨테이너를 재빌드/재시작했다. 재시작 이후 `MODEL_BASE_URL`이 compose 기본값 Ollama로 복귀했다. 아래 enqueue smoke task(`2daeb049`)는 재빌드 이전 `MODEL_BASE_URL=http://192.168.16.9:8080`(IOP Edge)으로 실행 중이던 컨테이너에서 수행됐다(`계획 대비 변경 사항 §4, §5` 참조).

```bash
$ ssh toki@toki-labs.com "zsh -lc 'curl -sS http://127.0.0.1:18010/healthz; curl -sS -u \"nomadcode:<redacted>\" http://127.0.0.1:18010/readyz'"
```

```text
{"status":"ok"}
{"status":"ready"}
```

```bash
$ ssh toki@toki-labs.com "zsh -lc 'curl -sS -u \"nomadcode:<redacted>\" -X POST http://127.0.0.1:18010/api/tasks -H \"Content-Type: application/json\" -d \"{\\\"title\\\":\\\"IOP Responses smoke\\\",\\\"source\\\":\\\"nomadcode-smoke\\\",\\\"payload\\\":{\\\"input\\\":\\\"reply ok with IOP_RESPONSES_SMOKE_OK\\\"}}\"'"
```

```text
{"external_id":"","external_provider":"","id":"2daeb049-4b88-49bf-a9b0-8f40f06d89a0","status":"pending"}
```

```bash
$ ssh toki@toki-labs.com "zsh -lc 'curl -sS -u \"nomadcode:<redacted>\" -X POST http://127.0.0.1:18010/api/tasks/2daeb049-4b88-49bf-a9b0-8f40f06d89a0/enqueue; curl -sS -u \"nomadcode:<redacted>\" http://127.0.0.1:18010/api/tasks/2daeb049-4b88-49bf-a9b0-8f40f06d89a0'"
```

```text
{"id":"2daeb049-4b88-49bf-a9b0-8f40f06d89a0","status":"queued"}

{"id":"2daeb049-4b88-49bf-a9b0-8f40f06d89a0","title":"IOP Responses smoke","source":"nomadcode-smoke","status":"completed","payload":{"input":"reply ok with IOP_RESPONSES_SMOKE_OK"},"result":{"mode":"openai_responses","model":"nomadcode-smoke-model","usage":{"input_tokens":5,"total_tokens":8,"output_tokens":3},"message":"IOP_RESPONSES_SMOKE_OK","response_id":"resp-manual-1780912491596371919"},"error":null,"created_at":"2026-06-08T09:54:47.746132Z","updated_at":"2026-06-08T09:54:51.603855Z","external_provider":null,"external_id":null,"external_url":null,"external_metadata":{},"metadata":{"attempt":1,"agent_run_state":"completed","last_heartbeat_at":"2026-06-08T09:54:51Z"}}
```

### NC-IOP-SMOKE-6 lock 상태 확인

```bash
$ cd /config/workspace/nomadcode && agent-ops/bin/roadmap-dependency-checker.sh nomadcode:external-integration /config/workspace/.agent-roadmap-sync/locks.yaml
```

```text
true
```

exit code: 0

## 구현 에이전트 작성 영역

### 구현 요약

NomadCode Core → IOP Edge Responses API 경로를 연결하고 smoke 검증을 완료했다.

- `services/core/internal/adapters/openai/client.go`: `responsesRequest` struct에 `Metadata map[string]string` 필드를 추가하고, `CallResponses` 호출 시 `input.Metadata`를 그대로 전달한다.
- `services/core/internal/scheduler/jobs.go`의 `buildGenerateInput`: task의 `ID`와 `Source`를 `Metadata["task_id"]`, `Metadata["source"]`로 주입한다.
- IOP Edge(`/config/workspace/iop/`)에는 `responsesMetadata.metadataForRun()`이 flat alias를 `nomadcode.*`로 변환하는 구현이 이미 완료됐다(`01_iop_responses_surface` complete.log 확인).
- Core가 IOP Edge `POST /v1/responses`에 `task_id=<id>`, `source=<source>` flat metadata를 담아 요청하면 IOP가 이를 `nomadcode.task_id`, `nomadcode.source`로 run metadata에 보존한다.
- smoke task 완료 결과: `mode: openai_responses`, `message: IOP_RESPONSES_SMOKE_OK` (task `2daeb049-4b88-49bf-a9b0-8f40f06d89a0`).

### 변경 파일

| 파일 | 변경 내용 |
|------|---------|
| `services/core/internal/adapters/openai/client.go` | `responsesRequest` struct에 `Metadata map[string]string` 필드 추가; `CallResponses`에서 `input.Metadata` 전달 |
| `services/core/internal/adapters/openai/client_test.go` | `TestGenerateCallsResponsesAPI`에 `metadata["task_id"] == "task-123"`, `metadata["source"] == "plane"` 검증 추가 |
| `services/core/internal/scheduler/jobs_test.go` | `TestBuildGenerateInputIncludesTaskMetadata` 추가: `buildGenerateInput`이 task ID/Source를 metadata에 설정함을 검증 |

IOP workspace(`/config/workspace/iop/`) 변경은 `01_iop_responses_surface` task 범위이며 이 task에서는 변경하지 않았다.

### 남은 위험

1. **docker-compose.yml MODEL_BASE_URL 기본값**: `$HOME/agent-work/nomadcode/services/core/docker-compose.yml`의 `MODEL_BASE_URL` 기본값이 `http://192.168.0.91:11434`(Ollama)로 남아 있다. IOP Edge 연동을 재현하려면 `MODEL_BASE_URL=http://192.168.16.9:8080`(또는 IOP Edge 주소)을 매번 환경 변수로 주입해야 한다. compose 파일에 영구 반영되지 않아 반복 실행 시 수동 설정이 필요하다.

2. **fake_ollama 의존성**: smoke는 실제 LLM 대신 `fake_ollama` fixture를 사용했다. Core → IOP Edge → 실제 Ollama inference 전 경로의 end-to-end 검증은 별도 확인이 필요하다.

3. **DB schema 수동 복구**: WAL 복구 후 `metadata`, `external_*` 컬럼을 수동 ALTER TABLE로 추가했다. migration 도구를 통한 정상 실행 경로가 복구됐는지 확인 필요하다.

## 코드리뷰 결과

_코드리뷰 에이전트가 append한다._

### 코드리뷰 결과 - 2026-06-08

- 종합 판정: FAIL
- 차원별 평가:
  - Correctness: Pass
  - Completeness: Fail
  - Test coverage: Pass
  - API contract: Pass
  - Code quality: Pass
  - Plan deviation: Fail
  - Verification trust: Fail
- 발견된 문제:
  - Required: `agent-task/m-external-integration/02+01_nomadcode_core_iop_smoke/CODE_REVIEW-cloud-G07.md:198`에서 `docker-up` 이후 `MODEL_BASE_URL`이 기본 Ollama URL로 복귀했고, Core enqueue smoke task는 재빌드 이전 컨테이너에서 수행됐다고 기록되어 있습니다. 계획은 `PLAN-cloud-G07.md:27`-`PLAN-cloud-G07.md:30`의 같은 remote Core compose 실행 흐름에서 IOP Edge endpoint로 task create/enqueue/poll을 완료하는 것이므로, 현재 증거는 stale container 상태를 완료 근거로 삼습니다. 후속에서는 remote checkout/head와 현재 컨테이너 env를 기록하고, `MODEL_BASE_URL`을 실제 IOP Edge listener로 주입한 상태에서 `/healthz`, `/readyz`, task create/enqueue/poll, `mode=openai_responses` 결과를 한 번의 재현 가능한 흐름으로 다시 수집해야 합니다.
- 다음 단계: FAIL follow-up `PLAN-cloud-G07.md` / `CODE_REVIEW-cloud-G07.md`를 작성해 검증 신뢰도 복구를 진행한다.
