diff --git a/update_catalog/docs/README.md b/update_catalog/docs/README.md
new file mode 100644
index 0000000..2c8697d
--- /dev/null
+++ b/update_catalog/docs/README.md
@@ -0,0 +1,91 @@
+# Helm Chart Upgrade 자동화 시스템
+
+## 개요
+
+Helm Chart 업그레이드 시 기존 버전과 신규 버전 간의 변경점을 **정형 데이터(Structured Diff JSON)**로 생성하고, LLM이 이를 해석하여 변경 요약·Breaking Change 분석·업그레이드 문서를 자동으로 생성하는 시스템이다.
+
+> 핵심 원칙: LLM은 Diff 생성자가 아니라 **Diff 해석자(Summarizer)** 역할만 수행한다.
+
+---
+
+## 로드맵
+
+| Step | 설명 | 상태 |
+|------|------|------|
+| **Step 1** | Skills 구현 (helm_diff · breaking_check · generate_doc · create_pr · deploy_validate) | 🔨 구현 중 |
+| **Step 2** | Agent 프레임워크 POC (OpenClaw vs Nanobot — 클러스터 배포 후 결정) | 📋 계획 |
+| **Step 3** | Agent 워크플로 정의 (Skills를 에이전트에 등록 + 스케줄/트리거 설정) | 💡 구상 |
+| **Step 4** | 보안 정책 수립 후 운영 (RBAC · NetworkPolicy · Skill allowlist) | 💡 구상 |
+
+> **결정**: 파이프라인 오케스트레이션 코드는 구현하지 않는다.
+> Skills(핵심 로직)만 구현하고, 순서 제어는 Agent가 담당한다.
+> → [결정 배경](decisions/001-agentic-first.md)
+
+---
+
+## 전체 아키텍처 흐름
+
+```
+[Step 1] Skills (독립 실행 가능한 개별 도구)
+helm_diff → breaking_change_check → generate_upgrade_doc → update_docs_file → deploy_validate → create_pr
+
+[Step 2~3] On-Cluster Agent (OpenClaw / Nanobot)
+Agent가 위 Skills를 등록하여 워크플로로 오케스트레이션
+트리거: Cron 스케줄 또는 K8s 이벤트
+```
+
+---
+
+## 문서 구조
+
+- [status.md](status.md) — 구현 진행 현황
+
+### 설계 문서 (`design/`)
+
+아키텍처 결정 사항과 컴포넌트 상세 설계를 담는다.
+구현과 독립적으로 유지되며, 결정이 바뀔 때만 업데이트한다.
+
+| 파일 | 설명 |
+|------|------|
+| [00-architecture-overview.md](design/00-architecture-overview.md) | 전체 아키텍처 + 설계 원칙 + 기술 스택 |
+| [01-helm-diff-engine.md](design/01-helm-diff-engine.md) | Chart Version Detector · Helm Diff Engine · Values/Template/CRD Diff 설계 |
+| [02-breaking-change-rules.md](design/02-breaking-change-rules.md) | Breaking Change Rule Engine 판단 로직 + 조건 정의 |
+| [03-llm-summarizer.md](design/03-llm-summarizer.md) | LLM Summarizer · Prompt 설계 · BUILD-README 갱신 설계 |
+| [04-skill-interface.md](design/04-skill-interface.md) | Skill 인터페이스 정의 (Agent-Skill 계약) |
+| [05-on-cluster-agent.md](design/05-on-cluster-agent.md) | On-Cluster Agent 설계 (Step 2~3) + 보안 고려사항 |
+
+### 구현 계획 (`implementation/`)
+
+각 Step의 작업 목록과 실행 순서를 담는다.
+작업이 진행되면서 자주 업데이트되며, 완료 후에는 참고용으로만 유지된다.
+
+| 파일 | 설명 |
+|------|------|
+| [01-skills.md](implementation/01-skills.md) | Step 1 Skills 구현 태스크 목록 + 우선순위 + 완료 기준 |
+| [02-agent.md](implementation/02-agent.md) | Step 2~4 Agent 구현 계획 (POC → 워크플로 → 보안) |
+
+### 의사결정 기록 (`decisions/`)
+
+중요한 아키텍처 결정 사항과 배경을 ADR(Architecture Decision Record) 형식으로 유지한다.
+
+| 파일 | 설명 |
+|------|------|
+| [001-agentic-first.md](decisions/001-agentic-first.md) | 파이프라인 오케스트레이션 건너뛰고 Agent 직접 구현 결정 |
+
+---
+
+## 설계 원칙
+
+1. **LLM은 Helm CLI를 직접 실행하지 않는다.**
+2. **Diff 생성은 100% deterministic 해야 한다.**
+3. **LLM 입력은 반드시 Structured JSON 형식이다.**
+4. **Breaking Change 판단은 코드 기반 Rule Engine이 먼저 수행한다.**
+5. **LLM은 설명 및 Markdown 생성만 담당한다.**
+6. **각 컴포넌트는 독립적인 Skill로 노출한다.** (Agent가 호출하는 계약)
+7. **운영 환경은 Git PR을 통해서만 변경한다.**
+
+---
+
+## 관련 문서
+
+- [1.구조 설계(structure tool+llm summarizer).md](../1.구조%20설계(structure%20tool+llm%20summarizer).md) — 초기 설계 원본
diff --git a/update_catalog/docs/architecture-local-vs-k8s.md b/update_catalog/docs/architecture-local-vs-k8s.md
new file mode 100644
index 0000000..b35eecc
--- /dev/null
+++ b/update_catalog/docs/architecture-local-vs-k8s.md
@@ -0,0 +1,87 @@
+# OpenClaw 기반 MCP Tool 아키텍처 (로컬 vs K8s)
+
+아래는 동일한 MCP Tool 세트를 **로컬 환경**과 **K8s 환경**에서 실행할 때의 아키텍처 비교다.
+
+---
+
+## 1) 로컬 환경 아키텍처
+
+```mermaid
+flowchart LR
+ subgraph LocalHost[Local Host]
+ OC[OpenClaw Agent]
+ Skills[Skills Registry]
+ Tools["MCP Tools
(helm_diff, breaking_check,
generate_doc, update_docs, create_pr, deploy_validate)"]
+ Repo["dip-catalog Repo
(manifests/helm/...)"]
+ Files["Docs/Outputs
+(upgrade.md, PR metadata)]
+ Secrets[Local Secrets
+(.env / keychain)"]
+ end
+
+ OC -->|Skill 호출| Tools
+ OC --> Skills
+ Tools --> Repo
+ Tools --> Files
+ Tools --> Secrets
+
+ OC -->|GitHub API| GH[GitHub]
+ OC -->|LLM API| LLM[LLM Provider]
+```
+
+**특징**
+- OpenClaw와 MCP Tools가 같은 머신에서 실행
+- `chart_path`는 로컬 디렉토리 기준으로 바로 접근
+- Secrets는 로컬 파일/.env/키체인으로 관리
+- 배포 검증이 필요하면 로컬 K8s(kind/k3d) 사용
+
+---
+
+## 2) K8s 환경 아키텍처
+
+```mermaid
+flowchart LR
+ subgraph K8s["Kubernetes Cluster"]
+ OC["OpenClaw Agent Pod"]
+ Skills["Skills Registry"]
+ Tools["MCP Tools
(helm_diff, breaking_check,
generate_doc, update_docs, create_pr, deploy_validate)"]
+ Repo["Git Repo Volume
+(dip-catalog)"]
+ NS[Test Namespace]
+ end
+
+ OC -->|Skill 호출| Tools
+ OC --> Skills
+ Tools --> Repo
+ Tools --> NS
+
+ OC -->|GitHub API| GH[GitHub]
+ OC -->|LLM API| LLM[LLM Provider]
+
+ Secrets[Secrets/ConfigMap] --> OC
+ Secrets --> Tools
+ NP[NetworkPolicy] --- OC
+```
+
+**특징**
+- OpenClaw가 Pod로 실행되고 Tools는 같은 Pod 또는 별도 Service로 동작
+- repo는 볼륨으로 마운트하거나 git clone으로 동기화
+- secrets는 K8s Secret/ConfigMap으로 주입
+- deploy_validate는 테스트 네임스페이스에 실제 배포
+- NetworkPolicy/RBAC로 최소 권한 운영
+
+---
+
+## 차이 요약
+
+| 구분 | 로컬 환경 | K8s 환경 |
+|------|-----------|----------|
+| 실행 위치 | 로컬 머신 | K8s Pod/Service |
+| repo 접근 | 로컬 파일 시스템 | 볼륨 마운트/클론 |
+| secrets | .env/키체인 | Secret/ConfigMap |
+| 배포 검증 | kind/k3d | 테스트 네임스페이스 |
+| 보안 통제 | OS 권한 | RBAC/NetworkPolicy |
+
+---
+
+필요하면 이 문서를 `design/`에 이동하거나, 시스템 설계 문서에 통합해도 돼.
diff --git a/update_catalog/docs/decisions/001-agentic-first.md b/update_catalog/docs/decisions/001-agentic-first.md
new file mode 100644
index 0000000..b8f69ab
--- /dev/null
+++ b/update_catalog/docs/decisions/001-agentic-first.md
@@ -0,0 +1,99 @@
+# ADR 001: 파이프라인 오케스트레이션 건너뛰고 Agent 직접 구현
+
+**상태**: 채택 (2026-03-03)
+
+---
+
+## 배경
+
+초기 설계는 다음 3단계 로드맵이었다:
+
+```
+Phase 1: 정적 분석 파이프라인
+ (MCP Tools + Python 오케스트레이션 코드)
+ ↓
+Phase 2A: GitHub Actions로 배포 검증 추가
+ ↓
+Phase 2B: On-Cluster AI Agent (OpenClaw/Nanobot)
+```
+
+이 구조에서 Phase 1은 두 가지를 포함하고 있었다:
+1. **MCP Tools** — `helm_diff`, `breaking_check` 등 핵심 로직
+2. **파이프라인 오케스트레이션 코드** — Tools를 순서대로 호출하는 Python 코드
+
+---
+
+## 결정
+
+**파이프라인 오케스트레이션 코드는 구현하지 않는다.**
+
+MCP Tools(핵심 로직)만 구현하고, 순서 제어는 처음부터 Agent(OpenClaw/Nanobot)가 담당하도록 한다.
+
+수정된 로드맵:
+
+```
+Step 1: MCP Tools 구현 (오케스트레이션 없이)
+ ↓
+Step 2: Agent 프레임워크 POC (OpenClaw vs Nanobot)
+ ↓
+Step 3: Agent 워크플로에 MCP Tools를 Skills로 연결
+ ↓
+Step 4: 보안 정책 수립 후 운영
+```
+
+---
+
+## 이유
+
+### 낭비가 되는 코드
+
+파이프라인 오케스트레이션 코드는 Phase 2B에서 Agent 워크플로 정의로 대체된다.
+
+```python
+# 이 코드는 Phase 2B에서 쓸모없어진다
+diff = helm_diff(...)
+breaking = breaking_check(diff)
+doc = generate_doc(diff, breaking)
+pr = create_pr(doc)
+```
+
+처음부터 Agent를 목표로 한다면 이 코드를 작성하고 나중에 버리는 것은 낭비다.
+
+### MCP Tools는 낭비가 아니다
+
+반면 MCP Tools 자체(helm_diff, breaking_check 등의 구현 로직)는 Agent에서도 그대로 사용된다.
+버려지는 코드가 없다.
+
+### K8s 클러스터 보유
+
+Staging K8s 클러스터가 이미 있으므로 On-Cluster Agent를 바로 구성할 수 있다.
+"클러스터 없이 로컬 파이프라인으로 먼저 검증"이라는 이유가 성립하지 않는다.
+
+### 테스트 용이성
+
+개별 MCP Tool은 파이프라인 없이도 독립적으로 단위 테스트 가능하다.
+
+---
+
+## 결과
+
+- 구현 시간 단축: 파이프라인 오케스트레이션 코드 작성 + 나중에 Agent로 재작성하는 이중 작업 제거
+- Agent 프레임워크 선택이 중요해짐: Step 2 POC로 검증 후 결정 필요
+- 중간 산출물 없음: 파이프라인이 없으므로 MCP Tools 완성 전까지는 E2E 동작 확인 불가
+ → POC 단계에서 단일 Tool 호출부터 점진적으로 검증
+
+---
+
+## 대안으로 고려했던 것
+
+### 파이프라인 먼저 구현 후 Agent로 전환
+
+- 장점: 중간 단계에서 E2E 동작 확인 가능, 디버깅 용이
+- 단점: 파이프라인 오케스트레이션 코드가 버려짐 (낭비), 구현 기간 길어짐
+- **기각 이유**: K8s 클러스터가 이미 있어 이 중간 단계가 불필요
+
+### GitHub Actions Phase 2A 추가
+
+- 장점: 배포 검증을 클러스터 없이 CI에서 먼저 구현 가능
+- 단점: Agent에서 동일 기능을 다시 구현해야 함 (이중 작업)
+- **기각 이유**: `deploy_validate` MCP Tool로 직접 구현, Agent가 조건부 호출
diff --git a/update_catalog/docs/decisions/002-dip-catalog-structure.md b/update_catalog/docs/decisions/002-dip-catalog-structure.md
new file mode 100644
index 0000000..17fb05c
--- /dev/null
+++ b/update_catalog/docs/decisions/002-dip-catalog-structure.md
@@ -0,0 +1,50 @@
+# ADR 002: dip-catalog 디렉토리 기반 Helm Chart 구조를 기준으로 설계
+
+**상태**: 채택 (2026-03-03)
+
+---
+
+## 배경
+
+대상 카탈로그는 GitHub 레포 내 `manifests/helm///` 구조로 관리된다.
+각 버전 디렉토리는 완전한 Helm chart 구조를 포함하며, 추가 문서와 기본 배포 values가 존재한다.
+
+- `README.md`, `BUILD-README.md`, `CUSTOM-README.md` (버전 디렉토리 내)
+- `custom-values.yaml` (배포 시 기본 values)
+
+이 구조는 일반 Helm repo/index 기반 감지 방식과 다르므로, 감지/입력/문서 생성 방식을 조정해야 한다.
+
+---
+
+## 결정
+
+1. **버전 감지는 디렉토리 기반**으로 수행한다.
+ - `manifests/helm//` 하위 버전 디렉토리 변화 또는 git diff로 신규 버전 감지
+2. **helm_diff 입력에 `chart_path`를 추가**하고 dip-catalog에서는 이를 우선 사용한다.
+3. **`custom-values.yaml`을 기본 values_override로 적용**한다.
+4. **README/BUILD/CUSTOM 문서를 요약하여 LLM 입력 컨텍스트(`docs_context`)로 제공**한다.
+ - 단, 컨텍스트는 “참고용”으로만 사용하고 diff에 없는 변경을 생성하지 않는다.
+
+---
+
+## 이유
+
+- 레포 구조가 Helm repo/index.yaml 방식이 아니므로 기존 감지 방법이 부정확하다.
+- 동일 chart라도 버전 디렉토리마다 커스텀 문서 및 기본 values가 존재해,
+ 업그레이드 문서 생성에 중요한 맥락이 된다.
+
+---
+
+## 결과
+
+- MCP Tool 인터페이스 및 설계 문서에 `chart_path`, `docs_context` 반영
+- LLM Summarizer는 문서 컨텍스트를 참고하되, Structured Diff JSON을 우선한다
+
+---
+
+## 대안으로 고려했던 것
+
+### Helm repo/index 기반 감지 유지
+- 장점: 기존 설계 재사용 가능
+- 단점: dip-catalog 구조에는 적용 불가
+- **기각 이유**: 실제 운영 구조와 불일치
diff --git a/update_catalog/docs/design/00-architecture-overview.md b/update_catalog/docs/design/00-architecture-overview.md
new file mode 100644
index 0000000..b6b3b07
--- /dev/null
+++ b/update_catalog/docs/design/00-architecture-overview.md
@@ -0,0 +1,145 @@
+# 전체 아키텍처 개요
+
+## 1. 목적
+
+Helm Chart 신규 버전을 카탈로그에 자동으로 추가하고, 해당 버전으로 배포/업그레이드 시 참고할 주의사항을 문서화한다.
+
+> **카탈로그 역할**: 신규 배포를 위한 Helm 차트 버전 보관소. 운영 클러스터 직접 변경과 무관.
+
+자동화 항목:
+- 신규 버전 차트 pull 및 카탈로그 디렉토리 추가
+- Structured Diff JSON 기반 변경 분석
+- `custom-values.yaml` 수정 필요 여부(Breaking) 판단
+- 업그레이드 주의사항 문서 자동 생성 (`CUSTOM-README.md`에 추가)
+- Git PR 자동 생성
+
+---
+
+## 2. 설계 원칙
+
+| # | 원칙 |
+|---|------|
+| 1 | LLM은 Helm CLI를 직접 구성하거나 실행하지 않는다. helm 실행은 결정론적 Skill이 전담하며, Agent/LLM은 Skill을 언제 호출할지만 결정한다 |
+| 2 | Diff 생성은 100% deterministic 해야 한다 |
+| 3 | LLM 입력은 반드시 Structured JSON 형식이다 |
+| 4 | Breaking Change 판단은 코드 기반 Rule Engine이 먼저 수행한다 |
+| 5 | LLM은 설명 및 Markdown 생성만 담당한다 |
+| 6 | 각 컴포넌트는 Agent Skill로 노출한다 (Agent가 직접 호출) |
+| 7 | 카탈로그 업데이트는 Git PR로 관리한다 (운영 환경 직접 변경 아님) |
+| 8 | 외부 입력(PR comment, webhook 등)은 신뢰 경계 밖으로 취급하며, 프롬프트 주입 방어를 기본 전제로 한다 |
+
+## 2.1 운영 기준 (Non-Functional)
+
+- **성능**: Diff 생성 + 요약 전체 파이프라인 5분 내 완료를 목표로 한다 (대형 차트는 예외).
+- **신뢰성**: 실패 시 재시도 3회, 재시도 후 실패는 알림 전송 + 중단.
+- **검토 정책**: `breaking=true` PR에는 `needs-review` 레이블을 부착한다. 담당자가 `custom-values.yaml` 수정 후 merge 여부를 판단한다. (`breaking=false` PR은 자동 merge 가능)
+
+---
+
+## 3. 전체 시스템 아키텍처
+
+### Step 1: Skills (에이전트 내부에서 호출되는 개별 기능)
+
+```
+[Chart Version Detector]
+ │ chart명, current/new version
+ ▼
+[chart_updater Skill]
+ │ helm pull → manifests/helm/// 생성
+ │ CUSTOM-README.md carry-over from previous version
+ ▼
+[helm_diff Skill]
+ │ values/template/CRD 비교
+ ▼
+[Structured Diff JSON]
+ │ { values, templates, crd, dependencies }
+ ▼
+[breaking_change_check Skill]
+ │ custom-values.yaml 기준 판단 (LLM 없음)
+ │ breaking=true: custom-values.yaml 수정 필요
+ │ breaking=false: 수정 불필요 (주의사항만)
+ ▼
+[generate_upgrade_doc Skill] ← 항상 실행
+ │ breaking=true → LLM 상세 가이드 (custom-values.yaml 수정 방법 포함)
+ │ breaking=false → 템플릿 기반 간단 요약
+ ▼
+[update_docs_file Skill]
+ │ CUSTOM-README.md에 업그레이드 주의사항 섹션 추가
+ ▼
+[create_pr Skill]
+ │ branch 생성 → PR 생성 (항상)
+ │ breaking=true → label: needs-review
+ │ breaking=false → label: auto-update
+```
+
+> 각 Skill은 독립적으로 호출 가능. 호출 순서는 Agent(Step 2~3)가 담당.
+
+> `deploy_validate` Skill은 Phase 2에서 구현 예정 (현재 스코프 밖).
+
+### Step 2~3: On-Cluster AI Agent
+
+```
+[OpenClaw / Nanobot on K8s]
+ │
+ ├── Skill: chart_version_detector → 신규 버전 감지
+ ├── Skill: chart_updater → 신규 버전 차트 pull
+ ├── Skill: helm_diff → Structured Diff JSON 생성
+ ├── Skill: breaking_check → custom-values.yaml 수정 필요 여부 판단
+ ├── Skill: generate_doc → 업그레이드 주의사항 문서 생성 (항상)
+ ├── Skill: update_docs → CUSTOM-README.md 업데이트
+ ├── Skill: create_pr → GitHub PR 생성
+ └── Channel: Discord → 알림 발송 (추후 Slack으로 변경 가능)
+
+트리거: Cron 스케줄 또는 K8s 이벤트
+```
+
+---
+
+## 4. 컴포넌트 책임 분리
+
+| 컴포넌트 | 역할 | 결정성 | LLM 사용 |
+|---------|------|--------|---------|
+| Chart Version Detector | 신규 버전 감지 | ✅ | ❌ |
+| Chart Updater | 신규 버전 차트 pull → 버전 디렉토리 생성 | ✅ | ❌ |
+| Helm Diff Engine | Structured Diff JSON 생성 | ✅ | ❌ |
+| Breaking Change Rule Engine | custom-values.yaml 수정 필요 여부 판단 | ✅ | ❌ |
+| LLM Summarizer | 업그레이드 주의사항 문서 생성 | ❌ | ✅ (breaking=true 시만) |
+| Docs Updater | CUSTOM-README.md 업그레이드 주의사항 섹션 추가 | ✅ | ❌ |
+| Git PR Bot | branch / commit / PR 생성 | ✅ | ❌ |
+
+---
+
+## 5. 기술 스택
+
+| 영역 | 선택 | 비고 |
+|------|------|------|
+| Helm CLI | helm 3.x | pull, template, diff |
+| 버전 감지 | ArtifactHub API / GitHub Release Webhook | |
+| Diff 처리 | Python (deepdiff 또는 직접 구현) | |
+| Rule Engine | Python | 코드 기반, custom-values.yaml 기준 |
+| LLM | Claude (Anthropic) | via Skill or API, breaking=true 시만 호출 |
+| 문서 저장 | `manifests/helm///CUSTOM-README.md` | 업그레이드 주의사항 섹션 추가 |
+| PR 생성 | GitHub API (PyGithub / gh CLI) | |
+| 스케줄링 | Agent 내장 스케줄러 (Cron) | Step 2~3 |
+| On-Cluster Agent | OpenClaw / Nanobot | Step 2~3 |
+| Skill Contract | JSON 스키마 (Skill I/O) | Step 1 계약 (Agent-Skill 인터페이스) |
+
+---
+
+## 6. 데이터 흐름 요약
+
+```
+입력: chart명 + current_version + new_version (레포 내 디렉토리 기준)
+중간: Structured Diff JSON (values/templates/crd/breaking) + CUSTOM-README.md (docs_context)
+출력: CUSTOM-README.md 업그레이드 주의사항 섹션 + GitHub PR
+```
+
+---
+
+## 7. 관련 설계 문서
+
+- [01-helm-diff-engine.md](01-helm-diff-engine.md) — Diff Engine 상세 설계
+- [02-breaking-change-rules.md](02-breaking-change-rules.md) — Rule Engine 판단 로직
+- [03-llm-summarizer.md](03-llm-summarizer.md) — LLM Summarizer + Docs Updater
+- [04-skill-interface.md](04-skill-interface.md) — Skill 인터페이스 정의
+- [05-on-cluster-agent.md](05-on-cluster-agent.md) — On-Cluster Agent 설계 (Step 2~3)
diff --git a/update_catalog/docs/design/01-helm-diff-engine.md b/update_catalog/docs/design/01-helm-diff-engine.md
new file mode 100644
index 0000000..06f1109
--- /dev/null
+++ b/update_catalog/docs/design/01-helm-diff-engine.md
@@ -0,0 +1,281 @@
+# Helm Diff Engine 설계
+
+## 1. 개요
+
+두 버전의 Helm Chart를 비교하여 **Structured Diff JSON**을 생성하는 컴포넌트.
+모든 출력은 deterministic하며 LLM을 사용하지 않는다.
+
+---
+
+## 2. Chart Version Detector
+
+### 역할
+새로운 Helm Chart 버전 출시를 감지한다.
+
+### 감지 방법
+
+| 방법 | 설명 | 적합 대상 |
+|------|------|---------|
+| **Repo 디렉토리 스캔** | `manifests/helm//` 하위 버전 디렉토리 변화를 감지 | dip-catalog 구조 |
+| Git diff 기반 감지 | 최신 커밋에서 추가된 `/` 디렉토리 파악 | dip-catalog 구조 |
+| Helm repo `index.yaml` 주기 스캔 | 등록된 repo의 index 파일 직접 파싱 | 외부/사내 Helm repo |
+| ArtifactHub API 조회 | `https://artifacthub.io/api/v1/packages/helm/{org}/{chart}` | 공개 chart |
+| GitHub Release Webhook | 차트 소스 저장소의 release 이벤트 구독 | GitHub 기반 차트 |
+| Cron 기반 스케줄링 | 위 방법들을 주기적으로 실행 | 공통 |
+
+### 출력
+
+```json
+{
+ "chart": "airflow",
+ "repo": "apache",
+ "current_version": "1.2.3",
+ "latest_version": "1.3.0",
+ "detected_at": "2025-01-01T00:00:00Z"
+}
+```
+
+### dip-catalog 차트 업데이트 흐름 (버전 디렉토리 유지)
+
+신규 버전이 감지되면 dip-catalog 구조에 맞춰 **버전 디렉토리를 추가**한다.
+기존 버전 디렉토리는 유지한다.
+
+```
+1) helm pull / --version # 최신 차트 다운로드
+2) tar xzf -.tgz # 압축 해제
+3) manifests/helm/// 로 이동 # 버전 디렉토리 생성
+4) chart_updater: 이전 버전 디렉토리에서 파일 복사
+ - custom-values.yaml → 그대로 복사
+ - CUSTOM-README.md → 그대로 복사 (배포 관련 내용 유지)
+ - BUILD-README.md → 복사 + 버전 번호 치환 (from_version → to_version)
+5) generate_upgrade_doc → CUSTOM-README.md에 업그레이드 주의사항 섹션 추가 (항상 실행)
+```
+
+---
+
+## 3. Helm Diff Engine
+
+### 입력
+
+```json
+{
+ "chart": "airflow",
+ "from_version": "1.2.3",
+ "to_version": "1.3.0",
+ "values_override": {},
+ "chart_path": "manifests/helm/airflow/1.3.0"
+}
+```
+
+> dip-catalog 구조에서는 `chart_path`를 우선 사용한다.
+
+### 처리 단계
+
+```
+1. (repo 구조) helm pull / --version → chart_old/
+2. (repo 구조) helm pull / --version → chart_new/
+1'. (dip-catalog) manifests/helm/// 복사 → chart_old/
+2'. (dip-catalog) manifests/helm/// 복사 → chart_new/
+3. values.yaml 비교 → Values Diff
+4. helm template 결과 비교 → Template Diff
+5. CRD schema 비교 → CRD Diff
+6. Chart.yaml 비교 → Dependency Diff
+7. 결과 병합 → Structured Diff JSON
+```
+
+### helm template 표준 옵션
+
+재현성을 위해 아래 옵션을 고정한다.
+
+```
+helm template \
+ --values values_override.yaml \
+ --include-crds \
+ --kube-version \
+ --api-versions
+```
+
+> `target_k8s_version`과 `api-versions`는 환경별 설정값으로 관리한다.
+> dip-catalog의 경우 `custom-values.yaml`이 존재하면 `values_override` 기본값으로 적용한다.
+
+### 에러 처리
+
+| 상황 | 대응 |
+|------|------|
+| helm pull 실패 | 재시도 3회 → 실패 시 알림 후 중단 |
+| chart 미존재 | 로그 기록 + 스킵 |
+| helm template 렌더링 오류 | 오류 내용 JSON에 포함, partial diff 생성 |
+| 네트워크 타임아웃 | 60s timeout 설정, 재시도 |
+
+---
+
+## 4. Values Diff 설계
+
+### 비교 항목
+
+- `added`: 신규 버전에서 추가된 key
+- `removed`: 신규 버전에서 삭제된 key
+- `changed`: default 값이 변경된 key
+- `type_changed`: 값의 타입이 변경된 key (string → int 등)
+
+### 처리 방식
+
+values.yaml을 **flat key** 형태로 변환 후 비교한다.
+
+- dip-catalog에서는 `custom-values.yaml`을 기본 values_override로 사용한다.
+
+```yaml
+# 중첩 구조 예시
+image:
+ tag: 1.2.3
+ pullPolicy: IfNotPresent
+
+# flat key 변환 결과
+image.tag: 1.2.3
+image.pullPolicy: IfNotPresent
+```
+
+> **주의**: key 이동(rename)은 `removed` + `added`로 표현된다. 의미적 rename 감지는 기본 비활성화하며, 필요한 경우 heuristic(유사도 기반) 옵션으로 제공한다.
+
+### 출력
+
+```json
+{
+ "values": {
+ "added": ["resources.limits.cpu", "resources.limits.memory"],
+ "removed": ["ingress.enabled"],
+ "changed": {
+ "image.tag": { "old": "1.2.3", "new": "1.3.0" },
+ "replicaCount": { "old": 1, "new": 2 }
+ },
+ "type_changed": [
+ { "key": "workers.replicas", "old_type": "string", "new_type": "integer" }
+ ]
+ }
+}
+```
+
+---
+
+## 5. Template Diff 설계
+
+### 처리 방식
+
+```bash
+helm template --values values_override.yaml > old.yaml
+helm template --values values_override.yaml > new.yaml
+```
+
+YAML을 **리소스 단위**로 분리 후 비교한다 (`kind` + `metadata.name` 기준).
+- `generateName`만 존재하는 리소스는 템플릿 파일명 + 순번으로 안정적 ID를 생성한다.
+- Cluster-scoped 리소스는 namespace를 무시한다.
+
+### 비교 리소스 타입
+
+| 리소스 | 분석 항목 |
+|--------|---------|
+| Deployment / StatefulSet | container image, env, resource limits, replicas |
+| Service | port, targetPort, type |
+| Ingress | rules, TLS, annotations |
+| ConfigMap | data key 추가/삭제/변경 |
+| CRD | 별도 CRD Diff로 처리 |
+| ServiceAccount | annotations |
+
+### 출력
+
+```json
+{
+ "templates": {
+ "Deployment/airflow-scheduler": {
+ "image_changed": true,
+ "image": { "old": "apache/airflow:1.2.3", "new": "apache/airflow:1.3.0" },
+ "env_added": ["AIRFLOW__CORE__NEW_SETTING"],
+ "env_removed": [],
+ "resource_limits_changed": true
+ },
+ "Service/airflow-webserver": {
+ "port_changed": false
+ }
+ }
+}
+```
+
+---
+
+## 6. CRD Diff 설계
+
+### 비교 항목
+
+| 항목 | 설명 |
+|------|------|
+| schema 변경 | OpenAPI v3 schema 필드 변경 |
+| required 필드 추가 | 기존 CR에 영향 |
+| field 제거 | 기존 CR의 해당 필드 무시됨 |
+| version 변경 | storage version 변경 시 migration 필요 |
+| webhook 변경 | conversion webhook 추가/제거 |
+
+### 출력
+
+```json
+{
+ "crd": {
+ "AirflowCluster": {
+ "changed": true,
+ "breaking": true,
+ "breaking_reasons": ["required field added: spec.executor"],
+ "schema_changed": true,
+ "version_changed": false,
+ "fields_removed": [],
+ "fields_added": ["spec.executor"],
+ "required_fields_added": ["spec.executor"]
+ }
+ }
+}
+```
+
+---
+
+## 7. Dependency Diff 설계
+
+Chart.yaml의 `dependencies` 블록 비교.
+
+```json
+{
+ "dependencies": {
+ "added": ["redis"],
+ "removed": [],
+ "version_changed": {
+ "postgresql": { "old": "12.1.0", "new": "13.0.0" }
+ }
+ }
+}
+```
+
+---
+
+## 8. 최종 Structured Diff JSON
+
+LLM Summarizer에 전달되는 통합 출력:
+
+```json
+{
+ "chart": "airflow",
+ "from_version": "1.2.3",
+ "to_version": "1.3.0",
+ "generated_at": "2025-01-01T00:00:00Z",
+ "values": { ... },
+ "templates": { ... },
+ "crd": { ... },
+ "dependencies": { ... },
+ "errors": []
+}
+```
+
+`errors` 필드에는 렌더링 실패 등의 부분적 오류를 포함한다. LLM은 이를 참고하여 분석 범위를 명시해야 한다.
+
+---
+
+## 9. 관련 문서
+
+- [02-breaking-change-rules.md](02-breaking-change-rules.md) — Breaking Change 판단 로직
+- [04-skill-interface.md](04-skill-interface.md) — `helm_diff` Skill 인터페이스
diff --git a/update_catalog/docs/design/02-breaking-change-rules.md b/update_catalog/docs/design/02-breaking-change-rules.md
new file mode 100644
index 0000000..efae7bc
--- /dev/null
+++ b/update_catalog/docs/design/02-breaking-change-rules.md
@@ -0,0 +1,158 @@
+# Breaking Change Rule Engine 설계
+
+## 1. 개요
+
+Structured Diff JSON을 입력받아 **코드 기반**으로 Breaking Change 여부를 판단한다.
+LLM을 사용하지 않으며, 결과는 완전히 deterministic하다.
+
+> **카탈로그 맥락에서의 Breaking 정의**: `custom-values.yaml`을 수정해야 하는 상황.
+> 카탈로그는 신규 배포를 위한 차트 보관소이며, 운영 클러스터 직접 변경과 무관하다.
+> Breaking 판단의 기준은 "기존 `custom-values.yaml`이 새 차트 버전에서 유효한가?"이다.
+
+> 설계 의도: LLM이 "이건 Breaking인 것 같아"라고 추론하는 대신, 코드가 명확한 기준으로 판단한다.
+
+---
+
+## 2. Breaking Change 판단 기준
+
+### 2.0 공통 원칙
+
+모든 values 관련 규칙에서 **`custom-values.yaml`에 해당 key가 존재하는지 여부**가 breaking 판단의 핵심 조건이다.
+
+- `custom-values.yaml`이 없거나 해당 key를 사용하지 않는 경우 → `warning`으로 처리 (실제 수정 불필요)
+- `custom-values.yaml`에 해당 key가 존재하는 경우 → `breaking`으로 처리 (수정 필요)
+
+### 2.1 Values 관련
+
+| 조건 | 판정 | 조건 상세 |
+|------|------|---------|
+| values key 삭제 | ✅ Breaking | 해당 key가 `custom-values.yaml`에 **있을** 때 — 기존 override가 무시됨 |
+| values key 삭제 | ⚠️ Warning | 해당 key가 `custom-values.yaml`에 **없을** 때 (또는 custom-values.yaml 없음) |
+| values type 변경 | ✅ Breaking | 해당 key가 `custom-values.yaml`에 **있을** 때 — 파싱 오류 또는 예상치 못한 동작 |
+| values type 변경 | ⚠️ Warning | 해당 key가 `custom-values.yaml`에 **없을** 때 |
+| values key 추가 | ℹ️ Info | 항상 — 신규 기능, 필요 시 custom-values.yaml에 추가 검토 |
+| default 값 변경 | ⚠️ Warning | custom-values.yaml에서 명시적으로 override하지 않는 경우 동작 변경 가능 |
+
+> **`custom-values.yaml` 없는 차트**: 해당 차트에 custom override가 없으므로 values 삭제는 모두 `warning`으로 처리.
+
+### 2.2 Template / 리소스 관련
+
+카탈로그 맥락에서 Template 변경은 `custom-values.yaml` 수정 요인이 아니므로 **Warning**으로 처리한다.
+담당자가 신규 배포 시 참고할 수 있도록 업그레이드 주의사항 문서에 기록한다.
+
+| 조건 | 판정 | 이유 |
+|------|------|------|
+| Service port 변경 | ⚠️ Warning | 카탈로그 신규 배포 시 주의사항. custom-values.yaml에서 port override 가능 |
+| Service type 변경 (ClusterIP → NodePort 등) | ⚠️ Warning | 네트워크 구성 변경 필요, 주의사항으로 기록 |
+| Deployment selector 변경 | ⚠️ Warning | 재배포 시 주의사항. custom-values.yaml 수정 요인 아님 |
+| StatefulSet selector 변경 | ⚠️ Warning | 동일 |
+| StatefulSet volumeClaimTemplate 변경 | ⚠️ Warning | 동일 |
+| container 이름 변경 | ⚠️ Warning | 참조 변경 필요, 주의사항 기록 |
+| 리소스 삭제 | ⚠️ Warning | 의존 서비스 변경 필요, 주의사항 기록 |
+| resource limits 변경 | ℹ️ Info | 참고 사항 |
+| replicas 기본값 변경 | ℹ️ Info | custom-values.yaml에서 명시적 설정 시 영향 없음 |
+
+### 2.3 CRD 관련
+
+CRD 변경은 기존 Custom Resource의 유효성에 영향을 미치므로 Breaking으로 처리한다.
+
+| 조건 | 판정 | 이유 |
+|------|------|------|
+| CRD field 삭제 | ✅ Breaking | 기존 CR의 해당 필드 손실 |
+| required field 추가 | ✅ Breaking | 기존 CR validation 실패 |
+| storage version 변경 | ✅ Breaking | migration 없이 롤백 불가 |
+| conversion webhook 제거 | ✅ Breaking | 구버전 API 호출 실패 |
+| field 추가 (optional) | ❌ Not Breaking | 하위 호환 |
+| schema 제약 완화 | ❌ Not Breaking | 기존 CR은 계속 유효 |
+
+### 2.4 Dependency 관련
+
+서브차트의 values를 `custom-values.yaml`에서 override하는 경우에만 breaking이다.
+
+| 조건 | 판정 | 조건 상세 |
+|------|------|---------|
+| 하위 chart major version 변경 | ✅ Breaking | `custom-values.yaml`에 해당 subchart prefix key가 **있을** 때 (예: `postgresql.*`) |
+| 하위 chart major version 변경 | ⚠️ Warning | `custom-values.yaml`에 해당 subchart 관련 key가 **없을** 때 |
+| 하위 chart 제거 | ⚠️ Warning | 항상 — 주의사항으로 기록 |
+| 하위 chart 추가 | ℹ️ Info | 항상 — 신규 리소스 생성만 발생 |
+
+### 2.5 Deprecated K8s API 감지
+
+| 조건 | 판정 | 이유 |
+|------|------|------|
+| Deprecated API 사용 (extensions/v1beta1 등) | ⚠️ Warning | K8s 버전에 따라 영향 있을 수 있음 |
+
+감지 방법: `helm template` 결과의 `apiVersion` 필드를 K8s deprecated API 목록과 대조.
+
+---
+
+## 3. Rule Engine 출력
+
+## 2.6 Rule 우선순위 및 충돌 처리
+
+- 동일 리소스에 여러 Rule이 매칭되면 **가장 높은 severity**를 최종 severity로 채택한다.
+- reasons는 모두 남기되, `severity`는 max 기준으로 집계한다.
+- `breaking=true`는 `reasons`에 항목이 하나라도 있을 때만 설정된다.
+
+### 3.1 기본 출력
+
+```json
+{
+ "breaking": true,
+ "severity": "high",
+ "reasons": [
+ {
+ "type": "values_key_removed",
+ "key": "ingress.enabled",
+ "detail": "custom-values.yaml에서 사용 중인 key가 삭제됨 — custom-values.yaml 수정 필요"
+ }
+ ],
+ "warnings": [
+ {
+ "type": "service_port_changed",
+ "resource": "Service/airflow-webserver",
+ "detail": "port changed from 8080 to 8081 — 신규 배포 시 참고"
+ },
+ {
+ "type": "values_key_removed",
+ "key": "old_setting",
+ "detail": "custom-values.yaml에서 사용하지 않는 key 삭제 — 수정 불필요"
+ }
+ ]
+}
+```
+
+### 3.2 severity 정의
+
+| severity | 조건 |
+|----------|------|
+| `critical` | CRD storage version 변경 |
+| `high` | custom-values.yaml에서 사용 중인 values key 삭제/타입 변경, CRD required field 추가 |
+| `medium` | custom-values.yaml에서 사용 중인 subchart dependency major version 변경 |
+| `warning` | Deprecated API 사용, 미사용 values key 삭제, template 변경 (port, resource 등) |
+
+---
+
+## 4. 확장 방법
+
+새로운 Breaking 조건을 추가하려면 Rule 정의 파일에 항목을 추가한다.
+각 Rule은 다음 인터페이스를 구현한다:
+
+```python
+class BreakingRule:
+ name: str
+ severity: str # critical / high / medium / warning
+
+ def check(self, diff: StructuredDiffJSON, custom_keys: set[str]) -> list[BreakingReason]:
+ ...
+```
+
+Rule 목록은 설정 파일(`rules.yaml`)로 활성화/비활성화 가능하도록 설계한다.
+
+---
+
+## 5. 관련 문서
+
+- [01-helm-diff-engine.md](01-helm-diff-engine.md) — Structured Diff JSON 생성
+- [03-llm-summarizer.md](03-llm-summarizer.md) — Breaking Change 결과를 LLM에 전달
+- [04-skill-interface.md](04-skill-interface.md) — `breaking_change_check` Skill 인터페이스
diff --git a/update_catalog/docs/design/03-llm-summarizer.md b/update_catalog/docs/design/03-llm-summarizer.md
new file mode 100644
index 0000000..224cbf6
--- /dev/null
+++ b/update_catalog/docs/design/03-llm-summarizer.md
@@ -0,0 +1,243 @@
+# LLM Summarizer + Docs Updater 설계
+
+## 1. 개요
+
+Structured Diff JSON과 Breaking Change 판단 결과를 입력받아 자연어 업그레이드 주의사항 문서를 생성하고,
+해당 차트 버전의 `CUSTOM-README.md`에 반영한다.
+
+> LLM의 유일한 역할: Structured JSON → Markdown 문서 변환. Diff 생성이나 Breaking 판단은 수행하지 않는다.
+
+---
+
+## 2. LLM Summarizer
+
+### 2.0 실행 조건
+- `generate_upgrade_doc`은 **항상 실행**한다 (breaking 여부 무관).
+- `breaking=true` → LLM을 사용하여 `custom-values.yaml` 수정 방법을 포함한 상세 가이드 생성.
+- `breaking=false` → 템플릿 기반 간단 요약 생성 (LLM 미호출).
+
+> 카탈로그는 신규 배포를 위한 차트 보관소이므로, breaking 여부와 무관하게 모든 업그레이드에 주의사항 기록이 필요하다.
+
+### 2.1 입력
+
+```json
+{
+ "chart": "airflow",
+ "from_version": "1.2.3",
+ "to_version": "1.3.0",
+ "values": { ... },
+ "templates": { ... },
+ "crd": { ... },
+ "dependencies": { ... },
+ "breaking": true,
+ "severity": "high",
+ "breaking_reasons": [
+ {
+ "type": "values_key_removed",
+ "key": "ingress.enabled",
+ "detail": "custom-values.yaml에서 사용 중인 key가 삭제됨 — 수정 필요"
+ }
+ ],
+ "warnings": [
+ {
+ "type": "service_port_changed",
+ "resource": "Service/airflow-webserver",
+ "detail": "port changed from 8080 to 8081"
+ }
+ ],
+ "docs_context": {
+ "CUSTOM-README.md": "..."
+ }
+}
+```
+
+> dip-catalog 구조에서는 버전 디렉토리 내 `CUSTOM-README.md` 내용을 `docs_context`로 제공한다.
+
+### 2.2 Prompt 설계
+
+```
+당신은 Kubernetes Helm 업그레이드 주의사항 문서를 작성하는 전문가입니다.
+
+두 Helm 차트 버전 간 변경점을 담은 Structured Diff JSON이 제공됩니다.
+이 카탈로그는 신규 배포를 위한 것이며, custom-values.yaml 수정 필요 여부가 핵심입니다.
+
+## 작업
+1. 핵심 변경 사항을 평문으로 요약합니다.
+2. custom-values.yaml 수정이 필요한 항목(breaking_reasons)을 상세히 설명합니다.
+3. 신규 배포 시 참고할 주의사항(warnings)을 기록합니다.
+4. 아래 형식의 간결한 Markdown 문서를 생성합니다.
+
+## 규칙
+- 입력 JSON에 없는 내용은 절대 추가하지 마세요.
+- 추측하지 마세요.
+- docs_context는 보조 설명에만 사용하고, diff에 없는 변경을 추가하지 마세요.
+- errors가 있으면 분석이 불완전할 수 있음을 명시하세요.
+- 배포 담당자가 바로 참고할 수 있도록 명확하게 작성하세요.
+
+## 출력 형식
+아래 Markdown 구조를 정확히 지키세요 (`## {to_version}` 헤더는 포함하지 마세요 — `update_docs_file`이 관리):
+
+### 변경 요약
+- from_version:
+- to_version:
+- <핵심 변경 사항 bullet points>
+
+### custom-values.yaml 수정 필요 항목
+
+
+
+### 배포 시 주의사항
+
+<없으면 섹션 생략>
+
+### 참고
+- severity:
+- breaking:
+
+---
+
+Input:
+
+```
+
+### 2.3 토큰 예산 관리
+
+대형 chart(airflow, kafka 등)는 Structured Diff JSON이 매우 클 수 있다.
+
+| 전략 | 설명 |
+|------|------|
+| 중요도 기반 필터링 | breaking_reasons와 changed 항목만 포함, unchanged는 제외 |
+| template diff 요약 | 리소스별 상세 diff 대신 `변경된 리소스 목록`만 전달 |
+| 청크 분할 | values / templates / crd를 각각 별도 LLM 호출 후 결과 합산 |
+| 토큰 상한 설정 | 입력 JSON 최대 크기 제한 (예: 50,000 tokens), 초과 시 요약 버전 사용 |
+
+### 2.4 출력 검증
+
+- **금지 규칙**: 입력 JSON에 없는 버전/리소스를 언급하면 실패 처리.
+- **서식 검증**: Markdown 구조가 규정과 다르면 1회 재생성, 실패 시 fallback 템플릿으로 대체.
+- **요약 품질 기준**: breaking_reasons 누락/허위 서술은 오류로 간주.
+
+### 2.5 예상 출력 (breaking=true)
+
+> `update_docs_file`이 `## {to_version}` 헤더를 추가하므로, LLM 출력은 `###` 수준부터 시작한다.
+
+```markdown
+### 변경 요약
+- from_version: 1.2.3
+- to_version: 1.3.0
+- Image tag 1.2.3 → 1.3.0 업데이트
+- CPU limit 기본값 추가 (all containers)
+- 새 환경변수 `AIRFLOW__CORE__NEW_SETTING` 추가 (scheduler)
+
+### custom-values.yaml 수정 필요 항목
+- **`ingress.enabled` 키 삭제**: 차트에서 해당 key가 제거되었습니다.
+ 현재 custom-values.yaml에서 `ingress.enabled: true`로 설정하고 있는 경우,
+ 신규 방식(`ingress.create: true` 등)으로 수정이 필요합니다.
+
+### 배포 시 주의사항
+- **Service port 변경**: `airflow-webserver` port 8080 → 8081.
+ Ingress, load balancer 설정 확인 필요.
+
+### 참고
+- severity: high
+- breaking: true
+```
+
+### 2.6 예상 출력 (breaking=false)
+
+> 템플릿 기반 fallback 출력 (LLM 미호출).
+
+```markdown
+### 변경 요약
+- from_version: 1.2.3
+- to_version: 1.3.0
+- Chart airflow 1.2.3 → 1.3.0 업데이트
+- Values: +2 추가 / -0 삭제 / ~3 변경
+- Templates: +0 추가 / -0 삭제
+
+### custom-values.yaml 수정 필요 항목
+없음
+
+### 참고
+- severity: warning
+- breaking: false
+```
+
+---
+
+## 3. Docs Updater 설계
+
+### 3.1 역할
+
+생성한 업그레이드 주의사항 Markdown을 해당 차트 버전의 `CUSTOM-README.md`에 반영한다.
+(`CUSTOM-README.md`는 배포에 관한 내용을 포함하는 문서로, 업그레이드 주의사항의 적합한 위치다.)
+
+### 3.2 처리 방식
+
+```
+1. diff/breaking 결과로 업그레이드 주의사항 생성 (breaking=true면 LLM, 아니면 템플릿)
+2. manifests/helm///CUSTOM-README.md에 반영
+ - "# Upgrade History" 섹션이 없으면 파일 끝에 추가
+ - 해당 버전 항목이 이미 있으면 skip (idempotent)
+3. 파일 저장
+```
+
+### 3.3 파일 구조 규칙
+
+- 각 차트 버전 디렉토리의 `CUSTOM-README.md`에 업그레이드 주의사항을 추가한다.
+- `CUSTOM-README.md`는 `chart_updater`가 이전 버전에서 carry-over하므로 기존 배포 내용은 유지된다.
+- `BUILD-README.md`는 차트 메타 정보(repo, 설치 명령어 등)를 유지한다.
+- 별도의 `docs/upgrade.md`는 생성하지 않는다.
+
+### 3.4 중복 방지
+
+이미 해당 버전 업그레이드 섹션이 존재하면 덮어쓰기 또는 스킵한다 (설정으로 제어).
+
+---
+
+## 4. Git PR Bot 설계
+
+### 4.1 자동화 단계
+
+```
+1. 새 branch 생성: update-{chart}/{to_version}
+2. 카탈로그 신규 버전 디렉토리 커밋 (manifests/helm///)
+3. CUSTOM-README.md 업그레이드 주의사항 섹션 포함
+4. git commit
+5. PR 생성
+```
+
+### 4.2 PR 메타데이터
+
+**PR 제목**:
+```
+update {chart}: {from_version} → {to_version}
+```
+
+**PR labels**:
+- `needs-review` (breaking=true 시) — 담당자 확인 권장
+- `auto-update` (breaking=false 시) — 자동 업데이트, 검토 선택적
+
+**PR description**:
+```markdown
+## Helm Chart Update: {chart} `{from_version}` → `{to_version}`
+
+**Severity**: `high`
+**Breaking**: ✅ custom-values.yaml 수정 필요 (담당자 확인 권장)
+
+### Breaking Changes
+- `[values_key_removed]` ingress.enabled — custom-values.yaml에서 사용 중인 key 삭제
+
+### Warnings
+- `[service_port_changed]` Service/airflow-webserver — port 8080 → 8081
+
+---
+*Generated by update-catalog automation*
+```
+
+---
+
+## 5. 관련 문서
+
+- [02-breaking-change-rules.md](02-breaking-change-rules.md) — Breaking Change 입력 생성
+- [04-skill-interface.md](04-skill-interface.md) — `generate_upgrade_doc`, `create_pr` Skill
diff --git a/update_catalog/docs/design/04-skill-interface.md b/update_catalog/docs/design/04-skill-interface.md
new file mode 100644
index 0000000..662ab06
--- /dev/null
+++ b/update_catalog/docs/design/04-skill-interface.md
@@ -0,0 +1,242 @@
+# Skill 인터페이스 정의
+
+## 1. 개요
+
+각 컴포넌트를 **OpenClaw Skill**로 노출한다.
+이 인터페이스는 Skills(Step 1)와 On-Cluster Agent(Step 2~3) 간의 **계약(contract)**이다.
+
+> Agent(OpenClaw/Nanobot)가 이 Skill들을 워크플로로 호출한다.
+
+---
+
+## 2. Skill 목록
+
+| Skill 이름 | 역할 | 해당 컴포넌트 |
+|-----------|------|-------------|
+| `helm_diff` | 두 버전 간 Structured Diff JSON 생성 | Helm Diff Engine |
+| `breaking_change_check` | Diff JSON에서 Breaking Change 판단 | Breaking Change Rule Engine |
+| `generate_upgrade_doc` | 업그레이드 주의사항 Markdown 문서 생성 (항상 실행) | LLM Summarizer |
+| `update_docs_file` | CUSTOM-README.md에 업그레이드 주의사항 섹션 추가 | Docs Updater |
+| `create_pr` | GitHub PR 생성 | Git PR Bot |
+| `deploy_validate` | test namespace에 배포 후 health 검증 **(Phase 2, 미구현)** | Deploy Validator |
+
+---
+
+## 3. Skill 상세 정의
+
+## 2.1 공통 에러 스키마
+
+모든 Skill은 실패 시 아래 형식으로 에러를 반환한다.
+
+```json
+{
+ "error": {
+ "code": "ERR_HELM_PULL" ,
+ "message": "helm pull failed",
+ "retryable": true,
+ "details": { "exit_code": 1 }
+ }
+}
+```
+
+- `retryable=true`인 경우에만 자동 재시도를 수행한다.
+
+## 2.2 Idempotency / Retry 정책
+
+- **read-only Skill**(helm_diff, breaking_change_check, generate_upgrade_doc)는 안전 재시도 가능.
+- **side-effect Skill**(update_docs_file, create_pr, deploy_validate)은 idempotency key를 사용한다.
+- `create_pr`는 동일 key 요청 시 기존 PR URL을 반환해야 한다.
+
+## 2.3 Auth/Secret 전달
+
+- 토큰/시크릿은 **env var 또는 K8s Secret**으로 주입한다.
+- 입력 payload에 직접 포함하지 않는다.
+
+### 3.1 `helm_diff`
+
+```yaml
+name: helm_diff
+description: >
+ 두 버전의 Helm Chart를 비교하여 Structured Diff JSON을 생성한다.
+ values, templates, CRD, dependencies 변경사항을 포함한다.
+
+input:
+ chart: string # 차트 이름 (예: "airflow")
+ repo: string # Helm repo 이름 또는 URL (repo 기반일 때만)
+ chart_path: string # 로컬 차트 경로 (dip-catalog 구조)
+ from_version: string # 기존 버전 (예: "1.2.3")
+ to_version: string # 신규 버전 (예: "1.3.0")
+ values_override: object # 사용자 정의 values (선택, dip-catalog은 custom-values.yaml 기본)
+
+output:
+ chart: string
+ from_version: string
+ to_version: string
+ generated_at: string # ISO 8601 timestamp
+ values: object # Values Diff
+ templates: object # Template Diff
+ crd: object # CRD Diff
+ dependencies: object # Dependency Diff
+ errors: array # 부분 실패 정보
+```
+
+### 3.2 `breaking_change_check`
+
+```yaml
+name: breaking_change_check
+description: >
+ Structured Diff JSON을 입력받아 Breaking Change 여부를 코드 기반으로 판단한다.
+ LLM을 사용하지 않으며 결과는 완전히 deterministic하다.
+
+input:
+ diff_json: object # helm_diff 출력 (Structured Diff JSON)
+
+output:
+ breaking: boolean
+ severity: string # critical / high / medium / warning
+ reasons: array # Breaking 사유 목록
+ warnings: array # 비중단 경고 목록
+```
+
+### 3.3 `generate_upgrade_doc`
+
+```yaml
+name: generate_upgrade_doc
+description: >
+ Structured Diff JSON과 Breaking Change 결과를 기반으로 업그레이드 주의사항 Markdown을 생성한다.
+ 항상 실행된다. breaking=true면 LLM 상세 가이드, breaking=false면 템플릿 기반 간단 요약.
+
+input:
+ diff_json: object # helm_diff 출력
+ breaking_result: object # breaking_change_check 출력
+ docs_context: object # CUSTOM-README.md 내용 (dip-catalog)
+ max_tokens: integer # LLM 입력 최대 토큰 수 (기본: 50000)
+
+output:
+ markdown: string # 생성된 Markdown 문서
+ truncated: boolean # 토큰 제한으로 입력이 잘렸는지 여부
+```
+
+### 3.4 `update_docs_file`
+
+```yaml
+name: update_docs_file
+description: >
+ CUSTOM-README.md에 업그레이드 주의사항 섹션을 추가한다.
+ CUSTOM-README.md는 배포 관련 정보를 담는 문서로, 업그레이드 주의사항의 적합한 위치다.
+ BUILD-README.md는 chart_updater의 carry-over로만 관리된다.
+
+input:
+ repo_path: string # 로컬 Git 저장소 경로
+ docs_file: string # 문서 파일 경로 (예: "manifests/helm///CUSTOM-README.md")
+ version: string # 삽입할 버전 표기 (예: "1.2.3 → 1.3.0")
+ content: string # 삽입할 Markdown 내용
+ overwrite: boolean # 기존 버전 섹션 덮어쓰기 여부 (기본: false)
+
+output:
+ success: boolean
+ file_path: string
+ already_existed: boolean
+```
+
+### 3.5 `create_pr`
+
+```yaml
+name: create_pr
+description: >
+ Helm Chart 업그레이드를 위한 GitHub PR을 생성한다.
+ branch 생성, commit, PR 생성을 포함한다.
+
+input:
+ chart: string # 차트 이름
+ from_version: string
+ to_version: string
+ repo_path: string # 로컬 Git 저장소 경로
+ doc_content: string # upgrade.md에 삽입할 내용
+ breaking: boolean # PR label 결정에 사용
+ severity: string # PR label 결정에 사용
+
+output:
+ pr_url: string
+ branch_name: string
+ labels: array
+```
+
+### 3.6 `deploy_validate` (Phase 2A+)
+
+```yaml
+name: deploy_validate
+description: >
+ Ephemeral test namespace에 Helm Chart를 배포하고 health를 검증한다.
+ 성공/실패 결과와 Pod 상태를 반환한다.
+
+input:
+ chart: string
+ repo: string
+ version: string
+ values_override: object
+ namespace: string # test namespace (예: "helm-test-airflow")
+ timeout: integer # 초 단위, 기본 300
+
+output:
+ success: boolean
+ dry_run_passed: boolean
+ pod_status: object # { running: int, pending: int, failed: int }
+ events: array # 비정상 K8s events
+ logs: string # 실패 시 관련 Pod 로그
+```
+
+---
+
+## 4. Agent 워크플로 호출 순서
+
+Agent(OpenClaw/Nanobot)가 Skill들을 등록하고, 아래 순서를 워크플로로 정의한다.
+
+```yaml
+# 개념적 호출 순서 (실제 워크플로 정의는 implementation/02-agent.md 참고)
+1. helm_diff(chart, repo, from_version, to_version)
+ ↓
+2. breaking_change_check(diff_json, custom_values) # custom-values.yaml 기준 판단
+ ↓
+3. generate_upgrade_doc(diff_json, breaking_result) # 항상 실행
+ │ breaking=true → LLM 상세 가이드 생성
+ │ breaking=false → 템플릿 기반 간단 요약
+ ↓
+4. update_docs_file(
+ repo_path,
+ docs_file="manifests/helm///CUSTOM-README.md", # to_version으로 경로 결정
+ version=" → ",
+ content=
+ ) # 항상 실행
+ ↓
+5. create_pr(chart, from_version, to_version, ...) # 항상 실행
+ │ breaking=true → label: needs-review
+ │ breaking=false → label: auto-update
+ ↓ exit 0 (항상)
+
+# Phase 2 (미구현):
+6. deploy_validate(chart, repo, to_version, namespace)
+```
+
+---
+
+## 5. 버전 관리
+
+이 인터페이스는 **명시적 버전**을 관리한다.
+
+```yaml
+skill_interface_version: "1.0"
+```
+
+Skill 입출력 변경 시:
+- **하위 호환 변경** (필드 추가): 마이너 버전 증가
+- **Breaking 변경** (필드 삭제/타입 변경): 메이저 버전 증가 + 마이그레이션 가이드 작성
+
+---
+
+## 6. 관련 문서
+
+- [01-helm-diff-engine.md](01-helm-diff-engine.md) — `helm_diff` 구현 설계
+- [02-breaking-change-rules.md](02-breaking-change-rules.md) — `breaking_change_check` 구현 설계
+- [03-llm-summarizer.md](03-llm-summarizer.md) — `generate_upgrade_doc`, `create_pr` 구현 설계
+- [05-on-cluster-agent.md](05-on-cluster-agent.md) — Agent가 이 인터페이스를 Skills로 사용하는 방법
diff --git a/update_catalog/docs/design/05-on-cluster-agent.md b/update_catalog/docs/design/05-on-cluster-agent.md
new file mode 100644
index 0000000..fb6a462
--- /dev/null
+++ b/update_catalog/docs/design/05-on-cluster-agent.md
@@ -0,0 +1,189 @@
+# On-Cluster AI Agent 설계 (Step 2~3)
+
+## 1. 개요
+
+Skills(Step 1)를 **클러스터 위에서 상시 동작하는 자율 에이전트**로 오케스트레이션한다.
+OpenClaw 또는 Nanobot을 오케스트레이터로 삼아, Step 1에서 구현한 Skill들을 등록하여 Helm 업그레이드 자동화 전체를 수행한다.
+
+> **전제**: Skills(Step 1) 구현 완료 + 인터페이스 버전 `1.0` 확정 이후 진행.
+
+---
+
+## 2. 아키텍처
+
+```
+[OpenClaw / Nanobot on K8s]
+ │
+ ├── Skill: helm_diff → Structured Diff JSON 생성
+ ├── Skill: breaking_check → Breaking Change 판단
+ ├── Skill: generate_doc → 업그레이드 주의사항 문서 생성 (항상 실행)
+ ├── Skill: update_docs → CUSTOM-README.md 업그레이드 주의사항 섹션 추가
+ ├── Skill: create_pr → GitHub PR 생성
+ # deploy_validate: Phase 2 예정
+ ├── Skill: k8s_event_watch → 클러스터 이벤트 감지
+ └── Channel: Slack / Telegram → 알림 발송
+```
+
+트리거:
+- Cron 스케줄 (신규 chart 버전 주기 감지)
+- K8s Event Watch (ArgoCD App 상태 변화 등)
+
+---
+
+## 3. OpenClaw vs Nanobot
+
+| 항목 | OpenClaw | Nanobot |
+|------|---------|---------|
+| 코드 규모 | 430K+ lines | ~4,000 lines |
+| 성숙도 | 높음 (100K+ GitHub stars) | 낮음 (신생) |
+| Skills 생태계 | 풍부 (ClawHub) | 기본 지원 |
+| K8s 배포 | 공식 Helm chart + K8s Operator | 직접 구성 필요 |
+| 수평 확장 | 불가 (Recreate 전략) | 미정 |
+| LLM 지원 | Claude, OpenAI, 로컬 모델 | Claude, OpenAI, Qwen 등 |
+| 커스터마이징 | TypeScript / YAML 워크플로 | 코드 직접 수정 용이 |
+| 보안 이슈 | 커뮤니티 이슈 있음 (Cisco, Palo Alto 조사) | 미검증 |
+
+**권장**:
+- 운영 안정성 우선 → **OpenClaw** (K8s Operator, 성숙한 생태계)
+- 경량 커스터마이징 우선 → **Nanobot** (코드 소규모, 직접 수정)
+
+---
+
+## 4. Skills → Agent 연결 전략
+
+Step 1에서 Skill 인터페이스를 표준으로 구현해두면 Agent 연결이 매끄럽다.
+
+```
+Step 1 (Skills) Step 2~3 (Agent)
+───────────────────────────── ─────────────────────────────
+helm_diff, breaking_check 등 OpenClaw/Nanobot Agent가
+Skill로 구현 완료 → 동일한 Skill들을 등록 후
+ 워크플로로 오케스트레이션
+```
+
+Agent 연결 시 추가되는 부분:
+- 실행 환경: K8s Pod (Agent)
+- 오케스트레이션: Agent 워크플로 정의 (Cron 스케줄 + 조건 분기)
+- 스케줄링: Agent 내장 스케줄러
+
+변경되지 않는 부분:
+- Skill 구현체 (helm_diff, breaking_check 등)
+- Structured Diff JSON 포맷
+- Breaking Change 규칙
+
+---
+
+## 5. 보안 고려사항
+
+## 4.1 운영 정책
+
+- `breaking=true` PR에는 `needs-review` 레이블을 부착한다. 담당자가 `custom-values.yaml` 수정 후 merge 여부를 판단한다.
+- `breaking=false` PR은 `auto-update` 레이블을 부착하며, 자동 merge가 가능하다.
+- Agent는 PR 생성까지만 수행한다. merge 책임은 담당자에게 있다.
+
+## 4.2 실패 처리
+
+- Skill 실패 시: 알림 전송 + 자동 중단. 재시도는 최대 3회.
+- `deploy_validate`는 Phase 2에서 구현 예정 (현재 스코프 밖).
+
+## 4.3 관찰성
+
+- 모든 Skill 호출은 audit log에 남긴다 (input hash + output status).
+- Prometheus metrics: 성공/실패 카운트, 평균 처리 시간, 재시도 횟수.
+- LLM 호출은 request_id를 부여하여 추적 가능해야 한다.
+
+On-Cluster AI Agent는 구조적 위험이 있다.
+
+| 위험 | 내용 | 대응 |
+|------|------|------|
+| 과도한 K8s 권한 | helm upgrade 권한 남용 | RBAC: test namespace만 허용, ServiceAccount 최소 권한 |
+| Skill 취약점 | 3rd-party Skill의 26%가 취약 (Cisco 조사) | 허용 Skill allowlist 관리, ClawHub Skill 검토 필수 |
+| Prompt Injection | PR comment, webhook 등 외부 콘텐츠로 에이전트 조작 | 입력 sanitization, 신뢰 범위(trust boundary) 명확화 |
+| 외부 통신 데이터 유출 | GitHub API, Slack 등 외부 전송 | NetworkPolicy: 허용 egress 목록 명시, 민감 데이터 마스킹 |
+| Shell 접근 | RCE 가능성 | shell skill 비활성화, read-only root filesystem, UID 1000 |
+
+> Palo Alto Networks 평가: "Shell 접근 + 개인 데이터 + 외부 통신" = **"lethal trifecta"**
+
+### 최소 보안 요구사항 (운영 투입 전 필수)
+
+```yaml
+# RBAC 예시: test namespace만 허용
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ namespace: helm-test
+rules:
+ - apiGroups: ["apps"]
+ resources: ["deployments", "statefulsets"]
+ verbs: ["get", "list", "create", "update", "delete"]
+ - apiGroups: [""]
+ resources: ["pods", "services", "configmaps"]
+ verbs: ["get", "list", "create", "update", "delete"]
+```
+
+```yaml
+# NetworkPolicy: 허용 egress만 통과
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: agent-egress-policy
+spec:
+ podSelector:
+ matchLabels:
+ app: helm-upgrade-agent
+ policyTypes: ["Egress"]
+ egress:
+ - to: # GitHub API
+ - ipBlock:
+ cidr: 140.82.112.0/20
+ - to: # Slack API
+ - ipBlock:
+ cidr: 35.190.0.0/16
+ - ports:
+ - port: 443
+```
+
+---
+
+## 6. K8s 배포 구성
+
+```yaml
+# OpenClaw Helm values 예시
+image:
+ tag: latest
+
+skills:
+ allowlist:
+ - helm_diff
+ - breaking_change_check
+ - generate_upgrade_doc
+ - update_docs_file
+ - create_pr
+ # deploy_validate: Phase 2에서 추가 예정
+
+securityContext:
+ runAsNonRoot: true
+ runAsUser: 1000
+ readOnlyRootFilesystem: true
+
+resources:
+ limits:
+ memory: "1Gi"
+ cpu: "500m"
+```
+
+---
+
+## 7. 도입 순서 권장
+
+1. Staging 클러스터에서 먼저 검증
+2. 보안 정책 확립 (RBAC, NetworkPolicy, Skill allowlist)
+3. Skill을 하나씩 추가하며 동작 확인
+4. 운영 클러스터 투입 시 `breaking=true` PR은 사람이 직접 머지 승인 유지
+
+---
+
+## 8. 관련 문서
+
+- [04-skill-interface.md](04-skill-interface.md) — Agent가 호출하는 Skill 인터페이스
+- [../implementation/02-agent.md](../implementation/02-agent.md) — Agent 구현 계획 (Step 2~4)
diff --git a/update_catalog/docs/implementation/01-skills.md b/update_catalog/docs/implementation/01-skills.md
new file mode 100644
index 0000000..be5c0d5
--- /dev/null
+++ b/update_catalog/docs/implementation/01-skills.md
@@ -0,0 +1,157 @@
+# Skills 구현 태스크 (Step 1)
+
+## 목표
+
+Agent(OpenClaw/Nanobot)가 직접 호출할 수 있는 Skill들을 구현한다.
+파이프라인 오케스트레이션 코드는 구현하지 않는다. 순서 제어는 Agent가 담당한다.
+
+> **범위**: 각 Skill이 독립적으로 호출 가능한 상태. Skill 간 호출 순서 코드는 제외.
+
+---
+
+## 태스크 목록
+
+### 🔴 높음 (핵심 기능)
+
+#### T1. Helm Diff Engine 구현 (`helm_diff` Skill)
+- [ ] **dip-catalog 경로 지원**: `chart_path` 입력으로 `manifests/helm///` 직접 사용
+- [ ] 신규 버전 감지 시 `manifests/helm///` 디렉토리 생성 (기존 유지)
+- [ ] 최신 차트 pull + 압축 해제 후 신규 버전 디렉토리에 반영
+- [ ] (repo 구조) `helm pull`로 두 버전 다운로드
+- [ ] (dip-catalog) 로컬 디렉토리 복사로 `chart_old/chart_new` 구성
+- [ ] values.yaml flat key 비교 (added / removed / changed / type_changed)
+- [ ] **`custom-values.yaml` 기본 적용** (values_override 기본값)
+- [ ] `helm template` 렌더링 후 리소스 단위 비교
+- [ ] CRD schema 비교 (schema / required / version / webhook)
+- [ ] dependency (Chart.yaml) 비교
+- [ ] Structured Diff JSON 스키마 확정 및 출력
+
+**완료 기준**: 임의의 두 chart 버전에 대해 Structured Diff JSON이 정상 생성됨
+
+#### T2. Breaking Change Rule Engine 구현 (`breaking_change_check` Skill)
+- **Breaking 정의**: `custom-values.yaml`을 수정해야 하는 상황 (카탈로그 맥락)
+- [ ] Values key 삭제 감지 — custom-values.yaml에 해당 key 있을 때만 breaking, 없으면 warning
+- [ ] Values type 변경 감지 — 동일 기준 (custom-values.yaml에 있을 때만 breaking)
+- [ ] Service port / type 변경 감지 — ⚠️ warning (custom-values.yaml 수정 불필요)
+- [ ] resource 삭제 감지 — ⚠️ warning (custom-values.yaml 수정 불필요)
+- [ ] Dependency major version 변경 — custom-values.yaml에 해당 subchart prefix key 있을 때만 breaking
+- [ ] CRD required field 추가 / field 삭제 / storage version 변경 감지
+- [ ] severity 분류 (critical / high / medium / warning)
+
+**완료 기준**: custom-values.yaml 기준으로 올바르게 breaking/warning 분류, False Positive 최소화
+
+#### T3. LLM Summarizer 구현 (`generate_upgrade_doc` Skill)
+- [ ] Structured Diff JSON + Breaking 결과를 LLM API에 전달
+- [ ] **docs_context 반영**: README/BUILD/CUSTOM 요약을 참고 컨텍스트로 포함
+- [ ] Prompt 구현 (설계 문서 기반)
+- [ ] 토큰 예산 관리 (50,000 token 상한, 초과 시 중요도 기반 필터링)
+- [ ] Markdown 출력 검증 (형식 일치 여부)
+
+**완료 기준**: 생성된 Markdown이 설계 문서의 Output Format을 준수하고, 없는 내용을 만들어내지 않음
+- breaking=true → LLM 상세 가이드 (USE_CLAUDE_CLI=1 필요)
+- breaking=false → 템플릿 기반 간단 요약
+- 항상 실행 (breaking 여부와 무관)
+
+#### T3-1. CUSTOM-README.md 업그레이드 주의사항 추가
+- [ ] generate_upgrade_doc 결과를 `CUSTOM-README.md`의 `# Upgrade History` 섹션에 추가
+- [ ] `update_docs_file` Skill 호출: `docs_file="manifests/helm///CUSTOM-README.md"`
+
+#### T4. Skill 인터페이스 구현
+- [ ] `helm_diff` Skill 구현 및 노출
+- [ ] `breaking_change_check` Skill 구현 및 노출
+- [ ] `generate_upgrade_doc` Skill 구현 및 노출
+- [ ] `update_docs_file` Skill 구현 및 노출
+- [ ] `create_pr` Skill 구현 및 노출
+- [ ] `deploy_validate` Skill 구현 및 노출
+- [ ] 인터페이스 버전 `1.0` 확정
+
+**완료 기준**: 각 Skill이 독립적으로 호출 가능하고 입출력 스키마가 문서와 일치
+
+---
+
+### 🟡 중간 (안정성)
+
+#### T5. 에러 처리 및 Fallback
+- [ ] `helm pull` 실패 시 재시도 (3회) + 알림
+- [ ] `helm template` 렌더링 실패 시 partial diff 생성
+- [ ] chart 미존재 시 스킵 + 로그
+- [ ] 네트워크 타임아웃 (60s) 처리
+- [ ] 각 Skill의 오류 시 에러 응답 포맷 일관화
+
+#### T6. Chart Version Detector 구현
+- [ ] **dip-catalog 디렉토리 스캔** (`manifests/helm//` 하위 버전 변화 감지)
+- [ ] **git diff 기반 신규 버전 탐지**
+- [ ] ArtifactHub API 연동
+- [ ] Helm repo `index.yaml` 스캔
+- [ ] GitHub Release Webhook 수신
+- [ ] Cron 스케줄 설정
+
+#### T7. Docs Updater 구현 (`update_docs_file` Skill)
+- [ ] `CUSTOM-README.md`에 `# Upgrade History` 섹션 추가 (기존 내용 유지)
+- [ ] 대상 경로: `manifests/helm///CUSTOM-README.md`
+- [ ] 중복 버전 처리 (overwrite 옵션)
+- [ ] `BUILD-README.md`는 `chart_updater`가 carry-over + 버전 번호 치환으로 관리 (update_docs_file 대상 아님)
+
+#### T8. Git PR Bot 구현 (`create_pr` Skill)
+- [ ] branch 생성 (`helm-upgrade/{chart}/{version}`)
+- [ ] commit (chart version 업데이트 + docs 수정)
+- [ ] PR 생성 (제목, labels, machine-readable metadata 블록 포함)
+
+#### T9. Deploy Validator 구현 (`deploy_validate` Skill)
+- [ ] `helm upgrade --dry-run` 실행
+- [ ] test namespace에 `helm upgrade` 배포
+- [ ] Pod Running + Ready 상태 확인 (타임아웃: 5분)
+- [ ] 비정상 K8s events 수집
+- [ ] 실패 시 Pod 로그 수집
+- [ ] namespace teardown
+
+---
+
+### 🟢 낮음 (개선)
+
+#### T10. 사용자 정의 values 오버라이드 지원
+- [ ] `values_override` 파일 경로 또는 inline YAML 지원
+- [ ] Diff 생성 시 오버라이드 values 적용
+- [ ] **dip-catalog 기본값**: `custom-values.yaml` 존재 시 자동 적용
+
+---
+
+## 구현 진행 현황
+
+- 현황 표는 [docs/status.md](../status.md)에서 관리
+
+---
+
+## 우선순위 요약
+
+```
+T4 (Skill 인터페이스 스키마 확정) ← 가장 먼저 (다른 모든 태스크의 계약)
+ ↓
+T1 (Helm Diff) + T2 (Rule Engine) ← 병렬 구현 가능
+ ↓
+T3 (LLM Summarizer) → T3-1 (CUSTOM-README.md 업그레이드 주의사항 추가)
+ ↓
+T7 (Docs Updater) + T8 (PR Bot) + T9 (Deploy Validator) ← 병렬 구현 가능
+ ↓
+T5 (에러 처리) + T6 (Version Detector) + T10 (values 오버라이드)
+```
+
+---
+
+## 완료 기준 (Step 1 전체)
+
+- [ ] 각 Skill이 독립적으로 호출 가능
+- [ ] airflow chart 임의 두 버전에 대해 각 Skill 단독 동작 확인
+- [ ] Breaking Change가 있는 버전과 없는 버전 모두 올바르게 처리
+- [ ] 생성된 PR에 machine-readable metadata 블록 포함
+- [ ] 에러 발생 시 해당 Skill만 실패하고 에러 응답 반환 (전체 중단 없음)
+- [ ] Skill 인터페이스 버전 `1.0` 확정
+
+---
+
+## 관련 설계 문서
+
+- [design/01-helm-diff-engine.md](../design/01-helm-diff-engine.md)
+- [design/02-breaking-change-rules.md](../design/02-breaking-change-rules.md)
+- [design/03-llm-summarizer.md](../design/03-llm-summarizer.md)
+- [design/04-skill-interface.md](../design/04-skill-interface.md)
diff --git a/update_catalog/docs/implementation/02-agent.md b/update_catalog/docs/implementation/02-agent.md
new file mode 100644
index 0000000..11ef633
--- /dev/null
+++ b/update_catalog/docs/implementation/02-agent.md
@@ -0,0 +1,139 @@
+# Agent 구현 태스크 (Step 2~4)
+
+## 전제 조건
+
+- [ ] Step 1 Skills 구현 완료 (인터페이스 버전 `1.0` 확정)
+- [ ] Staging K8s 클러스터 접근 가능
+
+---
+
+## Step 2: Agent 프레임워크 POC
+
+**목표**: OpenClaw와 Nanobot 중 하나를 실제 클러스터에서 검증하여 프레임워크를 확정한다.
+
+### POC 범위 (최소)
+
+- [ ] 각 프레임워크를 Staging 클러스터에 배포
+- [ ] `helm_diff` Skill 하나를 등록
+- [ ] Agent가 Skill을 호출하여 실제 결과 반환 확인
+- [ ] 두 프레임워크 비교 후 결정
+
+### 비교 기준
+
+| 항목 | OpenClaw | Nanobot | 결과 |
+|------|---------|---------|------|
+| K8s 배포 난이도 | 공식 Helm chart | 직접 구성 | - |
+| Skill 연결 방식 | - | - | - |
+| 로그/디버깅 편의성 | - | - | - |
+| 보안 설정 가능 여부 | - | - | - |
+
+> POC 완료 후 이 표를 채운다.
+
+**완료 기준**: 두 프레임워크 중 하나 선택, 선택 이유를 [decisions/](../decisions/) 에 기록
+
+---
+
+## Step 3: Agent 워크플로 정의
+
+**목표**: 선택한 프레임워크에서 모든 Skill을 등록하고 워크플로를 정의한다.
+
+### 태스크
+
+- [ ] 모든 Skill 등록
+ - `helm_diff`, `breaking_change_check`, `generate_upgrade_doc`
+ - `update_docs_file`, `create_pr`, `deploy_validate`
+- [ ] 워크플로 정의 (Cron 스케줄 기반)
+
+```yaml
+workflow: helm-upgrade-automation
+schedule: "0 8 * * *"
+
+steps:
+ - name: detect-versions
+ skill: chart_version_detector
+ output: new_versions[] # current_version + latest_version
+
+ - name: generate-diffs
+ skill: helm_diff
+ for_each: new_versions
+ output: diff_json[]
+ # 내부에서 최신 차트 pull + 버전 디렉토리 생성 수행
+
+ - name: check-breaking
+ skill: breaking_change_check
+ for_each: diff_json
+ output: breaking_results[]
+
+ - name: generate-docs
+ skill: generate_upgrade_doc
+ # Skill 내부에서 severity 분기: breaking=true or severity≥high → LLM 심층 요약, 그 외 → 간이 템플릿
+ for_each: [diff_json, breaking_results]
+ output: docs[]
+
+ - name: update-docs
+ skill: update_docs_file
+ for_each: docs
+ output: updated[]
+
+ - name: validate-deployments
+ skill: deploy_validate
+ condition: breaking=false and severity=high이면 스킵 → 사람 승인 필요
+ for_each: updated
+ output: validation_results[]
+
+ - name: create-prs
+ skill: create_pr
+ for_each: updated
+ output: pr_urls[]
+
+ - name: notify
+ channel: slack
+ message: "Helm upgrade PRs created: {pr_urls}"
+```
+
+- [ ] K8s 이벤트 기반 트리거 설정 (선택: ArgoCD App 상태 변화 등)
+- [ ] Staging 클러스터에서 E2E 동작 확인
+
+**완료 기준**: Staging에서 airflow chart 신규 버전 감지 → PR 생성까지 E2E 자동 실행
+
+---
+
+## Step 4: 보안 정책 수립 후 운영
+
+**목표**: 운영 클러스터 투입 전 보안 정책을 확립한다.
+
+### 태스크
+
+- [ ] RBAC 설정 (test namespace만 허용)
+
+```yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ namespace: helm-test
+rules:
+ - apiGroups: ["apps"]
+ resources: ["deployments", "statefulsets"]
+ verbs: ["get", "list", "create", "update", "delete"]
+ - apiGroups: [""]
+ resources: ["pods", "services", "configmaps"]
+ verbs: ["get", "list", "create", "update", "delete"]
+```
+
+- [ ] NetworkPolicy 설정 (허용 egress만 통과)
+ - GitHub API (`api.github.com`)
+ - LLM API (`api.anthropic.com` 또는 `api.openai.com`)
+ - Slack API (알림용)
+- [ ] Skill allowlist 관리 (shell skill 비활성화 필수)
+- [ ] 입력 sanitization (Prompt Injection 방지)
+- [ ] `breaking=true` PR은 사람이 직접 머지 승인 유지
+
+**완료 기준**: 보안 체크리스트 전항목 충족, 운영 클러스터 투입
+
+---
+
+## 관련 문서
+
+- [design/05-on-cluster-agent.md](../design/05-on-cluster-agent.md) — Agent 배포 설계 + 보안 고려사항
+- [design/04-skill-interface.md](../design/04-skill-interface.md) — Agent가 호출할 Skill 인터페이스
+- [decisions/001-agentic-first.md](../decisions/001-agentic-first.md) — 파이프라인 건너뛰기 결정 배경
diff --git a/update_catalog/docs/skill-test-guide.md b/update_catalog/docs/skill-test-guide.md
new file mode 100644
index 0000000..9cf8c52
--- /dev/null
+++ b/update_catalog/docs/skill-test-guide.md
@@ -0,0 +1,35 @@
+# OpenClaw Skill 테스트 가이드
+
+이 문서는 update_catalog 로컬 구현을 OpenClaw Skill로 테스트하는 방법을 정리한다.
+
+## 1) 사전 조건
+- OpenClaw 워크스페이스에 스킬 폴더가 있어야 함
+ - `~/.openclaw/workspace/skills/update-catalog-helm-diff/`
+ - `~/.openclaw/workspace/skills/update-catalog-breaking-check/`
+- Python/Helm 설치
+
+## 2) helm_diff 실행
+```bash
+/skill update-catalog-helm-diff --chart airflow --repo apache-airflow \
+ --chart-path /Users/songwonbin/openclaw-workspace/dip-catalog/manifests/helm/airflow/1.16.0 \
+ --from-version 1.16.0 --to-version 1.19.0
+```
+
+## 3) breaking_change_check 실행
+1) helm_diff 결과를 파일로 저장
+```bash
+python3 ~/.openclaw/workspace/skills/update-catalog-helm-diff/scripts/run.py \
+ --chart airflow --repo apache-airflow \
+ --chart-path /Users/songwonbin/openclaw-workspace/dip-catalog/manifests/helm/airflow/1.16.0 \
+ --from-version 1.16.0 --to-version 1.19.0 \
+ > /tmp/helm_diff.json
+```
+
+2) breaking_check 실행
+```bash
+/skill update-catalog-breaking-check --diff-file /tmp/helm_diff.json
+```
+
+## 4) 참고
+- repo 경로 변경 시 `UPDATE_CATALOG_ROOT` 환경변수로 수정 가능 (외부 repo override)
+- 기본적으로 스킬은 **내부 포함 소스**를 사용 (독립형)
diff --git a/update_catalog/docs/status.md b/update_catalog/docs/status.md
new file mode 100644
index 0000000..235a2a0
--- /dev/null
+++ b/update_catalog/docs/status.md
@@ -0,0 +1,16 @@
+# 구현 진행 현황
+
+## 워크플로 단계별 구현 현황(로컬 환경)
+
+| 단계 | 기능/스킬 | 구현 상태 | 비고 |
+|---|---|---|---|
+| B | current_version 결정 | ✅ 구현 | chart_version_detector |
+| C | BUILD-README repo 파싱 | ✅ 구현 | chart_version_detector |
+| D | latest_version 감지 | ✅ 구현 | chart_version_detector |
+| E | 최신 차트 pull + 버전 디렉토리 생성 | ✅ 구현 | chart_updater |
+| F | Diff 생성 | ✅ 구현 | helm_diff |
+| G | Breaking 판단 | ✅ 구현 | breaking_change_check |
+| H | 업그레이드 문서 생성 | ✅ 구현 | generate_upgrade_doc (항상 실행; breaking=true → LLM, breaking=false → 템플릿) |
+| I | CUSTOM-README.md 업그레이드 주의사항 추가 | ✅ 구현 | update_docs_file (manifests/helm///CUSTOM-README.md) |
+| J | deploy_validate | ❌ 미구현 | Phase 2 예정 |
+| K | create_pr | ✅ 구현 (미테스트) | create_pr |
diff --git a/update_catalog/docs/test/README.md b/update_catalog/docs/test/README.md
new file mode 100644
index 0000000..184a5f6
--- /dev/null
+++ b/update_catalog/docs/test/README.md
@@ -0,0 +1,6 @@
+# 테스트 문서 구조
+
+- env.md: 테스트 환경 정리
+- unit.md: 단위 테스트 케이스
+- integration.md: 통합 테스트 시나리오
+- logs/: 테스트 실행 기록
diff --git a/update_catalog/docs/test/env.md b/update_catalog/docs/test/env.md
new file mode 100644
index 0000000..a725cbe
--- /dev/null
+++ b/update_catalog/docs/test/env.md
@@ -0,0 +1,17 @@
+# 테스트 환경 (env)
+
+## 로컬 환경
+- OS: Darwin 24.6.0 (arm64) / Darwin Kernel Version 24.6.0
+- Python: 3.12.8
+- Helm: v3.18.3+g6838ebc
+- kubectl: v1.33.2 (Kustomize v5.6.0)
+- 기타 의존성: 미기록
+
+## 네트워크
+- Helm repo 접근: 미확인
+- GitHub API 접근: 미확인
+- LLM API 접근: 미확인
+
+## 실행 경로
+- 작업 디렉토리: ~/openclaw-workspace/ai_agent_test/update_catalog
+- 로그/결과 저장 위치: docs/test/logs/
diff --git a/update_catalog/docs/test/integration.md b/update_catalog/docs/test/integration.md
new file mode 100644
index 0000000..49a5180
--- /dev/null
+++ b/update_catalog/docs/test/integration.md
@@ -0,0 +1,14 @@
+# 통합 테스트 (integration)
+
+## 목적
+- helm_diff → breaking_change_check → generate_upgrade_doc 흐름 검증
+
+## 시나리오
+
+### I1. 기본 업그레이드
+- 입력: chart, from/to 버전
+- 기대: diff 생성 → breaking 판단 → 문서 생성
+
+### I2. breaking 발생 시나리오
+- 입력: Service port 변경 포함 chart
+- 기대: breaking=true, migration 단계 포함 문서
diff --git a/update_catalog/docs/test/unit.md b/update_catalog/docs/test/unit.md
new file mode 100644
index 0000000..fc42db0
--- /dev/null
+++ b/update_catalog/docs/test/unit.md
@@ -0,0 +1,14 @@
+# 단위 테스트 (unit)
+
+## 목적
+- 핵심 함수별 동작 검증 (helm_diff, breaking_change_check 등)
+
+## 테스트 케이스
+
+### U1. helm_diff 기본 동작
+- 입력: chart, from/to 버전
+- 기대: values/templates/dependencies 구조 출력
+
+### U2. breaking_change_check 기본 규칙
+- 입력: values removed / type_changed 포함 diff
+- 기대: breaking=true, severity>=high
diff --git a/update_catalog/docs/working-memory.md b/update_catalog/docs/working-memory.md
new file mode 100644
index 0000000..6242f1b
--- /dev/null
+++ b/update_catalog/docs/working-memory.md
@@ -0,0 +1,70 @@
+# 업데이트 카탈로그 문서 메모리 (요약)
+
+이 문서는 docs/design 및 docs/implementation 내용의 요점을 요약 저장한다.
+변경 시 이 파일을 업데이트한다.
+
+## design 요약
+
+### 00-architecture-overview
+- **목적**: Helm chart 신규 버전을 카탈로그(신규 배포용)에 추가하고, 업그레이드 주의사항을 CUSTOM-README.md에 자동 문서화.
+- **카탈로그 역할**: 운영 클러스터 직접 변경 아님. 신규 배포를 위한 차트 보관소.
+- **파이프라인**: chart_version_detector → helm_diff → breaking_change_check → generate_upgrade_doc(항상) → update_docs_file(CUSTOM-README.md) → create_pr.
+- **검토 정책**: breaking=true → needs-review 레이블(담당자 판단), breaking=false → auto-update 레이블. deploy_validate는 Phase 2.
+- 구성요소 책임 분리 및 스택(helm3, Python diff/rule, LLM, Git PR, OpenClaw/Nanobot).
+
+### 01-helm-diff-engine
+- 차트 버전 감지 방식: repo 디렉토리 스캔, git diff, index.yaml, ArtifactHub API, GitHub Release, cron.
+- dip-catalog 흐름: 최신 chart pull/untar → manifests/helm/// 디렉토리 생성(기존 유지).
+- 이전 버전 파일 복사: custom-values.yaml(그대로), CUSTOM-README.md(그대로), BUILD-README.md(버전 번호 치환).
+- generate_upgrade_doc → CUSTOM-README.md에 업그레이드 주의사항 섹션 추가 (항상 실행).
+- helm_diff 처리: values, template, CRD, dependencies 비교 → Structured Diff JSON.
+- values diff: flat key 비교(added/removed/changed/type_changed), rename은 removed+added.
+- template diff: 리소스 단위(kind+name) 비교, generateName 처리.
+- CRD diff: schema/required/version/webhook 변화 감지.
+- errors 포함하여 부분 실패 기록.
+
+### 02-breaking-change-rules
+- **Breaking 정의**: custom-values.yaml을 수정해야 하는 상황.
+- values_key_removed: custom-values.yaml에 해당 key가 있을 때만 breaking. 없으면 warning.
+- values_type_changed: 동일 기준.
+- service_port_changed, resource_removed: 카탈로그 맥락에서 warning (custom-values.yaml 수정 불필요).
+- dependency_major_changed: custom-values.yaml에 해당 subchart prefix key 있으면 breaking, 없으면 warning.
+- CRD 변경(field 삭제, required 추가, storage version 변경): breaking.
+- deprecated API는 warning.
+- severity: critical/high/medium/warning, 우선순위 규칙.
+
+### 03-llm-summarizer
+- **실행 조건**: generate_upgrade_doc 항상 실행. breaking=true → LLM 상세 가이드, breaking=false → 템플릿 요약.
+- LLM 입력: diff + breaking 결과 + docs_context(CUSTOM-README.md).
+- 규칙: 입력 JSON 외 내용 금지, 정해진 Markdown 포맷.
+- 토큰 관리: 필터링/요약/청크.
+- 출력 검증(포맷/허위 서술 금지).
+- **대상 파일**: CUSTOM-README.md (배포 관련 문서). BUILD-README.md는 차트 메타 정보 유지.
+
+### 04-skill-interface
+- Skill 계약: helm_diff, breaking_change_check, generate_upgrade_doc, update_docs_file, create_pr.
+- deploy_validate: Phase 2 예정 (현재 인터페이스만 정의, 구현 안 됨).
+- update_docs_file: CUSTOM-README.md에 업그레이드 주의사항 추가. docs_file 경로에 to_version 포함.
+- 워크플로: helm_diff → breaking_check → generate_doc(항상) → update_docs(CUSTOM-README.md, to_version 경로) → create_pr.
+- 공통 에러 스키마, idempotency, secret 전달 방식.
+- 인터페이스 버전 관리(1.0).
+
+### 05-on-cluster-agent
+- OpenClaw/Nanobot로 K8s 상시 에이전트 오케스트레이션.
+- 보안 고려: RBAC 최소권한, NetworkPolicy, skill allowlist, prompt injection 방지.
+- 운영 정책: breaking=true → needs-review 레이블(담당자 판단). breaking=false → auto-update.
+- skills allowlist: helm_diff, breaking_change_check, generate_upgrade_doc, update_docs_file, create_pr (deploy_validate는 Phase 2).
+
+## implementation 요약
+
+### 01-skills
+- Step 1 Skills 구현 태스크(T1~T10) + dip-catalog 최신 차트 pull/untar 반영.
+- 핵심: helm_diff, breaking_check, generate_upgrade_doc(항상 실행), 인터페이스 노출.
+- 문서 갱신은 CUSTOM-README.md 대상.
+- 안정성: 에러 처리, version detector, docs updater, PR bot.
+- 우선순위 로드맵 및 완료 기준.
+
+### 02-agent
+- Step 2~4: 프레임워크 POC(OpenClaw vs Nanobot), 워크플로 정의, 보안 정책 후 운영.
+- 워크플로: generate_upgrade_doc 항상 실행, deploy_validate는 Phase 2.
+- 결정사항은 decisions/에 기록.
diff --git a/update_catalog/scripts/run_flow.sh b/update_catalog/scripts/run_flow.sh
new file mode 100755
index 0000000..e038e38
--- /dev/null
+++ b/update_catalog/scripts/run_flow.sh
@@ -0,0 +1,198 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# 이 스크립트가 위치한 디렉토리 기준으로 프로젝트 루트를 설정
+UPDATE_CATALOG_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+
+# dip-catalog 레포 경로 (환경변수로 오버라이드 가능)
+CATALOG_ROOT="${CATALOG_ROOT:-/Users/songwonbin/openclaw-workspace/dip-catalog}"
+
+CHART="${CHART:-airflow}"
+DEFAULT_BRANCH="${DEFAULT_BRANCH:-main}"
+OUT_DIR="${OUT_DIR:-$(pwd)/update_catalog}"
+
+# 차트별 하위 디렉토리로 격리 → 다중 차트 동시 실행 시 충돌 방지
+CHART_OUT_DIR="$OUT_DIR/$CHART"
+mkdir -p "$CHART_OUT_DIR"
+
+# 이전 실행의 임시 마커 파일 정리
+rm -f "$CHART_OUT_DIR/no_update"
+
+VERSION_JSON="$CHART_OUT_DIR/chart_version.json"
+CHART_UPDATE_JSON="$CHART_OUT_DIR/chart_update.json"
+DIFF_JSON="$CHART_OUT_DIR/helm_diff.json"
+BREAKING_JSON="$CHART_OUT_DIR/breaking.json"
+UPGRADE_DOC_JSON="$CHART_OUT_DIR/upgrade_doc.json"
+UPDATE_DOCS_JSON="$CHART_OUT_DIR/update_docs.json"
+PR_JSON="$CHART_OUT_DIR/pr.json"
+
+# dip-catalog main 브랜치 최신화 (create_pr이 브랜치를 main에서 분기하므로 선행 필요)
+git -C "$CATALOG_ROOT" checkout "$DEFAULT_BRANCH"
+git -C "$CATALOG_ROOT" pull origin "$DEFAULT_BRANCH"
+
+# 0) chart_version_detector: FROM_VERSION / TO_VERSION / REPO 자동 감지
+python3 "$UPDATE_CATALOG_ROOT/skills/chart_version_detector/scripts/run.py" \
+ --catalog-root "$CATALOG_ROOT" --chart "$CHART" \
+ > "$VERSION_JSON"
+
+python3 - <// 생성
+# + FROM_VERSION에서 BUILD-README.md(버전 치환), CUSTOM-README.md, custom-values.yaml 복사
+python3 "$UPDATE_CATALOG_ROOT/skills/chart_updater/scripts/run.py" \
+ --catalog-root "$CATALOG_ROOT" \
+ --chart "$CHART" \
+ --repo "$REPO" \
+ --version "$TO_VERSION" \
+ --from-version "$FROM_VERSION" \
+ > "$CHART_UPDATE_JSON"
+
+python3 - < "$DIFF_JSON"
+
+# 3) breaking_change_check
+# FROM_VERSION의 custom-values.yaml을 기준으로 실제 사용 key만 breaking 판단
+CUSTOM_VALUES_PATH="$CATALOG_ROOT/manifests/helm/$CHART/$FROM_VERSION/custom-values.yaml"
+CUSTOM_VALUES_ARG=""
+if [[ -f "$CUSTOM_VALUES_PATH" ]]; then
+ CUSTOM_VALUES_ARG="--custom-values $CUSTOM_VALUES_PATH"
+fi
+python3 "$UPDATE_CATALOG_ROOT/skills/breaking_change_check/scripts/run.py" \
+ --diff-file "$DIFF_JSON" \
+ $CUSTOM_VALUES_ARG \
+ > "$BREAKING_JSON"
+
+python3 - < "$UPGRADE_DOC_JSON"
+
+python3 - < "$PR_JSON"
+
+# python3 - < Severity:
+ order = ["warning", "medium", "high", "critical"]
+ return order[max(order.index(current), order.index(candidate))] # type: ignore
+
+
+def _flatten_keys(d: Any, prefix: str = "") -> Set[str]:
+ """YAML dict를 dotted key path 집합으로 평탄화한다.
+
+ 예: {"webserver": {"defaultUser": {"enabled": true}}}
+ → {"webserver", "webserver.defaultUser", "webserver.defaultUser.enabled"}
+ """
+ keys: Set[str] = set()
+ if not isinstance(d, dict):
+ return keys
+ for k, v in d.items():
+ full_key = f"{prefix}.{k}" if prefix else k
+ keys.add(full_key)
+ if isinstance(v, dict):
+ keys.update(_flatten_keys(v, full_key))
+ return keys
+
+
+def breaking_change_check(payload: Dict[str, Any]) -> Dict[str, Any]:
+ inp = BreakingCheckInput(**payload)
+ diff = inp.diff_json
+
+ # custom-values.yaml key 집합 (없으면 None → 모든 key가 "미사용"으로 처리)
+ custom_keys: Optional[Set[str]] = (
+ _flatten_keys(inp.custom_values) if inp.custom_values else None
+ )
+
+ reasons: List[BreakingReason] = []
+ warnings: List[BreakingReason] = []
+ severity: Severity = "warning"
+
+ # values rules: custom-values.yaml에 있는 key 변경만 breaking
+ values = diff.get("values", {})
+ for k in values.get("removed", []):
+ if custom_keys is not None and k in custom_keys:
+ # 실제 사용 중인 key 삭제 → breaking (custom-values.yaml 수정 필요)
+ reasons.append(BreakingReason(
+ type="values_key_removed",
+ key=k,
+ detail="custom-values.yaml에서 사용 중인 key 삭제 — 수정 필요",
+ ))
+ severity = _severity_max(severity, "high")
+ else:
+ # 미사용 key 삭제 → warning (수정 불필요)
+ warnings.append(BreakingReason(
+ type="values_key_removed",
+ key=k,
+ detail="custom-values.yaml에서 사용하지 않는 key 삭제 — 수정 불필요",
+ ))
+
+ for item in values.get("type_changed", []):
+ k = item.get("key", "")
+ detail = f"{item.get('old_type')} → {item.get('new_type')}"
+ if custom_keys is not None and k in custom_keys:
+ reasons.append(BreakingReason(
+ type="values_type_changed",
+ key=k,
+ detail=f"custom-values.yaml에서 사용 중인 key 타입 변경 ({detail}) — 수정 필요",
+ ))
+ severity = _severity_max(severity, "high")
+ else:
+ warnings.append(BreakingReason(
+ type="values_type_changed",
+ key=k,
+ detail=f"미사용 key 타입 변경 ({detail}) — 수정 불필요",
+ ))
+
+ # template rules: 카탈로그 맥락에서 custom-values.yaml 수정 요인 아님 → warning
+ templates = diff.get("templates", {})
+ for rid, info in templates.items():
+ if info.get("removed"):
+ warnings.append(BreakingReason(
+ type="resource_removed",
+ resource=rid,
+ detail="리소스 삭제 — 신규 배포 시 참고",
+ ))
+ if rid.startswith("Service/") and info.get("port_changed"):
+ warnings.append(BreakingReason(
+ type="service_port_changed",
+ resource=rid,
+ detail="Service port 변경 — 신규 배포 시 Ingress/LB 설정 확인",
+ ))
+
+ # dependency rules: custom-values.yaml에 subchart prefix key가 있으면 breaking
+ deps = diff.get("dependencies", {})
+ for dep, change in deps.get("version_changed", {}).items():
+ old = change.get("old") or ""
+ new = change.get("new") or ""
+ try:
+ old_major = int(str(old).split(".")[0])
+ new_major = int(str(new).split(".")[0])
+ if new_major > old_major:
+ dep_prefix = f"{dep}."
+ dep_in_custom = (
+ custom_keys is not None
+ and any(k.startswith(dep_prefix) for k in custom_keys)
+ )
+ if dep_in_custom:
+ reasons.append(BreakingReason(
+ type="dependency_major_changed",
+ key=dep,
+ detail=f"{old} → {new} — custom-values.yaml에 해당 subchart 설정 있음, 수정 검토 필요",
+ ))
+ severity = _severity_max(severity, "medium")
+ else:
+ warnings.append(BreakingReason(
+ type="dependency_major_changed",
+ key=dep,
+ detail=f"{old} → {new} — custom-values.yaml에 해당 subchart 설정 없음",
+ ))
+ except Exception:
+ pass
+
+ breaking = len(reasons) > 0
+ out = BreakingCheckOutput(
+ breaking=breaking,
+ severity=severity,
+ reasons=reasons,
+ warnings=warnings,
+ )
+ return json.loads(out.model_dump_json())
diff --git a/update_catalog/skills/breaking_change_check/scripts/update_catalog/skill_interface.py b/update_catalog/skills/breaking_change_check/scripts/update_catalog/skill_interface.py
new file mode 100644
index 0000000..3ab7c14
--- /dev/null
+++ b/update_catalog/skills/breaking_change_check/scripts/update_catalog/skill_interface.py
@@ -0,0 +1,147 @@
+"""Skill Interface v1.0 for Helm upgrade automation."""
+
+from __future__ import annotations
+
+from typing import Any, Dict, List, Literal, Optional
+from pydantic import BaseModel, Field
+
+SKILL_INTERFACE_VERSION = "1.0"
+
+
+# -------------------------
+# Common Error Schema
+# -------------------------
+class SkillError(BaseModel):
+ code: str
+ message: str
+ retryable: bool = False
+ details: Optional[Dict[str, Any]] = None
+
+
+class ErrorResponse(BaseModel):
+ error: SkillError
+
+
+# -------------------------
+# Shared Types
+# -------------------------
+Severity = Literal["critical", "high", "medium", "warning"]
+
+
+class BreakingReason(BaseModel):
+ type: str
+ resource: Optional[str] = None
+ key: Optional[str] = None
+ detail: Optional[str] = None
+
+
+# -------------------------
+# helm_diff
+# -------------------------
+class HelmDiffInput(BaseModel):
+ chart: str
+ repo: Optional[str] = None
+ chart_path: Optional[str] = None
+ from_version: str
+ to_version: str
+ values_override: Optional[Dict[str, Any]] = None
+
+
+class HelmDiffOutput(BaseModel):
+ chart: str
+ from_version: str
+ to_version: str
+ generated_at: str
+ values: Dict[str, Any]
+ templates: Dict[str, Any]
+ crd: Dict[str, Any]
+ dependencies: Dict[str, Any]
+ errors: List[Dict[str, Any]] = Field(default_factory=list)
+
+
+# -------------------------
+# breaking_change_check
+# -------------------------
+class BreakingCheckInput(BaseModel):
+ diff_json: Dict[str, Any]
+ custom_values: Optional[Dict[str, Any]] = None # custom-values.yaml 내용 (없으면 전체 key 검사)
+
+
+class BreakingCheckOutput(BaseModel):
+ breaking: bool
+ severity: Severity
+ reasons: List[BreakingReason] = Field(default_factory=list)
+ warnings: List[BreakingReason] = Field(default_factory=list)
+
+
+# -------------------------
+# generate_upgrade_doc
+# -------------------------
+class GenerateDocInput(BaseModel):
+ diff_json: Dict[str, Any]
+ breaking_result: Dict[str, Any]
+ docs_context: Optional[Dict[str, str]] = None
+ max_tokens: int = 50000
+
+
+class GenerateDocOutput(BaseModel):
+ markdown: str
+ truncated: bool = False
+
+
+# -------------------------
+# update_docs_file
+# -------------------------
+class UpdateDocsInput(BaseModel):
+ repo_path: str
+ docs_file: str = "docs/upgrade.md"
+ version: str
+ content: str
+ overwrite: bool = False
+
+
+class UpdateDocsOutput(BaseModel):
+ success: bool
+ file_path: str
+ already_existed: bool = False
+
+
+# -------------------------
+# create_pr
+# -------------------------
+class CreatePRInput(BaseModel):
+ chart: str
+ from_version: str
+ to_version: str
+ repo_path: str
+ breaking: bool
+ severity: Severity
+ reasons: List[BreakingReason] = Field(default_factory=list)
+ warnings: List[BreakingReason] = Field(default_factory=list)
+
+
+class CreatePROutput(BaseModel):
+ pr_url: str
+ branch_name: str
+ labels: List[str]
+ committed: bool = False
+
+
+# -------------------------
+# deploy_validate
+# -------------------------
+class DeployValidateInput(BaseModel):
+ chart: str
+ repo: Optional[str] = None
+ version: str
+ values_override: Optional[Dict[str, Any]] = None
+ namespace: str
+ timeout: int = 300
+
+
+class DeployValidateOutput(BaseModel):
+ success: bool
+ dry_run_passed: bool
+ pod_status: Dict[str, int]
+ events: List[Dict[str, Any]] = Field(default_factory=list)
+ logs: Optional[str] = None
diff --git a/update_catalog/skills/chart_updater/SKILL.md b/update_catalog/skills/chart_updater/SKILL.md
new file mode 100644
index 0000000..1fcb1e4
--- /dev/null
+++ b/update_catalog/skills/chart_updater/SKILL.md
@@ -0,0 +1,33 @@
+---
+name: chart_updater
+description: Pull a Helm chart version and extract it into manifests/helm//.
+---
+
+# chart_updater
+
+Pull a chart from helm repo and create a versioned directory in dip-catalog.
+`from_version`이 주어지면 이전 버전 디렉토리에서 아래 파일을 새 버전 디렉토리로 복사한다 (이미 존재하는 파일은 건너뜀):
+- `BUILD-README.md` — 복사 + `from_version` → `version` 버전 번호 치환
+- `CUSTOM-README.md` — 그대로 복사
+- `custom-values.yaml` — 그대로 복사
+
+## Input schema
+```json
+{
+ "catalog_root": "string",
+ "chart": "string",
+ "repo": "string",
+ "version": "string",
+ "from_version": "string (optional)"
+}
+```
+
+## Output schema
+```json
+{
+ "success": "boolean",
+ "already_existed": "boolean",
+ "dest_dir": "string",
+ "copied_files": ["string"]
+}
+```
diff --git a/update_catalog/skills/chart_updater/scripts/run.py b/update_catalog/skills/chart_updater/scripts/run.py
new file mode 100755
index 0000000..3d29823
--- /dev/null
+++ b/update_catalog/skills/chart_updater/scripts/run.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+import argparse
+import json
+import os
+import sys
+from pathlib import Path
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--catalog-root", required=True)
+ parser.add_argument("--chart", required=True)
+ parser.add_argument("--repo", required=True)
+ parser.add_argument("--version", required=True)
+ parser.add_argument("--from-version", default=None)
+ args = parser.parse_args()
+
+ repo_root = os.environ.get("UPDATE_CATALOG_ROOT")
+ if repo_root:
+ sys.path.insert(0, str(Path(repo_root) / "src"))
+ else:
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+ from update_catalog.chart_updater import chart_updater # type: ignore
+
+ out = chart_updater({
+ "catalog_root": args.catalog_root,
+ "chart": args.chart,
+ "repo": args.repo,
+ "version": args.version,
+ "from_version": args.from_version,
+ })
+ print(json.dumps(out, ensure_ascii=False, indent=2))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/update_catalog/skills/chart_updater/scripts/update_catalog/__init__.py b/update_catalog/skills/chart_updater/scripts/update_catalog/__init__.py
new file mode 100644
index 0000000..16f23e1
--- /dev/null
+++ b/update_catalog/skills/chart_updater/scripts/update_catalog/__init__.py
@@ -0,0 +1 @@
+__all__ = ["skill_interface"]
diff --git a/update_catalog/skills/chart_updater/scripts/update_catalog/__pycache__/__init__.cpython-312.pyc b/update_catalog/skills/chart_updater/scripts/update_catalog/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..d4074ca
Binary files /dev/null and b/update_catalog/skills/chart_updater/scripts/update_catalog/__pycache__/__init__.cpython-312.pyc differ
diff --git a/update_catalog/skills/chart_updater/scripts/update_catalog/__pycache__/chart_updater.cpython-312.pyc b/update_catalog/skills/chart_updater/scripts/update_catalog/__pycache__/chart_updater.cpython-312.pyc
new file mode 100644
index 0000000..a110ebb
Binary files /dev/null and b/update_catalog/skills/chart_updater/scripts/update_catalog/__pycache__/chart_updater.cpython-312.pyc differ
diff --git a/update_catalog/skills/chart_updater/scripts/update_catalog/chart_updater.py b/update_catalog/skills/chart_updater/scripts/update_catalog/chart_updater.py
new file mode 100644
index 0000000..4add081
--- /dev/null
+++ b/update_catalog/skills/chart_updater/scripts/update_catalog/chart_updater.py
@@ -0,0 +1,97 @@
+"""chart_updater skill implementation.
+
+Pull latest chart from helm repo and extract into manifests/helm//.
+"""
+from __future__ import annotations
+
+import shutil
+import subprocess
+import tarfile
+from pathlib import Path
+from typing import Any, Dict, List
+
+
+def _run(cmd: list[str], cwd: str | None = None) -> str:
+ res = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
+ if res.returncode != 0:
+ raise RuntimeError(res.stderr.strip() or f"command failed: {' '.join(cmd)}")
+ return res.stdout
+
+
+# Files to carry over from the previous version directory.
+# (filename, replace_version): replace_version=True → substitute from_version with to_version in content.
+_CARRY_OVER_FILES = [
+ ("BUILD-README.md", True),
+ ("CUSTOM-README.md", False),
+ ("custom-values.yaml", False),
+]
+
+
+def _copy_custom_files(
+ src_dir: Path, dest_dir: Path, from_version: str, to_version: str
+) -> List[str]:
+ """Copy carry-over files from src_dir to dest_dir.
+
+ Skips files that already exist in dest_dir (idempotent).
+ Returns list of copied file names.
+ """
+ copied: List[str] = []
+ for fname, needs_replace in _CARRY_OVER_FILES:
+ src = src_dir / fname
+ dest = dest_dir / fname
+ if not src.exists() or dest.exists():
+ continue
+ content = src.read_text(encoding="utf-8")
+ if needs_replace:
+ content = content.replace(from_version, to_version)
+ dest.write_text(content, encoding="utf-8")
+ copied.append(fname)
+ return copied
+
+
+def chart_updater(payload: Dict[str, Any]) -> Dict[str, Any]:
+ catalog_root = Path(payload["catalog_root"])
+ chart = payload["chart"]
+ repo = payload["repo"]
+ version = payload["version"]
+ from_version: str | None = payload.get("from_version")
+
+ dest_dir = catalog_root / "manifests" / "helm" / chart / version
+ already_existed = dest_dir.exists()
+
+ if not already_existed:
+ work_dir = catalog_root / ".tmp_chart_pull"
+ work_dir.mkdir(parents=True, exist_ok=True)
+
+ _run(["helm", "pull", f"{repo}/{chart}", "--version", version], cwd=str(work_dir))
+
+ tgz = next(work_dir.glob(f"{chart}-*.tgz"), None)
+ if tgz is None:
+ raise FileNotFoundError("pulled chart archive not found")
+
+ with tarfile.open(tgz, "r:gz") as tar:
+ tar.extractall(path=work_dir)
+
+ extracted = work_dir / chart
+ if not extracted.exists():
+ # some charts may use different folder name; fallback to first dir
+ dirs = [p for p in work_dir.iterdir() if p.is_dir() and p.name != "__pycache__"]
+ if dirs:
+ extracted = dirs[0]
+
+ dest_dir.parent.mkdir(parents=True, exist_ok=True)
+ shutil.move(str(extracted), str(dest_dir))
+ tgz.unlink(missing_ok=True)
+
+ copied_files: List[str] = []
+ if from_version:
+ src_dir = catalog_root / "manifests" / "helm" / chart / from_version
+ if src_dir.exists():
+ copied_files = _copy_custom_files(src_dir, dest_dir, from_version, version)
+
+ return {
+ "success": True,
+ "already_existed": already_existed,
+ "dest_dir": str(dest_dir),
+ "copied_files": copied_files,
+ }
diff --git a/update_catalog/skills/chart_updater/scripts/update_catalog/skill_interface.py b/update_catalog/skills/chart_updater/scripts/update_catalog/skill_interface.py
new file mode 100644
index 0000000..4690204
--- /dev/null
+++ b/update_catalog/skills/chart_updater/scripts/update_catalog/skill_interface.py
@@ -0,0 +1,144 @@
+"""Skill Interface v1.0 for Helm upgrade automation."""
+
+from __future__ import annotations
+
+from typing import Any, Dict, List, Literal, Optional
+from pydantic import BaseModel, Field
+
+SKILL_INTERFACE_VERSION = "1.0"
+
+
+# -------------------------
+# Common Error Schema
+# -------------------------
+class SkillError(BaseModel):
+ code: str
+ message: str
+ retryable: bool = False
+ details: Optional[Dict[str, Any]] = None
+
+
+class ErrorResponse(BaseModel):
+ error: SkillError
+
+
+# -------------------------
+# Shared Types
+# -------------------------
+Severity = Literal["critical", "high", "medium", "warning"]
+
+
+class BreakingReason(BaseModel):
+ type: str
+ resource: Optional[str] = None
+ key: Optional[str] = None
+ detail: Optional[str] = None
+
+
+# -------------------------
+# helm_diff
+# -------------------------
+class HelmDiffInput(BaseModel):
+ chart: str
+ repo: Optional[str] = None
+ chart_path: Optional[str] = None
+ from_version: str
+ to_version: str
+ values_override: Optional[Dict[str, Any]] = None
+
+
+class HelmDiffOutput(BaseModel):
+ chart: str
+ from_version: str
+ to_version: str
+ generated_at: str
+ values: Dict[str, Any]
+ templates: Dict[str, Any]
+ crd: Dict[str, Any]
+ dependencies: Dict[str, Any]
+ errors: List[Dict[str, Any]] = Field(default_factory=list)
+
+
+# -------------------------
+# breaking_change_check
+# -------------------------
+class BreakingCheckInput(BaseModel):
+ diff_json: Dict[str, Any]
+
+
+class BreakingCheckOutput(BaseModel):
+ breaking: bool
+ severity: Severity
+ reasons: List[BreakingReason] = Field(default_factory=list)
+ warnings: List[BreakingReason] = Field(default_factory=list)
+
+
+# -------------------------
+# generate_upgrade_doc
+# -------------------------
+class GenerateDocInput(BaseModel):
+ diff_json: Dict[str, Any]
+ breaking_result: Dict[str, Any]
+ docs_context: Optional[Dict[str, str]] = None
+ max_tokens: int = 50000
+
+
+class GenerateDocOutput(BaseModel):
+ markdown: str
+ truncated: bool = False
+
+
+# -------------------------
+# update_docs_file
+# -------------------------
+class UpdateDocsInput(BaseModel):
+ repo_path: str
+ docs_file: str = "docs/upgrade.md"
+ version: str
+ content: str
+ overwrite: bool = False
+
+
+class UpdateDocsOutput(BaseModel):
+ success: bool
+ file_path: str
+ already_existed: bool = False
+
+
+# -------------------------
+# create_pr
+# -------------------------
+class CreatePRInput(BaseModel):
+ chart: str
+ from_version: str
+ to_version: str
+ repo_path: str
+ doc_content: str
+ breaking: bool
+ severity: Severity
+
+
+class CreatePROutput(BaseModel):
+ pr_url: str
+ branch_name: str
+ labels: List[str]
+
+
+# -------------------------
+# deploy_validate
+# -------------------------
+class DeployValidateInput(BaseModel):
+ chart: str
+ repo: Optional[str] = None
+ version: str
+ values_override: Optional[Dict[str, Any]] = None
+ namespace: str
+ timeout: int = 300
+
+
+class DeployValidateOutput(BaseModel):
+ success: bool
+ dry_run_passed: bool
+ pod_status: Dict[str, int]
+ events: List[Dict[str, Any]] = Field(default_factory=list)
+ logs: Optional[str] = None
diff --git a/update_catalog/skills/chart_version_detector/SKILL.md b/update_catalog/skills/chart_version_detector/SKILL.md
new file mode 100644
index 0000000..a4a7c7b
--- /dev/null
+++ b/update_catalog/skills/chart_version_detector/SKILL.md
@@ -0,0 +1,29 @@
+---
+name: chart_version_detector
+description: Detect current chart version from dip-catalog and resolve latest version from helm repo.
+---
+
+# chart_version_detector
+
+Determine current_version from manifests/helm// directories and parse repo info from BUILD-README.md.
+Then resolve latest_version from helm repo.
+
+## Input schema
+```json
+{
+ "catalog_root": "string",
+ "chart": "string"
+}
+```
+
+## Output schema
+```json
+{
+ "chart": "string",
+ "catalog_root": "string",
+ "current_version": "string",
+ "repo": "string | null",
+ "repo_url": "string | null",
+ "latest_version": "string | null"
+}
+```
diff --git a/update_catalog/skills/chart_version_detector/scripts/run.py b/update_catalog/skills/chart_version_detector/scripts/run.py
new file mode 100755
index 0000000..dd09331
--- /dev/null
+++ b/update_catalog/skills/chart_version_detector/scripts/run.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+import argparse
+import json
+import os
+import sys
+from pathlib import Path
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--catalog-root", required=True)
+ parser.add_argument("--chart", required=True)
+ args = parser.parse_args()
+
+ repo_root = os.environ.get("UPDATE_CATALOG_ROOT")
+ if repo_root:
+ sys.path.insert(0, str(Path(repo_root) / "src"))
+ else:
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+ from update_catalog.chart_version_detector import chart_version_detector # type: ignore
+
+ out = chart_version_detector({
+ "catalog_root": args.catalog_root,
+ "chart": args.chart,
+ })
+ print(json.dumps(out, ensure_ascii=False, indent=2))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/update_catalog/skills/chart_version_detector/scripts/update_catalog/__init__.py b/update_catalog/skills/chart_version_detector/scripts/update_catalog/__init__.py
new file mode 100644
index 0000000..16f23e1
--- /dev/null
+++ b/update_catalog/skills/chart_version_detector/scripts/update_catalog/__init__.py
@@ -0,0 +1 @@
+__all__ = ["skill_interface"]
diff --git a/update_catalog/skills/chart_version_detector/scripts/update_catalog/__pycache__/__init__.cpython-312.pyc b/update_catalog/skills/chart_version_detector/scripts/update_catalog/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..8004075
Binary files /dev/null and b/update_catalog/skills/chart_version_detector/scripts/update_catalog/__pycache__/__init__.cpython-312.pyc differ
diff --git a/update_catalog/skills/chart_version_detector/scripts/update_catalog/__pycache__/chart_version_detector.cpython-312.pyc b/update_catalog/skills/chart_version_detector/scripts/update_catalog/__pycache__/chart_version_detector.cpython-312.pyc
new file mode 100644
index 0000000..5d27026
Binary files /dev/null and b/update_catalog/skills/chart_version_detector/scripts/update_catalog/__pycache__/chart_version_detector.cpython-312.pyc differ
diff --git a/update_catalog/skills/chart_version_detector/scripts/update_catalog/chart_version_detector.py b/update_catalog/skills/chart_version_detector/scripts/update_catalog/chart_version_detector.py
new file mode 100644
index 0000000..e643c60
--- /dev/null
+++ b/update_catalog/skills/chart_version_detector/scripts/update_catalog/chart_version_detector.py
@@ -0,0 +1,131 @@
+"""chart_version_detector skill implementation.
+
+Determine current_version from dip-catalog directory, parse repo info from BUILD-README,
+then resolve latest_version from helm repo.
+"""
+from __future__ import annotations
+
+import json
+import re
+import subprocess
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Tuple
+
+import yaml
+
+
+def _run(cmd: List[str]) -> str:
+ res = subprocess.run(cmd, capture_output=True, text=True)
+ if res.returncode != 0:
+ raise RuntimeError(res.stderr.strip() or f"command failed: {' '.join(cmd)}")
+ return res.stdout
+
+
+def _parse_version(v: str) -> Tuple[int, ...]:
+ # basic semver compare: take numeric parts only
+ v = v.strip()
+ v = re.split(r"[+-]", v)[0]
+ parts = v.split(".")
+ out = []
+ for p in parts:
+ try:
+ out.append(int(p))
+ except ValueError:
+ out.append(0)
+ return tuple(out)
+
+
+def _current_version_from_dirs(chart_dir: Path) -> Optional[str]:
+ versions = [p.name for p in chart_dir.iterdir() if p.is_dir()]
+ if not versions:
+ return None
+ versions_sorted = sorted(versions, key=_parse_version)
+ return versions_sorted[-1]
+
+
+def _current_version_from_chart_yaml(chart_dir: Path) -> Optional[str]:
+ chart_yaml = chart_dir / "Chart.yaml"
+ if not chart_yaml.exists():
+ return None
+ with chart_yaml.open("r", encoding="utf-8") as f:
+ data = yaml.safe_load(f) or {}
+ return data.get("version")
+
+
+def _parse_repo_from_build_readme(path: Path) -> Tuple[Optional[str], Optional[str]]:
+ if not path.exists():
+ return None, None
+ text = path.read_text(encoding="utf-8")
+ m = re.search(r"helm\s+repo\s+add\s+(\S+)\s+(\S+)", text)
+ if not m:
+ return None, None
+ return m.group(1), m.group(2)
+
+
+def _ensure_repo(repo: str, url: str) -> None:
+ try:
+ out = _run(["helm", "repo", "list"])
+ if repo in out:
+ return
+ except Exception:
+ pass
+ _run(["helm", "repo", "add", repo, url])
+ _run(["helm", "repo", "update"])
+
+
+def _latest_version(repo: str, chart: str) -> Optional[str]:
+ out = _run(["helm", "search", "repo", f"{repo}/{chart}", "--versions"])
+ lines = [l for l in out.splitlines() if l.strip()]
+ if len(lines) < 2:
+ return None
+ # header at line 0; next line is latest
+ parts = re.split(r"\s+", lines[1].strip())
+ if len(parts) >= 2:
+ return parts[1]
+ return None
+
+
+def chart_version_detector(payload: Dict[str, Any]) -> Dict[str, Any]:
+ catalog_root = Path(payload["catalog_root"])
+ chart = payload["chart"]
+ chart_dir = catalog_root / "manifests" / "helm" / chart
+
+ if not chart_dir.exists():
+ return {
+ "chart": chart,
+ "catalog_root": str(catalog_root),
+ "current_version": None,
+ "repo": None,
+ "repo_url": None,
+ "latest_version": None,
+ "error": {"code": "CHART_NOT_FOUND", "message": f"chart path not found: {chart_dir}"},
+ }
+
+ current = _current_version_from_dirs(chart_dir)
+ if current is None:
+ return {
+ "chart": chart,
+ "catalog_root": str(catalog_root),
+ "current_version": None,
+ "repo": None,
+ "repo_url": None,
+ "latest_version": None,
+ "error": {"code": "VERSION_NOT_FOUND", "message": f"no version directories under {chart_dir}"},
+ }
+
+ build_readme = chart_dir / current / "BUILD-README.md"
+ repo, url = _parse_repo_from_build_readme(build_readme)
+
+ latest = None
+ if repo and url:
+ _ensure_repo(repo, url)
+ latest = _latest_version(repo, chart)
+
+ return {
+ "chart": chart,
+ "catalog_root": str(catalog_root),
+ "current_version": current,
+ "repo": repo,
+ "repo_url": url,
+ "latest_version": latest,
+ }
diff --git a/update_catalog/skills/chart_version_detector/scripts/update_catalog/skill_interface.py b/update_catalog/skills/chart_version_detector/scripts/update_catalog/skill_interface.py
new file mode 100644
index 0000000..4690204
--- /dev/null
+++ b/update_catalog/skills/chart_version_detector/scripts/update_catalog/skill_interface.py
@@ -0,0 +1,144 @@
+"""Skill Interface v1.0 for Helm upgrade automation."""
+
+from __future__ import annotations
+
+from typing import Any, Dict, List, Literal, Optional
+from pydantic import BaseModel, Field
+
+SKILL_INTERFACE_VERSION = "1.0"
+
+
+# -------------------------
+# Common Error Schema
+# -------------------------
+class SkillError(BaseModel):
+ code: str
+ message: str
+ retryable: bool = False
+ details: Optional[Dict[str, Any]] = None
+
+
+class ErrorResponse(BaseModel):
+ error: SkillError
+
+
+# -------------------------
+# Shared Types
+# -------------------------
+Severity = Literal["critical", "high", "medium", "warning"]
+
+
+class BreakingReason(BaseModel):
+ type: str
+ resource: Optional[str] = None
+ key: Optional[str] = None
+ detail: Optional[str] = None
+
+
+# -------------------------
+# helm_diff
+# -------------------------
+class HelmDiffInput(BaseModel):
+ chart: str
+ repo: Optional[str] = None
+ chart_path: Optional[str] = None
+ from_version: str
+ to_version: str
+ values_override: Optional[Dict[str, Any]] = None
+
+
+class HelmDiffOutput(BaseModel):
+ chart: str
+ from_version: str
+ to_version: str
+ generated_at: str
+ values: Dict[str, Any]
+ templates: Dict[str, Any]
+ crd: Dict[str, Any]
+ dependencies: Dict[str, Any]
+ errors: List[Dict[str, Any]] = Field(default_factory=list)
+
+
+# -------------------------
+# breaking_change_check
+# -------------------------
+class BreakingCheckInput(BaseModel):
+ diff_json: Dict[str, Any]
+
+
+class BreakingCheckOutput(BaseModel):
+ breaking: bool
+ severity: Severity
+ reasons: List[BreakingReason] = Field(default_factory=list)
+ warnings: List[BreakingReason] = Field(default_factory=list)
+
+
+# -------------------------
+# generate_upgrade_doc
+# -------------------------
+class GenerateDocInput(BaseModel):
+ diff_json: Dict[str, Any]
+ breaking_result: Dict[str, Any]
+ docs_context: Optional[Dict[str, str]] = None
+ max_tokens: int = 50000
+
+
+class GenerateDocOutput(BaseModel):
+ markdown: str
+ truncated: bool = False
+
+
+# -------------------------
+# update_docs_file
+# -------------------------
+class UpdateDocsInput(BaseModel):
+ repo_path: str
+ docs_file: str = "docs/upgrade.md"
+ version: str
+ content: str
+ overwrite: bool = False
+
+
+class UpdateDocsOutput(BaseModel):
+ success: bool
+ file_path: str
+ already_existed: bool = False
+
+
+# -------------------------
+# create_pr
+# -------------------------
+class CreatePRInput(BaseModel):
+ chart: str
+ from_version: str
+ to_version: str
+ repo_path: str
+ doc_content: str
+ breaking: bool
+ severity: Severity
+
+
+class CreatePROutput(BaseModel):
+ pr_url: str
+ branch_name: str
+ labels: List[str]
+
+
+# -------------------------
+# deploy_validate
+# -------------------------
+class DeployValidateInput(BaseModel):
+ chart: str
+ repo: Optional[str] = None
+ version: str
+ values_override: Optional[Dict[str, Any]] = None
+ namespace: str
+ timeout: int = 300
+
+
+class DeployValidateOutput(BaseModel):
+ success: bool
+ dry_run_passed: bool
+ pod_status: Dict[str, int]
+ events: List[Dict[str, Any]] = Field(default_factory=list)
+ logs: Optional[str] = None
diff --git a/update_catalog/skills/create_pr/SKILL.md b/update_catalog/skills/create_pr/SKILL.md
new file mode 100644
index 0000000..688e012
--- /dev/null
+++ b/update_catalog/skills/create_pr/SKILL.md
@@ -0,0 +1,49 @@
+---
+name: create_pr
+description: Create a GitHub PR for the chart upgrade.
+user-invokable: false
+---
+
+# create_pr (Skill)
+
+브랜치 생성 → 커밋 → 푸시 → GitHub PR 생성을 수행한다.
+breaking=true이면 `needs-review` 레이블, 아니면 `auto-update` 레이블을 붙인다.
+실행 후 `main` 브랜치로 복귀하여 다음 cronjob 실행을 위한 클린 상태를 유지한다.
+
+## 브랜치 명명 규칙
+
+`update-{chart}/{to_version}` — 예: `update-airflow/1.19.0`
+
+## 재실행 안전성 (Idempotent)
+
+- 브랜치가 이미 존재하면 체크아웃 후 추가 커밋
+- PR이 이미 존재하면 URL만 반환 (중복 PR 생성 안 함)
+
+## Input schema
+```json
+{
+ "repo_path": "string",
+ "chart": "string",
+ "from_version": "string",
+ "to_version": "string",
+ "breaking": "boolean",
+ "severity": "critical|high|medium|warning",
+ "reasons": "array (breaking_change_check.reasons)",
+ "warnings": "array (breaking_change_check.warnings)"
+}
+```
+
+## Output schema
+```json
+{
+ "pr_url": "string",
+ "branch_name": "string",
+ "labels": ["string"],
+ "committed": "boolean"
+}
+```
+
+## 의존 도구
+
+- `git` — 브랜치/커밋/푸시
+- `gh` (GitHub CLI) — PR 생성 및 레이블 관리. `gh auth login` 완료 필요.
diff --git a/update_catalog/skills/create_pr/scripts/run.py b/update_catalog/skills/create_pr/scripts/run.py
new file mode 100644
index 0000000..2427ab0
--- /dev/null
+++ b/update_catalog/skills/create_pr/scripts/run.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python3
+import argparse
+import json
+import os
+import sys
+from pathlib import Path
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--repo-path", required=True, help="dip-catalog git repo 경로")
+ parser.add_argument("--chart", required=True)
+ parser.add_argument("--from-version", required=True)
+ parser.add_argument("--to-version", required=True)
+ parser.add_argument("--breaking-file", required=True, help="breaking_change_check 출력 JSON 경로")
+ args = parser.parse_args()
+
+ repo_root = os.environ.get("UPDATE_CATALOG_ROOT")
+ if repo_root:
+ sys.path.insert(0, str(Path(repo_root) / "src"))
+ else:
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+ from update_catalog.create_pr import create_pr # type: ignore
+
+ with open(args.breaking_file, "r", encoding="utf-8") as f:
+ breaking_data = json.load(f)
+
+ out = create_pr({
+ "repo_path": args.repo_path,
+ "chart": args.chart,
+ "from_version": args.from_version,
+ "to_version": args.to_version,
+ "breaking": breaking_data.get("breaking", False),
+ "severity": breaking_data.get("severity", "warning"),
+ "reasons": breaking_data.get("reasons", []),
+ "warnings": breaking_data.get("warnings", []),
+ })
+ print(json.dumps(out, ensure_ascii=False, indent=2))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/update_catalog/skills/create_pr/scripts/update_catalog/__init__.py b/update_catalog/skills/create_pr/scripts/update_catalog/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/update_catalog/skills/create_pr/scripts/update_catalog/create_pr.py b/update_catalog/skills/create_pr/scripts/update_catalog/create_pr.py
new file mode 100644
index 0000000..3c1b341
--- /dev/null
+++ b/update_catalog/skills/create_pr/scripts/update_catalog/create_pr.py
@@ -0,0 +1,164 @@
+"""create_pr skill implementation.
+
+Branch → Commit → Push → GitHub PR 생성.
+"""
+from __future__ import annotations
+
+import json
+import subprocess
+from pathlib import Path
+from typing import Any, Dict, List, Optional
+
+from .skill_interface import CreatePRInput, CreatePROutput, BreakingReason # type: ignore
+
+
+def _git(args: List[str], cwd: Path) -> str:
+ res = subprocess.run(["git"] + args, cwd=cwd, capture_output=True, text=True)
+ if res.returncode != 0:
+ raise RuntimeError(f"git {' '.join(args)} failed: {res.stderr.strip()}")
+ return res.stdout.strip()
+
+
+def _gh(args: List[str], cwd: Path) -> str:
+ res = subprocess.run(["gh"] + args, cwd=cwd, capture_output=True, text=True)
+ if res.returncode != 0:
+ raise RuntimeError(f"gh {' '.join(args)} failed: {res.stderr.strip()}")
+ return res.stdout.strip()
+
+
+def _get_existing_pr_url(branch_name: str, cwd: Path) -> Optional[str]:
+ try:
+ url = _gh(
+ ["pr", "list", "--head", branch_name, "--json", "url", "--jq", ".[0].url"],
+ cwd=cwd,
+ )
+ return url.strip() or None
+ except Exception:
+ return None
+
+
+def _ensure_label(label: str, cwd: Path) -> None:
+ """레이블이 없으면 생성한다."""
+ label_colors = {
+ "needs-review": "e11d48", # 빨강
+ "auto-update": "16a34a", # 초록
+ }
+ try:
+ _gh(["label", "list", "--json", "name", "--jq", f'.[] | select(.name == "{label}") | .name'], cwd=cwd)
+ except Exception:
+ pass
+ color = label_colors.get(label, "0075ca")
+ try:
+ _gh(["label", "create", label, "--color", color, "--force"], cwd=cwd)
+ except Exception:
+ pass # 레이블 생성 실패해도 PR 생성은 계속
+
+
+def _build_pr_body(
+ chart: str,
+ from_version: str,
+ to_version: str,
+ breaking: bool,
+ severity: str,
+ reasons: List[BreakingReason],
+ warnings: List[BreakingReason],
+) -> str:
+ lines = [
+ f"## Helm Chart Update: {chart} `{from_version}` → `{to_version}`",
+ "",
+ f"**Severity**: `{severity}` ",
+ f"**Breaking**: {'✅ Human review required before merge' if breaking else '❌ No breaking changes'}",
+ "",
+ ]
+ if reasons:
+ lines.append("### Breaking Changes")
+ for r in reasons:
+ key = r.get("key") or r.get("resource") or ""
+ lines.append(f"- `[{r['type']}]` {key} — {r.get('detail', '')}")
+ lines.append("")
+ if warnings:
+ lines.append(f"### Warnings ({len(warnings)} removed keys not used in custom-values.yaml)")
+ for w in warnings:
+ lines.append(f"- `[{w['type']}]` {w.get('key', '')} — {w.get('detail', '')}")
+ lines.append("")
+ if not reasons:
+ lines += ["No breaking changes detected.", ""]
+ lines.append("---")
+ lines.append("*Generated by update-catalog automation*")
+ return "\n".join(lines)
+
+
+def create_pr(payload: Dict[str, Any]) -> Dict[str, Any]:
+ inp = CreatePRInput(**payload)
+ repo_path = Path(inp.repo_path)
+ chart = inp.chart
+ from_version = inp.from_version
+ to_version = inp.to_version
+ breaking = inp.breaking
+ severity = inp.severity
+ reasons = inp.reasons
+ warnings = inp.warnings
+
+ branch_name = f"update-{chart}/{to_version}"
+ chart_dir = f"manifests/helm/{chart}/{to_version}"
+
+ # 1. main으로 이동 후 최신화
+ _git(["checkout", "main"], cwd=repo_path)
+ _git(["pull"], cwd=repo_path)
+
+ # 2. 브랜치 생성 또는 기존 브랜치 체크아웃
+ local_exists = bool(_git(["branch", "--list", branch_name], cwd=repo_path).strip())
+ remote_exists = bool(_git(["branch", "-r", "--list", f"origin/{branch_name}"], cwd=repo_path).strip())
+
+ if local_exists:
+ _git(["checkout", branch_name], cwd=repo_path)
+ elif remote_exists:
+ _git(["checkout", "-b", branch_name, f"origin/{branch_name}"], cwd=repo_path)
+ else:
+ _git(["checkout", "-b", branch_name], cwd=repo_path)
+
+ # 3. 변경 파일 스테이징
+ _git(["add", chart_dir], cwd=repo_path)
+
+ # 4. 변경사항이 있으면 커밋
+ status = _git(["status", "--porcelain", chart_dir], cwd=repo_path)
+ committed = False
+ if status.strip():
+ breaking_tag = " [BREAKING]" if breaking else ""
+ commit_msg = f"update {chart}/{to_version}{breaking_tag}"
+ _git(["commit", "-m", commit_msg], cwd=repo_path)
+ committed = True
+
+ # 5. 원격 브랜치에 푸시
+ _git(["push", "-u", "origin", branch_name], cwd=repo_path)
+
+ # 6. 레이블 준비
+ labels = ["needs-review"] if breaking else ["auto-update"]
+ for label in labels:
+ _ensure_label(label, cwd=repo_path)
+
+ # 7. PR 생성 (이미 존재하면 URL만 반환)
+ pr_url = _get_existing_pr_url(branch_name, repo_path)
+ if not pr_url:
+ breaking_tag = " [BREAKING]" if breaking else ""
+ title = f"update {chart}: {from_version} → {to_version}{breaking_tag}"
+ body = _build_pr_body(chart, from_version, to_version, breaking, severity, reasons, warnings)
+ label_args: List[str] = []
+ for label in labels:
+ label_args += ["--label", label]
+ pr_url = _gh(
+ ["pr", "create", "--title", title, "--body", body, "--base", "main"] + label_args,
+ cwd=repo_path,
+ )
+
+ # 8. main으로 복귀 (다음 cronjob 실행을 위해 클린 상태 유지)
+ _git(["checkout", "main"], cwd=repo_path)
+
+ return json.loads(
+ CreatePROutput(
+ pr_url=pr_url,
+ branch_name=branch_name,
+ labels=labels,
+ committed=committed,
+ ).model_dump_json()
+ )
diff --git a/update_catalog/skills/create_pr/scripts/update_catalog/skill_interface.py b/update_catalog/skills/create_pr/scripts/update_catalog/skill_interface.py
new file mode 100644
index 0000000..da375f4
--- /dev/null
+++ b/update_catalog/skills/create_pr/scripts/update_catalog/skill_interface.py
@@ -0,0 +1,28 @@
+"""Skill Interface for create_pr."""
+from __future__ import annotations
+
+from typing import Any, Dict, List, Literal, Optional
+from pydantic import BaseModel, Field
+
+Severity = Literal["critical", "high", "medium", "warning"]
+
+# BreakingReason은 dict로 처리 (breaking_change_check 출력 그대로 수신)
+BreakingReason = Dict[str, Any]
+
+
+class CreatePRInput(BaseModel):
+ chart: str
+ from_version: str
+ to_version: str
+ repo_path: str
+ breaking: bool
+ severity: Severity
+ reasons: List[BreakingReason] = Field(default_factory=list)
+ warnings: List[BreakingReason] = Field(default_factory=list)
+
+
+class CreatePROutput(BaseModel):
+ pr_url: str
+ branch_name: str
+ labels: List[str]
+ committed: bool = False
diff --git a/update_catalog/skills/deploy_validate/SKILL.md b/update_catalog/skills/deploy_validate/SKILL.md
new file mode 100644
index 0000000..6a7cb7e
--- /dev/null
+++ b/update_catalog/skills/deploy_validate/SKILL.md
@@ -0,0 +1,33 @@
+---
+name: deploy_validate
+description: Deploy chart to a test namespace and validate health.
+user-invokable: false
+---
+
+# deploy_validate (Skill)
+
+Runs helm upgrade in a test namespace and reports health.
+
+## Input schema
+```json
+{
+ "chart": "string",
+ "repo": "string | null",
+ "chart_path": "string | null",
+ "version": "string",
+ "values_override": "object | null",
+ "namespace": "string",
+ "timeout": "number (default 300)"
+}
+```
+
+## Output schema
+```json
+{
+ "success": "boolean",
+ "dry_run_passed": "boolean",
+ "pod_status": "object",
+ "events": "array",
+ "logs": "string | null"
+}
+```
diff --git a/update_catalog/skills/generate_upgrade_doc/SKILL.md b/update_catalog/skills/generate_upgrade_doc/SKILL.md
new file mode 100644
index 0000000..a4ef368
--- /dev/null
+++ b/update_catalog/skills/generate_upgrade_doc/SKILL.md
@@ -0,0 +1,27 @@
+---
+name: generate_upgrade_doc
+description: Generate upgrade Markdown from structured diff and breaking results.
+user-invokable: false
+---
+
+# generate_upgrade_doc
+
+Produce Markdown upgrade notes from Structured Diff JSON and breaking analysis.
+
+## Input schema
+```json
+{
+ "diff_json": "object",
+ "breaking_result": "object",
+ "docs_context": "object | null",
+ "max_tokens": "number (default 50000)"
+}
+```
+
+## Output schema
+```json
+{
+ "markdown": "string",
+ "truncated": "boolean"
+}
+```
diff --git a/update_catalog/skills/generate_upgrade_doc/scripts/run.py b/update_catalog/skills/generate_upgrade_doc/scripts/run.py
new file mode 100755
index 0000000..3b1020d
--- /dev/null
+++ b/update_catalog/skills/generate_upgrade_doc/scripts/run.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+import argparse
+import json
+import os
+import sys
+from pathlib import Path
+
+
+DEFAULT_REPO = None
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--diff-file", required=True)
+ parser.add_argument("--breaking-file", required=True)
+ parser.add_argument("--docs-context", default=None)
+ parser.add_argument("--max-tokens", type=int, default=50000)
+ args = parser.parse_args()
+
+ repo_root = os.environ.get("UPDATE_CATALOG_ROOT")
+ if repo_root:
+ sys.path.insert(0, str(Path(repo_root) / "src"))
+ else:
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+ from update_catalog.generate_upgrade_doc import generate_upgrade_doc # type: ignore
+
+ with open(args.diff_file, "r", encoding="utf-8") as f:
+ diff_json = json.load(f)
+ with open(args.breaking_file, "r", encoding="utf-8") as f:
+ breaking = json.load(f)
+
+ docs_context = None
+ if args.docs_context:
+ with open(args.docs_context, "r", encoding="utf-8") as f:
+ text = f.read()
+ docs_context = {"CUSTOM-README.md": text}
+
+ out = generate_upgrade_doc({
+ "diff_json": diff_json,
+ "breaking_result": breaking,
+ "docs_context": docs_context,
+ "max_tokens": args.max_tokens,
+ })
+ print(json.dumps(out, ensure_ascii=False, indent=2))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/update_catalog/skills/generate_upgrade_doc/scripts/update_catalog/__init__.py b/update_catalog/skills/generate_upgrade_doc/scripts/update_catalog/__init__.py
new file mode 100644
index 0000000..16f23e1
--- /dev/null
+++ b/update_catalog/skills/generate_upgrade_doc/scripts/update_catalog/__init__.py
@@ -0,0 +1 @@
+__all__ = ["skill_interface"]
diff --git a/update_catalog/skills/generate_upgrade_doc/scripts/update_catalog/__pycache__/__init__.cpython-312.pyc b/update_catalog/skills/generate_upgrade_doc/scripts/update_catalog/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..38a728c
Binary files /dev/null and b/update_catalog/skills/generate_upgrade_doc/scripts/update_catalog/__pycache__/__init__.cpython-312.pyc differ
diff --git a/update_catalog/skills/generate_upgrade_doc/scripts/update_catalog/__pycache__/generate_upgrade_doc.cpython-312.pyc b/update_catalog/skills/generate_upgrade_doc/scripts/update_catalog/__pycache__/generate_upgrade_doc.cpython-312.pyc
new file mode 100644
index 0000000..268cabd
Binary files /dev/null and b/update_catalog/skills/generate_upgrade_doc/scripts/update_catalog/__pycache__/generate_upgrade_doc.cpython-312.pyc differ
diff --git a/update_catalog/skills/generate_upgrade_doc/scripts/update_catalog/__pycache__/skill_interface.cpython-312.pyc b/update_catalog/skills/generate_upgrade_doc/scripts/update_catalog/__pycache__/skill_interface.cpython-312.pyc
new file mode 100644
index 0000000..3569ff1
Binary files /dev/null and b/update_catalog/skills/generate_upgrade_doc/scripts/update_catalog/__pycache__/skill_interface.cpython-312.pyc differ
diff --git a/update_catalog/skills/generate_upgrade_doc/scripts/update_catalog/generate_upgrade_doc.py b/update_catalog/skills/generate_upgrade_doc/scripts/update_catalog/generate_upgrade_doc.py
new file mode 100644
index 0000000..c785552
--- /dev/null
+++ b/update_catalog/skills/generate_upgrade_doc/scripts/update_catalog/generate_upgrade_doc.py
@@ -0,0 +1,150 @@
+"""generate_upgrade_doc skill implementation.
+
+항상 실행된다 (breaking 여부 무관).
+- breaking=true + USE_CLAUDE_CLI=1: LLM으로 custom-values.yaml 수정 방법 포함 상세 가이드 생성
+- 그 외: 템플릿 기반 간단 요약 생성
+"""
+from __future__ import annotations
+
+import json
+import os
+import subprocess
+from typing import Any, Dict, List
+
+from .skill_interface import GenerateDocInput, GenerateDocOutput
+
+
+def _fallback_summary(diff: Dict[str, Any], breaking: Dict[str, Any]) -> str:
+ chart = diff.get("chart")
+ to_version = diff.get("to_version")
+ from_version = diff.get("from_version")
+
+ values = diff.get("values", {})
+ templates = diff.get("templates", {})
+ deps = diff.get("dependencies", {})
+
+ values_added = len(values.get("added", []))
+ values_removed = len(values.get("removed", []))
+ values_changed = len(values.get("changed", {}))
+ values_type_changed = len(values.get("type_changed", []))
+
+ template_added = sum(1 for v in templates.values() if isinstance(v, dict) and v.get("added"))
+ template_removed = sum(1 for v in templates.values() if isinstance(v, dict) and v.get("removed"))
+
+ breaking_flag = breaking.get("breaking", False)
+ severity = breaking.get("severity", "warning")
+ reasons = breaking.get("reasons", [])
+ warnings_list = breaking.get("warnings", [])
+
+ lines: List[str] = []
+
+ # update_docs_file이 ## {version} 헤더를 별도 추가하므로 여기서는 생략
+ lines.append("### 변경 요약")
+ if from_version:
+ lines.append(f"- from_version: {from_version}")
+ if to_version:
+ lines.append(f"- to_version: {to_version}")
+ if chart and from_version and to_version:
+ lines.append(f"- Chart `{chart}` {from_version} → {to_version} 업데이트")
+ lines.append(f"- Values: +{values_added} / -{values_removed} / ~{values_changed} / type~{values_type_changed}")
+ lines.append(f"- Templates: +{template_added} / -{template_removed}")
+ if deps:
+ added = len(deps.get("added", []))
+ removed = len(deps.get("removed", []))
+ changed = len(deps.get("version_changed", {}))
+ lines.append(f"- Dependencies: +{added} / -{removed} / ~{changed}")
+
+ lines.append("\n### custom-values.yaml 수정 필요 항목")
+ if breaking_flag:
+ for r in reasons:
+ key = r.get("key") or r.get("resource") or ""
+ detail = r.get("detail") or ""
+ lines.append(f"- **`{key}`**: {detail}".rstrip())
+ else:
+ lines.append("없음")
+
+ if warnings_list:
+ lines.append("\n### 배포 시 주의사항")
+ for w in warnings_list:
+ resource = w.get("resource") or w.get("key") or ""
+ detail = w.get("detail") or ""
+ wtype = w.get("type", "")
+ lines.append(f"- **{wtype}** {resource}: {detail}".rstrip())
+
+ lines.append("\n### 참고")
+ lines.append(f"- severity: {severity}")
+ lines.append(f"- breaking: {str(breaking_flag).lower()}")
+
+ return "\n".join(lines) + "\n"
+
+
+def _build_prompt(diff: Dict[str, Any], breaking: Dict[str, Any], docs_context: Dict[str, Any] | None) -> str:
+ prompt = (
+ "당신은 Kubernetes Helm 업그레이드 문서를 작성하는 전문가입니다.\n\n"
+ "두 Helm 차트 버전 간 변경점을 담은 Structured Diff JSON이 제공됩니다.\n\n"
+ "## 작업\n"
+ "1. 핵심 변경 사항을 평문으로 요약합니다.\n"
+ "2. 각 변경의 운영 영향(예: 롤링 업데이트, 재시작 등)을 설명합니다.\n"
+ "3. Breaking Change를 강조하고 구체적인 마이그레이션 절차를 제공합니다.\n"
+ "4. 아래 형식의 간결한 Markdown 문서를 생성합니다.\n\n"
+ "## 규칙\n"
+ "- 입력 JSON에 없는 내용은 절대 추가하지 마세요.\n"
+ "- 추측하지 마세요.\n"
+ "- 값이 비어 있거나 null인 필드는 언급하지 마세요.\n"
+ "- docs_context는 보조 설명에만 사용하고, diff에 없는 변경을 추가하지 마세요.\n"
+ "- errors가 있으면 분석이 불완전할 수 있음을 명시하세요.\n"
+ "- SRE/DevOps 엔지니어가 바로 실행할 수 있도록 명확하게 작성하세요.\n\n"
+ "## 출력 형식\n"
+ "아래 Markdown 구조를 정확히 지키세요 (## {to_version} 헤더는 포함하지 마세요):\n\n"
+ "### 변경 요약\n"
+ "- from_version: \n"
+ "- to_version: \n"
+ "- <핵심 변경 사항 요약>\n\n"
+ "### custom-values.yaml 수정 필요 항목\n"
+ "\n"
+ "\n\n"
+ "### 배포 시 주의사항\n"
+ "\n"
+ "<없으면 섹션 생략>\n\n"
+ "### 참고\n"
+ "- severity: \n"
+ "- breaking: \n\n"
+ "---\n\n"
+ "Input:\n"
+ )
+ payload = {
+ **diff,
+ **breaking,
+ }
+ if docs_context:
+ payload["docs_context"] = docs_context
+ prompt += json.dumps(payload, ensure_ascii=False)
+ return prompt
+
+
+def _run_claude(prompt: str) -> str:
+ res = subprocess.run(["claude", "-p", prompt], capture_output=True, text=True)
+ if res.returncode != 0:
+ raise RuntimeError(res.stderr.strip() or "claude CLI failed")
+ return res.stdout.strip()
+
+
+def generate_upgrade_doc(payload: Dict[str, Any]) -> Dict[str, Any]:
+ inp = GenerateDocInput(**payload)
+
+ use_claude = os.environ.get("USE_CLAUDE_CLI", "0") == "1"
+ breaking = inp.breaking_result.get("breaking", False)
+ markdown: str
+
+ # LLM 호출 조건: USE_CLAUDE_CLI=1 AND breaking=true
+ if use_claude and breaking:
+ try:
+ prompt = _build_prompt(inp.diff_json, inp.breaking_result, inp.docs_context)
+ markdown = _run_claude(prompt)
+ except Exception:
+ markdown = _fallback_summary(inp.diff_json, inp.breaking_result)
+ else:
+ markdown = _fallback_summary(inp.diff_json, inp.breaking_result)
+
+ out = GenerateDocOutput(markdown=markdown, truncated=False)
+ return json.loads(out.model_dump_json())
diff --git a/update_catalog/skills/generate_upgrade_doc/scripts/update_catalog/skill_interface.py b/update_catalog/skills/generate_upgrade_doc/scripts/update_catalog/skill_interface.py
new file mode 100644
index 0000000..f5be9b9
--- /dev/null
+++ b/update_catalog/skills/generate_upgrade_doc/scripts/update_catalog/skill_interface.py
@@ -0,0 +1,145 @@
+"""Skill Interface v1.0 for Helm upgrade automation."""
+
+from __future__ import annotations
+
+from typing import Any, Dict, List, Literal, Optional
+from pydantic import BaseModel, Field
+
+SKILL_INTERFACE_VERSION = "1.0"
+
+
+# -------------------------
+# Common Error Schema
+# -------------------------
+class SkillError(BaseModel):
+ code: str
+ message: str
+ retryable: bool = False
+ details: Optional[Dict[str, Any]] = None
+
+
+class ErrorResponse(BaseModel):
+ error: SkillError
+
+
+# -------------------------
+# Shared Types
+# -------------------------
+Severity = Literal["critical", "high", "medium", "warning"]
+
+
+class BreakingReason(BaseModel):
+ type: str
+ resource: Optional[str] = None
+ key: Optional[str] = None
+ detail: Optional[str] = None
+
+
+# -------------------------
+# helm_diff
+# -------------------------
+class HelmDiffInput(BaseModel):
+ chart: str
+ repo: Optional[str] = None
+ chart_path: Optional[str] = None
+ from_version: str
+ to_version: str
+ values_override: Optional[Dict[str, Any]] = None
+
+
+class HelmDiffOutput(BaseModel):
+ chart: str
+ from_version: str
+ to_version: str
+ generated_at: str
+ values: Dict[str, Any]
+ templates: Dict[str, Any]
+ crd: Dict[str, Any]
+ dependencies: Dict[str, Any]
+ errors: List[Dict[str, Any]] = Field(default_factory=list)
+
+
+# -------------------------
+# breaking_change_check
+# -------------------------
+class BreakingCheckInput(BaseModel):
+ diff_json: Dict[str, Any]
+
+
+class BreakingCheckOutput(BaseModel):
+ breaking: bool
+ severity: Severity
+ breaking_reasons: List[BreakingReason] = Field(default_factory=list)
+ warnings: List[BreakingReason] = Field(default_factory=list)
+
+
+# -------------------------
+# generate_upgrade_doc
+# -------------------------
+class GenerateDocInput(BaseModel):
+ diff_json: Dict[str, Any]
+ breaking_result: Dict[str, Any]
+ docs_context: Optional[Dict[str, str]] = None
+ max_tokens: int = 50000
+
+
+class GenerateDocOutput(BaseModel):
+ markdown: str
+ truncated: bool = False
+
+
+# -------------------------
+# update_docs_file
+# -------------------------
+class UpdateDocsInput(BaseModel):
+ repo_path: str
+ docs_file: str = "BUILD-README.md"
+ version: str
+ content: str
+ overwrite: bool = False
+
+
+class UpdateDocsOutput(BaseModel):
+ success: bool
+ file_path: str
+ already_existed: bool = False
+
+
+# -------------------------
+# create_pr
+# -------------------------
+class CreatePRInput(BaseModel):
+ chart: str
+ from_version: str
+ to_version: str
+ repo_path: str
+ doc_content: str
+ breaking: bool
+ severity: Severity
+
+
+class CreatePROutput(BaseModel):
+ pr_url: str
+ branch_name: str
+ labels: List[str]
+
+
+# -------------------------
+# deploy_validate
+# -------------------------
+class DeployValidateInput(BaseModel):
+ chart: str
+ repo: Optional[str] = None
+ chart_path: Optional[str] = None
+ version: str
+ values_override: Optional[Dict[str, Any]] = None
+ namespace: str
+ timeout: int = 300
+
+
+class DeployValidateOutput(BaseModel):
+ success: bool
+ dry_run_passed: bool
+ pod_status: Dict[str, int]
+ events: List[Dict[str, Any]] = Field(default_factory=list)
+ logs: Optional[str] = None
diff --git a/update_catalog/skills/helm_diff/SKILL.md b/update_catalog/skills/helm_diff/SKILL.md
new file mode 100644
index 0000000..339a645
--- /dev/null
+++ b/update_catalog/skills/helm_diff/SKILL.md
@@ -0,0 +1,36 @@
+---
+name: helm_diff
+description: Compare two Helm chart versions and emit Structured Diff JSON.
+user-invokable: false
+---
+
+# helm_diff (Skill)
+
+Deterministic Helm diff generator. Produces a structured JSON diff across values, templates, CRDs, and dependencies.
+
+## Input schema
+```json
+{
+ "chart": "string",
+ "repo": "string | null",
+ "chart_path": "string | null",
+ "from_version": "string",
+ "to_version": "string",
+ "values_override": "string | object | null"
+}
+```
+
+## Output schema
+```json
+{
+ "chart": "string",
+ "from_version": "string",
+ "to_version": "string",
+ "generated_at": "string (ISO8601)",
+ "values": "object",
+ "templates": "object",
+ "crd": "object",
+ "dependencies": "object",
+ "errors": "array"
+}
+```
diff --git a/update_catalog/skills/helm_diff/scripts/run.py b/update_catalog/skills/helm_diff/scripts/run.py
new file mode 100755
index 0000000..056b46f
--- /dev/null
+++ b/update_catalog/skills/helm_diff/scripts/run.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+import argparse
+import json
+import os
+import sys
+from pathlib import Path
+
+DEFAULT_REPO = None
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--chart", required=True)
+ parser.add_argument("--repo")
+ parser.add_argument("--chart-path")
+ parser.add_argument("--from-version", required=True)
+ parser.add_argument("--to-version", required=True)
+ args = parser.parse_args()
+
+ repo_root = os.environ.get("UPDATE_CATALOG_ROOT")
+ if repo_root:
+ sys.path.insert(0, str(Path(repo_root) / "src"))
+ else:
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+ from update_catalog.helm_diff import helm_diff # type: ignore
+
+ payload = {
+ "chart": args.chart,
+ "repo": args.repo,
+ "chart_path": args.chart_path,
+ "from_version": args.from_version,
+ "to_version": args.to_version,
+ "values_override": None,
+ }
+ out = helm_diff(payload)
+ print(json.dumps(out, ensure_ascii=False, indent=2))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/update_catalog/skills/helm_diff/scripts/update_catalog/__init__.py b/update_catalog/skills/helm_diff/scripts/update_catalog/__init__.py
new file mode 100644
index 0000000..16f23e1
--- /dev/null
+++ b/update_catalog/skills/helm_diff/scripts/update_catalog/__init__.py
@@ -0,0 +1 @@
+__all__ = ["skill_interface"]
diff --git a/update_catalog/skills/helm_diff/scripts/update_catalog/__pycache__/__init__.cpython-312.pyc b/update_catalog/skills/helm_diff/scripts/update_catalog/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..c2f0d0d
Binary files /dev/null and b/update_catalog/skills/helm_diff/scripts/update_catalog/__pycache__/__init__.cpython-312.pyc differ
diff --git a/update_catalog/skills/helm_diff/scripts/update_catalog/__pycache__/helm_diff.cpython-312.pyc b/update_catalog/skills/helm_diff/scripts/update_catalog/__pycache__/helm_diff.cpython-312.pyc
new file mode 100644
index 0000000..068203e
Binary files /dev/null and b/update_catalog/skills/helm_diff/scripts/update_catalog/__pycache__/helm_diff.cpython-312.pyc differ
diff --git a/update_catalog/skills/helm_diff/scripts/update_catalog/__pycache__/skill_interface.cpython-312.pyc b/update_catalog/skills/helm_diff/scripts/update_catalog/__pycache__/skill_interface.cpython-312.pyc
new file mode 100644
index 0000000..3a54459
Binary files /dev/null and b/update_catalog/skills/helm_diff/scripts/update_catalog/__pycache__/skill_interface.cpython-312.pyc differ
diff --git a/update_catalog/skills/helm_diff/scripts/update_catalog/helm_diff.py b/update_catalog/skills/helm_diff/scripts/update_catalog/helm_diff.py
new file mode 100644
index 0000000..f77dcc0
--- /dev/null
+++ b/update_catalog/skills/helm_diff/scripts/update_catalog/helm_diff.py
@@ -0,0 +1,252 @@
+"""helm_diff skill implementation (deterministic)."""
+from __future__ import annotations
+
+import json
+import os
+import shutil
+import subprocess
+import tempfile
+from datetime import datetime, timezone
+from pathlib import Path
+from typing import Any, Dict, List, Tuple
+
+import yaml
+
+from .skill_interface import HelmDiffInput, HelmDiffOutput
+
+
+def _run(cmd: List[str], cwd: str | None = None, timeout: int = 60) -> str:
+ res = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)
+ if res.returncode != 0:
+ raise RuntimeError(f"command failed: {' '.join(cmd)}\n{res.stderr}")
+ return res.stdout
+
+
+def _load_yaml(path: Path) -> Dict[str, Any]:
+ if not path.exists():
+ return {}
+ with path.open("r", encoding="utf-8") as f:
+ return yaml.safe_load(f) or {}
+
+
+def _flatten(d: Dict[str, Any], parent_key: str = "", sep: str = ".") -> Dict[str, Any]:
+ items: Dict[str, Any] = {}
+ for k, v in d.items():
+ new_key = f"{parent_key}{sep}{k}" if parent_key else str(k)
+ if isinstance(v, dict):
+ items.update(_flatten(v, new_key, sep=sep))
+ else:
+ items[new_key] = v
+ return items
+
+
+def _diff_values(old: Dict[str, Any], new: Dict[str, Any]) -> Dict[str, Any]:
+ old_flat = _flatten(old)
+ new_flat = _flatten(new)
+ added = sorted([k for k in new_flat.keys() if k not in old_flat])
+ removed = sorted([k for k in old_flat.keys() if k not in new_flat])
+ changed: Dict[str, Dict[str, Any]] = {}
+ type_changed: List[Dict[str, Any]] = []
+ for k in old_flat.keys() & new_flat.keys():
+ if old_flat[k] != new_flat[k]:
+ if type(old_flat[k]) != type(new_flat[k]):
+ type_changed.append({"key": k, "old_type": type(old_flat[k]).__name__, "new_type": type(new_flat[k]).__name__})
+ else:
+ changed[k] = {"old": old_flat[k], "new": new_flat[k]}
+ return {
+ "values": {
+ "added": added,
+ "removed": removed,
+ "changed": changed,
+ "type_changed": type_changed,
+ }
+ }
+
+
+def _split_manifest(yaml_text: str) -> List[Dict[str, Any]]:
+ docs = []
+ for doc in yaml.safe_load_all(yaml_text):
+ if not doc or not isinstance(doc, dict):
+ continue
+ docs.append(doc)
+ return docs
+
+
+def _resource_id(obj: Dict[str, Any], fallback_idx: int) -> str:
+ kind = obj.get("kind", "Unknown")
+ meta = obj.get("metadata", {}) or {}
+ name = meta.get("name")
+ if not name and meta.get("generateName"):
+ name = f"{meta.get('generateName')}__gen__{fallback_idx}"
+ ns = meta.get("namespace")
+ if kind and name:
+ if kind in {"Namespace", "CustomResourceDefinition"}:
+ return f"{kind}/{name}"
+ return f"{kind}/{name}" if not ns else f"{kind}/{name}"
+ return f"{kind}/__unknown__{fallback_idx}"
+
+
+def _index_resources(docs: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
+ idx: Dict[str, Dict[str, Any]] = {}
+ for i, doc in enumerate(docs):
+ rid = _resource_id(doc, i)
+ idx[rid] = doc
+ return idx
+
+
+def _diff_templates(old_yaml: str, new_yaml: str) -> Dict[str, Any]:
+ old_docs = _split_manifest(old_yaml)
+ new_docs = _split_manifest(new_yaml)
+ old_idx = _index_resources(old_docs)
+ new_idx = _index_resources(new_docs)
+
+ changes: Dict[str, Any] = {}
+ for rid, new_obj in new_idx.items():
+ if rid not in old_idx:
+ changes[rid] = {"added": True}
+ continue
+ old_obj = old_idx[rid]
+ # Minimal diff: detect image/env/ports for Deployments/StatefulSets/Services
+ kind = new_obj.get("kind")
+ if kind in {"Deployment", "StatefulSet"}:
+ def _containers(obj: Dict[str, Any]) -> List[Dict[str, Any]]:
+ return (((obj.get("spec") or {}).get("template") or {}).get("spec") or {}).get("containers") or []
+ old_cont = _containers(old_obj)
+ new_cont = _containers(new_obj)
+ old_images = {c.get("name"): c.get("image") for c in old_cont}
+ new_images = {c.get("name"): c.get("image") for c in new_cont}
+ image_changed = old_images != new_images
+ env_added: List[str] = []
+ env_removed: List[str] = []
+ def _env_keys(cont: Dict[str, Any]) -> set:
+ return {e.get("name") for e in (cont.get("env") or []) if e.get("name")}
+ for name, cont in {c.get("name"): c for c in new_cont}.items():
+ old_env = _env_keys({c.get("name"): c for c in old_cont}.get(name, {}) or {})
+ new_env = _env_keys(cont)
+ env_added += sorted(list(new_env - old_env))
+ env_removed += sorted(list(old_env - new_env))
+ changes[rid] = {
+ "image_changed": image_changed,
+ "image": {"old": old_images, "new": new_images},
+ "env_added": sorted(list(set(env_added))),
+ "env_removed": sorted(list(set(env_removed))),
+ }
+ elif kind == "Service":
+ def _ports(obj: Dict[str, Any]) -> List[Tuple[Any, Any]]:
+ ports = (obj.get("spec") or {}).get("ports") or []
+ return [(p.get("port"), p.get("targetPort")) for p in ports]
+ changes[rid] = {
+ "port_changed": _ports(old_obj) != _ports(new_obj)
+ }
+ for rid in old_idx.keys() - new_idx.keys():
+ changes[rid] = {"removed": True}
+ return {"templates": changes}
+
+
+def _diff_chart_yaml(old_chart: Dict[str, Any], new_chart: Dict[str, Any]) -> Dict[str, Any]:
+ old_deps = {d.get("name"): d for d in (old_chart.get("dependencies") or [])}
+ new_deps = {d.get("name"): d for d in (new_chart.get("dependencies") or [])}
+ added = sorted([k for k in new_deps.keys() if k not in old_deps])
+ removed = sorted([k for k in old_deps.keys() if k not in new_deps])
+ version_changed: Dict[str, Dict[str, Any]] = {}
+ for k in old_deps.keys() & new_deps.keys():
+ if old_deps[k].get("version") != new_deps[k].get("version"):
+ version_changed[k] = {"old": old_deps[k].get("version"), "new": new_deps[k].get("version")}
+ return {"dependencies": {"added": added, "removed": removed, "version_changed": version_changed}}
+
+
+def _resolve_chart_dir(base: Path, chart_name: str) -> Path:
+ # repo pull: chart is under base/
+ candidate = base / chart_name
+ if (candidate / "Chart.yaml").exists():
+ return candidate
+ # local copy: Chart.yaml at base root
+ if (base / "Chart.yaml").exists():
+ return base
+ # fallback: first subdir with Chart.yaml
+ for p in base.iterdir():
+ if p.is_dir() and (p / "Chart.yaml").exists():
+ return p
+ return candidate
+
+
+def helm_diff(payload: Dict[str, Any]) -> Dict[str, Any]:
+ inp = HelmDiffInput(**payload)
+ errors: List[Dict[str, Any]] = []
+
+ with tempfile.TemporaryDirectory() as tmp:
+ tmpdir = Path(tmp)
+ chart_old = tmpdir / "chart_old"
+ chart_new = tmpdir / "chart_new"
+ chart_old.mkdir()
+ chart_new.mkdir()
+
+ try:
+ if inp.chart_path:
+ # dip-catalog local path for from_version
+ from_path = Path(inp.chart_path)
+ if not from_path.exists():
+ raise FileNotFoundError(f"chart_path not found: {from_path}")
+ shutil.copytree(from_path, chart_old, dirs_exist_ok=True)
+
+ # to_version: if repo provided, pull from repo (mixed mode)
+ if inp.repo:
+ _run([
+ "helm",
+ "pull",
+ f"{inp.repo}/{inp.chart}",
+ "--version",
+ inp.to_version,
+ "--untar",
+ "--untardir",
+ str(chart_new),
+ ])
+ else:
+ # local-to-local mode (use sibling version directory)
+ to_path = from_path.parent / inp.to_version
+ shutil.copytree(to_path, chart_new, dirs_exist_ok=True)
+ else:
+ if not inp.repo:
+ raise ValueError("repo is required when chart_path is not provided")
+ _run(["helm", "pull", f"{inp.repo}/{inp.chart}", "--version", inp.from_version, "--untar", "--untardir", str(chart_old)])
+ _run(["helm", "pull", f"{inp.repo}/{inp.chart}", "--version", inp.to_version, "--untar", "--untardir", str(chart_new)])
+ except Exception as e:
+ errors.append({"stage": "helm_pull", "message": str(e)})
+
+ chart_old_dir = _resolve_chart_dir(chart_old, inp.chart)
+ chart_new_dir = _resolve_chart_dir(chart_new, inp.chart)
+
+ # values diff
+ values_old = _load_yaml(chart_old_dir / "values.yaml")
+ values_new = _load_yaml(chart_new_dir / "values.yaml")
+ values_diff = _diff_values(values_old, values_new)
+
+ # template diff
+ templates_diff: Dict[str, Any] = {"templates": {}}
+ try:
+ old_yaml = _run(["helm", "template", str(chart_old_dir), "--include-crds"])
+ new_yaml = _run(["helm", "template", str(chart_new_dir), "--include-crds"])
+ templates_diff = _diff_templates(old_yaml, new_yaml)
+ except Exception as e:
+ errors.append({"stage": "helm_template", "message": str(e)})
+
+ # CRD diff placeholder (minimal)
+ crd_diff = {"crd": {}}
+
+ # dependencies diff
+ chart_old_yaml = _load_yaml(chart_old_dir / "Chart.yaml")
+ chart_new_yaml = _load_yaml(chart_new_dir / "Chart.yaml")
+ dep_diff = _diff_chart_yaml(chart_old_yaml, chart_new_yaml)
+
+ out = HelmDiffOutput(
+ chart=inp.chart,
+ from_version=inp.from_version,
+ to_version=inp.to_version,
+ generated_at=datetime.now(timezone.utc).isoformat(),
+ values=values_diff.get("values", {}),
+ templates=templates_diff.get("templates", {}),
+ crd=crd_diff.get("crd", {}),
+ dependencies=dep_diff.get("dependencies", {}),
+ errors=errors,
+ )
+ return json.loads(out.model_dump_json())
diff --git a/update_catalog/skills/helm_diff/scripts/update_catalog/skill_interface.py b/update_catalog/skills/helm_diff/scripts/update_catalog/skill_interface.py
new file mode 100644
index 0000000..4690204
--- /dev/null
+++ b/update_catalog/skills/helm_diff/scripts/update_catalog/skill_interface.py
@@ -0,0 +1,144 @@
+"""Skill Interface v1.0 for Helm upgrade automation."""
+
+from __future__ import annotations
+
+from typing import Any, Dict, List, Literal, Optional
+from pydantic import BaseModel, Field
+
+SKILL_INTERFACE_VERSION = "1.0"
+
+
+# -------------------------
+# Common Error Schema
+# -------------------------
+class SkillError(BaseModel):
+ code: str
+ message: str
+ retryable: bool = False
+ details: Optional[Dict[str, Any]] = None
+
+
+class ErrorResponse(BaseModel):
+ error: SkillError
+
+
+# -------------------------
+# Shared Types
+# -------------------------
+Severity = Literal["critical", "high", "medium", "warning"]
+
+
+class BreakingReason(BaseModel):
+ type: str
+ resource: Optional[str] = None
+ key: Optional[str] = None
+ detail: Optional[str] = None
+
+
+# -------------------------
+# helm_diff
+# -------------------------
+class HelmDiffInput(BaseModel):
+ chart: str
+ repo: Optional[str] = None
+ chart_path: Optional[str] = None
+ from_version: str
+ to_version: str
+ values_override: Optional[Dict[str, Any]] = None
+
+
+class HelmDiffOutput(BaseModel):
+ chart: str
+ from_version: str
+ to_version: str
+ generated_at: str
+ values: Dict[str, Any]
+ templates: Dict[str, Any]
+ crd: Dict[str, Any]
+ dependencies: Dict[str, Any]
+ errors: List[Dict[str, Any]] = Field(default_factory=list)
+
+
+# -------------------------
+# breaking_change_check
+# -------------------------
+class BreakingCheckInput(BaseModel):
+ diff_json: Dict[str, Any]
+
+
+class BreakingCheckOutput(BaseModel):
+ breaking: bool
+ severity: Severity
+ reasons: List[BreakingReason] = Field(default_factory=list)
+ warnings: List[BreakingReason] = Field(default_factory=list)
+
+
+# -------------------------
+# generate_upgrade_doc
+# -------------------------
+class GenerateDocInput(BaseModel):
+ diff_json: Dict[str, Any]
+ breaking_result: Dict[str, Any]
+ docs_context: Optional[Dict[str, str]] = None
+ max_tokens: int = 50000
+
+
+class GenerateDocOutput(BaseModel):
+ markdown: str
+ truncated: bool = False
+
+
+# -------------------------
+# update_docs_file
+# -------------------------
+class UpdateDocsInput(BaseModel):
+ repo_path: str
+ docs_file: str = "docs/upgrade.md"
+ version: str
+ content: str
+ overwrite: bool = False
+
+
+class UpdateDocsOutput(BaseModel):
+ success: bool
+ file_path: str
+ already_existed: bool = False
+
+
+# -------------------------
+# create_pr
+# -------------------------
+class CreatePRInput(BaseModel):
+ chart: str
+ from_version: str
+ to_version: str
+ repo_path: str
+ doc_content: str
+ breaking: bool
+ severity: Severity
+
+
+class CreatePROutput(BaseModel):
+ pr_url: str
+ branch_name: str
+ labels: List[str]
+
+
+# -------------------------
+# deploy_validate
+# -------------------------
+class DeployValidateInput(BaseModel):
+ chart: str
+ repo: Optional[str] = None
+ version: str
+ values_override: Optional[Dict[str, Any]] = None
+ namespace: str
+ timeout: int = 300
+
+
+class DeployValidateOutput(BaseModel):
+ success: bool
+ dry_run_passed: bool
+ pod_status: Dict[str, int]
+ events: List[Dict[str, Any]] = Field(default_factory=list)
+ logs: Optional[str] = None
diff --git a/update_catalog/skills/update_docs_file/SKILL.md b/update_catalog/skills/update_docs_file/SKILL.md
new file mode 100644
index 0000000..dcdf27d
--- /dev/null
+++ b/update_catalog/skills/update_docs_file/SKILL.md
@@ -0,0 +1,29 @@
+---
+name: update_docs_file
+description: Insert upgrade content into BUILD-README.md for a target chart version.
+user-invokable: false
+---
+
+# update_docs_file (Skill)
+
+Update a chart version directory's BUILD-README.md by inserting upgrade content.
+
+## Input schema
+```json
+{
+ "repo_path": "string",
+ "docs_file": "string (default: BUILD-README.md)",
+ "version": "string",
+ "content": "string",
+ "overwrite": "boolean"
+}
+```
+
+## Output schema
+```json
+{
+ "success": "boolean",
+ "file_path": "string",
+ "already_existed": "boolean"
+}
+```
diff --git a/update_catalog/skills/update_docs_file/scripts/run.py b/update_catalog/skills/update_docs_file/scripts/run.py
new file mode 100755
index 0000000..158924e
--- /dev/null
+++ b/update_catalog/skills/update_docs_file/scripts/run.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+import argparse
+import json
+import os
+import sys
+from pathlib import Path
+
+
+DEFAULT_REPO = None
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--repo-path", required=True)
+ parser.add_argument("--docs-file", default="BUILD-README.md")
+ parser.add_argument("--version", required=True)
+ parser.add_argument("--content-file", required=True)
+ parser.add_argument("--overwrite", action="store_true")
+ args = parser.parse_args()
+
+ repo_root = os.environ.get("UPDATE_CATALOG_ROOT")
+ if repo_root:
+ sys.path.insert(0, str(Path(repo_root) / "src"))
+ else:
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+ from update_catalog.update_docs_file import update_docs_file # type: ignore
+
+ content = Path(args.content_file).read_text(encoding="utf-8")
+ out = update_docs_file({
+ "repo_path": args.repo_path,
+ "docs_file": args.docs_file,
+ "version": args.version,
+ "content": content,
+ "overwrite": bool(args.overwrite),
+ })
+ print(json.dumps(out, ensure_ascii=False, indent=2))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/update_catalog/skills/update_docs_file/scripts/update_catalog/__init__.py b/update_catalog/skills/update_docs_file/scripts/update_catalog/__init__.py
new file mode 100644
index 0000000..16f23e1
--- /dev/null
+++ b/update_catalog/skills/update_docs_file/scripts/update_catalog/__init__.py
@@ -0,0 +1 @@
+__all__ = ["skill_interface"]
diff --git a/update_catalog/skills/update_docs_file/scripts/update_catalog/__pycache__/__init__.cpython-312.pyc b/update_catalog/skills/update_docs_file/scripts/update_catalog/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..ef4bd95
Binary files /dev/null and b/update_catalog/skills/update_docs_file/scripts/update_catalog/__pycache__/__init__.cpython-312.pyc differ
diff --git a/update_catalog/skills/update_docs_file/scripts/update_catalog/__pycache__/skill_interface.cpython-312.pyc b/update_catalog/skills/update_docs_file/scripts/update_catalog/__pycache__/skill_interface.cpython-312.pyc
new file mode 100644
index 0000000..3d2f075
Binary files /dev/null and b/update_catalog/skills/update_docs_file/scripts/update_catalog/__pycache__/skill_interface.cpython-312.pyc differ
diff --git a/update_catalog/skills/update_docs_file/scripts/update_catalog/__pycache__/update_docs_file.cpython-312.pyc b/update_catalog/skills/update_docs_file/scripts/update_catalog/__pycache__/update_docs_file.cpython-312.pyc
new file mode 100644
index 0000000..d24a5b0
Binary files /dev/null and b/update_catalog/skills/update_docs_file/scripts/update_catalog/__pycache__/update_docs_file.cpython-312.pyc differ
diff --git a/update_catalog/skills/update_docs_file/scripts/update_catalog/skill_interface.py b/update_catalog/skills/update_docs_file/scripts/update_catalog/skill_interface.py
new file mode 100644
index 0000000..86212a1
--- /dev/null
+++ b/update_catalog/skills/update_docs_file/scripts/update_catalog/skill_interface.py
@@ -0,0 +1,145 @@
+"""Skill Interface v1.0 for Helm upgrade automation."""
+
+from __future__ import annotations
+
+from typing import Any, Dict, List, Literal, Optional
+from pydantic import BaseModel, Field
+
+SKILL_INTERFACE_VERSION = "1.0"
+
+
+# -------------------------
+# Common Error Schema
+# -------------------------
+class SkillError(BaseModel):
+ code: str
+ message: str
+ retryable: bool = False
+ details: Optional[Dict[str, Any]] = None
+
+
+class ErrorResponse(BaseModel):
+ error: SkillError
+
+
+# -------------------------
+# Shared Types
+# -------------------------
+Severity = Literal["critical", "high", "medium", "warning"]
+
+
+class BreakingReason(BaseModel):
+ type: str
+ resource: Optional[str] = None
+ key: Optional[str] = None
+ detail: Optional[str] = None
+
+
+# -------------------------
+# helm_diff
+# -------------------------
+class HelmDiffInput(BaseModel):
+ chart: str
+ repo: Optional[str] = None
+ chart_path: Optional[str] = None
+ from_version: str
+ to_version: str
+ values_override: Optional[Dict[str, Any]] = None
+
+
+class HelmDiffOutput(BaseModel):
+ chart: str
+ from_version: str
+ to_version: str
+ generated_at: str
+ values: Dict[str, Any]
+ templates: Dict[str, Any]
+ crd: Dict[str, Any]
+ dependencies: Dict[str, Any]
+ errors: List[Dict[str, Any]] = Field(default_factory=list)
+
+
+# -------------------------
+# breaking_change_check
+# -------------------------
+class BreakingCheckInput(BaseModel):
+ diff_json: Dict[str, Any]
+
+
+class BreakingCheckOutput(BaseModel):
+ breaking: bool
+ severity: Severity
+ breaking_reasons: List[BreakingReason] = Field(default_factory=list)
+ warnings: List[BreakingReason] = Field(default_factory=list)
+
+
+# -------------------------
+# generate_upgrade_doc
+# -------------------------
+class GenerateDocInput(BaseModel):
+ diff_json: Dict[str, Any]
+ breaking_result: Dict[str, Any]
+ docs_context: Optional[Dict[str, str]] = None
+ max_tokens: int = 50000
+
+
+class GenerateDocOutput(BaseModel):
+ markdown: str
+ truncated: bool = False
+
+
+# -------------------------
+# update_docs_file
+# -------------------------
+class UpdateDocsInput(BaseModel):
+ repo_path: str
+ docs_file: str # 예: "manifests/helm///CUSTOM-README.md"
+ version: str
+ content: str
+ overwrite: bool = False
+
+
+class UpdateDocsOutput(BaseModel):
+ success: bool
+ file_path: str
+ already_existed: bool = False
+
+
+# -------------------------
+# create_pr
+# -------------------------
+class CreatePRInput(BaseModel):
+ chart: str
+ from_version: str
+ to_version: str
+ repo_path: str
+ doc_content: str
+ breaking: bool
+ severity: Severity
+
+
+class CreatePROutput(BaseModel):
+ pr_url: str
+ branch_name: str
+ labels: List[str]
+
+
+# -------------------------
+# deploy_validate
+# -------------------------
+class DeployValidateInput(BaseModel):
+ chart: str
+ repo: Optional[str] = None
+ chart_path: Optional[str] = None
+ version: str
+ values_override: Optional[Dict[str, Any]] = None
+ namespace: str
+ timeout: int = 300
+
+
+class DeployValidateOutput(BaseModel):
+ success: bool
+ dry_run_passed: bool
+ pod_status: Dict[str, int]
+ events: List[Dict[str, Any]] = Field(default_factory=list)
+ logs: Optional[str] = None
diff --git a/update_catalog/skills/update_docs_file/scripts/update_catalog/update_docs_file.py b/update_catalog/skills/update_docs_file/scripts/update_catalog/update_docs_file.py
new file mode 100644
index 0000000..f44c7b8
--- /dev/null
+++ b/update_catalog/skills/update_docs_file/scripts/update_catalog/update_docs_file.py
@@ -0,0 +1,62 @@
+"""update_docs_file skill implementation."""
+from __future__ import annotations
+
+import json
+from pathlib import Path
+from typing import Any, Dict
+
+from .skill_interface import UpdateDocsInput, UpdateDocsOutput
+
+
+HEADER = "# Upgrade History"
+
+
+def _insert_section(text: str, version: str, content: str) -> tuple[str, bool]:
+ marker = f"## {version}"
+ if marker in text:
+ return text, True
+
+ if HEADER in text:
+ parts = text.split(HEADER, 1)
+ head = parts[0] + HEADER
+ rest = parts[1].lstrip("\n")
+ new_section = f"\n\n{marker}\n{content.strip()}\n"
+ return head + new_section + "\n" + rest, False
+
+ # If no header, prepend
+ new_text = f"{HEADER}\n\n{marker}\n{content.strip()}\n\n{text}"
+ return new_text, False
+
+
+def update_docs_file(payload: Dict[str, Any]) -> Dict[str, Any]:
+ inp = UpdateDocsInput(**payload)
+ repo_path = Path(inp.repo_path)
+ docs_path = repo_path / inp.docs_file
+
+ text = ""
+ if docs_path.exists():
+ text = docs_path.read_text(encoding="utf-8")
+
+ new_text, existed = _insert_section(text, inp.version, inp.content)
+ if existed and not inp.overwrite:
+ out = UpdateDocsOutput(success=True, file_path=str(docs_path), already_existed=True)
+ return json.loads(out.model_dump_json())
+
+ if existed and inp.overwrite:
+ # overwrite: replace section between marker and next header
+ marker = f"## {inp.version}"
+ parts = new_text.split(marker, 1)
+ if len(parts) > 1:
+ after = parts[1]
+ tail_idx = after.find("\n## ")
+ if tail_idx != -1:
+ after = after[tail_idx:]
+ else:
+ after = ""
+ new_text = parts[0] + marker + "\n" + inp.content.strip() + "\n" + after
+
+ docs_path.parent.mkdir(parents=True, exist_ok=True)
+ docs_path.write_text(new_text, encoding="utf-8")
+
+ out = UpdateDocsOutput(success=True, file_path=str(docs_path), already_existed=existed)
+ return json.loads(out.model_dump_json())