---
name: dev-runtime-deploy
description: dev 배포, dev-runtime 배포, Edge/Node dev 환경 배포 요청에서 dev commit count 기반 git-flow release를 만들고 clean sync, 전체 테스트, rebuild, 원격 배포, OpenAI-compatible capacity smoke, release finish와 tag push를 수행하는 절차
---

# dev-runtime-deploy

## 목적

dev-runtime provider pool을 `dev` 기준 git-flow release로 배포한다. `dev` commit count로 release version을 만들고 release branch에서 전체 테스트, rebuild, Edge/Node 재시작, provider snapshot 기반 capacity 검증을 완료한 뒤에만 release를 finish하고 tag를 원격에 반영한다.

## 언제 호출할지

- 사용자가 `dev 배포`, `dev-runtime 배포`, `배포해봐`처럼 dev 환경 배포를 요청할 때
- Edge, mac-codex-node(CLI adapter + mac-mlx-vllm provider resource), GX10 vLLM node, OneXPlayer Lemonade node, RTX5090 Lemonade node로 이루어진 dev-runtime provider pool을 갱신할 때
- OpenAI-compatible `/v1/responses` 또는 `/v1/chat/completions` 경로가 provider capacity만큼 채워지는지 검증할 때
- dev 환경의 remote runner, node 접속 정보, provider capacity, model alias 변경을 배포 절차에 반영할 때

## 입력

- `env`: 배포 대상 환경. 기본값은 `dev`이다.
- `model`: OpenAI-compatible model alias. 지정하지 않으면 dev 환경 문서의 기준 model을 사용한다.
- `capacity_targets`: provider별 기대 capacity. 지정하지 않으면 dev 환경 인벤토리 또는 `agent-test/dev/*` 문서에서 읽는다.
- 배포 기준 branch는 항상 `dev`이며 다른 `source_ref`로 대체하지 않는다.
- dev-runtime 배포 범위에서 `apps/agent/**`와 정확한 package `packages/go/agenttask`는 제외한다. `packages/go/agentprovider/**`, `packages/go/agentruntime`, `packages/go/credentiallease`는 Edge/mac Node가 사용하므로 제외하지 않는다.

## Git Flow release 규칙

- develop branch는 `dev`, production branch는 `main`을 사용한다.
- release version은 clean sync된 `origin/dev` HEAD에서 `git rev-list --count HEAD`로 계산한 `dev-<commit-count>`이다.
- release branch는 git-flow 기본 prefix를 적용한 `release/dev-<commit-count>`, tag는 `dev-<commit-count>`를 사용한다.
- 별도 버전 파일은 만들지 않는다. release branch와 최종 tag가 배포 버전 기록이다.
- release branch에서 빌드와 배포를 수행하고 모든 필수 테스트와 live capacity smoke가 성공한 경우에만 `git flow release finish`를 실행한다.
- finish는 local production/develop merge와 tag 생성을 수행하고, 검증된 `main`, `dev`, tag, release branch 삭제를 atomic push로 원격에 함께 반영한다.
- 검증 실패 시 finish, tag 생성, `main`/`dev` 반영을 수행하지 않고 release branch를 유지한다.
- 같은 version의 게시된 release branch가 있고 tag가 없으면 새 branch를 만들지 않고 해당 release를 재개한다.

## 먼저 확인할 것

- [ ] `agent-ops/rules/project/domain/testing/rules.md`를 읽고 사용자 실행 파이프라인 검증 기준을 확인한다.
- [ ] `go build -o /tmp/iop-inventory-query ./scripts/inventory-query`로 current checkout query binary를 만든다.
- [ ] `/tmp/iop-inventory-query --env dev`로 env projection을 먼저 확인한다.
- [ ] 필요한 selector별 bounded 조회는 아래 명령으로 실행한다:
  - node: `/tmp/iop-inventory-query --env dev --node <node-id-or-alias>`
  - provider: `/tmp/iop-inventory-query --env dev --provider <provider-id>`
  - model: `/tmp/iop-inventory-query --env dev --model <model-alias>`
- [ ] 직접 실행 exit 2가 schema/file 오류일 때만 canonical inventory 전체 읽기로 fallback하고, exit 1은 zero-match로 보고한다.
- [ ] dev-runtime provider pool과 compose/local profile을 섞지 않는다. dev-runtime은 native Edge/Node runtime과 dev-runtime config를 기준으로 한다.
- [ ] 원격 runner의 non-login SSH는 Go/Flutter 경로가 비어 있을 수 있다. 실행 전에 `PATH=/opt/homebrew/bin:/Users/toki/SDK/flutter/bin:/Users/toki/go/bin:/Users/toki/.pub-cache/bin:$PATH`를 설정하거나 `/bin/zsh -lc`로 toolchain 경로를 확인한다.
- [ ] provider runtime option은 `agent-test/inventory-dev.yaml`의 model family별 값을 우선한다. 현재 Qwen은 mac-mlx-vllm의 `tool_call_parser=qwen`, `reasoning_parser=qwen3`, `default_chat_template_kwargs.enable_thinking=true`를 사용한다. GX10 Laguna는 `tool_call_parser=poolside_v1`, `reasoning_parser=poolside_v1`, `default_chat_template_kwargs.enable_thinking=true`, Pi `preserve_thinking=true`와 host `/home/toki/iop-gx10-vllm/laguna-s-2.1-thinking.jinja`의 `<think>\n` generation prefix를 사용한다. Gemma/Qwen/Laguna parser와 chat template profile을 서로 복사하지 않는다.
- [ ] OneXPlayer SSH 접속 정보는 현재 host 기준 `ssh r0bin@192.168.0.59`이다. `toki` 사용자명 또는 remote runner를 경유한 OneXPlayer 접속을 사용하지 않는다.
- [ ] RTX5090 SSH는 현재 작업 호스트의 local SSH config alias `ssh iop-dev-rtx5090`을 사용한다. 이 alias는 public-key batch 인증을 사용하며, host identity와 공개키 지문은 inventory의 `rtx5090-lemonade-node.ssh_access`에서 확인한다.
- [ ] 현재 구현의 completion 검증 대상은 legacy `/v1/completions`가 아니라 `/v1/chat/completions`이다. `/v1/completions`는 route가 구현되어 있을 때만 별도 검증한다.
- [ ] 원격 runner에 git-flow가 설치되어 있고 `gitflow.branch.master=main`, `gitflow.branch.develop=dev`, `gitflow.prefix.release=release/`인지 확인한다.
- [ ] shallow clone이 아니며 `origin/dev`, `origin/main`, tag를 fetch할 수 있는지 확인한다.
- [ ] For a deployment that first merges a feature source into `dev`, run `git fetch --prune origin dev main <feature> --tags`, then `git remote prune origin`, and require the recorded exact feature SHA to match the fetched remote feature SHA. For a regular `dev` deployment, run `git fetch --prune origin dev main --tags`, then `git remote prune origin`.
- [ ] Use `git ls-remote --heads origin 'refs/heads/release/*'` as the authoritative remote release-head check. Treat stale local remote-tracking refs removed by prune as cleanup evidence, not as remote release blockers.
- [ ] `origin/main`이 `origin/dev`의 ancestor인지 확인한다. 아니면 배포하지 않고 branch 정합화를 먼저 요구한다.
- [ ] 계산된 tag가 이미 local이나 origin에 있으면 삭제·이동·덮어쓰기하지 않고 완료 상태 또는 partial finish 상태를 보고한다.
- [ ] 계산된 `release/dev-<count>` 이외의 release branch가 있으면 중단한다. AVH git-flow는 다른 release가 진행 중이면 새 release start를 허용하지 않는다.
- [ ] token, secret header, bootstrap token, private key 경로는 최종 보고에 원문으로 출력하지 않는다.

## 실행 절차

1. **환경 인벤토리 확정**
   - 배포 대상 runner, repo path, Edge id, Control Plane status URL, OpenAI base URL, Edge admin URL, Node SSH 정보를 문서에서 확정한다.
   - provider pool 대상 model alias와 provider별 capacity를 확정한다.
   - 필수 정보가 없거나 서로 충돌하면 배포를 시작하지 말고 누락/충돌 항목을 보고한다.

2. **dev clean sync와 release version 확정**
   - Run `git fetch --prune origin dev main --tags` in the remote runner checkout, then `git remote prune origin`, and clean-sync local `dev` to `origin/dev`. When the deployment first merges a feature source, fetch the exact feature ref with `git fetch --prune origin dev main <feature> --tags` before pruning the remote-tracking namespace.
   - Determine active remote releases only from the authoritative output of `git ls-remote --heads origin 'refs/heads/release/*'`. Record changes to local `refs/remotes/origin/release/*` before and after prune only as stale-ref cleanup evidence.
   - dirty 파일은 보존 대상으로 보지 않는다. 배포 전 clean 상태를 만든다.
   - 기본 cleanup은 `git clean -fd`이다. `git clean -fdx`는 config, token, secret, runtime artifact까지 삭제할 수 있으므로 사용하지 않는다.
   - shallow clone이 아닌지, `HEAD`와 `origin/dev`가 같은 commit인지, `origin/main`이 `origin/dev`의 ancestor인지 확인한다.
   - local `main`도 `origin/main`과 같은 commit으로 맞춘다.
   - sync 후 `git status --short --branch`, `git log --oneline -1`, `git rev-list --count HEAD`를 기록한다.
   - commit count가 `<count>`이면 release version을 `dev-<count>`로 확정한다.
   - 이 시점의 `origin/dev`를 `DEV_BASE_SHA`, `origin/main`을 `MAIN_BASE_SHA`로 기록한다.

3. **git-flow release 시작**
   - `dev-<count>` local/remote tag가 없어야 한다.
   - `release/dev-<count>`가 local과 origin에 모두 없으면 clean `dev`에서 `git flow release start dev-<count>`와 `git flow release publish dev-<count>`를 실행한다.
   - tag 없이 origin의 `release/dev-<count>`만 있으면 `git flow release track dev-<count>`로 checkout하여 기존 release를 재개한다. local branch도 있으면 local/remote가 같은 commit이고 clean한 경우에만 그대로 재개한다.
   - 다른 release branch가 있거나 같은 release의 local/remote가 diverge한 상태에서는 자동 삭제·reset하지 않고 중단한다.
   - release HEAD는 `DEV_BASE_SHA`와 같거나 그 descendant여야 한다.
   - 빌드·배포할 release HEAD를 `DEPLOY_SHA`, 그 tree를 `DEPLOY_TREE`로 기록한다.

4. **빌드 전 전체 테스트**
   - clean source 기준으로 Control Plane, Edge, Node와 공용 runtime package를 테스트한다. `apps/agent/**`와 정확한 package `packages/go/agenttask`는 dev-runtime 범위에서 제외한다.
   - zsh의 newline word splitting이나 package-parallel 실행에 의존하지 않는다. 아래 package 목록을 한 package씩 순차 실행한다. `agentprovider/catalog`의 짧은 process probe는 package 병렬 부하에서 timeout이 날 수 있으므로 병렬 실패만으로 source 실패를 단정하지 않고 순차 실행 결과를 기준으로 한다.

     ```bash
     while IFS= read -r package; do
       go test -count=1 "$package"
     done < <(
       go list ./apps/control-plane/... ./apps/edge/... ./apps/node/... ./packages/go/... ./scripts/... |
         sed '/^iop\/packages\/go\/agenttask$/d'
     )
     ```

   - client/Flutter, proto, Makefile, script, config 변경이 배포 범위에 포함되면 해당 도메인 규칙의 전체 테스트도 추가한다.
   - 전체 테스트가 실패하면 build/deploy를 진행하지 않고 실패 패키지와 핵심 오류를 보고한다.

5. **전체 rebuild**
   - 같은 source ref에서 dev-runtime Edge binary, mac node binary, Linux ARM64 node binary, Windows AMD64 node binary를 모두 다시 빌드한다.
   - 네 binary 모두 `-trimpath`로 빌드한다.

     ```bash
     go build -trimpath -o build/dev-runtime/bin/edge ./apps/edge/cmd/edge
     go build -trimpath -o build/dev-runtime/bin/iop-node ./apps/node/cmd/node
     GOOS=linux GOARCH=arm64 go build -trimpath -o build/dev-runtime/bin/iop-node-linux-arm64 ./apps/node/cmd/node
     GOOS=windows GOARCH=amd64 go build -trimpath -o build/dev-runtime/bin/iop-node-windows-amd64.exe ./apps/node/cmd/node
     ```

   - stale binary가 의심되거나 `config refresh` subcommand, admin port, version 출력이 맞지 않으면 clean sync부터 다시 시작한다.
   - `go version -m`의 module source commit, SHA-256, 산출물 경로, timestamp, 크기, 실행 가능 여부를 기록하고 네 binary가 `DEPLOY_SHA`를 가리키는지 확인한다.

6. **빌드 후 기본 동작 테스트**
   - 빌드된 Edge binary로 `config check`, `config refresh --help`, `config refresh --mode dry-run`을 실행한다.
   - `openai.enabled=true`이면 `openai.provider_id`가 비어 있지 않아야 한다. 누락 시 runtime의 ignored `edge.yaml`을 현재 provider identity에 맞게 보완하고 원본을 release별 경로로 백업한 뒤 다시 `config check`한다. token과 header는 출력하지 않는다.
   - 빌드 후에도 4단계의 동일한 순차 package 테스트를 실행한다. 실행하지 못하면 사유와 남은 위험을 보고한다.
   - dry-run이 `rejected` 또는 예상 밖 `restart_required`를 반환하면 배포를 멈추고 config diff를 보고한다.
   - Flutter test/build가 tracked `ios/Flutter/*.xcconfig`, `macos/Flutter/*.xcconfig`에 CocoaPods include를 추가하거나 untracked `ios/Podfile`, `macos/Podfile`을 만들 수 있다. proto diff와 구분해 확인한 뒤 이번 검증이 만든 정확한 파일만 원복·제거하고 checkout을 clean으로 되돌린다.

7. **배포와 재시작**
   - candidate Edge를 먼저 재시작해 `18082`, `18083`, `18084`, `19093` listener를 확인한 뒤 Node를 시작한다. 잘못된 config payload로 Node가 non-retryable `internal config error`를 받고 종료된 상태에서는 Edge만 고쳐도 Node가 자동 복귀하지 않는다.
   - Edge와 mac-codex-node(CLI adapter + mac-mlx-vllm provider vllm-mlx process)는 원격 runner에서 빌드 산출물 기준으로 재시작한다.
   - GX10 node는 Linux ARM64 node binary를 배포하고 기존 node process를 재시작한다.
   - 처음 교체할 때 원본은 `pre-<release>`로 보존한다. 같은 release의 후속 candidate를 재배포하면 원본 backup을 덮어쓰지 말고 현재 실패 candidate를 `pre-<release>-<short-sha>`로 옮긴 뒤 교체한다.
   - GX10 Laguna vLLM container를 재생성할 때 host template `/home/toki/iop-gx10-vllm/laguna-s-2.1-thinking.jinja`를 container `/run/iop/laguna-s-2.1-thinking.jinja`에 read-only bind하고 `--chat-template`로 지정한다. stock template로 되돌리면 enabled 요청이 첫 생성 토큰으로 `</think>`를 내보내 Pi thinking이 비는 회귀가 생길 수 있다.
   - OneXPlayer node는 현재 host에서 `ssh r0bin@192.168.0.59`로 접속한다. artifact가 remote runner에 있으면 현재 host를 통해 전달한 뒤 Windows host에서 교체한다.
   - OneXPlayer에서는 SSH 세션 안의 `Start-Process`로 장기 실행을 시작하지 않는다. `Win32_Process.Create` 또는 동등한 세션 독립 실행 방식으로 `iop-node.exe --config node.yaml serve`를 시작한다.
   - RTX5090 node는 현재 host에서 `ssh iop-dev-rtx5090`으로 접속한다. IOP 관련 Windows 부팅 owner는 두지 않고, operator는 `C:/Users/r0bin/iop-field/remote-llm-toggle.ps1` 또는 이를 한 번 호출하는 `RemoteLLM_mode.ahk`로 수동 전환한다. IOP dev 배포는 Startup shortcut, Run entry, Task Scheduler, Windows service를 생성하거나 복원하지 않는다. 기본 Toggle은 상태를 반전시키므로 배포 자동화에서 호출하지 말고 `-Action Status|Up|Down`을 명시한다. Node binary만 즉시 재시작할 때는 `Win32_Process.Create` 또는 동등한 세션 독립 실행 방식을 사용하고 SSH 세션 내부의 `Start-Process`에 장기 실행을 의존하지 않는다.
   - RTX5090 Lemonade는 `host=0.0.0.0`, CUDA backend, Q5 GGUF + Q8 KV, context `262144` 기준을 inventory와 대조한다. localhost-only bind는 Node에서 provider endpoint에 접속할 수 없으므로 배포 완료로 보지 않는다.

8. **배포 후 연결 검증**
   - Edge, OpenAI-compatible listener, Node TCP, admin port, Control Plane status port가 열려 있는지 확인한다.
   - Control Plane status에서 Edge가 connected이고 dev-runtime 기준 4개 node가 connected인지 확인한다.
   - 각 node의 `provider_snapshots`에서 provider `id`, `capacity`, `in_flight`, `queued`, `health`, `served_models`를 확인한다.
   - `/v1/models`가 대상 model alias를 노출하는지 확인한다.

9. **Run the OpenAI-compatible capacity smoke**
   - Qualify `/v1/responses` and `/v1/chat/completions` separately. Do not fail solely because the unimplemented legacy `/v1/completions` endpoint is absent.
   - Use a short request that asks for a structured 700-1200-token answer and explicitly bounds provider-native thinking. Compute the emitted JSON Unicode rune count, `runes/4 + runes/16` estimate, and context class. Do not reuse long-context or repetition fixtures as a normal-capacity oracle.
   - In managed mode, authenticate `GET /v1/credentials/routes` with the same principal token used at OpenAI ingress. Require one active exact route alias, then intersect its `resource_selector`, profile, and upstream model with one healthy connected provider snapshot.
   - Run `scripts/e2e-openai-managed-capacity-smoke.sh` for one endpoint and route at a time. Require the emitted request to classify as `normal`, then send the selected provider's `capacity` plus one request. If it classifies as `long`, fail this normal-capacity gate and use the separate long-context admission smoke with `long_context_capacity`; never add capacity from a provider excluded by the authenticated route.
   - Keep current projected Ornith cases separate: qualify `ornith:35b` only through `onexplayer-lemonade`, and qualify `ornith-fast` only through `rtx5090-lemonade`. Run Chat and Responses independently for each alias. Do not mutate routes, capacities, or timeout values to make the smoke pass.
   - Require HTTP 200 for every request, exactly one endpoint-native success terminal and one `[DONE]` per stream, selected-provider peak `in_flight` equal to eligible capacity, `queued >= 1`, and selected final counters `0/0`. Treat a missing route match, changed capacity, wrong context class, missing status sample, or incomplete terminal as a fail-closed release blocker.
   - Every invocation must allocate a distinct mode-0700 directory under ignored `agent-test/runs/**`. Accept only request/result/status files bound to that invocation's manifest and timestamps. Preserve raw routes, request bodies, response bodies, curl errors, token/header values, route ids, slot ids, prompts, and output only inside that directory; retain only the script's allowlisted sanitized summary as task or tracked evidence.
   - For unprojected or legacy provider pools, retain the existing model-group capacity rule. Current baselines are Laguna `laguna-s:2.1` on GX10 capacity `4` (five requests) and Qwen `qwen3.6:35b` on mac-mlx-vllm capacity `2` (three requests).
   - Allow Qwen and Laguna reasoning/thinking text as normal output. For Laguna, compare Pi `high` thinking events with `off` zero-thinking events and final text. For agent/tool-call qualification, test forced, automatic, streaming, and multi-turn tool calls separately from capacity qualification.

10. **git-flow release finish와 tag 반영**
   - 빌드 전·후 테스트, 배포 후 연결 검증, `/v1/responses`와 `/v1/chat/completions` capacity smoke가 모두 성공했는지 다시 확인한다. Managed route가 있으면 `scripts/e2e-openai-managed-capacity-smoke.sh`의 네 Ornith route/endpoint case와 current-run provenance gate가 모두 통과해야 한다.
   - 하나라도 실패했거나 필수 검증이 실행되지 않았으면 finish하지 않고 `release/dev-<count>` branch를 유지한다.
   - 현재 release HEAD가 `DEPLOY_SHA`와 같은지 확인한다. 달라졌으면 배포 산출물과 source가 달라진 것이므로 finish하지 않는다.
   - finish 직전에 `git fetch origin dev main --tags`를 다시 실행하고 `origin/dev=DEV_BASE_SHA`, `origin/main=MAIN_BASE_SHA`, remote tag 없음이 모두 유지되는지 확인한다. 하나라도 달라졌으면 finish하지 않고 release branch를 유지한다.
   - `git flow release finish -h`에서 `--keepremote` 지원 여부를 먼저 확인한다. 지원하면 `git flow release finish --keepremote -m "Release dev-<count>" dev-<count>`를 사용한다. 지원하지 않는 구현에서는 `-p/--push`를 주지 않은 `git flow release finish -m "Release dev-<count>" dev-<count>`가 원격 release branch를 건드리지 않는지 help로 확인한 뒤 사용한다. 옵션 파싱 실패는 ref mutation 전 실패인지 확인하고 같은 명령을 추측해 반복하지 않는다.
   - 생성된 local `dev-<count>` tag의 tree가 `DEPLOY_TREE`와 같은지 확인한다. 다르면 원격에 push하지 않는다.
   - tag tree가 같으면 `git push --atomic origin refs/heads/main:refs/heads/main refs/heads/dev:refs/heads/dev refs/tags/dev-<count>:refs/tags/dev-<count> :refs/heads/release/dev-<count>`로 production/develop/tag 반영과 remote release branch 삭제를 한 번에 수행한다.
   - local finish 검증이나 atomic push가 실패하면 `DEPLOY_SHA`로 detach한 뒤 local `main=MAIN_BASE_SHA`, `dev=DEV_BASE_SHA`, `release/dev-<count>=DEPLOY_SHA`를 복원하고 생성된 local tag를 삭제한 다음 release branch로 돌아간다. remote는 atomic push 전 상태여야 한다.
   - 성공 후 local과 origin의 `main`, `dev` 반영 상태, local/remote tag 존재, local/remote release branch 삭제를 확인한다.
   - 원격 runner도 `git fetch --prune origin dev main --tags`, clean `dev` sync, local release branch 삭제 순서로 정리한다. ignored runtime config와 build artifact는 삭제하지 않는다.

11. **결과 보고**
   - source ref, clean sync 결과, 테스트 결과, 빌드 산출물, process/port 상태, connected node 목록, provider capacity snapshot, capacity smoke 관측값을 보고한다.
   - release version, release branch publish, finish, `main`/`dev` 반영, local/remote tag 상태를 보고한다.
   - 실패한 단계가 있으면 다음 단계를 진행했는지 여부를 명확히 구분한다.
   - capacity smoke가 타이밍 문제로 관측 실패했으면 요청 성공과 별도로 `capacity 관측 미충족`으로 보고하고, 프롬프트 길이 또는 status polling 간격 조정을 제안한다.

## 재발 차단 진단

| 증상 | 원인 판정 | 조치 |
|---|---|---|
| non-login SSH에서 `go: command not found` | runner PATH 미초기화 | PATH를 명시하거나 login zsh에서 `command -v go/flutter/protoc`를 확인한다. |
| `agentprovider/catalog` version probe timeout | 여러 package 동시 실행에 따른 짧은 probe 경합 가능성 | 전체 package를 순차 재실행한다. 순차 실패일 때만 source blocker로 판정한다. |
| Windows cross-build가 Unix file owner 타입에서 실패 | OS별 credential key-file 검증 분리 누락 | `packages/go/credentiallease`의 Unix/Windows test와 실제 Windows ACL smoke를 통과시킨 뒤 다시 빌드한다. |
| Edge 로그의 `openai_compat adapter instance key "<provider-id>" conflicts with provider id` | profile-backed provider와 같은 이름의 정상 backing을 중복 emit하는 mapper 또는 stale Edge | provider/adapter 설정이나 agent orchestration을 삭제하지 않는다. `go test -race ./apps/edge/internal/node`와 candidate Edge source identity를 확인하고 Edge를 rebuild/restart한다. |
| Node가 `internal config error` 후 재접속하지 않음 | config rejection은 일시 네트워크 단절과 달리 process를 종료할 수 있음 | fixed Edge를 먼저 올린 뒤 mac/GX10/OneXPlayer/RTX5090 Node를 각각 다시 시작한다. |
| Edge service test 종료 후 `in_flight`가 남음 | 같은 clock 값에서 run ID가 충돌한 회귀 가능성 | `go test -race ./apps/edge/internal/service`와 run ID 동시성 회귀 테스트를 확인한다. |
| `git flow release finish --keepremote`가 option parse 단계에서 실패 | 설치된 git-flow 구현이 해당 long option을 지원하지 않음 | ref 무변경을 확인하고 help에 따라 no-push finish를 사용한 뒤 atomic push에서 release 삭제를 수행한다. |

위 오류는 dev-runtime Edge/Node 배포 문제다. 이를 통과시키기 위해 `agent-ops/rules/common/**`, common orchestration `dispatch.py`, `apps/agent/**`를 수정·삭제하지 않는다.

## 실행 결과 검증

- [ ] 원격 runner local `dev`가 `origin/dev`로 clean sync되었는가
- [ ] `origin/main`이 `origin/dev`의 ancestor이고 local `main`/`dev`가 원격 기준과 일치하는가
- [ ] `dev` HEAD commit count로 `dev-<count>` version을 계산했는가
- [ ] `release/dev-<count>` branch를 새로 게시했거나 동일 version의 기존 branch를 안전하게 재개했는가
- [ ] 빌드 전 dev-runtime 범위의 순차 Go 테스트와 필요한 추가 전체 테스트가 통과했는가
- [ ] dev-runtime Edge/mac/Linux ARM64/Windows AMD64 binary가 같은 release branch commit에서 rebuild되었는가
- [ ] 빌드 후 config check, refresh help, refresh dry-run, 전체 테스트가 통과했는가
- [ ] Edge, mac-codex-node, GX10 vLLM node, OneXPlayer Lemonade node, RTX5090 Lemonade node가 재시작되고 4개 node(mac-codex, gx10-vllm, onexplayer-lemonade, rtx5090-lemonade)가 connected 상태인가
- [ ] OneXPlayer 접속과 실행이 `r0bin@192.168.0.59` 및 세션 독립 실행 방식으로 수행되었는가
- [ ] RTX5090 접속이 `ssh iop-dev-rtx5090` public-key batch 인증으로 성공하고, IOP 관련 boot autostart가 없으며, 수동 toggle의 exact Ornith profile·`0.0.0.0:13305` listener·Node Edge 연결이 확인되고 Startup/Run/Task/Service 항목을 생성하지 않았는가.
- [ ] `/v1/responses` capacity smoke에서 총 capacity만큼 `in_flight`가 차고 초과 요청이 queue에 잡혔는가
- [ ] `/v1/chat/completions` capacity smoke에서 총 capacity만큼 `in_flight`가 차고 초과 요청이 queue에 잡혔는가
- [ ] 완료 후 provider `in_flight=0`, `queued=0`으로 회복되었는가
- [ ] 모든 필수 검증 성공 후에만 release finish를 실행했는가
- [ ] finish 직전 origin `main`/`dev`가 시작 시점 SHA와 같은지 재검증했는가
- [ ] `dev-<count>` tag tree가 실제 배포한 release tree와 같은가
- [ ] atomic push로 origin `main`/`dev`와 tag가 함께 반영되고 remote release branch가 삭제되었는가
- 검증 실패 시: 실패 단계, 실패한 host/provider/endpoint, 관측된 snapshot, 진행 중단 여부를 보고한다.

## 출력 형식

```text
dev-runtime 배포 결과
- Release: version=<dev-count>, branch=<release/dev-count>, finish=<done|not-run|partial>, tag=<local/remote status>
- Source: <branch/ref/commit>, clean=<yes|no>
- Pre-build tests: <command> - <pass|fail|not-run>
- Build: edge=<path>, mac-node=<path>, linux-arm64-node=<path>, windows-amd64-node=<path>
- Post-build checks: config-check=<pass|fail>, refresh-help=<pass|fail>, refresh-dry-run=<status>, tests=<pass|fail|not-run>
- Deployment: edge=<pid/status>, mac-node=<pid/status>, gx10=<pid/status>, onexplayer=<pid/status>, rtx5090=<pid/status>
- Ports: <port summary>
- Nodes: <node_id connected summary>
- Providers: <provider_id capacity/in_flight/queued/health summary>
- OpenAI-compatible: models=<pass|fail>, responses-capacity=<pass|fail>, chat-completions-capacity=<pass|fail>
- Capacity evidence: <endpoint별 max in_flight/queued snapshot>
- Blockers/Risk: <없음 또는 내용>
```

## 금지 사항

- dirty checkout이나 stale binary를 그대로 배포하지 않는다.
- `git clean -fdx`를 기본 cleanup으로 사용하지 않는다.
- `dev` 이외의 branch나 임의 source ref를 dev release 기준으로 사용하지 않는다.
- shallow clone이나 `origin/dev`와 HEAD가 다른 상태에서 release version을 계산하지 않는다.
- `origin/main`이 `origin/dev`의 ancestor가 아닌 상태에서 release를 시작하지 않는다.
- 기존 tag를 자동 삭제, 이동, 덮어쓰기하지 않는다.
- 같은 release의 local/remote branch가 diverge한 상태를 자동 reset하지 않는다.
- 빌드 전 전체 테스트 실패 후 배포를 계속하지 않는다.
- 필수 테스트, 배포 후 연결 검증, capacity smoke 중 하나라도 실패하거나 실행되지 않았는데 release를 finish하거나 tag를 생성하지 않는다.
- tag tree와 실제 배포 tree가 다른 상태에서 원격 tag를 push하지 않는다.
- `main`, `dev`, tag를 개별 push하여 원격에 partial finish 상태를 만들지 않는다.
- OneXPlayer에 `toki` 사용자명으로 접속하거나 remote runner에서 다시 OneXPlayer로 SSH 접속하지 않는다.
- OneXPlayer 장기 실행을 Windows OpenSSH 세션 내부 `Start-Process`에 의존하지 않는다.
- RTX5090 Node용 Windows Task Scheduler 항목을 생성하거나 재생성하지 않는다.
- Qwen reasoning/thinking 출력을 실패로 판정하거나 exact-output smoke를 기본 성공 기준으로 삼지 않는다.
- Qwen provider를 agent/tool-call 용도로 쓰면서 provider별 `--tool-call-parser`/`--chat-template` 검증 없이 배포 완료로 선언하지 않는다.
- OpenAI-compatible 요청 성공만으로 capacity 검증 성공을 선언하지 않는다. provider snapshot의 `in_flight/queued` 관측을 함께 남긴다.
- 이 프로젝트 전용 배포 절차를 `agent-ops/rules/common/_templates` 또는 common skill에 넣지 않는다.
