diff --git a/agent-contract/index.md b/agent-contract/index.md index 9d6ae54..458f3d2 100644 --- a/agent-contract/index.md +++ b/agent-contract/index.md @@ -12,7 +12,7 @@ | id | 읽는 조건 | path | |----|-----------|------| -| `iop.openai-compatible-api` | OpenAI-compatible API, Responses API, Chat Completions, `model` route, Codex/CLI workspace, `metadata.workspace` | `agent-contract/provided/openai-compatible-api.md` | +| `iop.openai-compatible-api` | OpenAI-compatible API, Responses API, Chat Completions, `model` route, Codex/CLI workspace, NomadCode authoring metadata, `metadata.workspace`, `metadata.task_id`, `metadata.source` | `agent-contract/provided/openai-compatible-api.md` | ## 소비 계약 diff --git a/agent-contract/provided/openai-compatible-api.md b/agent-contract/provided/openai-compatible-api.md index e142afe..6925157 100644 --- a/agent-contract/provided/openai-compatible-api.md +++ b/agent-contract/provided/openai-compatible-api.md @@ -52,6 +52,31 @@ CLI agent 실행으로 라우팅되는 요청의 최소 형태: - 기존 metadata 계약인 `metadata.request_id`, `metadata.nomadcode.task_id`, `metadata.nomadcode.source`, `metadata.inference.target`은 유지한다. - CLI route의 `metadata.workspace`는 이 문서의 계약 기준이다. 구현은 이 값을 Edge service의 run workspace와 Node CLI adapter의 process working directory로 전달해야 한다. +## NomadCode Authoring Handoff + +NomadCode Core가 IOP Edge HTTP 표면으로 workspace authoring 작업을 넘길 때의 최소 요청 형태: + +```json +{ + "model": "codex", + "input": "Todo 항목에 필요한 산출물을 현재 checkout에 작성해줘.", + "metadata": { + "workspace": "/config/workspace/nomadcode-slot-123", + "task_id": "todo-123", + "source": "nomadcode" + } +} +``` + +NomadCode task/source context는 flat `metadata.task_id`와 `metadata.source`로 전달할 수 있다. +동일한 의미의 structured 형태가 필요하면 `metadata.nomadcode.task_id`와 `metadata.nomadcode.source`를 사용하며, flat alias와 structured 값이 함께 있으면 structured 값을 우선한다. + +이 handoff는 `model`, `input`, `metadata.workspace`, task/source metadata만으로 충분해야 한다. +호출자는 `metadata.cli`, root-level `iop` wrapper, IOP CLI 직접 실행, prompt 본문 workspace 주입을 요구받지 않는다. + +Workspace-bound route는 workspace가 없거나 상대 경로이면 OpenAI-compatible error로 거부한다. +존재하지 않는 경로, 권한 오류, agent process exit failure는 기본 cwd fallback으로 숨기지 않고 호출자가 실패로 구분할 수 있어야 한다. + ## Chat Completions `/v1/chat/completions`도 같은 metadata 원칙을 따른다. CLI route의 workspace는 `metadata.workspace`에 둔다. diff --git a/agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md b/agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md index 4385307..d9e9e4d 100644 --- a/agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md +++ b/agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md @@ -47,8 +47,8 @@ Edge service와 Node runtime 사이에서 workspace가 metadata 문자열로만 - [x] [run-workspace] `SubmitRunRequest`와 `BuildRunRequest`가 workspace를 명시 필드로 갖고 proto `RunRequest.Workspace`에 채운다. 검증: service unit test가 metadata와 workspace를 별도 필드로 보존하는지 확인한다. - [x] [node-spec] Node router가 `RunRequest.Workspace`를 `ExecutionSpec.Workspace`로 유지한다. 검증: router/node test에서 workspace 값이 adapter execution spec까지 보존된다. -- [ ] [agent-cwd] workspace-bound agent process 실행기가 `ExecutionSpec.Workspace`를 process working directory로 적용한다. 검증: 이 마일스톤에서 지원하는 agent target profile의 cwd 적용 test가 통과한다. -- [ ] [session-workspace] logical session이 다른 workspace를 같은 target/session으로 재사용하지 않는다. 검증: 같은 target/session이라도 workspace가 다르면 별도 session을 만들거나 명시 오류를 반환한다. +- [x] [agent-cwd] workspace-bound agent process 실행기가 `ExecutionSpec.Workspace`를 process working directory로 적용한다. 검증: 이 마일스톤에서 지원하는 agent target profile의 cwd 적용 test가 통과한다. +- [x] [session-workspace] logical session이 다른 workspace를 같은 target/session으로 재사용하지 않는다. 검증: 같은 target/session이라도 workspace가 다르면 별도 session을 만들거나 명시 오류를 반환한다. ### Epic: [nomad-http-smoke] NomadCode Todo Authoring Readiness @@ -56,7 +56,7 @@ NomadCode가 Plane `Todo` projection 전 단계에서 IOP Edge HTTP 호출로 wo - [ ] [workspace-authoring-smoke] `metadata.workspace`가 가리키는 임시 checkout에서 workspace-bound agent target이 파일을 생성/수정할 수 있음을 검증한다. 검증: OpenAI-compatible `/v1/responses` smoke가 workspace 안 marker 또는 git diff를 남기고, 프로세스 cwd가 workspace 밖으로 벗어나지 않는다. - [ ] [nomad-metadata-shape] NomadCode Core가 workspace slot path를 flat `metadata.workspace`로, task/source context를 `metadata.task_id`/`metadata.source` 또는 `metadata.nomadcode.*`로 전달하는 호출 shape를 맞춘다. 검증: `../nomadcode`의 OpenAI Responses client/scheduler fixture와 IOP Edge smoke fixture가 같은 metadata contract를 사용한다. -- [ ] [nomad-handoff-contract] NomadCode authoring 호출 shape를 계약 문서와 운영 문서에 연결한다. 검증: 외부 호출은 `model`, `input`, `metadata.workspace`, task/source metadata만으로 충분하며 `metadata.cli`, root-level `iop` wrapper, IOP CLI 직접 실행을 요구하지 않는다. +- [x] [nomad-handoff-contract] NomadCode authoring 호출 shape를 계약 문서와 운영 문서에 연결한다. 검증: 외부 호출은 `model`, `input`, `metadata.workspace`, task/source metadata만으로 충분하며 `metadata.cli`, root-level `iop` wrapper, IOP CLI 직접 실행을 요구하지 않는다. - [ ] [failure-surface] workspace 누락, 존재하지 않는 경로, 권한 오류, agent process exit failure가 호출자에게 구분 가능한 실패로 드러난다. 검증: 잘못된 workspace 요청이 조용히 기본 cwd에서 실행되지 않는다. ## 완료 리뷰 diff --git a/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/code_review_local_G06_0.log b/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/code_review_local_G06_0.log new file mode 100644 index 0000000..029d4f0 --- /dev/null +++ b/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/code_review_local_G06_0.log @@ -0,0 +1,200 @@ + + +# Code Review Reference - FAILURE_SURFACE + +> **[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, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-13 +task=m-openai-workspace-agent-execution-contract/05_failure_surface, plan=0, tag=FAILURE_SURFACE + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md` +- Task ids: + - `failure-surface`: workspace 누락, 존재하지 않는 경로, 권한 오류, agent process exit failure가 호출자에게 구분 가능한 실패로 드러난다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-local-G06.md` → `code_review_local_G06_N.log`, `PLAN-local-G06.md` → `plan_local_G06_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-openai-workspace-agent-execution-contract/05_failure_surface/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다. +4. PASS이고 task group이 `m-openai-workspace-agent-execution-contract`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [FAILURE_SURFACE-1] Node CLI adapter의 workspace preflight를 추가하고 모든 CLI launch path가 같은 오류 분류를 사용하게 한다. | [x] | +| [FAILURE_SURFACE-2] workspace not found, not directory, inaccessible, process exit failure regression test를 추가하고 기존 cwd/session tests를 유지한다. | [x] | + +## 구현 체크리스트 + +- [x] [FAILURE_SURFACE-1] Node CLI adapter의 workspace preflight를 추가하고 모든 CLI launch path가 같은 오류 분류를 사용하게 한다. +- [x] [FAILURE_SURFACE-2] workspace not found, not directory, inaccessible, process exit failure regression test를 추가하고 기존 cwd/session tests를 유지한다. +- [x] `go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai`와 `go test -count=1 ./apps/node/... ./apps/edge/...`를 실행한다. +- [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_local_G06_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_local_G06_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 active task 디렉터리 `agent-task/m-openai-workspace-agent-execution-contract/05_failure_surface/`를 `agent-task/archive/YYYY/MM/m-openai-workspace-agent-execution-contract/05_failure_surface/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-openai-workspace-agent-execution-contract/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G07.md`와 `CODE_REVIEW-local-G07.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +- `git diff --check` 검증 시 파일 끝에 공백 라인이 새로 유입되어 실패하였던 문제를 인지하고, `cli_internal_test.go` 맨 마지막 부분의 불필요한 공백을 제거하여 수정 완료했습니다. 이 외에는 계획 대비 변경된 설계나 구현 변경 사항은 없습니다. + +## 주요 설계 결정 + +- `apps/node/internal/adapters/cli/cli.go`에 `prepareWorkspaceDir` 헬퍼 함수를 신설하여 공백 워크스페이스는 검증하지 않고 통과(기존 no-workspace 하위 호환성 유지)시키되, 비어있지 않은 경로의 경우 `os.Stat` 및 디렉토리 상태 체크, 열기 시도(access)를 미리 거치게 함으로써 `workspace not found`, `workspace inaccessible`, `workspace is not a directory` 오류로 구분하여 CLI 프로세스 실행 전에 즉각 실패 반환하도록 처리하였습니다. +- 이를 각 CLI 실행 진입 경로(`oneshot`, `persistent` [터미널 여부 포함], `codex_app_server`, `opencode_sse`)에서 공통 헬퍼를 통해 호출하게 만들어 에러 반환 문구를 표준화하였습니다. +- 한편 `workspaceDir` 함수는 세션 키 정규화 등의 용도로 안전하게 남겨두어 전체 라이프사이클의 호환성이 깨지지 않도록 하였습니다. + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- `workspaceDir`가 session key normalization 용도로 남고 process launch validation은 새 helper로만 이루어지는지 확인한다. +- 모든 CLI launch path가 같은 helper를 지나며 blank workspace의 기존 동작이 유지되는지 확인한다. +- workspace not found/not directory/inaccessible와 process exit failure가 테스트에서 구분되는지 확인한다. +- Edge HTTP missing/relative workspace behavior가 의도치 않게 바뀌지 않았는지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. +- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다. + +### FAILURE_SURFACE-1 중간 검증 +```bash +$ go test -count=1 ./apps/node/internal/adapters/cli +ok iop/apps/node/internal/adapters/cli 47.066s +``` + +### FAILURE_SURFACE-2 중간 검증 +```bash +$ go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai +ok iop/apps/node/internal/adapters/cli 46.644s +ok iop/apps/edge/internal/openai 1.508s +``` + +### 최종 검증 +```bash +$ go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai +ok iop/apps/node/internal/adapters/cli 46.644s +ok iop/apps/edge/internal/openai 1.508s + +$ go test -count=1 ./apps/node/... ./apps/edge/... +ok iop/apps/node/cmd/node 0.010s +ok iop/apps/node/internal/adapters 0.020s +ok iop/apps/node/internal/adapters/cli 46.786s +? iop/apps/node/internal/adapters/cli/internal/testutil [no test files] +ok iop/apps/node/internal/adapters/cli/status 39.814s +? iop/apps/node/internal/adapters/mock [no test files] +ok iop/apps/node/internal/adapters/ollama 0.025s +ok iop/apps/node/internal/adapters/vllm 0.016s +ok iop/apps/node/internal/bootstrap 0.275s +ok iop/apps/node/internal/node 0.127s +ok iop/apps/node/internal/router 0.006s +? iop/apps/node/internal/runtime [no test files] +ok iop/apps/node/internal/store 0.067s +ok iop/apps/node/internal/terminal 0.595s +ok iop/apps/node/internal/transport 5.144s +ok iop/apps/edge/cmd/edge 0.038s +ok iop/apps/edge/internal/bootstrap 0.016s +ok iop/apps/edge/internal/controlplane 4.450s +ok iop/apps/edge/internal/edgecmd 0.011s +ok iop/apps/edge/internal/events 0.004s +ok iop/apps/edge/internal/input 0.008s +ok iop/apps/edge/internal/input/a2a 0.006s +ok iop/apps/edge/internal/node 0.006s +ok iop/apps/edge/internal/openai 1.510s +ok iop/apps/edge/internal/opsconsole 0.007s +ok iop/apps/edge/internal/service 0.005s +ok iop/apps/edge/internal/transport 2.012s + +$ git diff --check +(No output, exited with 0) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## 섹션 소유권 + +| Section | Owner | Note | +|---------|-------|------| +| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these. | +| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify; code-review copies it into `complete.log` only on PASS. | +| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only. | +| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only; final checkbox is mandatory before saving. | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section. | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content. | +| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required to proceed. | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Implementing agent must not modify. | +| 검증 결과 | Implementing agent | Fill command output only; command changes require `계획 대비 변경 사항`. | +| 코드리뷰 결과 | Review agent appends | Not included in stub. | + +## 코드리뷰 결과 + +- 종합 판정: FAIL +- 차원별 평가: + - correctness: Fail + - completeness: Fail + - test coverage: Fail + - API contract: Fail + - code quality: Pass + - plan deviation: Pass + - verification trust: Pass +- 발견된 문제: + - Required - `apps/node/internal/adapters/cli/oneshot.go:26`, `apps/node/internal/adapters/cli/persistent.go:67`, `apps/node/internal/adapters/cli/codex_app_server.go:73`, `apps/node/internal/adapters/cli/opencode_sse.go:108`: workspace preflight/session creation error is returned before a `RuntimeEvent{Type: error}` is emitted. `apps/node/internal/transport/session.go:45` only logs `OnRunRequest` errors, while the Edge/OpenAI caller waits on run events via `collectRunResult`; therefore a bad workspace can surface as a stream timeout or closed stream instead of the required `workspace not found` / `workspace inaccessible` failure. Fix by emitting a runtime error event with the exact returned error before returning pre-start preflight/session errors on every CLI launch mode, or by adding an equivalent central adapter error-event wrapper that avoids duplicate events for errors already emitted by running processes. + - Required - `apps/node/internal/adapters/cli/cli_internal_test.go:997`, `apps/node/internal/adapters/cli/cli_internal_test.go:1024`, `apps/node/internal/adapters/cli/cli_internal_test.go:1062`: the regression tests assert returned errors, but not the run-stream error event that OpenAI-compatible callers consume; the process-exit test also logs missing exit status/code instead of failing. Add assertions that bad workspace executions emit `EventTypeError` with the workspace classification for one-shot, persistent, and the specialized launch modes whose preflight can fail before process start; make process-exit failure assert its exit status/code remains visible and non-workspace. +- 다음 단계: FAIL 후속 계획으로 `PLAN-local-G07.md`와 `CODE_REVIEW-local-G07.md`를 작성한다. diff --git a/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/code_review_local_G07_1.log b/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/code_review_local_G07_1.log new file mode 100644 index 0000000..21c6422 --- /dev/null +++ b/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/code_review_local_G07_1.log @@ -0,0 +1,200 @@ + + +# Code Review Reference - REVIEW_FAILURE_SURFACE + +> **[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, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-13 +task=m-openai-workspace-agent-execution-contract/05_failure_surface, plan=1, tag=REVIEW_FAILURE_SURFACE + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md` +- Task ids: + - `failure-surface`: workspace 누락, 존재하지 않는 경로, 권한 오류, agent process exit failure가 호출자에게 구분 가능한 실패로 드러난다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-local-G07.md` -> `code_review_local_G07_N.log`, `PLAN-local-G07.md` -> `plan_local_G07_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-openai-workspace-agent-execution-contract/05_failure_surface/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다. +4. PASS이고 task group이 `m-openai-workspace-agent-execution-contract`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REVIEW_FAILURE_SURFACE-1] 모든 CLI launch mode의 pre-start workspace/session 오류가 반환되기 전에 `RuntimeEvent{Type: error}`로 전달되게 한다. | [x] | +| [REVIEW_FAILURE_SURFACE-2] workspace preflight와 process exit regression test가 run-stream error event와 exit status/code를 강하게 검증하도록 보강한다. | [x] | + +## 구현 체크리스트 + +- [x] [REVIEW_FAILURE_SURFACE-1] 모든 CLI launch mode의 pre-start workspace/session 오류가 반환되기 전에 `RuntimeEvent{Type: error}`로 전달되게 한다. +- [x] [REVIEW_FAILURE_SURFACE-2] workspace preflight와 process exit regression test가 run-stream error event와 exit status/code를 강하게 검증하도록 보강한다. +- [x] `go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai`와 `go test -count=1 ./apps/node/... ./apps/edge/...`를 실행한다. +- [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_local_G07_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_local_G07_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 active task 디렉터리 `agent-task/m-openai-workspace-agent-execution-contract/05_failure_surface/`를 `agent-task/archive/YYYY/MM/m-openai-workspace-agent-execution-contract/05_failure_surface/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-openai-workspace-agent-execution-contract/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G08.md`와 `CODE_REVIEW-local-G08.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +- `opencode_sse.go` 수정 도중 TargetContent 치환 실수로 `opts := parseOpencodeRunArgs(profile.Args)` 파싱 로직 및 `Execute` 시그니처가 유실되었던 문제를 신속히 발견하고 원상복구했습니다. 이 외에는 계획과 다른 임의 변경 사항은 전혀 없습니다. + +## 주요 설계 결정 + +- CLI 어댑터 패키지에 `emitReturnedError` 헬퍼 함수를 추가하고, 프로세스 생성/세션 연동 사전 단계(pre-start)에서 발생하는 오류를 상위 반환하기 전에 `sink.Emit`을 거쳐 `RuntimeEvent{Type: EventTypeError}` 형태로 방출하도록 구현했습니다. +- 이를 통해 CLI 실행 시작 오류나 잘못된 워크스페이스 입력(missing/relative 등)에 대한 예외가 스트림 호출자에게도 `EventTypeError`로서 구분 가능한 오류 문자열과 함께 도달하게 보장합니다. +- `cli_internal_test.go` 내부 테스트 코드에 `testSink`를 활용하여 returned error뿐만 아니라 `EventTypeError` 이벤트 자체의 방출 여부, `RunID` 매칭 유무, 오류 내용의 키워드 검사 등을 각 launch mode(`oneshot`, `persistent`, `codex_app_server`, `opencode_sse`)별로 꼼꼼하게 검증하는 하위 테스트들을 추가했습니다. +- `TestCLIProcessExitFailureRemainsDistinct` 테스트에서 exit status가 7로 제대로 캡처되고 있는지 단순 로그 확인(`t.Logf`) 대신 실제 Assert 수준(`t.Errorf`)으로 변경하여 비정상 프로세스 종료의 회귀 보증을 강화했습니다. + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- bad workspace one-shot/persistent/codex-app-server/opencode-sse 실행이 returned error와 sink `EventTypeError`를 모두 남기는지 확인한다. +- pre-start error event가 기존 process command failure/read failure 경로에 중복 error event를 만들지 않는지 확인한다. +- process exit failure test가 exit status/code 부재를 실패로 처리하는지 확인한다. +- Edge OpenAI missing/relative workspace validation behavior가 바뀌지 않았는지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. +- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다. + +### REVIEW_FAILURE_SURFACE-1 중간 검증 +```bash +$ go test -count=1 ./apps/node/internal/adapters/cli +ok iop/apps/node/internal/adapters/cli 46.700s +``` + +### REVIEW_FAILURE_SURFACE-2 중간 검증 +```bash +$ go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai +ok iop/apps/node/internal/adapters/cli 46.605s +ok iop/apps/edge/internal/openai 1.509s +``` + +### 최종 검증 +```bash +$ go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai +ok iop/apps/node/internal/adapters/cli 46.605s +ok iop/apps/edge/internal/openai 1.509s + +$ go test -count=1 ./apps/node/... ./apps/edge/... +ok iop/apps/node/cmd/node 0.010s +ok iop/apps/node/internal/adapters 0.011s +ok iop/apps/node/internal/adapters/cli 46.670s +? iop/apps/node/internal/adapters/cli/internal/testutil [no test files] +ok iop/apps/node/internal/adapters/cli/status 39.718s +? iop/apps/node/internal/adapters/mock [no test files] +ok iop/apps/node/internal/adapters/ollama 0.011s +ok iop/apps/node/internal/adapters/vllm 0.012s +ok iop/apps/node/internal/bootstrap 0.264s +ok iop/apps/node/internal/node 0.120s +ok iop/apps/node/internal/router 0.004s +? iop/apps/node/internal/runtime [no test files] +ok iop/apps/node/internal/store 0.045s +ok iop/apps/node/internal/terminal 0.554s +ok iop/apps/node/internal/transport 5.133s +ok iop/apps/edge/cmd/edge 0.026s +ok iop/apps/edge/internal/bootstrap 0.013s +ok iop/apps/edge/internal/controlplane 4.450s +ok iop/apps/edge/internal/edgecmd 0.008s +ok iop/apps/edge/internal/events 0.003s +ok iop/apps/edge/internal/input 0.005s +ok iop/apps/edge/internal/input/a2a 0.005s +ok iop/apps/edge/internal/node 0.004s +ok iop/apps/edge/internal/openai 1.509s +ok iop/apps/edge/internal/opsconsole 0.006s +ok iop/apps/edge/internal/service 0.005s +ok iop/apps/edge/internal/transport 2.009s + +$ git diff --check +(No output, exited with 0) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## 섹션 소유권 + +| Section | Owner | Note | +|---------|-------|------| +| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these. | +| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify; code-review copies it into `complete.log` only on PASS. | +| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only. | +| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only; final checkbox is mandatory before saving. | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section. | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content. | +| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required to proceed. | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | 계획에서 추출한 리뷰 포인트 | +| 검증 결과 | Implementing agent | Fill command output only; command changes require `계획 대비 변경 사항`. | +| 코드리뷰 결과 | Review agent appends | Not included in stub. | + +## 코드리뷰 결과 + +- 종합 판정: FAIL +- 차원별 평가: + - correctness: Pass + - completeness: Fail + - test coverage: Fail + - API contract: Pass + - code quality: Pass + - plan deviation: Fail + - verification trust: Warn +- 발견된 문제: + - Required - `apps/node/internal/adapters/cli/cli_internal_test.go:1072`, `apps/node/internal/adapters/cli/cli_internal_test.go:1118`: the new `ExecuteCodexAppServer` and `ExecuteOpencodeSSE` regression cases do not set `Mode`, so `executorFor` routes both profiles through the default persistent executor instead of `codexAppServerExecutor` or `opencodeExecutor` (`apps/node/internal/adapters/cli/cli.go:266`). The tests therefore do not exercise the specialized pre-start error-event paths that the plan explicitly required. Set `Mode: modeCodexAppServer` and `Mode: modeOpencodeSSE` in those cases, keep the nonexistent workspace so no external CLI is started, and assert the emitted error includes the specialized wrapper (`start codex app-server` / `start opencode server`) plus `workspace not found`. +- 다음 단계: FAIL 후속 계획으로 `PLAN-local-G08.md`와 `CODE_REVIEW-local-G08.md`를 작성한다. diff --git a/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/code_review_local_G08_2.log b/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/code_review_local_G08_2.log new file mode 100644 index 0000000..d39ba0d --- /dev/null +++ b/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/code_review_local_G08_2.log @@ -0,0 +1,188 @@ + + +# Code Review Reference - REVIEW_REVIEW_FAILURE_SURFACE + +> **[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, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-13 +task=m-openai-workspace-agent-execution-contract/05_failure_surface, plan=2, tag=REVIEW_REVIEW_FAILURE_SURFACE + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md` +- Task ids: + - `failure-surface`: workspace 누락, 존재하지 않는 경로, 권한 오류, agent process exit failure가 호출자에게 구분 가능한 실패로 드러난다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-local-G08.md` -> `code_review_local_G08_N.log`, `PLAN-local-G08.md` -> `plan_local_G08_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-openai-workspace-agent-execution-contract/05_failure_surface/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다. +4. PASS이고 task group이 `m-openai-workspace-agent-execution-contract`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REVIEW_REVIEW_FAILURE_SURFACE-1] codex-app-server/opencode-sse bad workspace tests가 실제 specialized executor를 타도록 `Mode`와 assertion을 보강한다. | [x] | + +## 구현 체크리스트 + +- [x] [REVIEW_REVIEW_FAILURE_SURFACE-1] codex-app-server/opencode-sse bad workspace tests가 실제 specialized executor를 타도록 `Mode`와 assertion을 보강한다. +- [x] `go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai`와 `go test -count=1 ./apps/node/... ./apps/edge/...`를 실행한다. +- [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_local_G08_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_local_G08_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [x] PASS이면 active task 디렉터리 `agent-task/m-openai-workspace-agent-execution-contract/05_failure_surface/`를 `agent-task/archive/YYYY/MM/m-openai-workspace-agent-execution-contract/05_failure_surface/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [x] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-openai-workspace-agent-execution-contract/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G08.md`와 `CODE_REVIEW-local-G08.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +- 없음. 계획대로 정확히 수정 및 검증을 완료하였습니다. + +## 주요 설계 결정 + +- `apps/node/internal/adapters/cli/cli_internal_test.go` 내부 테스트 코드 중 `ExecuteCodexAppServer` 및 `ExecuteOpencodeSSE` 서브테스트 프로필에 각각 `Mode: modeCodexAppServer`와 `Mode: modeOpencodeSSE` 속성을 명시적으로 부여하여, 기존의 persistentExecutor가 아닌 올바른 specialized executor인 `codexAppServerExecutor`와 `opencodeExecutor`를 제대로 경유하여 실행되도록 수정하였습니다. +- 두 테스트의 error assertion에서도 단순히 `workspace not found`뿐만 아니라 해당 specialized executor들의 래핑 에러 문구(`start codex app-server` 및 `start opencode server`)도 같이 캡처하여 검증하도록 assertion 로직을 대폭 보강했습니다. + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- `codex-app-server` bad workspace test profile이 `modeCodexAppServer`를 사용해 `codexAppServerExecutor`를 실제로 타는지 확인한다. +- `opencode-sse` bad workspace test profile이 `modeOpencodeSSE`를 사용해 `opencodeExecutor`를 실제로 타는지 확인한다. +- returned error와 `EventTypeError.Error`가 specialized wrapper와 `workspace not found`를 모두 포함하는지 확인한다. +- 외부 `codex`/`opencode` binary가 없어도 nonexistent workspace preflight 단계에서 deterministic하게 실패하는지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. +- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다. + +### REVIEW_REVIEW_FAILURE_SURFACE-1 중간 검증 +```bash +$ go test -count=1 ./apps/node/internal/adapters/cli +ok iop/apps/node/internal/adapters/cli 46.690s +``` + +### 최종 검증 +```bash +$ go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai +ok iop/apps/node/internal/adapters/cli 46.639s +ok iop/apps/edge/internal/openai 1.508s + +$ go test -count=1 ./apps/node/... ./apps/edge/... +ok iop/apps/node/cmd/node 0.015s +ok iop/apps/node/internal/adapters 0.016s +ok iop/apps/node/internal/adapters/cli 46.684s +? iop/apps/node/internal/adapters/cli/internal/testutil [no test files] +ok iop/apps/node/internal/adapters/cli/status 39.744s +? iop/apps/node/internal/adapters/mock [no test files] +ok iop/apps/node/internal/adapters/ollama 0.010s +ok iop/apps/node/internal/adapters/vllm 0.008s +ok iop/apps/node/internal/bootstrap 0.268s +ok iop/apps/node/internal/node 0.120s +ok iop/apps/node/internal/router 0.004s +? iop/apps/node/internal/runtime [no test files] +ok iop/apps/node/internal/store 0.054s +ok iop/apps/node/internal/terminal 0.568s +ok iop/apps/node/internal/transport 5.129s +ok iop/apps/edge/cmd/edge 0.042s +ok iop/apps/edge/internal/bootstrap 0.018s +ok iop/apps/edge/internal/controlplane 4.445s +ok iop/apps/edge/internal/edgecmd 0.006s +ok iop/apps/edge/internal/events 0.002s +ok iop/apps/edge/internal/input 0.005s +ok iop/apps/edge/internal/input/a2a 0.006s +ok iop/apps/edge/internal/node 0.005s +ok iop/apps/edge/internal/openai 1.508s +ok iop/apps/edge/internal/opsconsole 0.008s +ok iop/apps/edge/internal/service 0.005s +ok iop/apps/edge/internal/transport 2.012s + +$ git diff --check +(No output, exited with 0) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## 섹션 소유권 + +| Section | Owner | Note | +|---------|-------|------| +| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these. | +| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify; code-review copies it into `complete.log` only on PASS. | +| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only. | +| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only; final checkbox is mandatory before saving. | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section. | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content. | +| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required to proceed. | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | 계획에서 추출한 리뷰 포인트 | +| 검증 결과 | Implementing agent | Fill command output only; command changes require `계획 대비 변경 사항`. | +| 코드리뷰 결과 | Review agent appends | Not included in stub. | + +## 코드리뷰 결과 + +- 종합 판정: PASS +- 차원별 평가: + - correctness: Pass + - completeness: Pass + - test coverage: Pass + - API contract: Pass + - code quality: Pass + - plan deviation: Pass + - verification trust: Pass +- 발견된 문제: 없음 +- 다음 단계: PASS이므로 `complete.log`를 작성하고 task directory를 archive로 이동한다. diff --git a/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/complete.log b/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/complete.log new file mode 100644 index 0000000..cd3c915 --- /dev/null +++ b/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/complete.log @@ -0,0 +1,44 @@ +# Complete - m-openai-workspace-agent-execution-contract/05_failure_surface + +## 완료 일시 + +2026-06-13 + +## 요약 + +Node CLI workspace failure surface follow-up을 3회 리뷰 루프로 완료했다. 최종 판정은 PASS다. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_local_G06_0.log` | `code_review_local_G06_0.log` | FAIL | workspace preflight/session creation error가 caller-visible run error event로 전달되지 않았고, regression test가 run-stream error event를 검증하지 않았다. | +| `plan_local_G07_1.log` | `code_review_local_G07_1.log` | FAIL | codex-app-server/opencode-sse bad workspace tests가 `Mode`를 지정하지 않아 specialized executor를 타지 않았다. | +| `plan_local_G08_2.log` | `code_review_local_G08_2.log` | PASS | specialized mode routing과 wrapper assertion 보강이 완료되었고 검증이 통과했다. | + +## 구현/정리 내용 + +- CLI adapter workspace preflight helper를 추가하고 one-shot, persistent, codex app-server, opencode SSE launch path에 적용했다. +- pre-start returned error를 `RuntimeEvent{Type: error}`로 emit해 Edge/OpenAI caller가 workspace failure를 stream surface에서 구분할 수 있게 했다. +- workspace not found, not directory, inaccessible, process exit distinctness, specialized launch mode error-event regression을 보강했다. + +## 최종 검증 + +- `go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai` - PASS; `apps/node/internal/adapters/cli`와 `apps/edge/internal/openai` 모두 통과했다. +- `go test -count=1 ./apps/node/... ./apps/edge/...` - PASS; Node/Edge 전체 Go package 검증이 통과했다. +- `git diff --check` - PASS; 출력 없이 0 exit로 통과했다. + +## Roadmap Completion + +- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md` +- Completed task ids: + - `failure-surface`: PASS; evidence=`agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/plan_local_G08_2.log`, `agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/code_review_local_G08_2.log`; verification=`go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai`, `go test -count=1 ./apps/node/... ./apps/edge/...`, `git diff --check` +- Not completed task ids: 없음 + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/plan_local_G06_0.log b/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/plan_local_G06_0.log new file mode 100644 index 0000000..c05fa77 --- /dev/null +++ b/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/plan_local_G06_0.log @@ -0,0 +1,273 @@ + + +# Plan - FAILURE_SURFACE + +## 이 파일을 읽는 구현 에이전트에게 + +`CODE_REVIEW-local-G06.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 필수 마지막 단계다. 검증을 실행하고 실제 출력과 구현 메모를 기록한 뒤 active 파일을 그대로 두고 리뷰 준비 상태로 보고한다. 사용자 결정, 사용자 소유 외부 환경, 또는 범위 충돌 없이는 진행할 수 없을 때만 review stub의 `사용자 리뷰 요청` 섹션을 증거와 함께 채우고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 증거 공백은 사용자 리뷰 요청이 아니다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용이다. + +## 배경 + +`nomad-http-smoke`는 HTTP로 받은 workspace-bound agent 작업이 조용히 기본 cwd에서 실행되지 않는 것을 요구한다. Edge는 missing/relative workspace를 이미 400으로 거부하지만, Node CLI adapter의 실제 process launch는 존재하지 않는 경로, 파일 경로, 권한 오류를 launch 시점의 일반 OS 오류로만 노출한다. 실패 표면을 명시하면 다음 authoring smoke가 실패 원인을 구분할 수 있다. + +## 사용자 리뷰 요청 흐름 + +구현 중 blocker는 active `CODE_REVIEW-local-G06.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: + - `failure-surface`: workspace 누락, 존재하지 않는 경로, 권한 오류, agent process exit failure가 호출자에게 구분 가능한 실패로 드러난다. +- 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/update-roadmap/SKILL.md` +- `agent-ops/skills/common/agent-contract/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/platform-common/rules.md` +- `agent-ops/rules/project/domain/testing/rules.md` +- `agent-ops/skills/project/e2e-smoke/SKILL.md` +- `apps/edge/internal/openai/responses_handler.go` +- `apps/edge/internal/openai/chat_handler.go` +- `apps/edge/internal/openai/types.go` +- `apps/edge/internal/openai/server_test.go` +- `apps/node/internal/adapters/cli/cli.go` +- `apps/node/internal/adapters/cli/oneshot.go` +- `apps/node/internal/adapters/cli/persistent.go` +- `apps/node/internal/adapters/cli/codex_app_server.go` +- `apps/node/internal/adapters/cli/opencode_sse.go` +- `apps/node/internal/adapters/cli/cli_internal_test.go` +- `apps/edge/internal/service/service.go` +- `apps/edge/internal/service/run_dispatch.go` +- `apps/edge/internal/service/service_test.go` +- `configs/edge.yaml` + +### 테스트 환경 규칙 + +- test_env: `local`. +- `agent-test/local/rules.md`를 읽었다. local quick check는 현재 checkout에서 실행하고 Docker-in-Docker는 쓰지 않는다. +- matched profiles: `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`. +- 적용 명령: node 변경은 `go test ./apps/node/...`, edge OpenAI 경계 변경은 `go test ./apps/edge/...`와 OpenAI-compatible smoke 동등 확인, 실행 파이프라인 변경은 full-cycle 실제 구동 기준을 요구한다. +- 이 plan은 실패 preflight와 unit regression을 만든다. 실제 HTTP workspace authoring full-cycle은 sibling `06+05_authoring_http_smoke`에서 수행한다. +- `<확인 필요>` 값은 없었다. Go test cache가 실패를 숨기지 않도록 최종 명령은 `-count=1`을 쓴다. + +### 테스트 커버리지 공백 + +- missing/relative workspace의 HTTP 400은 `apps/edge/internal/openai/server_test.go:1184-1212`, `1245-1273`에서 이미 커버된다. +- absolute workspace 전달은 `apps/edge/internal/openai/server_test.go:1214-1229`, `1275-1290`에서 커버된다. +- CLI oneshot/persistent cwd 적용은 `apps/node/internal/adapters/cli/cli_internal_test.go:794-929`에서 커버된다. +- 존재하지 않는 workspace, workspace가 파일인 경우, permission denied preflight는 현재 직접 regression test가 없다. 이 plan에서 추가한다. +- agent process exit failure는 `executeCommand`의 기존 `cmd.Wait` error 경로를 유지하되 workspace preflight error와 문구가 섞이지 않는지 테스트로 구분한다. + +### 심볼 참조 + +- renamed/removed symbols: none. +- `workspaceDir`는 제거하지 않고 session key normalization 용도로 유지한다. +- 새 helper를 만들면 call site는 `apps/node/internal/adapters/cli/oneshot.go:26`, `persistent.go:357`, `persistent.go:380`, `codex_app_server.go:395`, `opencode_sse.go:290`다. + +### 분할 판단 + +- split decision policy를 plan 파일 선택 전에 평가했다. +- shared task group: `m-openai-workspace-agent-execution-contract`. +- sibling subtasks: + - `05_failure_surface`: 독립. Node CLI process launch failure 표면을 정리한다. + - `06+05_authoring_http_smoke`: `05_failure_surface`의 `complete.log` 이후 실행한다. 실제 HTTP smoke는 실패 표면 정리 뒤에 안정적으로 판정한다. + - `07_nomad_metadata_shape`: 독립. sibling repo `../nomadcode`의 호출 shape 정렬이다. +- 이 subtask는 predecessor가 없다. archive에는 같은 task group의 `01`~`04` 완료가 있으므로 새 번호는 `05`부터 이어간다. + +### 범위 결정 근거 + +- Edge HTTP missing/relative workspace validation은 이미 동작하므로 메시지나 status를 바꾸지 않는다. +- OpenAI route catalog, service proto, session isolation은 이미 완료된 milestone task이므로 수정하지 않는다. +- NomadCode metadata shape와 실제 HTTP authoring smoke는 각각 `07_nomad_metadata_shape`, `06+05_authoring_http_smoke`로 분리한다. +- external CLI 설치나 provider 계정이 필요한 검증은 이 plan 범위가 아니다. + +### 빌드 등급 + +- `local-G06`: Node CLI process launch에 닿지만 변경 파일과 call site가 작고, deterministic Go regression으로 검증 가능하다. + +## 구현 체크리스트 + +- [ ] [FAILURE_SURFACE-1] Node CLI adapter의 workspace preflight를 추가하고 모든 CLI launch path가 같은 오류 분류를 사용하게 한다. +- [ ] [FAILURE_SURFACE-2] workspace not found, not directory, inaccessible, process exit failure regression test를 추가하고 기존 cwd/session tests를 유지한다. +- [ ] `go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai`와 `go test -count=1 ./apps/node/... ./apps/edge/...`를 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 의존 관계 및 구현 순서 + +이 subtask는 runtime predecessor가 없다. 구현 순서는 helper 추가, call site 적용, regression test, 전체 검증 순서다. + +### [FAILURE_SURFACE-1] Workspace Preflight + +#### 문제 + +`apps/node/internal/adapters/cli/oneshot.go:24-28`와 persistent/server launch path는 workspace 문자열을 trim한 뒤 `cmd.Dir`에 넣는다. 존재하지 않는 경로와 파일 경로는 `cmd.Start` 또는 terminal start의 일반 오류로 드러나며, 어떤 launch path에서는 메시지가 서로 다르다. + +```go +// apps/node/internal/adapters/cli/oneshot.go:24-28 +func (c *CLI) executeCommand(ctx context.Context, spec runtime.ExecutionSpec, profile config.CLIProfileConf, args []string, prompt string, sink runtime.EventSink) (string, error) { + cmd := exec.CommandContext(ctx, profile.Command, args...) + if dir := workspaceDir(spec.Workspace); dir != "" { + cmd.Dir = dir + } +``` + +```go +// apps/node/internal/adapters/cli/persistent.go:350-381 +if profile.Terminal { + opts := terminal.Options{ + Command: profile.Command, + Args: profile.Args, + Env: profile.Env, + Rows: terminalRows, + Cols: terminalCols, + Dir: workspaceDir(workspace), + } + // ... +} else { + cmd = exec.Command(profile.Command, profile.Args...) + // ... + if dir := workspaceDir(workspace); dir != "" { + cmd.Dir = dir + } +``` + +#### 해결 방법 + +`apps/node/internal/adapters/cli/cli.go`에 `prepareWorkspaceDir` helper를 추가한다. blank workspace는 기존 no-workspace 동작을 위해 `""`를 반환한다. non-blank workspace는 `os.Stat`과 directory check를 거쳐 다음 문자열을 포함하는 error로 분류한다. + +```go +// apps/node/internal/adapters/cli/cli.go:493-495 +func workspaceDir(w string) string { + return strings.TrimSpace(w) +} +``` + +```go +// after +func prepareWorkspaceDir(w string) (string, error) { + dir := workspaceDir(w) + if dir == "" { + return "", nil + } + info, err := os.Stat(dir) + if err != nil { + if os.IsNotExist(err) { + return "", fmt.Errorf("cli adapter: workspace not found: %s", dir) + } + return "", fmt.Errorf("cli adapter: workspace inaccessible: %s: %w", dir, err) + } + if !info.IsDir() { + return "", fmt.Errorf("cli adapter: workspace is not a directory: %s", dir) + } + if f, err := os.Open(dir); err != nil { + return "", fmt.Errorf("cli adapter: workspace inaccessible: %s: %w", dir, err) + } else { + _ = f.Close() + } + return dir, nil +} +``` + +각 launch path는 `cmd.Dir` 또는 `terminal.Options.Dir` 설정 전에 helper error를 즉시 반환한다. `workspaceDir`는 `newSessionKey`에서 계속 사용한다. + +#### 수정 파일 및 체크리스트 + +- [ ] `apps/node/internal/adapters/cli/cli.go`: helper 추가, 필요한 import 확인. +- [ ] `apps/node/internal/adapters/cli/oneshot.go`: `executeCommand`에서 helper 사용. +- [ ] `apps/node/internal/adapters/cli/persistent.go`: terminal/non-terminal path에서 helper 사용. +- [ ] `apps/node/internal/adapters/cli/codex_app_server.go`: app-server process launch에서 helper 사용. +- [ ] `apps/node/internal/adapters/cli/opencode_sse.go`: opencode server launch에서 helper 사용. + +#### 테스트 작성 + +테스트 작성: yes. `apps/node/internal/adapters/cli/cli_internal_test.go`에 helper unit test를 추가한다. not found, file path, blank workspace를 검증한다. permission denied는 root/CI 권한에 따라 불안정할 수 있으므로 `os.Geteuid() == 0`이면 skip하는 조건부 test로만 둔다. + +#### 중간 검증 + +```bash +go test -count=1 ./apps/node/internal/adapters/cli +``` + +기대 결과: workspace preflight regression과 기존 cwd/session tests가 모두 통과한다. + +### [FAILURE_SURFACE-2] Failure Propagation Tests + +#### 문제 + +Edge는 `apps/edge/internal/openai/responses_handler.go:92-105`에서 submit/run error를 OpenAI-compatible error로 반환하지만, Node CLI adapter의 workspace 오류가 분류되지 않으면 호출자는 잘못된 workspace와 agent process failure를 안정적으로 구분하기 어렵다. + +```go +// apps/edge/internal/openai/responses_handler.go:92-105 +if err != nil { + writeError(w, http.StatusBadGateway, "node_dispatch_error", err.Error()) + return +} +defer handle.Close() + +s.completeResponse(w, r, req, handle, outputPolicy) +``` + +#### 해결 방법 + +`executeCommand` 기반 tests에서 workspace preflight error와 process exit error를 각각 만든다. process exit은 존재하는 workspace에서 `sh -lc 'exit 7'` 또는 동등한 profile을 실행해 `cli adapter: command ... exit` 계열 error가 workspace preflight와 다른지 확인한다. Edge HTTP tests는 기존 missing/relative coverage를 유지하고, 추가 변경이 없으면 새 Edge test는 만들지 않는다. + +#### 수정 파일 및 체크리스트 + +- [ ] `apps/node/internal/adapters/cli/cli_internal_test.go`: `TestCLIWorkspacePreflightFailures` 추가. +- [ ] `apps/node/internal/adapters/cli/cli_internal_test.go`: `TestCLIProcessExitFailureRemainsDistinct` 추가 또는 기존 command failure test 확장. +- [ ] `apps/edge/internal/openai/server_test.go`: Edge error behavior를 바꾸지 않았다면 수정하지 않는다. + +#### 테스트 작성 + +테스트 작성: yes. 실패 표면 task는 bug fix 성격이므로 regression test가 필수다. + +#### 중간 검증 + +```bash +go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai +``` + +기대 결과: Node CLI adapter와 Edge OpenAI boundary tests가 모두 통과한다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `apps/node/internal/adapters/cli/cli.go` | FAILURE_SURFACE-1 | +| `apps/node/internal/adapters/cli/oneshot.go` | FAILURE_SURFACE-1 | +| `apps/node/internal/adapters/cli/persistent.go` | FAILURE_SURFACE-1 | +| `apps/node/internal/adapters/cli/codex_app_server.go` | FAILURE_SURFACE-1 | +| `apps/node/internal/adapters/cli/opencode_sse.go` | FAILURE_SURFACE-1 | +| `apps/node/internal/adapters/cli/cli_internal_test.go` | FAILURE_SURFACE-1, FAILURE_SURFACE-2 | +| `apps/edge/internal/openai/server_test.go` | FAILURE_SURFACE-2 only if Edge behavior changes | + +## 최종 검증 + +```bash +go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai +go test -count=1 ./apps/node/... ./apps/edge/... +git diff --check +``` + +기대 결과: 모든 명령이 0 exit. Go test cache output은 허용하지 않는다. `git diff --check` output은 없어야 한다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/plan_local_G07_1.log b/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/plan_local_G07_1.log new file mode 100644 index 0000000..8846598 --- /dev/null +++ b/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/plan_local_G07_1.log @@ -0,0 +1,151 @@ + + +# Plan - REVIEW_FAILURE_SURFACE + +## 이 파일을 읽는 구현 에이전트에게 + +`CODE_REVIEW-local-G07.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 필수 마지막 단계다. 검증을 실행하고 실제 출력과 구현 메모를 기록한 뒤 active 파일을 그대로 두고 리뷰 준비 상태로 보고한다. 사용자 결정, 사용자 소유 외부 환경, 또는 범위 충돌 없이는 진행할 수 없을 때만 review stub의 `사용자 리뷰 요청` 섹션에 증거와 재개 조건을 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 증거 공백은 사용자 리뷰 요청이 아니다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용이다. + +## 배경 + +첫 리뷰에서 workspace preflight helper 자체와 직접 반환 error는 추가되었지만, foreground Node 실행에서 caller가 소비하는 표면은 `adapter.Execute`의 반환값이 아니라 `RuntimeEvent{Type: error}` 스트림이다. `apps/node/internal/transport/session.go`는 `OnRunRequest` error를 로그만 남기므로, preflight/session 생성 실패가 error event 없이 반환되면 OpenAI-compatible caller는 `workspace not found` 같은 분류를 받지 못하고 timeout/stream close로 보일 수 있다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md` +- Task ids: + - `failure-surface`: workspace 누락, 존재하지 않는 경로, 권한 오류, agent process exit failure가 호출자에게 구분 가능한 실패로 드러난다. +- Completion mode: check-on-pass + +## 리뷰 근거 + +- Archived plan: `agent-task/m-openai-workspace-agent-execution-contract/05_failure_surface/plan_local_G06_0.log` +- Archived review: `agent-task/m-openai-workspace-agent-execution-contract/05_failure_surface/code_review_local_G06_0.log` +- Required issue 1: pre-start workspace/session errors return before emitting a run error event. +- Required issue 2: regression tests assert direct returned errors but not the run-stream error event, and process-exit status is logged instead of asserted. + +## 범위 결정 근거 + +- Edge HTTP missing/relative workspace validation은 이미 기존 테스트가 커버하므로 status/message를 바꾸지 않는다. +- Node transport protocol이나 Edge event bus 구조는 바꾸지 않는다. CLI adapter가 pre-start 실패를 runtime error event로 내보내게 만들어 기존 stream 수집 경로에 맞춘다. +- NomadCode metadata shape, authoring HTTP smoke, roadmap 문서 갱신은 이 follow-up 범위가 아니다. + +## 빌드 등급 + +- `local-G07`: Node CLI adapter와 Edge/OpenAI caller surface 사이의 이벤트 계약을 다루지만, 수정 범위는 CLI adapter error propagation과 deterministic Go regression으로 제한된다. + +## 구현 체크리스트 + +- [ ] [REVIEW_FAILURE_SURFACE-1] 모든 CLI launch mode의 pre-start workspace/session 오류가 반환되기 전에 `RuntimeEvent{Type: error}`로 전달되게 한다. +- [ ] [REVIEW_FAILURE_SURFACE-2] workspace preflight와 process exit regression test가 run-stream error event와 exit status/code를 강하게 검증하도록 보강한다. +- [ ] `go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai`와 `go test -count=1 ./apps/node/... ./apps/edge/...`를 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 의존 관계 및 구현 순서 + +이 follow-up은 같은 split subtask의 후속 루프이며 새 runtime predecessor는 없다. 구현 순서는 error-event helper 정리, launch mode 적용, regression test 보강, 검증 실행 순서다. + +### [REVIEW_FAILURE_SURFACE-1] Preflight Error Event Propagation + +#### 문제 + +`prepareWorkspaceDir` 오류는 `executeCommand`, `startProfileSession`, `startCodexAppServerProc`, `startOpencodeLocalServer`에서 process start 전에 반환된다. 그런데 `apps/node/internal/transport/session.go:45`는 `OnRunRequest` 반환 error를 Edge로 보내지 않고 로그만 남긴다. 따라서 OpenAI-compatible caller가 기다리는 `collectRunResult` 경로에는 workspace 분류가 도달하지 않는다. + +#### 해결 방법 + +CLI adapter 실행 진입점에서 pre-start 오류를 반환하기 전에 `sink.Emit(ctx, runtime.RuntimeEvent{RunID: spec.RunID, Type: runtime.EventTypeError, Error: err.Error(), Timestamp: time.Now()})`를 보낸다. 중복 이벤트를 피하기 위해 이미 process 실행 뒤 자체 error event를 내보내는 command failure/read failure 경로는 그대로 둔다. + +권장 구조: + +```go +func emitReturnedError(ctx context.Context, sink runtime.EventSink, runID string, err error) error { + if err == nil { + return nil + } + _ = sink.Emit(ctx, runtime.RuntimeEvent{ + RunID: runID, + Type: runtime.EventTypeError, + Error: err.Error(), + Timestamp: time.Now(), + }) + return err +} +``` + +적용 지점: + +- `apps/node/internal/adapters/cli/oneshot.go`: `prepareWorkspaceDir` 실패. +- `apps/node/internal/adapters/cli/persistent.go`: `resolveSession` 실패 중 workspace preflight/session creation 오류. +- `apps/node/internal/adapters/cli/codex_app_server.go`: `resolveCodexAppServerSession` 실패 중 app-server start/preflight 오류. +- `apps/node/internal/adapters/cli/opencode_sse.go`: `resolveOpencodeSession` 실패 중 local server start/preflight 오류. + +#### 수정 파일 및 체크리스트 + +- [ ] `apps/node/internal/adapters/cli/cli.go` 또는 적절한 CLI package 파일에 반환 error event helper를 추가한다. +- [ ] `apps/node/internal/adapters/cli/oneshot.go`에서 workspace preflight error를 event로 보낸다. +- [ ] `apps/node/internal/adapters/cli/persistent.go`에서 pre-start resolve/session error를 event로 보낸다. +- [ ] `apps/node/internal/adapters/cli/codex_app_server.go`에서 app-server pre-start error를 event로 보낸다. +- [ ] `apps/node/internal/adapters/cli/opencode_sse.go`에서 opencode local server pre-start error를 event로 보낸다. + +#### 테스트 작성 + +테스트 작성: yes. `apps/node/internal/adapters/cli/cli_internal_test.go` 또는 mode별 기존 internal test에 bad workspace 실행이 `EventTypeError`를 남기는지 확인한다. + +#### 중간 검증 + +```bash +go test -count=1 ./apps/node/internal/adapters/cli +``` + +기대 결과: bad workspace direct error와 sink error event가 모두 확인되고 기존 cwd/session tests가 통과한다. + +### [REVIEW_FAILURE_SURFACE-2] Regression Strengthening + +#### 문제 + +현재 `TestCLIWorkspacePreflightFailures`는 직접 반환 error만 확인한다. `TestCLIProcessExitFailureRemainsDistinct`는 exit status/code가 없어도 `t.Logf`만 남기므로, caller가 process exit failure를 구분하는 계약을 회귀 테스트로 강제하지 못한다. + +#### 해결 방법 + +workspace preflight tests는 sink events 안에서 `EventTypeError`와 workspace 분류 문구를 확인한다. specialized launch mode는 외부 CLI를 실제 시작하지 않도록 존재하지 않는 workspace를 사용해 preflight 단계에서 종료되게 구성한다. process-exit test는 `exit status 7` 또는 `exit code 7`이 없으면 실패해야 한다. + +#### 수정 파일 및 체크리스트 + +- [ ] `apps/node/internal/adapters/cli/cli_internal_test.go`: one-shot/persistent bad workspace 실행에서 returned error와 sink `EventTypeError`를 함께 검증한다. +- [ ] `apps/node/internal/adapters/cli/cli_internal_test.go` 또는 mode별 internal test: `codex-app-server`, `opencode-sse` bad workspace preflight도 error event를 검증한다. +- [ ] `apps/node/internal/adapters/cli/cli_internal_test.go`: process exit failure가 workspace error가 아니고 exit status/code를 포함하지 않으면 테스트 실패로 처리한다. + +#### 테스트 작성 + +테스트 작성: yes. 이번 follow-up의 핵심은 기존 unit pass가 놓친 caller-visible event surface를 고정하는 것이다. + +#### 중간 검증 + +```bash +go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai +``` + +기대 결과: CLI adapter regression과 Edge OpenAI boundary tests가 모두 통과한다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `apps/node/internal/adapters/cli/cli.go` | REVIEW_FAILURE_SURFACE-1 | +| `apps/node/internal/adapters/cli/oneshot.go` | REVIEW_FAILURE_SURFACE-1 | +| `apps/node/internal/adapters/cli/persistent.go` | REVIEW_FAILURE_SURFACE-1 | +| `apps/node/internal/adapters/cli/codex_app_server.go` | REVIEW_FAILURE_SURFACE-1 | +| `apps/node/internal/adapters/cli/opencode_sse.go` | REVIEW_FAILURE_SURFACE-1 | +| `apps/node/internal/adapters/cli/cli_internal_test.go` | REVIEW_FAILURE_SURFACE-2 | + +## 최종 검증 + +```bash +go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai +go test -count=1 ./apps/node/... ./apps/edge/... +git diff --check +``` + +기대 결과: 모든 명령이 0 exit. Go test cache output은 허용하지 않는다. `git diff --check` output은 없어야 한다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/plan_local_G08_2.log b/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/plan_local_G08_2.log new file mode 100644 index 0000000..a385901 --- /dev/null +++ b/agent-task/archive/2026/06/m-openai-workspace-agent-execution-contract/05_failure_surface/plan_local_G08_2.log @@ -0,0 +1,115 @@ + + +# Plan - REVIEW_REVIEW_FAILURE_SURFACE + +## 이 파일을 읽는 구현 에이전트에게 + +`CODE_REVIEW-local-G08.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 필수 마지막 단계다. 검증을 실행하고 실제 출력과 구현 메모를 기록한 뒤 active 파일을 그대로 두고 리뷰 준비 상태로 보고한다. 사용자 결정, 사용자 소유 외부 환경, 또는 범위 충돌 없이는 진행할 수 없을 때만 review stub의 `사용자 리뷰 요청` 섹션에 증거와 재개 조건을 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 증거 공백은 사용자 리뷰 요청이 아니다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용이다. + +## 배경 + +G07 구현은 pre-start error를 `RuntimeEvent{Type: error}`로 emit하도록 코드를 보강했다. 그러나 새 regression 중 `ExecuteCodexAppServer`와 `ExecuteOpencodeSSE`는 profile `Mode`를 지정하지 않아 실제 specialized executor가 아니라 기본 persistent executor를 타고 있다. 따라서 specialized launch path의 error-event propagation을 테스트로 잠그지 못한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md` +- Task ids: + - `failure-surface`: workspace 누락, 존재하지 않는 경로, 권한 오류, agent process exit failure가 호출자에게 구분 가능한 실패로 드러난다. +- Completion mode: check-on-pass + +## 리뷰 근거 + +- Archived plan: `agent-task/m-openai-workspace-agent-execution-contract/05_failure_surface/plan_local_G07_1.log` +- Archived review: `agent-task/m-openai-workspace-agent-execution-contract/05_failure_surface/code_review_local_G07_1.log` +- Required issue: `apps/node/internal/adapters/cli/cli_internal_test.go`의 codex/opencode workspace preflight tests가 `Mode` 없이 작성되어 `executorFor`에서 specialized executor로 라우팅되지 않는다. + +## 범위 결정 근거 + +- G07의 production code 변경은 계획한 error-event propagation 구조를 갖추고 있으므로 이 follow-up은 테스트 의미 보강에 집중한다. +- 외부 `codex`/`opencode` binary를 실제 실행하지 않는다. nonexistent workspace preflight가 command start 전에 실패하도록 유지한다. +- Edge HTTP missing/relative workspace validation, Node transport protocol, roadmap 문서는 수정하지 않는다. + +## 빌드 등급 + +- `local-G08`: 명시적이고 deterministic한 테스트 라우팅 보강이다. 변경 범위가 작고 로컬 Go test로 판정 가능하다. + +## 구현 체크리스트 + +- [ ] [REVIEW_REVIEW_FAILURE_SURFACE-1] codex-app-server/opencode-sse bad workspace tests가 실제 specialized executor를 타도록 `Mode`와 assertion을 보강한다. +- [ ] `go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai`와 `go test -count=1 ./apps/node/... ./apps/edge/...`를 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 의존 관계 및 구현 순서 + +이 follow-up은 같은 split subtask의 두 번째 후속 루프이며 새 runtime predecessor는 없다. 구현 순서는 test profile mode 수정, specialized wrapper assertion 추가, 검증 실행 순서다. + +### [REVIEW_REVIEW_FAILURE_SURFACE-1] Specialized Mode Test Routing + +#### 문제 + +`apps/node/internal/adapters/cli/cli_internal_test.go`의 `ExecuteCodexAppServer`와 `ExecuteOpencodeSSE` subtests는 `Persistent: true`와 args만 지정한다. `executorFor`는 args가 아니라 `profile.Mode`로 specialized executor를 선택하므로, 현재 테스트는 각각 `codexAppServerExecutor`와 `opencodeExecutor`를 타지 않는다. + +#### 해결 방법 + +각 profile에 mode를 명시한다. + +```go +"codex-app": { + Mode: modeCodexAppServer, + Command: "codex", + Persistent: true, + Args: []string{"--app-server"}, +} + +"opencode-sse-target": { + Mode: modeOpencodeSSE, + Command: "opencode", + Persistent: true, + Args: []string{"serve"}, +} +``` + +nonexistent workspace를 유지하면 helper가 command start 전에 실패하므로 외부 CLI 설치 여부와 무관하게 deterministic하다. + +Assertion은 단순 `workspace not found`뿐 아니라 specialized wrapper도 확인한다. + +- codex: `start codex app-server`와 `workspace not found` +- opencode: `start opencode server`와 `workspace not found` + +returned error와 sink `EventTypeError` 모두 같은 조건을 만족해야 한다. + +#### 수정 파일 및 체크리스트 + +- [ ] `apps/node/internal/adapters/cli/cli_internal_test.go`: codex-app-server test profile에 `Mode: modeCodexAppServer`를 추가한다. +- [ ] `apps/node/internal/adapters/cli/cli_internal_test.go`: opencode-sse test profile에 `Mode: modeOpencodeSSE`를 추가한다. +- [ ] `apps/node/internal/adapters/cli/cli_internal_test.go`: returned error와 `EventTypeError.Error`가 specialized wrapper와 workspace classification을 모두 포함하는지 검증한다. + +#### 테스트 작성 + +테스트 작성: yes. 이번 follow-up 자체가 테스트 의미 보강이다. + +#### 중간 검증 + +```bash +go test -count=1 ./apps/node/internal/adapters/cli +``` + +기대 결과: bad workspace tests가 specialized mode에서도 외부 binary 실행 없이 preflight error event를 검증하고, 기존 cwd/session/process-exit tests가 통과한다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `apps/node/internal/adapters/cli/cli_internal_test.go` | REVIEW_REVIEW_FAILURE_SURFACE-1 | + +## 최종 검증 + +```bash +go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai +go test -count=1 ./apps/node/... ./apps/edge/... +git diff --check +``` + +기대 결과: 모든 명령이 0 exit. Go test cache output은 허용하지 않는다. `git diff --check` output은 없어야 한다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-openai-workspace-agent-execution-contract/06+05_authoring_http_smoke/CODE_REVIEW-cloud-G07.md b/agent-task/m-openai-workspace-agent-execution-contract/06+05_authoring_http_smoke/CODE_REVIEW-cloud-G07.md new file mode 100644 index 0000000..c1c7ff3 --- /dev/null +++ b/agent-task/m-openai-workspace-agent-execution-contract/06+05_authoring_http_smoke/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,157 @@ + + +# Code Review Reference - AUTHORING_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, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-13 +task=m-openai-workspace-agent-execution-contract/06+05_authoring_http_smoke, plan=0, tag=AUTHORING_SMOKE + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md` +- Task ids: + - `workspace-authoring-smoke`: `metadata.workspace`가 가리키는 임시 checkout에서 workspace-bound agent target이 파일을 생성/수정할 수 있음을 검증한다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +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 디렉터리를 `agent-task/archive/YYYY/MM/m-openai-workspace-agent-execution-contract/06+05_authoring_http_smoke/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다. +4. PASS이고 task group이 `m-openai-workspace-agent-execution-contract`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [AUTHORING_SMOKE-1] `iop-edge smoke openai`에 workspace-aware request 옵션과 request-body regression test를 추가한다. | [ ] | +| [AUTHORING_SMOKE-2] deterministic CLI workspace authoring e2e smoke를 추가해 marker가 workspace 안에만 생성되는지 검증한다. | [ ] | + +## 구현 체크리스트 + +- [ ] [AUTHORING_SMOKE-1] `iop-edge smoke openai`에 workspace-aware request 옵션과 request-body regression test를 추가한다. +- [ ] [AUTHORING_SMOKE-2] deterministic CLI workspace authoring e2e smoke를 추가해 marker가 workspace 안에만 생성되는지 검증한다. +- [ ] `go test -count=1 ./apps/edge/cmd/edge ./apps/edge/internal/openai ./apps/node/internal/adapters/cli`와 새 e2e smoke script를 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다. +- [ ] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다. +- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 active task 디렉터리 `agent-task/m-openai-workspace-agent-execution-contract/06+05_authoring_http_smoke/`를 `agent-task/archive/YYYY/MM/m-openai-workspace-agent-execution-contract/06+05_authoring_http_smoke/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-openai-workspace-agent-execution-contract/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- `--workspace`가 flat `metadata.workspace`로만 전송되고 prompt 본문이나 `metadata.cli`로 우회하지 않는지 확인한다. +- `--expect-file` 검증이 workspace 밖 path traversal을 허용하지 않는지 확인한다. +- 새 e2e script가 deterministic CLI profile을 쓰며 외부 provider 계정에 의존하지 않는지 확인한다. +- marker가 workspace 안에만 생성되고 repo root/temp parent에는 남지 않는지 검증 출력과 script를 대조한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. +- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다. + +### AUTHORING_SMOKE-1 중간 검증 +```bash +$ go test -count=1 ./apps/edge/cmd/edge +(output) +``` + +### AUTHORING_SMOKE-2 중간 검증 +```bash +$ ./scripts/e2e-openai-cli-workspace.sh +(output) +``` + +### 최종 검증 +```bash +$ go test -count=1 ./apps/edge/cmd/edge ./apps/edge/internal/openai ./apps/node/internal/adapters/cli +(output) + +$ ./scripts/e2e-openai-cli-workspace.sh +(output) + +$ ./scripts/e2e-openai-ollama.sh +(output) + +$ git diff --check +(output) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## 섹션 소유권 + +| Section | Owner | Note | +|---------|-------|------| +| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these. | +| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify; code-review copies it into `complete.log` only on PASS. | +| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only. | +| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only; final checkbox is mandatory before saving. | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section. | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content. | +| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required to proceed. | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Implementing agent must not modify. | +| 검증 결과 | Implementing agent | Fill command output only; command changes require `계획 대비 변경 사항`. | +| 코드리뷰 결과 | Review agent appends | Not included in stub. | diff --git a/agent-task/m-openai-workspace-agent-execution-contract/06+05_authoring_http_smoke/PLAN-cloud-G07.md b/agent-task/m-openai-workspace-agent-execution-contract/06+05_authoring_http_smoke/PLAN-cloud-G07.md new file mode 100644 index 0000000..29310bc --- /dev/null +++ b/agent-task/m-openai-workspace-agent-execution-contract/06+05_authoring_http_smoke/PLAN-cloud-G07.md @@ -0,0 +1,248 @@ + + +# Plan - AUTHORING_SMOKE + +## 이 파일을 읽는 구현 에이전트에게 + +`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 필수 마지막 단계다. 검증을 실행하고 실제 출력과 구현 메모를 기록한 뒤 active 파일을 그대로 두고 리뷰 준비 상태로 보고한다. 사용자 결정, 사용자 소유 외부 환경, 또는 범위 충돌 없이는 진행할 수 없을 때만 review stub의 `사용자 리뷰 요청` 섹션을 증거와 함께 채우고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 증거 공백은 사용자 리뷰 요청이 아니다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용이다. + +## 배경 + +`nomad-http-smoke`의 핵심은 NomadCode가 IOP Edge HTTP 표면만 호출해 workspace checkout에 파일 변경을 남길 수 있는지 확인하는 것이다. 현재 `iop-edge smoke openai`는 `metadata.task_id`와 `metadata.source`만 보내고 workspace를 넣지 않는다. 기존 `scripts/e2e-openai-ollama.sh`도 Ollama route만 검증하므로 CLI workspace-bound authoring route를 별도 deterministic smoke로 증명해야 한다. + +## 사용자 리뷰 요청 흐름 + +구현 중 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: + - `workspace-authoring-smoke`: `metadata.workspace`가 가리키는 임시 checkout에서 workspace-bound agent target이 파일을 생성/수정할 수 있음을 검증한다. +- 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-ops/skills/project/e2e-smoke/SKILL.md` +- `apps/edge/internal/edgecmd/smoke_openai.go` +- `apps/edge/cmd/edge/main_test.go` +- `apps/edge/internal/openai/responses_handler.go` +- `apps/edge/internal/openai/server_test.go` +- `apps/edge/internal/service/service.go` +- `apps/edge/internal/service/run_dispatch.go` +- `apps/node/internal/adapters/cli/cli.go` +- `apps/node/internal/adapters/cli/oneshot.go` +- `apps/node/internal/adapters/cli/persistent.go` +- `apps/node/internal/adapters/cli/cli_internal_test.go` +- `scripts/e2e-openai-ollama.sh` +- `configs/edge.yaml` + +### 테스트 환경 규칙 + +- test_env: `local`. +- `agent-test/local/rules.md`를 읽었다. 현재 checkout의 quick check를 우선하고, Docker-in-Docker는 쓰지 않는다. +- matched profiles: `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`. +- 적용 명령: edge 변경은 `go test ./apps/edge/...`, node 변경은 `go test ./apps/node/...`, 테스트 도구 변경은 해당 도구 직접 실행, OpenAI-compatible 경계 변경은 `iop-edge smoke openai` 또는 동등한 `/healthz`, `/v1/models`, `/v1/responses` 확인. +- 이 plan은 새 deterministic CLI workspace HTTP smoke를 직접 실행한다. Go test cache가 실패를 숨기지 않도록 `-count=1`을 쓴다. +- `<확인 필요>` 값은 없었다. + +### 테스트 커버리지 공백 + +- `apps/edge/cmd/edge/main_test.go:1043-1087`은 smoke command success를 검증하지만 request body에 workspace가 들어가는지는 검증하지 않는다. +- `apps/edge/internal/edgecmd/smoke_openai.go:203-216`은 responses metadata에 workspace를 보내지 않는다. +- `scripts/e2e-openai-ollama.sh:203-207`은 Ollama route만 검증하고 workspace-bound CLI route를 만들지 않는다. +- `configs/edge.yaml:59-64`는 `workspace_required: true` 예시를 갖고 있지만 이를 실행하는 smoke가 없다. +- 이 plan에서 command-level body assertion과 실제 marker-file smoke를 추가한다. + +### 심볼 참조 + +- renamed/removed symbols: none. +- 추가 flag는 `smokeOpenAICmd` local vars와 Cobra flag binding에만 닿는다. + +### 분할 판단 + +- split decision policy를 plan 파일 선택 전에 평가했다. +- shared task group: `m-openai-workspace-agent-execution-contract`. +- sibling subtasks: + - `05_failure_surface`: predecessor. workspace 실패 표면을 먼저 정리한다. + - `06+05_authoring_http_smoke`: 이 plan. `05`의 `complete.log`가 필요하다. + - `07_nomad_metadata_shape`: 독립. sibling repo metadata producer 정렬이다. +- predecessor `05`: active `agent-task/m-openai-workspace-agent-execution-contract/05_failure_surface/complete.log` 없음, archive candidate 없음. 구현은 `05_failure_surface` PASS 후 시작해야 한다. + +### 범위 결정 근거 + +- 기존 Ollama smoke는 유지한다. CLI workspace smoke는 behavior와 fixture가 달라 기존 script를 과도하게 복잡하게 만들지 않는다. +- 실제 Codex/OpenCode provider 계정에 의존하지 않고 deterministic local `sh` CLI profile로 검증한다. +- NomadCode producer 변경은 `07_nomad_metadata_shape`에서 처리한다. +- failure error classification은 `05_failure_surface` 범위다. + +### 빌드 등급 + +- `cloud-G07`: shell/CLI workflow, bin script orchestration, process control, HTTP smoke evidence가 중심이며 실제 runtime 증거 신뢰가 중요하다. + +## 구현 체크리스트 + +- [ ] [AUTHORING_SMOKE-1] `iop-edge smoke openai`에 workspace-aware request 옵션과 request-body regression test를 추가한다. +- [ ] [AUTHORING_SMOKE-2] deterministic CLI workspace authoring e2e smoke를 추가해 marker가 workspace 안에만 생성되는지 검증한다. +- [ ] `go test -count=1 ./apps/edge/cmd/edge ./apps/edge/internal/openai ./apps/node/internal/adapters/cli`와 새 e2e smoke script를 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 의존 관계 및 구현 순서 + +이 subtask directory의 `06+05`가 runtime source of truth다. 구현 전에 `agent-task/m-openai-workspace-agent-execution-contract/05_failure_surface/complete.log` 또는 matching archive `05_*`/`05+*` `complete.log`가 있어야 한다. 현재 plan 작성 시점에는 없다. + +### [AUTHORING_SMOKE-1] Smoke Command Workspace Metadata + +#### 문제 + +`iop-edge smoke openai`는 responses payload에 task/source metadata는 넣지만 workspace는 넣지 않는다. + +```go +// apps/edge/internal/edgecmd/smoke_openai.go:34-39 +var ( + smokeModel string + smokeBaseURL string + smokePrompt string + smokeTimeout string +) +``` + +```go +// apps/edge/internal/edgecmd/smoke_openai.go:203-216 +responsesPayload := map[string]interface{}{ + "model": smokeModel, + "input": prompt, + "stream": false, + "metadata": map[string]interface{}{ + "request_id": "iop-edge-smoke", + "task_id": "smoke", + "source": "iop-edge-smoke", + "inference": map[string]string{ + "target": smokeModel, + }, + }, +} +``` + +#### 해결 방법 + +`--workspace` flag를 추가한다. 값이 비어 있으면 기존 payload를 유지하고, 값이 있으면 `metadata.workspace`를 flat string으로 넣는다. 추가로 `--expect-file`과 `--expect-contains`를 넣어 command가 smoke 후 workspace 안 파일을 검사할 수 있게 한다. + +```go +// after +if smokeWorkspace != "" { + metadata["workspace"] = smokeWorkspace +} +// after successful /v1/responses +if smokeExpectFile != "" { + path := filepath.Join(smokeWorkspace, smokeExpectFile) + // workspace 안 상대 경로만 허용하고 파일 내용 검사 +} +``` + +`apps/edge/cmd/edge/main_test.go`의 httptest server에서 `/v1/responses` body를 decode해 workspace가 flag 값과 같은지 확인한다. + +#### 수정 파일 및 체크리스트 + +- [ ] `apps/edge/internal/edgecmd/smoke_openai.go`: `--workspace`, `--expect-file`, `--expect-contains` flag와 metadata/file 검증 추가. +- [ ] `apps/edge/cmd/edge/main_test.go`: request body assertion test 추가. + +#### 테스트 작성 + +테스트 작성: yes. `TestSmokeOpenAICommandSuccess`를 확장하거나 새 test를 추가해 workspace flag가 `metadata.workspace` flat string으로 전송되는지 검증한다. `--expect-file`은 temp workspace에 파일을 미리 만들거나 httptest 응답 후 검사하는 방식으로 정상/실패 boundary를 검증한다. + +#### 중간 검증 + +```bash +go test -count=1 ./apps/edge/cmd/edge +``` + +기대 결과: smoke command tests가 모두 통과한다. + +### [AUTHORING_SMOKE-2] CLI Workspace E2E Script + +#### 문제 + +`scripts/e2e-openai-ollama.sh:130-136`은 model route를 Ollama adapter로만 구성한다. `scripts/e2e-openai-ollama.sh:221-225`의 command smoke도 workspace를 넘기지 않는다. + +```bash +# scripts/e2e-openai-ollama.sh:130-136 + model_routes: + - model: "client-request-model" + adapter: "ollama" + target: "$MODEL" + - model: "$MODEL" + adapter: "ollama" + target: "$MODEL" +``` + +#### 해결 방법 + +새 script `scripts/e2e-openai-cli-workspace.sh`를 추가한다. 기존 Ollama script는 그대로 두고, temp workspace와 temp Edge/Node config를 만든 뒤 CLI profile을 deterministic `sh -lc` command로 구성한다. `/v1/responses` 또는 `iop-edge smoke openai --workspace`를 호출하면 profile이 cwd에 marker file을 쓰고 stdout에 token을 출력해야 한다. + +검증 조건: + +- `metadata.workspace`는 absolute temp workspace path다. +- route는 `workspace_required: true`인 CLI adapter route다. +- marker file은 workspace 안에 생성된다. +- repo root와 temp parent에는 marker가 없어야 한다. +- `git -C "$WORKSPACE" status --short` 또는 marker content로 변경을 확인한다. + +#### 수정 파일 및 체크리스트 + +- [ ] `scripts/e2e-openai-cli-workspace.sh`: 새 deterministic smoke script 추가. +- [ ] `scripts/e2e-openai-ollama.sh`: 수정하지 않는다. 중복 공통화가 명확히 작아지는 경우에만 helper 추출을 고려한다. +- [ ] `configs/edge.yaml`: 기본 예시가 충분하면 수정하지 않는다. + +#### 테스트 작성 + +테스트 작성: yes. 이 item 자체가 smoke test 추가다. 별도 Go unit은 AUTHORING_SMOKE-1에서 command payload를 커버한다. + +#### 중간 검증 + +```bash +./scripts/e2e-openai-cli-workspace.sh +``` + +기대 결과: `/healthz`, `/v1/models`, `/v1/responses` 또는 `iop-edge smoke openai`가 통과하고 marker file이 workspace 안에만 있다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `apps/edge/internal/edgecmd/smoke_openai.go` | AUTHORING_SMOKE-1 | +| `apps/edge/cmd/edge/main_test.go` | AUTHORING_SMOKE-1 | +| `scripts/e2e-openai-cli-workspace.sh` | AUTHORING_SMOKE-2 | +| `scripts/e2e-openai-ollama.sh` | 읽기/회귀 대상, 수정 금지 기본값 | +| `configs/edge.yaml` | 예시 부족 발견 시만 AUTHORING_SMOKE-2 | + +## 최종 검증 + +```bash +go test -count=1 ./apps/edge/cmd/edge ./apps/edge/internal/openai ./apps/node/internal/adapters/cli +./scripts/e2e-openai-cli-workspace.sh +./scripts/e2e-openai-ollama.sh +git diff --check +``` + +기대 결과: 모든 명령이 0 exit. Go test cache output은 허용하지 않는다. `git diff --check` output은 없어야 한다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-openai-workspace-agent-execution-contract/07_nomad_metadata_shape/CODE_REVIEW-cloud-G07.md b/agent-task/m-openai-workspace-agent-execution-contract/07_nomad_metadata_shape/CODE_REVIEW-cloud-G07.md new file mode 100644 index 0000000..6361dc0 --- /dev/null +++ b/agent-task/m-openai-workspace-agent-execution-contract/07_nomad_metadata_shape/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,154 @@ + + +# Code Review Reference - NOMAD_METADATA + +> **[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, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-13 +task=m-openai-workspace-agent-execution-contract/07_nomad_metadata_shape, plan=0, tag=NOMAD_METADATA + +## 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 + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +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 디렉터리를 `agent-task/archive/YYYY/MM/m-openai-workspace-agent-execution-contract/07_nomad_metadata_shape/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다. +4. PASS이고 task group이 `m-openai-workspace-agent-execution-contract`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [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를 유지함을 테스트한다. | [ ] | + +## 구현 체크리스트 + +- [ ] [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의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다. +- [ ] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다. +- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 active task 디렉터리 `agent-task/m-openai-workspace-agent-execution-contract/07_nomad_metadata_shape/`를 `agent-task/archive/YYYY/MM/m-openai-workspace-agent-execution-contract/07_nomad_metadata_shape/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-openai-workspace-agent-execution-contract/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- NomadCode가 `metadata.workspace`를 object가 아니라 flat string path로 보내는지 확인한다. +- `metadata.task_id`와 `metadata.source`가 보존되며 IOP 계약에 없는 provider/work item fields를 root metadata에 임의 추가하지 않았는지 확인한다. +- IOP test가 old object workspace shape를 400으로 고정하는지 확인한다. +- IOP repo와 `../nomadcode/services/core` 검증이 모두 실제 출력으로 기록됐는지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. +- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다. + +### NOMAD_METADATA-1 중간 검증 +```bash +$ cd ../nomadcode/services/core && go test -count=1 ./internal/adapters/openai ./internal/authoring ./internal/scheduler +(output) +``` + +### NOMAD_METADATA-2 중간 검증 +```bash +$ go test -count=1 ./apps/edge/internal/openai ./apps/edge/cmd/edge +(output) +``` + +### 최종 검증 +```bash +$ cd ../nomadcode/services/core && go test -count=1 ./... +(output) + +$ go test -count=1 ./apps/edge/internal/openai ./apps/edge/cmd/edge +(output) + +$ git diff --check +(output) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## 섹션 소유권 + +| Section | Owner | Note | +|---------|-------|------| +| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these. | +| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify; code-review copies it into `complete.log` only on PASS. | +| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only. | +| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only; final checkbox is mandatory before saving. | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section. | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content. | +| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required to proceed. | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Implementing agent must not modify. | +| 검증 결과 | Implementing agent | Fill command output only; command changes require `계획 대비 변경 사항`. | +| 코드리뷰 결과 | Review agent appends | Not included in stub. | diff --git a/agent-task/m-openai-workspace-agent-execution-contract/07_nomad_metadata_shape/PLAN-cloud-G07.md b/agent-task/m-openai-workspace-agent-execution-contract/07_nomad_metadata_shape/PLAN-cloud-G07.md new file mode 100644 index 0000000..ace4dfa --- /dev/null +++ b/agent-task/m-openai-workspace-agent-execution-contract/07_nomad_metadata_shape/PLAN-cloud-G07.md @@ -0,0 +1,240 @@ + + +# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/apps/edge/README.md b/apps/edge/README.md index 52ae46a..81f49e3 100644 --- a/apps/edge/README.md +++ b/apps/edge/README.md @@ -167,7 +167,7 @@ Edge 외부 입력은 OpenAI-compatible HTTP API와 A2A JSON-RPC HTTP API 두 이 표면은 외부 모델 클라이언트 호환을 위한 표준 경로다. Edge/Node 운영 제어, CLI logical session, background run, cancel/terminate-session, capabilities/status/session/transport command, node lifecycle event 같은 IOP 고유 기능은 OpenAI-compatible 요청에 억지로 싣지 않고 IOP native protocol(protobuf-socket) 계열에서 다룬다. -외부 프로젝트가 참조할 요청 계약 원문은 repo root의 `agent-contract/provided/openai-compatible-api.md`에 둔다. CLI agent route에서 사용할 workspace는 OpenAI request의 `metadata.workspace`에 둔다. +외부 프로젝트가 참조할 요청 계약 원문은 repo root의 `agent-contract/provided/openai-compatible-api.md`에 둔다. CLI agent route에서 사용할 workspace는 OpenAI request의 `metadata.workspace`에 둔다. NomadCode authoring handoff의 요청 shape도 같은 계약 원문을 기준으로 한다. ```yaml openai: diff --git a/apps/edge/cmd/edge/main_test.go b/apps/edge/cmd/edge/main_test.go index 577409f..74291f1 100644 --- a/apps/edge/cmd/edge/main_test.go +++ b/apps/edge/cmd/edge/main_test.go @@ -2,6 +2,8 @@ package main import ( "bytes" + "encoding/json" + "io" "net/http" "net/http/httptest" "os" @@ -1086,6 +1088,99 @@ func TestSmokeOpenAICommandSuccess(t *testing.T) { } } +func TestSmokeOpenAICommandWorkspace(t *testing.T) { + // Create a temporary directory for workspace + tmpDir, err := os.MkdirTemp("", "iop-smoke-workspace-*") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create an expect file inside the workspace + expectFile := "test_marker.txt" + expectContent := "hello smoke test" + if err := os.WriteFile(filepath.Join(tmpDir, expectFile), []byte(expectContent), 0644); err != nil { + t.Fatalf("failed to write expect file: %v", err) + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/healthz": + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status":"ok"}`)) + case "/v1/models": + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"object":"list","data":[{"id":"test-model","object":"model","created":123456,"owned_by":"iop"}]}`)) + case "/v1/responses": + // Assert request body + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + t.Errorf("failed to read request body: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + var reqPayload map[string]interface{} + if err := json.Unmarshal(bodyBytes, &reqPayload); err != nil { + t.Errorf("failed to unmarshal request body: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + // Validate metadata.workspace + metadata, ok := reqPayload["metadata"].(map[string]interface{}) + if !ok { + t.Errorf("metadata missing in request body") + } else { + gotWS := metadata["workspace"] + if gotWS != tmpDir { + t.Errorf("expected workspace %q, got %q", tmpDir, gotWS) + } + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"id":"resp-123","object":"response","created_at":123456,"model":"test-model","output_text":"responses pong","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"responses pong"}]}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`)) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + + root := rootCmd() + var out bytes.Buffer + root.SetOut(&out) + root.SetErr(&out) + root.SetArgs([]string{ + "smoke", "openai", + "--model", "test-model", + "--base-url", server.URL, + "--prompt", "ping", + "--timeout", "5s", + "--workspace", tmpDir, + "--expect-file", expectFile, + "--expect-contains", "smoke test", + }) + if err := root.Execute(); err != nil { + t.Fatalf("smoke openai command failed: %v\n%s", err, out.String()) + } + + got := out.String() + wants := []string{ + "Step 1: Checking /healthz ... [OK]", + "Step 2: Checking /v1/models ... [OK]", + "Step 3: Checking /v1/responses ... [OK]", + "Responses Output Text: \"responses pong\"", + "IOP Edge OpenAI Smoke Test SUCCESS!", + } + for _, w := range wants { + if !strings.Contains(got, w) { + t.Errorf("smoke openai success output missing: %q\nFull output:\n%s", w, got) + } + } +} + func TestSmokeOpenAICommandFailure(t *testing.T) { // 1. Non-200 /healthz error server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/apps/edge/internal/edgecmd/smoke_openai.go b/apps/edge/internal/edgecmd/smoke_openai.go index 381d58e..516a4e8 100644 --- a/apps/edge/internal/edgecmd/smoke_openai.go +++ b/apps/edge/internal/edgecmd/smoke_openai.go @@ -7,6 +7,8 @@ import ( "io" "net" "net/http" + "os" + "path/filepath" "strings" "time" @@ -32,10 +34,13 @@ Typical local flow: } var ( - smokeModel string - smokeBaseURL string - smokePrompt string - smokeTimeout string + smokeModel string + smokeBaseURL string + smokePrompt string + smokeTimeout string + smokeWorkspace string + smokeExpectFile string + smokeExpectContains string ) type openAIModel struct { @@ -201,19 +206,24 @@ If --base-url is not provided, it auto-discovers the OpenAI URL from the effecti } fmt.Fprintf(cmd.OutOrStdout(), "Step 3: Checking /v1/responses ... ") - responsesPayload := map[string]interface{}{ - "model": smokeModel, - "input": prompt, - "stream": false, - "metadata": map[string]interface{}{ - "request_id": "iop-edge-smoke", - "task_id": "smoke", - "source": "iop-edge-smoke", - "inference": map[string]string{ - "target": smokeModel, - }, + metadata := map[string]interface{}{ + "request_id": "iop-edge-smoke", + "task_id": "smoke", + "source": "iop-edge-smoke", + "inference": map[string]string{ + "target": smokeModel, }, } + if smokeWorkspace != "" { + metadata["workspace"] = smokeWorkspace + } + + responsesPayload := map[string]interface{}{ + "model": smokeModel, + "input": prompt, + "stream": false, + "metadata": metadata, + } payloadBytes, err := json.Marshal(responsesPayload) if err != nil { return fmt.Errorf("failed to marshal responses request payload: %w", err) @@ -243,6 +253,27 @@ If --base-url is not provided, it auto-discovers the OpenAI URL from the effecti } fmt.Fprintf(cmd.OutOrStdout(), "[OK]\n") fmt.Fprintf(cmd.OutOrStdout(), "Responses Output Text: %q\n\n", outputText) + + if smokeExpectFile != "" { + if smokeWorkspace == "" { + return fmt.Errorf("--expect-file requires --workspace to be set") + } + expectFilePath := filepath.Join(smokeWorkspace, smokeExpectFile) + rel, err := filepath.Rel(smokeWorkspace, expectFilePath) + if err != nil || strings.HasPrefix(rel, "..") { + return fmt.Errorf("expect-file %q must be within the workspace", smokeExpectFile) + } + data, err := os.ReadFile(expectFilePath) + if err != nil { + return fmt.Errorf("failed to read expected file %q: %w", expectFilePath, err) + } + if smokeExpectContains != "" { + if !strings.Contains(string(data), smokeExpectContains) { + return fmt.Errorf("expected file %q does not contain expected substring %q", smokeExpectFile, smokeExpectContains) + } + } + } + fmt.Fprintf(cmd.OutOrStdout(), "IOP Edge OpenAI Smoke Test SUCCESS!\n") return nil @@ -253,6 +284,9 @@ If --base-url is not provided, it auto-discovers the OpenAI URL from the effecti c.Flags().StringVar(&smokeBaseURL, "base-url", "", "override OpenAI base URL (e.g. http://127.0.0.1:18081)") c.Flags().StringVar(&smokePrompt, "prompt", "ping", "prompt text to send") c.Flags().StringVar(&smokeTimeout, "timeout", "10s", "client timeout duration") + c.Flags().StringVar(&smokeWorkspace, "workspace", "", "path to the temporary workspace directory") + c.Flags().StringVar(&smokeExpectFile, "expect-file", "", "relative path under workspace to check for existence after smoke") + c.Flags().StringVar(&smokeExpectContains, "expect-contains", "", "substring expected to be found in the expected file") c.MarkFlagRequired("model") return c diff --git a/apps/node/internal/adapters/cli/cli.go b/apps/node/internal/adapters/cli/cli.go index 4e0484a..ee716f4 100644 --- a/apps/node/internal/adapters/cli/cli.go +++ b/apps/node/internal/adapters/cli/cli.go @@ -17,6 +17,7 @@ import ( "strconv" "strings" "sync" + "time" "go.uber.org/zap" @@ -493,3 +494,39 @@ func closeProfileSession(_ context.Context, sess *profileSession) error { func workspaceDir(w string) string { return strings.TrimSpace(w) } + +func prepareWorkspaceDir(w string) (string, error) { + dir := workspaceDir(w) + if dir == "" { + return "", nil + } + info, err := os.Stat(dir) + if err != nil { + if os.IsNotExist(err) { + return "", fmt.Errorf("cli adapter: workspace not found: %s", dir) + } + return "", fmt.Errorf("cli adapter: workspace inaccessible: %s: %w", dir, err) + } + if !info.IsDir() { + return "", fmt.Errorf("cli adapter: workspace is not a directory: %s", dir) + } + if f, err := os.Open(dir); err != nil { + return "", fmt.Errorf("cli adapter: workspace inaccessible: %s: %w", dir, err) + } else { + _ = f.Close() + } + return dir, nil +} + +func emitReturnedError(ctx context.Context, sink runtime.EventSink, runID string, err error) error { + if err == nil { + return nil + } + _ = sink.Emit(ctx, runtime.RuntimeEvent{ + RunID: runID, + Type: runtime.EventTypeError, + Error: err.Error(), + Timestamp: time.Now(), + }) + return err +} diff --git a/apps/node/internal/adapters/cli/cli_internal_test.go b/apps/node/internal/adapters/cli/cli_internal_test.go index ffd50a2..778083b 100644 --- a/apps/node/internal/adapters/cli/cli_internal_test.go +++ b/apps/node/internal/adapters/cli/cli_internal_test.go @@ -927,3 +927,273 @@ func TestCLIPersistentTerminalCwd(t *testing.T) { t.Errorf("expected output to contain workspace path %q, got: %q", tmpDir, output) } } + +func TestCLIWorkspacePreflightFailures(t *testing.T) { + // 1. prepareWorkspaceDir helper tests + t.Run("Helper", func(t *testing.T) { + // Blank workspace + dir, err := prepareWorkspaceDir("") + if err != nil || dir != "" { + t.Errorf("expected empty dir and nil error for blank workspace, got dir=%q, err=%v", dir, err) + } + dir, err = prepareWorkspaceDir(" ") + if err != nil || dir != "" { + t.Errorf("expected empty dir and nil error for whitespace workspace, got dir=%q, err=%v", dir, err) + } + + // Not found directory + nonExistentPath := "/nonexistent/path/for/workspace/test" + dir, err = prepareWorkspaceDir(nonExistentPath) + if err == nil || !strings.Contains(err.Error(), "cli adapter: workspace not found") { + t.Errorf("expected 'workspace not found' error, got dir=%q, err=%v", dir, err) + } + + // File is not a directory + tmpFile, err := os.CreateTemp("", "test-workspace-file-*") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + _ = tmpFile.Close() + + dir, err = prepareWorkspaceDir(tmpFile.Name()) + if err == nil || !strings.Contains(err.Error(), "cli adapter: workspace is not a directory") { + t.Errorf("expected 'workspace is not a directory' error, got dir=%q, err=%v", dir, err) + } + + // Inaccessible directory + if os.Geteuid() != 0 { + // Create a directory and make it inaccessible + inaccessibleDir := t.TempDir() + "/inaccessible" + if err := os.Mkdir(inaccessibleDir, 0000); err != nil { + t.Fatalf("failed to create inaccessible dir: %v", err) + } + dir, err = prepareWorkspaceDir(inaccessibleDir) + if err == nil || !strings.Contains(err.Error(), "cli adapter: workspace inaccessible") { + t.Errorf("expected 'workspace inaccessible' error, got dir=%q, err=%v", dir, err) + } + // Restore permission for cleanup + _ = os.Chmod(inaccessibleDir, 0700) + } else { + t.Log("running as root; skipping inaccessible workspace helper test") + } + }) + + // 2. Execute path preflight error propagation + t.Run("ExecuteOneshot", func(t *testing.T) { + c := New(config.CLIConf{ + Profiles: map[string]config.CLIProfileConf{ + "oneshot-pwd": { + Command: "pwd", + }, + }, + }, nil) + + spec := runtime.ExecutionSpec{ + RunID: "oneshot-run-id", + Target: "oneshot-pwd", + Workspace: "/nonexistent/path/for/workspace/test", + } + sink := &testSink{} + err := c.Execute(context.Background(), spec, sink) + if err == nil { + t.Fatal("expected execute to fail for nonexistent workspace") + } + if !strings.Contains(err.Error(), "cli adapter: workspace not found") { + t.Errorf("expected 'workspace not found' error, got: %v", err) + } + + // Verify sink EventTypeError event + hasErrorEvent := false + for _, ev := range sink.events { + if ev.Type == runtime.EventTypeError { + hasErrorEvent = true + if ev.RunID != spec.RunID { + t.Errorf("expected EventTypeError RunID to be %q, got %q", spec.RunID, ev.RunID) + } + if !strings.Contains(ev.Error, "cli adapter: workspace not found") { + t.Errorf("expected EventTypeError Error to contain 'workspace not found', got %q", ev.Error) + } + } + } + if !hasErrorEvent { + t.Errorf("expected sink to contain EventTypeError event, but it did not") + } + }) + + t.Run("ExecutePersistent", func(t *testing.T) { + c := New(config.CLIConf{ + Profiles: map[string]config.CLIProfileConf{ + "persistent-pwd": { + Command: "sh", + Persistent: true, + }, + }, + }, nil) + + spec := runtime.ExecutionSpec{ + RunID: "persistent-run-id", + Target: "persistent-pwd", + Workspace: "/nonexistent/path/for/workspace/test", + SessionID: "sess-preflight-fail", + SessionMode: runtime.SessionModeCreateIfMissing, + Input: map[string]any{"prompt": "pwd\n"}, + } + sink := &testSink{} + err := c.Execute(context.Background(), spec, sink) + if err == nil { + t.Fatal("expected execute to fail for nonexistent workspace") + } + if !strings.Contains(err.Error(), "cli adapter: workspace not found") { + t.Errorf("expected 'workspace not found' error, got: %v", err) + } + + // Verify sink EventTypeError event + hasErrorEvent := false + for _, ev := range sink.events { + if ev.Type == runtime.EventTypeError { + hasErrorEvent = true + if ev.RunID != spec.RunID { + t.Errorf("expected EventTypeError RunID to be %q, got %q", spec.RunID, ev.RunID) + } + if !strings.Contains(ev.Error, "cli adapter: workspace not found") { + t.Errorf("expected EventTypeError Error to contain 'workspace not found', got %q", ev.Error) + } + } + } + if !hasErrorEvent { + t.Errorf("expected sink to contain EventTypeError event, but it did not") + } + }) + + t.Run("ExecuteCodexAppServer", func(t *testing.T) { + c := New(config.CLIConf{ + Profiles: map[string]config.CLIProfileConf{ + "codex-app": { + Mode: modeCodexAppServer, + Command: "codex", + Persistent: true, + Args: []string{"--app-server"}, + }, + }, + }, nil) + + spec := runtime.ExecutionSpec{ + RunID: "codex-run-id", + Target: "codex-app", + Workspace: "/nonexistent/path/for/workspace/test", + SessionID: "sess-codex-preflight-fail", + SessionMode: runtime.SessionModeCreateIfMissing, + Input: map[string]any{"prompt": "some prompt"}, + } + sink := &testSink{} + err := c.Execute(context.Background(), spec, sink) + if err == nil { + t.Fatal("expected execute to fail for nonexistent workspace") + } + if !strings.Contains(err.Error(), "cli adapter: start codex app-server") || + !strings.Contains(err.Error(), "cli adapter: workspace not found") { + t.Errorf("expected 'start codex app-server' and 'workspace not found' errors, got: %v", err) + } + + // Verify sink EventTypeError event + hasErrorEvent := false + for _, ev := range sink.events { + if ev.Type == runtime.EventTypeError { + hasErrorEvent = true + if ev.RunID != spec.RunID { + t.Errorf("expected EventTypeError RunID to be %q, got %q", spec.RunID, ev.RunID) + } + if !strings.Contains(ev.Error, "cli adapter: start codex app-server") || + !strings.Contains(ev.Error, "cli adapter: workspace not found") { + t.Errorf("expected EventTypeError Error to contain 'start codex app-server' and 'workspace not found', got %q", ev.Error) + } + } + } + if !hasErrorEvent { + t.Errorf("expected sink to contain EventTypeError event, but it did not") + } + }) + + t.Run("ExecuteOpencodeSSE", func(t *testing.T) { + c := New(config.CLIConf{ + Profiles: map[string]config.CLIProfileConf{ + "opencode-sse-target": { + Mode: modeOpencodeSSE, + Command: "opencode", + Persistent: true, + Args: []string{"serve"}, // triggers opencode-sse mode + }, + }, + }, nil) + + spec := runtime.ExecutionSpec{ + RunID: "opencode-run-id", + Target: "opencode-sse-target", + Workspace: "/nonexistent/path/for/workspace/test", + SessionID: "sess-opencode-preflight-fail", + SessionMode: runtime.SessionModeCreateIfMissing, + Input: map[string]any{"prompt": "some prompt"}, + } + sink := &testSink{} + err := c.Execute(context.Background(), spec, sink) + if err == nil { + t.Fatal("expected execute to fail for nonexistent workspace") + } + if !strings.Contains(err.Error(), "cli adapter: start opencode server") || + !strings.Contains(err.Error(), "cli adapter: workspace not found") { + t.Errorf("expected 'start opencode server' and 'workspace not found' errors, got: %v", err) + } + + // Verify sink EventTypeError event + hasErrorEvent := false + for _, ev := range sink.events { + if ev.Type == runtime.EventTypeError { + hasErrorEvent = true + if ev.RunID != spec.RunID { + t.Errorf("expected EventTypeError RunID to be %q, got %q", spec.RunID, ev.RunID) + } + if !strings.Contains(ev.Error, "cli adapter: start opencode server") || + !strings.Contains(ev.Error, "cli adapter: workspace not found") { + t.Errorf("expected EventTypeError Error to contain 'start opencode server' and 'workspace not found', got %q", ev.Error) + } + } + } + if !hasErrorEvent { + t.Errorf("expected sink to contain EventTypeError event, but it did not") + } + }) +} + +func TestCLIProcessExitFailureRemainsDistinct(t *testing.T) { + c := New(config.CLIConf{ + Profiles: map[string]config.CLIProfileConf{ + "fail-oneshot": { + Command: "sh", + Args: []string{"-c", "exit 7"}, + }, + }, + }, nil) + + tmpDir := t.TempDir() + spec := runtime.ExecutionSpec{ + Target: "fail-oneshot", + Workspace: tmpDir, + } + sink := &testSink{} + err := c.Execute(context.Background(), spec, sink) + if err == nil { + t.Fatal("expected command execution to fail") + } + + errStr := err.Error() + // Command failure should NOT look like a workspace error. + if strings.Contains(errStr, "workspace not found") || + strings.Contains(errStr, "workspace is not a directory") || + strings.Contains(errStr, "workspace inaccessible") { + t.Errorf("process exit failure should be distinct from workspace preflight error, got: %q", errStr) + } + if !strings.Contains(errStr, "exit status 7") && !strings.Contains(errStr, "exit code 7") { + t.Errorf("expected error to mention exit status/code 7, got: %q", errStr) + } +} diff --git a/apps/node/internal/adapters/cli/codex_app_server.go b/apps/node/internal/adapters/cli/codex_app_server.go index a9e8f73..ff26b4a 100644 --- a/apps/node/internal/adapters/cli/codex_app_server.go +++ b/apps/node/internal/adapters/cli/codex_app_server.go @@ -72,7 +72,7 @@ type codexAppServerExecutor struct { func (e *codexAppServerExecutor) Execute(ctx context.Context, spec runtime.ExecutionSpec, profile config.CLIProfileConf, sink runtime.EventSink) error { sess, err := e.resolveCodexAppServerSession(ctx, spec, profile) if err != nil { - return err + return emitReturnedError(ctx, sink, spec.RunID, err) } sess.mu.Lock() defer sess.mu.Unlock() @@ -392,7 +392,11 @@ func startCodexAppServerProc(ctx context.Context, profile config.CLIProfileConf, if len(profile.Env) > 0 { cmd.Env = append(cmd.Environ(), profile.Env...) } - if dir := workspaceDir(workspace); dir != "" { + dir, err := prepareWorkspaceDir(workspace) + if err != nil { + return nil, err + } + if dir != "" { cmd.Dir = dir } diff --git a/apps/node/internal/adapters/cli/oneshot.go b/apps/node/internal/adapters/cli/oneshot.go index b71e936..ce4a3bd 100644 --- a/apps/node/internal/adapters/cli/oneshot.go +++ b/apps/node/internal/adapters/cli/oneshot.go @@ -23,7 +23,11 @@ func (c *CLI) executeOneShot(ctx context.Context, spec runtime.ExecutionSpec, pr func (c *CLI) executeCommand(ctx context.Context, spec runtime.ExecutionSpec, profile config.CLIProfileConf, args []string, prompt string, sink runtime.EventSink) (string, error) { cmd := exec.CommandContext(ctx, profile.Command, args...) - if dir := workspaceDir(spec.Workspace); dir != "" { + dir, err := prepareWorkspaceDir(spec.Workspace) + if err != nil { + return "", emitReturnedError(ctx, sink, spec.RunID, err) + } + if dir != "" { cmd.Dir = dir } @@ -33,15 +37,15 @@ func (c *CLI) executeCommand(ctx context.Context, spec runtime.ExecutionSpec, pr stdout, err := cmd.StdoutPipe() if err != nil { - return "", fmt.Errorf("cli adapter: stdout pipe: %w", err) + return "", emitReturnedError(ctx, sink, spec.RunID, fmt.Errorf("cli adapter: stdout pipe: %w", err)) } stderr, err := cmd.StderrPipe() if err != nil { - return "", fmt.Errorf("cli adapter: stderr pipe: %w", err) + return "", emitReturnedError(ctx, sink, spec.RunID, fmt.Errorf("cli adapter: stderr pipe: %w", err)) } if err := cmd.Start(); err != nil { - return "", fmt.Errorf("cli adapter: start %q: %w", profile.Command, err) + return "", emitReturnedError(ctx, sink, spec.RunID, fmt.Errorf("cli adapter: start %q: %w", profile.Command, err)) } _ = sink.Emit(ctx, runtime.RuntimeEvent{ diff --git a/apps/node/internal/adapters/cli/opencode_sse.go b/apps/node/internal/adapters/cli/opencode_sse.go index 43d7148..5fca4f5 100644 --- a/apps/node/internal/adapters/cli/opencode_sse.go +++ b/apps/node/internal/adapters/cli/opencode_sse.go @@ -107,7 +107,7 @@ func (e *opencodeExecutor) Execute(ctx context.Context, spec runtime.ExecutionSp sess, err := e.resolveOpencodeSession(ctx, spec, profile, opts) if err != nil { - return err + return emitReturnedError(ctx, sink, spec.RunID, err) } sess.mu.Lock() @@ -287,8 +287,12 @@ func startOpencodeLocalServer(ctx context.Context, profile config.CLIProfileConf } else { dir = workspace } - if dir := workspaceDir(dir); dir != "" { - cmd.Dir = dir + validatedDir, err := prepareWorkspaceDir(dir) + if err != nil { + return "", nil, err + } + if validatedDir != "" { + cmd.Dir = validatedDir } stdout, err := cmd.StdoutPipe() diff --git a/apps/node/internal/adapters/cli/persistent.go b/apps/node/internal/adapters/cli/persistent.go index 830e738..f7a18f0 100644 --- a/apps/node/internal/adapters/cli/persistent.go +++ b/apps/node/internal/adapters/cli/persistent.go @@ -66,7 +66,7 @@ func (m completionMatcher) match(line string) bool { func (e *persistentExecutor) Execute(ctx context.Context, spec runtime.ExecutionSpec, profile config.CLIProfileConf, sink runtime.EventSink) error { sess, err := e.resolveSession(ctx, spec, profile) if err != nil { - return err + return emitReturnedError(ctx, sink, spec.RunID, err) } matcher, err := newCompletionMatcher(profile.CompletionMarker) @@ -345,6 +345,11 @@ func startProfileSession(_ context.Context, key sessionKey, profile config.CLIPr done: doneCh, } + dir, err := prepareWorkspaceDir(workspace) + if err != nil { + return nil, err + } + var cmd *exec.Cmd if profile.Terminal { @@ -354,7 +359,7 @@ func startProfileSession(_ context.Context, key sessionKey, profile config.CLIPr Env: profile.Env, Rows: terminalRows, Cols: terminalCols, - Dir: workspaceDir(workspace), + Dir: dir, } core, err := terminal.StartSession(context.Background(), opts) if err != nil { @@ -377,7 +382,7 @@ func startProfileSession(_ context.Context, key sessionKey, profile config.CLIPr if len(profile.Env) > 0 { cmd.Env = append(cmd.Environ(), profile.Env...) } - if dir := workspaceDir(workspace); dir != "" { + if dir != "" { cmd.Dir = dir } stdin, err := cmd.StdinPipe() diff --git a/scripts/e2e-openai-cli-workspace.sh b/scripts/e2e-openai-cli-workspace.sh new file mode 100755 index 0000000..785972b --- /dev/null +++ b/scripts/e2e-openai-cli-workspace.sh @@ -0,0 +1,191 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +TMP_DIR=$(mktemp -d) + +EDGE_PID="" +NODE_PID="" +EDGE_FD_OPEN=0 + +cleanup() { + if [ "$EDGE_FD_OPEN" -eq 1 ]; then + { echo "/exit" >&3; } 2>/dev/null || true + exec 3>&- 2>/dev/null || true + fi + if [ -n "$EDGE_PID" ]; then wait "$EDGE_PID" 2>/dev/null || true; fi + if [ -n "$NODE_PID" ]; then kill "$NODE_PID" 2>/dev/null || true; fi + rm -rf "$TMP_DIR" 2>/dev/null || true +} +trap cleanup EXIT + +PORT=$((31000 + RANDOM % 5000)) +BOOTSTRAP_PORT=$((21000 + RANDOM % 5000)) +OPENAI_PORT=$((36000 + RANDOM % 5000)) +EDGE_METRICS_PORT=$((46000 + RANDOM % 5000)) +NODE_METRICS_PORT=$((51000 + RANDOM % 5000)) +MODEL="cli-smoke-model" + +wait_port() { + local host="$1" + local port="$2" + local label="$3" + local deadline=$((SECONDS + 20)) + while ! timeout 1 bash -c 'cat < /dev/null > /dev/tcp/"$1"/"$2"' _ "$host" "$port" 2>/dev/null; do + if (( SECONDS >= deadline )); then + echo "[openai-cli-workspace] $label did not open on $host:$port" + return 1 + fi + sleep 0.2 + done +} + +EDGE_CONFIG="$TMP_DIR/edge.yaml" +NODE_CONFIG="$TMP_DIR/node.yaml" +WORKSPACE="$TMP_DIR/real_workspace" + +mkdir -p "$WORKSPACE" +git -C "$WORKSPACE" init +git -C "$WORKSPACE" config user.name "test" +git -C "$WORKSPACE" config user.email "test@example.com" + +cat > "$EDGE_CONFIG" < "$NODE_CONFIG" < "$EDGE_OUT" 2>&1 & +EDGE_PID=$! +exec 3> "$TMP_DIR/edge_fifo" +EDGE_FD_OPEN=1 + +wait_port 127.0.0.1 "$PORT" "edge node transport" +wait_port 127.0.0.1 "$OPENAI_PORT" "edge openai api" + +IOP_NODE_CONFIG="$NODE_CONFIG" "$REPO_ROOT/scripts/dev/node.sh" > "$NODE_OUT" 2>&1 & +NODE_PID=$! + +deadline=$((SECONDS + 30)) +while ! grep -q '\[node0-evt\] connected reason="registered"' "$EDGE_OUT"; do + if (( SECONDS >= deadline )); then + echo "[openai-cli-workspace] node registration timed out" + echo "=== EDGE OUTPUT ==="; cat "$EDGE_OUT" + echo "=== NODE OUTPUT ==="; cat "$NODE_OUT" + exit 1 + fi + sleep 0.2 +done + +RESPONSES_OUT="$TMP_DIR/responses.json" +curl -fsS \ + -H "Content-Type: application/json" \ + -d '{"model":"'"$MODEL"'","input":"echo '\''marker_content'\'' > marker.txt && echo '\''IOP_CLI_SMOKE_OK'\''","stream":false,"metadata":{"request_id":"e2e-openai-cli-workspace","task_id":"task-smoke","source":"manual","workspace":"'"$WORKSPACE"'","inference":{"target":"smoke-sh"}}}' \ + "http://127.0.0.1:$OPENAI_PORT/v1/responses" > "$RESPONSES_OUT" + +grep -q "IOP_CLI_SMOKE_OK" "$RESPONSES_OUT" +grep -q '"output_text"' "$RESPONSES_OUT" + +# 1. marker file이 workspace 안에 생성되었는지 검증 +if [ ! -f "$WORKSPACE/marker.txt" ]; then + echo "[cli-workspace] marker file not found in workspace" + exit 1 +fi +if [ "$(cat "$WORKSPACE/marker.txt" | xargs)" != "marker_content" ]; then + echo "[cli-workspace] marker content mismatch" + exit 1 +fi + +# 2. repo root와 temp parent에는 marker가 없어야 한다 +if [ -f "$REPO_ROOT/marker.txt" ]; then + echo "[cli-workspace] marker file leaked to repo root" + exit 1 +fi +if [ -f "$TMP_DIR/marker.txt" ]; then + echo "[cli-workspace] marker file leaked to temp parent" + exit 1 +fi + +# 3. git -C "$WORKSPACE" status --short로 변경 확인 +GIT_STATUS=$(git -C "$WORKSPACE" status --short) +if [[ "$GIT_STATUS" != *"marker.txt"* ]]; then + echo "[cli-workspace] git status does not show marker.txt" + echo "Actual git status: $GIT_STATUS" + exit 1 +fi + +# 4. iop-edge smoke openai 검증 (expect-file, expect-contains 사용) +SMOKE_OUT="$TMP_DIR/iop-edge-smoke-openai.txt" +(cd "$REPO_ROOT" && go run ./apps/edge/cmd/edge smoke openai \ + --model "$MODEL" \ + --base-url "http://127.0.0.1:$OPENAI_PORT" \ + --prompt "echo 'marker_smoke' > marker_smoke.txt && echo 'IOP_CLI_SMOKE_OK'" \ + --workspace "$WORKSPACE" \ + --expect-file "marker_smoke.txt" \ + --expect-contains "marker_smoke" \ + --timeout 20s) > "$SMOKE_OUT" + +grep -q "Step 3: Checking /v1/responses ... \[OK\]" "$SMOKE_OUT" +grep -q "IOP Edge OpenAI Smoke Test SUCCESS!" "$SMOKE_OUT" + +if grep -i -E "node reported error|error run_id=|\[[^]]+-evt\] error|panic:" "$EDGE_OUT" "$NODE_OUT" >/dev/null; then + echo "[openai-cli-workspace] detected failure marker in logs" + echo "=== EDGE OUTPUT ==="; cat "$EDGE_OUT" + echo "=== NODE OUTPUT ==="; cat "$NODE_OUT" + exit 1 +fi + +echo "[openai-cli-workspace] OpenAI-compatible CLI workspace authoring test PASSED."