유닛 여섯 — 정의와 완료 조건
유닛은 계획 단위다. 배포 단위가 아니다 (unit-boundaries.md). 여섯 전부
같은 모듈 · 같은 실행파일들에 들어가고, 유닛이 나뉘는 이유는 브랜치 하나 ·
게이트 하나로 진행을 재기 위해서다.
권장은 여섯이었고 다섯으로 왔다. U-화면을 U-관측API 와 U-제어판에 각각
합쳤고 근거는 aidlc-state.md 「실행 계획」 4절이다 — 요약하면 폭이 1 이라
따로 가를 사람이 없고, 안 합치면 CP1 이 초록이 될 수 없다. 합쳐도 파일은
ui/ 아래로 분리한다.
U-바닥 스키마 · 상태 상수 · 조회 메서드 · 정책 자리 · 경계 검사 CP0
U-관측API GET /v1/runs · GET /v1/nodes · StepView.chosen
+ 중앙 화면 S0 · S0b · S1 · S1b · S2 CP1
U-대기열 409 -> QUEUED · FIFO · WakeQueued · 202 · 기동 시 깨우기 CP2
U-drain 정책 파일 · 광고에 싣기 · 후보 제외 · 통보 · 두 모드 · 해제 CP3
U-제어판 enodectl serve · 프로세스 제어 · 신원 · 능력 · 현재 작업
· drain 토글 + 호스트 화면 S3 · S5 CP4
U-MCP runctl mcp — stdio JSON-RPC · 도구 일곱 CP5
여섯째는 2026-09-05 에 들어왔다. CP2 가 닫힌 뒤 진행자가 Mediator MCP 의
이월을 되돌렸고 근거는 requirements/decisions.md 5절이다. 앞의 다섯과
성격이 다르다 — dhseo 장면 밖이고, 딛는 것이 CP1 의 라우트뿐이라 지금
이미 착수 가능하며 U-drain · U-제어판과 병렬로 돈다.
읽는 순서 — 이 문서가 유닛마다의 무엇을 · 어디를 · 언제 끝났나이고,
유닛 사이의 접점과 순서는 unit-of-work-dependency.md, 요구사항과 화면과
게이트가 어느 유닛에 걸리는지는 unit-of-work-story-map.md 에 있다.
표기 — 파일 표의 NEW 는 신설, o 는 고친다, c 는 부르기만 하고 안 고친다.
U-바닥
0일차 직렬. 다른 넷이 전부 이것을 딛는다. 늦으면 전원이 선다.
하는 일
스키마와 타입과 조회를 놓는다. 라우트를 하나도 안 만든다 — 이 유닛이 끝난
시점에 밖에서 보이는 변화는 없고, 기존 라우트 15개가 그대로 돈다는 것이 이
유닛의 성질이다.
| 출처 | FR |
|---|---|
enode-features.md 3.1.1 |
FR-1 (GET /v1/nodes 의 저장 쪽) · FR-2 (GET /v1/runs 의 저장 쪽) · FR-3 (StepView.chosen) |
enode-features.md 3.2.1 |
FR-6 (runs.state 어휘에 QUEUED) |
enode-features.md 3.2.2 |
FR-13 (정책 자리 — nodes 열과 UpsertAdvert 시그니처) |
딛는 것
없다. 이것이 바닥이다.
만지는 파일
internal/store/schema.sql o nodes 에 정책 열. ADD COLUMN IF NOT EXISTS
internal/store/store.go o UpsertAdvert 가 정책을 받고 이전 정책을 돌려준다
internal/store/observe.go o StepView 에 Chosen. DB 에 이미 있는 열이다
internal/store/list.go NEW ListNodes · ListRuns
internal/store/queue.go NEW QUEUED 상수 · DrainingNodes · ListQueued
internal/contract/advert.go o Policy{Drain}
internal/match/match.go o Reject 에 json 태그 셋. Match 는 안 건드린다
internal/boundary/imports_test.go NEW 경계 검사. 이름은 Functional Design 이 바꿔도 된다
internal/match/match.go 가 여기 있는 것은 requirements.md §8.4 ①-b 의
결정이다. unit-boundaries.md §2 는 「아무 유닛도 안 만진다」로 적었고 그
문장이 이 한 줄만큼 바뀐다. Match 의 시그니처와 로직은 그대로라 NFR-I4 는
지켜진다.
steps.chosen 은 스키마에 이미 있다 (schema.sql 의 ALTER TABLE steps ADD COLUMN IF NOT EXISTS chosen). FR-3 은 그것을 StepView 에 싣는 일이지
열을 만드는 일이 아니다.
완료 조건
기존 테스트 전부 초록. 회귀 0
기존 라우트 15개가 그대로 돈다
nodes 에 정책 열이 있고 UpsertAdvert 가 이전 정책을 돌려준다
store.QUEUED 상수가 있다. runs.state 에 CHECK 가 없어 마이그레이션이 필요 없다
ListNodes · ListRuns · DrainingNodes · ListQueued 가 있고 각각 테스트가 있다
StepView 에 Chosen 이 실려 나온다
contract.Policy{Drain} 이 있고 값은 "" · graceful · at-boundary 셋뿐이다
match.Reject 가 {code, as, reason} 로 직렬화된다. 옛 DB 행이 그대로 읽힌다
경계 검사 테스트가 금지 임포트 넷을 기계로 센다 (아래)
커버리지 · 린트 · glyphscan 초록
경계 검사 테스트 — go/parser 로 각 패키지의 임포트를 읽어 넷을 확인한다
(unit-boundaries.md §5 · NFR-P1 ~ NFR-P5). 이것이 CP0 에 들어가면 겹침을
사람이 안 세도 된다.
internal/panel -> internal/store 금지
internal/panel -> internal/api 금지
internal/api/ui -> internal/store 금지
internal/enode -> internal/panel 금지
패키지가 아직 없을 때도 초록이어야 한다 — U-바닥 시점에 internal/panel 과
internal/api/ui 는 존재하지 않는다. 없으면 통과가 아니라 건너뛴다를
검사기가 구분해서 적는다.
게이트 — CP0 「기동이 안 깨졌다」
go test ./... && go vet ./...
go run ./scripts/glyphscan.go
golangci-lint run
커버리지 게이트 (.coverage-contract.yml)
curl 로 기존 라우트 하나 — GET /v1/capabilities
U-관측API
중앙이 보이기 시작한다. 이 유닛이 화면 다섯을 함께 진다.
하는 일
| 출처 | FR |
|---|---|
enode-features.md 3.1.1 |
FR-1 · FR-2 · FR-4 (임대는 GET /v1/nodes 에 내장. 별도 표면 없음) · FR-5 (중앙 현황판 정적 파일) |
requirements.md §8.4 ① · ①-a · ①-b |
행마다 current[] 와 reject |
enode-features.md 3.2.2 |
FR-16 의 화면 몫 — draining 배지 |
딛는 것
U-바닥 store.ListNodes · store.ListRuns · StepView.Chosen · contract.Policy
match.Reject 의 json 태그
만지는 파일
internal/api/list.go NEW 두 라우트의 핸들러
internal/api/ui/ NEW 정적 파일 + embed. S0 · S0b · S1 · S1b · S2
internal/api/api.go o 등록 줄 셋. /v1/nodes · /v1/runs · /ui/
internal/store/list.go c
internal/api/api.go 는 등록 줄만 만진다. 핸들러는 새 파일이다.
CSP 에 unsafe-inline 을 안 쓰므로 화면의 JS 와 CSS 가 인라인이 아니라
별도 파일이다 (requirements.md §3.3). 그것이 ui/ 의 파일 구성을 정한다.
완료 조건 — API
GET /v1/nodes ADR-065 모양 + draining. 만료 안 된 광고만. principal 안 냄
GET /v1/runs {observed_at, runs:[…]}. 시간 역순
행 = {run_id, state, verdict, work_id, created_at, ended_at,
assigned, current[], reject}
current[] CLAIMED 단계마다 {node, step, attempt, started_at}. 없으면 안 낸다
reject {code, as, reason}. GET /v1/runs/{id} 와 같은 글자다
필터 state · since · work · limit. 기본 100 · 상한 1000 · 넘으면 자른다
페이지네이션 없다. limit 뿐
StepView.chosen GET /v1/runs/{id} 의 단계에 실려 나온다
토큰 두 라우트가 기존 auth 미들웨어로 보호된다 (NFR-S4)
입력 검증 state · draining 은 enum · since 는 RFC3339 · work 는 정확 일치 (NFR-S5)
헤더 GET /ui/ 에 CSP · nosniff · DENY · Referrer-Policy 넷 (NFR-S6)
오류 문구에 내부 경로 · 스택이 없다 (NFR-S9)
완료 조건 — 화면. 버튼 전부를 센다
「누르면 걸린다」만으로는 빠진 버튼이 통과된다 (scene-gates.md §2.1).
재료는 requirements.md §8.3 이다.
S0 토큰 표시 · 숨김 토글 (눈 아이콘)
현황판 열기
토큰은 브라우저 세션 저장소에만 둔다
S0b 위 둘이 그대로 산다
입력 테두리가 붉다
「401 — Mediator 가 이 토큰을 받지 않았다」가 인라인으로 붙는다
S1 필터 칩 다섯 — 전체 · RUNNING · QUEUED · SUCCEEDED · FAILED
Run 행의 열기 (>) -> S2
카드에는 조작이 없다. 중앙은 읽기 전용이다
카드가 노드 수만큼 있다. 임대된 카드에 not_after 카운트다운
카드에 observed_at 과 expires_at 을 함께 얹는다 (NFR-O5)
expires_at 까지 30초 이하면 카드가 흐려진다 (NFR-O3)
draining 카드에 배지. 진행 중(C4) 과 대기 중(owner)(C5) 을 가른다 (NFR-O6)
at-boundary 카드는 카운트다운 대신 「경계에서 · 최대 <not_after>」
마지막 갱신 시각이 있다 (NFR-O2)
S1b 갱신이 연속 3회 실패하면 카운트다운이 멎고 실패 표시가 뜬다 (NFR-O4)
필터 칩과 배지의 글자는 SUCCEEDED 다. SEALED 가 아니다 (§8.6)
S2 닫기 (X) -> S1
단계 노드 고르기 -> 오른쪽 상세
「함대 격자의 이 노드로」 (->) -> S1 의 그 카드
단계 배지와 범례는 CLAIMED 다. RUNNING 이 아니다 (§8.6)
범례에 FAILED 와 ASKED 를 더한다. 원본의 넷으로는 색 없는 단계가 생긴다
트랜스크립트가 아니라 단계 상세다. produced 는 ledger 라우트를 조인한다
폴링 5초. SSE · WebSocket 없음
게이트 — CP1 「보인다」
curl -H "Authorization: Bearer $T" $M/v1/nodes | jq '.nodes[] | {label, lease, draining}'
curl -H "Authorization: Bearer $T" "$M/v1/runs?limit=5" | jq '.runs[] | {run_id, state, current}'
브라우저로 GET /ui/ — S0 에 토큰을 넣으면 S1 이 뜬다
Mediator 를 멈추면 S1b 로 간다
QUEUED 필터 칩은 CP1 시점에 값이 없는 채로 옳게 동작한다 — 어휘가 아직
안 들어왔기 때문이다. 칩이 존재하고 필터가 걸리는 것까지가 CP1 이고, 실제
QUEUED 행이 뜨는 것은 CP2 가 확인한다.
U-대기열
하는 일
| 출처 | FR |
|---|---|
enode-features.md 3.2.1 |
FR-6 (409 -> QUEUED, 202) · FR-7 (steps 를 제출 시점에) · FR-8 (WakeQueued 가 도착 순서로 전부) · FR-9 (깨우기 지점) · FR-10 (기동 시 한 번) · FR-11 (draining 노드 제외의 중앙 쪽) |
값의 근거는 requirements.md §3.1 · §3.2 다.
딛는 것
U-바닥 store.QUEUED 상수 · store.ListQueued · store.DrainingNodes
만지는 파일
internal/api/queue.go NEW 승격 · 202 응답 조립
internal/store/queue.go o WakeQueued 를 더한다. 바닥이 만든 파일이다
internal/api/api.go o submit 의 QUEUED 분기 · busy 에 DrainingNodes 합침
postResult · postCancel · postNodes 재기동 감지 뒤 깨우기
internal/store/release.go o 부분 반납 뒤 깨우기
internal/store/reap.go o Reap 뒤 깨우기
cmd/mediator/main.go o 기동 시 한 번
internal/match/match.go 는 안 만진다. busy 는 api.go 의 submit 이
만들어 넘긴다 (NFR-I4 · ADR-014 결정 3).
완료 조건
409 가 QUEUED 가 된다. 응답은 202. 422 는 그대로 FAILED
같은 run_id 재제출은 200 + 기존 Run. 202 는 새로 QUEUED 가 되는 순간만 (NFR-I7)
QUEUED Run 의 steps 가 제출 시점에 전부 PENDING 으로 있다. node_id 는 빈 문자열
runAcquires 와 raiseAsks 는 승격 시점으로 미뤄져 있다
Records.Open 은 제출 시점에 한다. 대기 중 취소가 봉인까지 닿는다
WakeQueued 가 created_at 오름차순으로 전부 훑는다. 앞이 못 가도 뒤를 안 막는다
승격은 Run 하나마다 독립 트랜잭션. 하나가 실패해도 나머지를 계속 본다
재진입 · 동시 호출 안전. 임대 기본키 충돌이 경쟁을 막는다
깨우기가 다섯 지점에 있다 — postResult · postCancel · Reap · 재기동 감지 · 부분 반납
(여섯째인 drain 해제는 U-drain 이 단다)
기동 시 WakeQueued 한 번. 재기동 후에도 QUEUED 가 참이다
reaper 주기에 안 얹었다
상태 READY 를 안 만들었다 (NFR-I2)
dry-run 응답이 안 바뀌었다 (NFR-I6)
화면 — S1 의 QUEUED 행이 어느 카드에도 안 얹혀 있다
runctl 은 고칠 것이 없다 — do 가 StatusCode/100 == 2 로 받아 202
가 통과하고, Terminal(state) 이 SUCCEEDED 와 FAILED 뿐이라 --wait 도
QUEUED 에서 계속 폴링한다 (requirements.md §2.1).
게이트 — CP2 「기다린다」
runctl submit a.json -> RUNNING
runctl submit b.json -> 202 · QUEUED
runctl status <b> -> QUEUED
a 가 끝난 뒤 runctl status <b> -> RUNNING
runctl submit c.json (없는 능력) -> 422 · FAILED
S1 에 QUEUED 행이 보이고 어느 카드에도 안 얹혀 있다
U-drain
하는 일
| 출처 | FR |
|---|---|
enode-features.md 3.2.2 |
FR-11 (후보 제외의 노드 쪽) · FR-12 (정책 파일) · FR-13 (경로 — 읽기 · 싣기 · 복사 · 되돌리기 · Worker 에 넘기기) · FR-14 (두 모드) · FR-15 (해제는 소유자 명시) · FR-16 (drain 중에도 광고) |
enode-features.md 3.2.1 |
FR-9 의 여섯째 깨우기 — 해제를 받은 광고 처리 |
정책 파일의 위치와 형식은 이 유닛의 Functional Design 이 정한다 —
decisions.md 1절이 명시적으로 거기에 뒀다. 기존 enode 설정 파일 옆, 같은
yaml 이 시작값이다.
딛는 것
U-바닥 contract.Policy{Drain} · nodes 정책 열 · UpsertAdvert 의 이전 정책 반환
U-대기열 store.WakeQueued — 해제를 감지한 광고 처리가 부른다
만지는 파일
internal/enode/policy.go NEW Read · Write. 이 패키지가 정본이다
internal/enode/advertise.go o 광고 직전에 정책을 읽어 싣는다
internal/enode/claim.go o Worker.SetDrain — 응답의 값으로 움직인다
internal/store/store.go o UpsertAdvert 가 정책 열을 채운다
internal/api/api.go o 광고 응답에 policy 한 줄
postResult 끝의 at-boundary 종료 블록
해제를 감지하면 WakeQueued
internal/store/reap.go c Cancel 을 부르기만 한다
완료 조건
정본은 노드의 파일이다. 중앙 라우트를 안 만들었다 (NFR-I3)
정책 파일은 소유자만 쓸 수 있는 권한으로 만들어진다 (NFR-S3)
데몬이 광고 직전에 파일을 읽어 광고에 싣는다
UpsertAdvert 가 nodes 열에 복사본을 두고 이전 정책을 돌려준다
광고 응답에 policy 가 실려 돌아오고 데몬이 그 값을 Worker 에 넘긴다
— 파일을 직접 읽었는데도 응답의 값을 쓰는 이유는 §4.2 에 있다
submit 이 BusyNodes 에 DrainingNodes 를 합쳐 넘긴다. match.go 는 안 만진다
graceful 새 임대만 막는다. 도는 것은 끝까지 간다
at-boundary 도는 단계를 끝까지 두고, 결과 보고 뒤 임대를 놓고 더 안 집는다
Mediator 가 postResult 끝에서 store.Cancel(run, "drain:<node_id>")
다중 노드 Run 도 Run 전체가 FAILED 다
단계 도중에 안 끊는다 (NFR-I5)
verdict 가 drain 취소와 진짜 실패를 가른다. 새 열 0 — Cancel 이 이미 쓴다
해제는 소유자 명시. 도는 Run 이 끝나도 자동으로 안 풀린다
풀면 다음 광고에서 후보로 돌아오고 WakeQueued 가 대기 Run 을 보낸다
drain 중에도 광고를 계속 보낸다. draining 을 실어서
dry-run 은 draining 을 안 본다. 응답 변경 없음 (NFR-I6)
모드 전이 graceful <-> at-boundary 는 파일로만 된다. 라우트는 CP4 에서 막는다
Reap 은 안 건드린다. 회수 UPDATE 가 임대를 쥔 Run 만 보므로 QUEUED 는
그 부분질의에 안 들어온다 — 「대기 상한 없음」이 코드에서 이미 참이다
(aidlc-state.md 실행 계획 2절).
게이트 — CP3 「돌려받는다」
화면이 없다. 정책 파일과 GET /v1/nodes 로 닫는다 — 제어판은 뒤에 온다.
단계 둘 이상인 계약을 돌린다 — 경계가 있어야 at-boundary 를 잰다
정책 파일에 at-boundary 를 쓴다
GET /v1/nodes 에 draining: at-boundary
도는 단계가 끝난 뒤 GET /v1/nodes 에 lease 없음
새 계약을 내면 그 노드로 안 간다 (409 -> QUEUED)
drain 을 푼다 -> 대기 Run 이 RUNNING
U-제어판
노드에 HTTP 서버가 처음 선다. 이 유닛이 화면 둘을 함께 진다.
하는 일
| 출처 | FR |
|---|---|
enode-features.md 3.1.2 |
FR-17 (enodectl serve) · FR-18 (프로세스 제어) · FR-19 (신원) · FR-20 (탐지 능력 읽기 전용) · FR-21 (현재 작업) · FR-23 (Mediator 연결 상태) · FR-24 (호스트 화면 S3 · S5) |
enode-features.md 3.1.2 · 3.2.2 |
FR-22 (drain 토글) |
requirements.md §8.4 ② · ③ · ③-a |
노드 전부 서브 · ?node= · 마지막 관측 캐시 (FR-25) |
딛는 것
U-관측API GET /v1/nodes · GET /v1/runs/{id} — 현재 작업과 마지막 관측을 조립한다
U-drain internal/enode 의 policy.Read · policy.Write. 자기 파서를 안 만든다
만지는 파일
internal/panel/ NEW Config · Server · New · 관측 캐시
Mediator 클라이언트 · 프로세스 제어
internal/panel/ui/ NEW 정적 파일. S3 · S5
cmd/enodectl/serve.go NEW case "serve" 의 본체
cmd/enodectl/main.go o case "serve" 등록
names · pidOf · identityOf · ownsConfig
· signalStop 을 internal/panel 로 내린다
cmd/enodectl/proc_unix.go · proc_windows.go o processAlive 를 같이 내린다
internal/enode/policy.go c Read · Write 를 부르기만 한다
internal/panel 은 internal/store 도 internal/api 도 임포트하지 않는다 —
HTTP 로만 Mediator 를 본다 (NFR-P1 · NFR-P2). U-바닥의 경계 검사가 센다.
라우트
GET / 정적 파일. S3 · S5
GET /api/nodes 이 기계의 노드 목록. 사이드바가 읽는다
GET /api/status?node= 신원 · 프로세스 · 능력 · 현재 작업
· Mediator 마지막 응답 시각 · observed · stale
GET /api/policy?node= 현재 정책
PUT /api/policy?node= 걸기와 풀기. 모드 바꾸기는 409
POST /api/start?node= 데몬 띄우기
POST /api/stop?node= 데몬 멈추기
GET /api/logs?node= 로그 꼬리
?node= 를 생략하면 names() 의 첫째이고, names() 에 없는 이름이면 404 다.
완료 조건 — 서버
enodectl serve [name]. 이 기계의 노드를 전부 서브한다. 이름은 처음 선택될 노드
별도 프로세스. 새 실행파일 없음. packaging/ 을 안 건드렸다
바인딩 기본값 127.0.0.1:8081. 밖에서 안 붙는다 (NFR-S1 · SECURITY-08)
--listen 이 127.0.0.1 밖이면 정책 파일의 panel_token 필수. 없으면 기동 거부
Mediator 토큰을 재사용하지 않는다 (NFR-S2 · SECURITY-12)
Mediator 를 부를 토큰은 그 기계의 기존 enode 설정에서 읽는다. 소스에 안 박는다 (NFR-S7)
헤더 넷을 정적 표면에 건다 (NFR-S6). CSP 가 인라인 JS · CSS 를 막는다
오류 문구에 내부 경로 · 스택이 없다 (NFR-S9)
폴링 — 5초마다 GET /v1/nodes 를 한 번. Mediator 요청이 노드 수와 무관하다
관측 캐시 — 노드마다 마지막 성공 관측. serve 가 도는 동안만 산다
/api/status 가 observed{seen_at, expires_at, lease, instance, at} 과 stale 을 낸다
캐시가 비면 observed 를 안 내고 stale 이 true
현재 작업의 출처는 Mediator 조회다. 데몬 로그를 안 읽는다
PUT /api/policy 가 모드 전이(graceful <-> at-boundary)에 409 를 낸다
탐지 능력에 편집 자리가 없다 (ADR-017 결정 3)
완료 조건 — 화면. 버튼 전부를 센다
S3 (도는 노드)
사이드바의 노드 고르기
「로그」 · 「재시작」 · 「정지」 헤더 셋
재시작은 stop 뒤 start 의 합성이다. 새 라우트 없음
graceful (라디오) · at-boundary (라디오)
「drain 걸기」
[건 뒤 = C3]
「drain 풀기 · 함대로 돌려주기」 이것이 빠지면 통과가 아니다
모드 배지 (graceful | at-boundary). 라디오는 사라진다
「지금 도는 작업 없음 — 되찾은 상태」
트랜스크립트 (->) 자리만 · 비활성
헤더의 「로그」는 데몬 로그다. 다른 물건이다
신원 블록 — node_id · label · principal · instance 전부
principal 은 이 화면에만 나온다
임대 진행 막대 — lease.not_after 와 설정의 TTLSeconds 로 그린다
Mediator 마지막 응답 시각. 실패가 이어지면 drain 통보가 늦어진다는 문구
S5 (멈춘 노드)
사이드바의 노드 고르기
「마지막 로그」 헤더 하나. 로그 · 재시작 · 정지가 아니다
「시작」 주 동작이다
drain 블록 없음 멈춘 노드는 이미 자원을 내놓은 상태다
마지막 관측 — seen_at · expires_at · lease · instance
캐시가 비면 — 로 그린다
게이트 — CP4 「한 장면」
scene-gates.md §1 의 ① ~ ⑦ 을 끝까지 돈다. 현황판과 제어판이 같은 사실을
보인다.
① dhseo 의 보드가 함대에 있다
② taeels 가 계약을 낸다 — 보드의 LED 점멸 패턴을 바꾸는 프로그램. 보드가 잡힌다
③ 둘째 계약이 온다. 보드가 하나라 QUEUED. 현황판에 둘 다 보인다
④ dhseo 가 제어판을 열어 at-boundary 로 drain 을 건다
⑤ 도는 단계가 끝난다. 경계에서 임대가 풀린다. 현황판에 draining
⑥ dhseo 가 보드를 쓴다. LED 가 새 패턴으로 깜빡인다
⑦ dhseo 가 drain 을 푼다. 대기 중이던 둘째 계약이 보드를 집는다
더해서 화면 검증 — S3 의 걸기 전과 건 뒤(C3), S1 배지가 「draining · 진행 중」
과 「draining · 대기 중(owner)」로 갈리는 것, S5 에서 start 가 뜨는 것.
U-MCP
하는 일
runctl 로 접수하던 것을 MCP 로도 접수한다. 그리고 현황판이 그리는 것과
같은 자료를 도구로 낸다.
감싼다 기존 REST 일곱. 새 라우트 0 · Mediator 무변경
전송 stdio JSON-RPC 2.0 하나. streamable HTTP 를 안 연다
인증 ENODE_MEDIATOR · ENODE_TOKEN 환경변수. runctl 과 같은 규칙
새 의존 0. 프로토콜을 손으로 짠다
새 의미가 0 개다 (ADR-022 §9.3). 이 유닛이 만드는 것은 어휘가 아니라
통로다 — 어휘가 늘면 그것은 이 유닛의 범위를 넘은 것이다.
딛는 것
U-관측API GET /v1/nodes · GET /v1/runs 가 있어야 fleet.list · runs.list 가 산다
라우트만 딛는다. 패키지를 임포트하지 않는다
다른 유닛을 하나도 안 딛는다. U-바닥도 U-대기열도 U-drain 도 U-제어판도
아니다 — 이 유닛은 HTTP 클라이언트이고 store 도 api 도 안 부른다.
만지는 파일
NEW internal/mcp/ stdio 프레이밍 · initialize · tools/list · tools/call
NEW internal/mcp/tools.go 도구 일곱을 runctl.Client 에 잇는다
o internal/runctl/client.go Nodes · Runs 두 메서드를 더한다 (FR-29)
o cmd/runctl/main.go case "mcp" 한 줄
접점이 0 이다. 위 넷을 다른 유닛이 아무도 안 만진다
(unit-boundaries.md §2 의 MCP 열). 그래서 병합 순서에 자리를 안 잡는다.
완료 조건
1 runctl mcp 가 stdio 로 서고 initialize 에 protocolVersion 을 낸다
2 tools/list 가 일곱을 낸다 — 이름이 decisions.md 5.3 의 글자와 같다
3 도구마다 inputSchema 가 있다. 필수 인자가 빠지면 오류를 낸다 (ADR-066)
4 capabilities.list 가 GET /v1/capabilities 와 같은 자료를 낸다
5 fleet.list 가 GET /v1/nodes 와 같은 자료를 낸다 — 글자로 대조한다
6 runs.list 가 GET /v1/runs 와 같은 자료를 낸다. 필터 넷을 그대로 받는다
7 run.submit 으로 낸 Run 이 현황판 S1 에 그대로 뜬다
8 run.plan 이 dry-run 이다 — 점유를 안 한다
9 run.cancel 이 200 과 상태를 낸다
10 record.get 이 종료 전이면 409 를 그대로 사상한다 (I4 를 안 우회한다)
11 토큰이 도구 인자에 없다. ENODE_TOKEN 이 비면 initialize 에서 실패한다
12 토큰이 응답 본문에도 로그에도 안 나온다 — 글자로 센다 (SECURITY-12)
13 go.mod 가 안 바뀐다. 직접 의존이 셋 그대로다
14 새 실행파일이 0 이다. packaging/ 이 안 바뀐다
15 cmd/mediator 와 internal/api 와 internal/store 를 한 줄도 안 고쳤다
게이트 — CP5 「감싼다」
scene-gates.md §2 의 CP5 다. dhseo 장면 밖이라 CP4 의 조건이 아니다 —
CP5 가 빨개도 CP4 는 초록일 수 있다.
runctl mcp 를 띄우고 tools/list 가 일곱
run.submit 으로 낸 Run 이 GET /v1/runs 와 화면 S1 에 똑같이 뜬다
fleet.list 와 GET /v1/nodes 를 글자로 대조한다
go.mod 무변경 · packaging/ 무변경 · Mediator 무변경
게이트가 그 유닛 밖의 이유로 빨간 경우
진행자가 사실을 aidlc-docs/audit.md 에 적고 조각을 보류로 표시한 뒤
넘어간다. 보류는 통과가 아니다 — CP4 전에 전부 초록이어야 한다
(scene-gates.md §4). 집행자는 그 유닛을 구현하지 않은 사람이다.