#!/usr/bin/env python # -*- coding: utf-8 -*- """ Harness 소개 덱 생성 — python-pptx. (스크립트가 진실원천: 내용 수정 후 재생성) 실행: python docs/gen_intro_deck.py → docs/Harness_소개덱_v1.pptx """ import os from pptx import Presentation from pptx.util import Inches, Pt, Emu from pptx.dml.color import RGBColor from pptx.enum.text import PP_ALIGN, MSO_ANCHOR from pptx.enum.shapes import MSO_SHAPE # ── 테마(하네스 브랜드: 딥 인디고 + 퍼플/시안 액센트) ───────────────── INK = RGBColor(0x0F, 0x14, 0x2E) # 딥 인디고(배경/제목) INDIGO = RGBColor(0x31, 0x2E, 0x81) # 인디고 PURPLE = RGBColor(0x7C, 0x3A, 0xED) # 퍼플 액센트 CYAN = RGBColor(0x06, 0xB6, 0xD4) # 시안 액센트 AMBER = RGBColor(0xF5, 0x9E, 0x0B) # 앰버 포인트 LIGHT = RGBColor(0xEE, 0xEC, 0xFB) # 연보라 배경 CARD = RGBColor(0xF6, 0xF7, 0xFB) GRAY = RGBColor(0x55, 0x5A, 0x6B) WHITE = RGBColor(0xFF, 0xFF, 0xFF) FONT = "맑은 고딕" HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(HERE) BANNER = os.path.join(ROOT, "zioinfo_banner.png") ICON = os.path.join(ROOT, "zioinfo_icon.png") prs = Presentation() prs.slide_width = Inches(13.333) prs.slide_height = Inches(7.5) SW, SH = prs.slide_width, prs.slide_height BLANK = prs.slide_layouts[6] def _set(run, size, bold=False, color=INK, font=FONT): run.font.size = Pt(size); run.font.bold = bold run.font.color.rgb = color; run.font.name = font def box(slide, x, y, w, h, fill=None, line=None, line_w=1.0, round_=False): shp = slide.shapes.add_shape( MSO_SHAPE.ROUNDED_RECTANGLE if round_ else MSO_SHAPE.RECTANGLE, Inches(x), Inches(y), Inches(w), Inches(h)) shp.shadow.inherit = False if fill is None: shp.fill.background() else: shp.fill.solid(); shp.fill.fore_color.rgb = fill if line is None: shp.line.fill.background() else: shp.line.color.rgb = line; shp.line.width = Pt(line_w) return shp def text(slide, x, y, w, h, runs, align=PP_ALIGN.LEFT, anchor=MSO_ANCHOR.TOP, space=4, line_spacing=None): """runs: list of paragraphs; each para = list of (txt, size, bold, color).""" tb = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h)) tf = tb.text_frame; tf.word_wrap = True; tf.vertical_anchor = anchor tf.margin_left = tf.margin_right = Inches(0.05) tf.margin_top = tf.margin_bottom = Inches(0.02) for i, para in enumerate(runs): p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() p.alignment = align; p.space_after = Pt(space) if line_spacing: p.line_spacing = line_spacing for spec in para: txt, size, bold, color = spec[0], spec[1], spec[2], spec[3] fnt = spec[4] if len(spec) > 4 else FONT r = p.add_run(); r.text = txt; _set(r, size, bold, color, fnt) return tb def header(slide, title, kicker=None, n=None): """상단 헤더 바 + 제목.""" box(slide, 0, 0, 13.333, 1.15, fill=INK) box(slide, 0, 1.15, 13.333, 0.06, fill=PURPLE) runs = [] if kicker: runs.append([(kicker, 12, True, CYAN)]) runs.append([(title, 26, True, WHITE)]) text(slide, 0.6, 0.12, 11.5, 0.95, runs, anchor=MSO_ANCHOR.MIDDLE, space=2) if n is not None: text(slide, 12.2, 0.12, 0.9, 0.95, [[(f"{n:02d}", 22, True, AMBER)]], align=PP_ALIGN.RIGHT, anchor=MSO_ANCHOR.MIDDLE) def footer(slide): text(slide, 0.6, 7.05, 8, 0.35, [[("Harness — Claude Code Team-Architecture Factory", 9, False, GRAY)]]) text(slide, 11.0, 7.05, 1.9, 0.35, [[("v1.3.1", 9, False, GRAY)]], align=PP_ALIGN.RIGHT) def card(slide, x, y, w, h, title, lines, accent=PURPLE, tsize=15): box(slide, x, y, w, h, fill=CARD, line=RGBColor(0xE2,0xE3,0xEE), line_w=0.75, round_=True) box(slide, x, y, 0.12, h, fill=accent) runs = [[(title, tsize, True, INK)]] for ln in lines: runs.append([("· ", 11, True, accent), (ln, 11.5, False, GRAY)]) text(slide, x+0.3, y+0.22, w-0.45, h-0.4, runs, space=4) # ── 슬라이드 1: 타이틀 ─────────────────────────────────────────────── s = prs.slides.add_slide(BLANK) box(s, 0, 0, 13.333, 7.5, fill=INK) box(s, 0, 0, 0.25, 7.5, fill=PURPLE) box(s, 0.25, 0, 0.1, 7.5, fill=CYAN) if os.path.exists(BANNER): try: s.shapes.add_picture(BANNER, Inches(8.0), Inches(0.7), height=Inches(2.2)) except Exception: pass text(s, 0.9, 2.0, 9.5, 2.4, [ [("Harness", 60, True, WHITE)], [("Claude Code를 위한 팀 아키텍처 팩토리", 22, True, CYAN)], ], space=10) text(s, 0.95, 4.5, 11, 1.6, [ [("도메인 한 문장을 ", 16, False, LIGHT), ("에이전트 팀 + 스킬", 16, True, AMBER), ("로 변환하는 메타 스킬", 16, False, LIGHT)], [("\"이 프로젝트용 하네스 구성해줘\" → 전문 에이전트 팀이 자동 생성·협업", 13, False, RGBColor(0xB9,0xBD,0xD6))], ], space=8) text(s, 0.95, 6.6, 11, 0.5, [[("소개 덱 · 2026-06 · Apache-2.0", 11, False, RGBColor(0x8A,0x8F,0xB0))]]) # ── 슬라이드 2: 왜 필요한가 ────────────────────────────────────────── s = prs.slides.add_slide(BLANK); header(s, "왜 Harness인가?", "THE PROBLEM", 2) text(s, 0.6, 1.5, 12.1, 1.0, [ [("복잡한 작업일수록 ", 16, False, GRAY), ("여러 전문 에이전트의 협업", 16, True, INK), ("이 필요하지만 —", 16, False, GRAY)], ], space=4) card(s, 0.6, 2.6, 3.95, 3.6, "매번 수작업", [ "에이전트 역할을 매번 즉석에서 prompt에 작성", "다음 세션에서 재사용 불가", "팀 통신·협업 프로토콜이 매번 제각각", ], accent=AMBER) card(s, 4.75, 2.6, 3.95, 3.6, "구성 일관성 부재", [ "누가(에이전트) / 어떻게(스킬) 가 뒤섞임", "패턴 없이 임기응변 → 누락·중복", "품질이 작성자 역량에 좌우", ], accent=AMBER) card(s, 8.9, 2.6, 3.85, 3.6, "Harness의 답", [ "한 문장 → 에이전트 팀 + 스킬 자동 생성", "파일로 영속화 → 세션 간 재사용", "6가지 검증된 아키텍처 패턴 적용", ], accent=PURPLE) footer(s) # ── 슬라이드 3: Harness란 ──────────────────────────────────────────── s = prs.slides.add_slide(BLANK); header(s, "Harness란 무엇인가", "THE FACTORY", 3) text(s, 0.6, 1.5, 12.1, 1.4, [ [("Harness는 ", 18, False, GRAY), ("메타 스킬(스킬을 만드는 스킬)", 18, True, PURPLE), ("입니다.", 18, False, GRAY)], [("도메인 설명을 입력하면 → 전문 에이전트 팀과 그들이 사용할 스킬 세트를 ", 14, False, GRAY), ("코드(.md 파일)로 생성", 14, True, INK), ("합니다.", 14, False, GRAY)], ], space=8) # 플로우 4단계 labels = [("도메인 한 문장", CYAN), ("팩토리(메타 스킬)", PURPLE), ("에이전트 팀 + 스킬", INDIGO), ("작업 자동 수행", AMBER)] x = 0.6 for i, (lab, col) in enumerate(labels): box(s, x, 3.3, 2.7, 1.5, fill=col, round_=True) text(s, x+0.15, 3.3, 2.4, 1.5, [[(lab, 14, True, WHITE)]], align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE) if i < 3: text(s, x+2.75, 3.3, 0.5, 1.5, [[("→", 26, True, GRAY)]], align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE) x += 3.18 text(s, 0.6, 5.3, 12.1, 1.4, [ [("핵심: ", 14, True, INK), ("에이전트(누가) 와 스킬(어떻게) 를 분리", 14, True, PURPLE), ("한다. 에이전트는 역할·협업을, 스킬은 방법·절차를 담는다.", 14, False, GRAY)], [("→ 다른 도메인에서도 스킬을 재사용하고, 에이전트만 갈아끼울 수 있다.", 13, False, GRAY)], ], space=6) footer(s) # ── 슬라이드 4: 핵심 가치 ──────────────────────────────────────────── s = prs.slides.add_slide(BLANK); header(s, "핵심 가치 세 가지", "WHY IT WORKS", 4) card(s, 0.6, 1.6, 3.95, 4.6, "1. 분리(Separation)", [ "에이전트 = 누가 (역할·원칙·통신)", "스킬 = 어떻게 (절차·패턴·스크립트)", "오케스트레이터 = 언제·어떤 순서로", "→ 책임이 명확, 변경이 국소적", ], accent=CYAN, tsize=16) card(s, 4.75, 1.6, 3.95, 4.6, "2. 재사용(Reuse)", [ ".claude/agents·skills 파일로 영속", "세션이 바뀌어도 그대로 재사용", "스킬은 여러 에이전트가 공유 가능", "→ 빌드할수록 자산이 쌓인다", ], accent=PURPLE, tsize=16) card(s, 8.9, 1.6, 3.85, 4.6, "3. 진화(Evolution)", [ "고정물이 아니라 살아있는 시스템", "실행 후 피드백 → 에이전트·스킬 갱신", "CLAUDE.md 변경 이력으로 추적", "→ 퇴행 없이 점점 좋아진다", ], accent=AMBER, tsize=16) footer(s) # ── 슬라이드 5: 6가지 아키텍처 패턴 ────────────────────────────────── s = prs.slides.add_slide(BLANK); header(s, "6가지 팀 아키텍처 패턴", "PATTERNS", 5) pats = [ ("파이프라인", "순차 의존 작업 — A→B→C 단계별 전달"), ("팬아웃/팬인", "병렬 독립 작업 후 결과 통합"), ("전문가 풀", "상황별로 필요한 전문가만 선택 호출"), ("생성-검증", "생성 후 독립 에이전트가 품질 검수"), ("감독자", "중앙 에이전트가 상태 관리·동적 분배"), ("계층적 위임", "상위가 하위에 재귀적으로 위임"), ] gx, gy = 0.6, 1.55 for i, (name, desc) in enumerate(pats): r, c = divmod(i, 3) x = gx + c*4.18; y = gy + r*2.65 card(s, x, y, 3.95, 2.4, name, [desc], accent=[CYAN,PURPLE,INDIGO][c], tsize=16) footer(s) # ── 슬라이드 6: 7-Phase 워크플로우 ─────────────────────────────────── s = prs.slides.add_slide(BLANK); header(s, "구성 워크플로우 (7 Phase)", "HOW", 6) phases = [ ("0", "현황 감사", "기존 에이전트·스킬·CLAUDE.md 확인, drift 감지"), ("1", "도메인 분석", "작업 유형·기술스택·데이터모델 파악"), ("2", "팀 아키텍처 설계", "실행 모드(팀/서브/하이브리드) + 패턴 선택"), ("3", "에이전트 정의", ".claude/agents/*.md (역할·협업·model:opus)"), ("4", "스킬 생성", ".claude/skills/*/SKILL.md (방법·references)"), ("5", "통합·오케스트레이션", "오케스트레이터 스킬 + CLAUDE.md 포인터"), ("6", "검증·테스트", "구조·트리거·드라이런 검증"), ] y = 1.5 for num, title, desc in phases: box(s, 0.6, y, 0.62, 0.62, fill=PURPLE, round_=True) text(s, 0.6, y, 0.62, 0.62, [[(num, 18, True, WHITE)]], align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE) text(s, 1.45, y, 3.2, 0.62, [[(title, 15, True, INK)]], anchor=MSO_ANCHOR.MIDDLE) text(s, 4.7, y, 8.0, 0.62, [[(desc, 12.5, False, GRAY)]], anchor=MSO_ANCHOR.MIDDLE) y += 0.72 text(s, 0.6, 6.55, 12, 0.4, [[("이후 Phase 7(진화): 매 실행 후 피드백을 반영해 지속 갱신", 12, True, CYAN)]]) footer(s) # ── 슬라이드 7: 설계가 먼저다 ──────────────────────────────────────── s = prs.slides.add_slide(BLANK); header(s, "설계가 먼저다", "DESIGN FIRST", 7) text(s, 0.6, 1.5, 12.1, 1.3, [ [("플러그인은 하네스를 ", 16, False, GRAY), ("배포·증폭하는 그릇", 16, True, INK), ("일 뿐, 설계를 대신 잘해주지 않는다.", 16, False, GRAY)], [("허술한 설계는 ", 13.5, False, GRAY), ("잘 배포될수록 더 큰 부채", 13.5, True, AMBER), ("가 된다 — 결함이 여러 사람·세션·프로젝트로 그대로 퍼진다.", 13.5, False, GRAY)], ], space=6) box(s, 0.6, 2.95, 5.85, 1.0, fill=CYAN, round_=True) text(s, 0.8, 2.95, 5.5, 1.0, [[("① 제대로 설계하고 동작·검증한 뒤", 15, True, WHITE)]], anchor=MSO_ANCHOR.MIDDLE) text(s, 6.5, 2.95, 0.6, 1.0, [[("→", 26, True, GRAY)]], align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE) box(s, 7.2, 2.95, 5.55, 1.0, fill=PURPLE, round_=True) text(s, 7.4, 2.95, 5.2, 1.0, [[("② 비로소 플러그인으로 묶는다", 15, True, WHITE)]], anchor=MSO_ANCHOR.MIDDLE) text(s, 0.6, 4.2, 12, 0.4, [[("패키징 전 점검 — 좋은 설계의 4축", 13, True, INK)]]) axes = [("에이전트 분리", "누가? 역할 겹침·과부하 없게", CYAN), ("스킬 경계", "어떻게? 절차와 역할 분리", PURPLE), ("아키텍처 패턴", "6패턴 중 작업에 맞게", INDIGO), ("트리거 설계", "발동/비발동 정확히", AMBER)] x = 0.6 for name, desc, col in axes: card(s, x, 4.65, 2.92, 1.8, name, [desc], accent=col, tsize=14) x += 3.07 footer(s) # ── 슬라이드 8: 플러그인 생태계 ────────────────────────────────────── s = prs.slides.add_slide(BLANK); header(s, "플러그인 생태계", "ECOSYSTEM", 8) text(s, 0.6, 1.45, 12, 0.7, [ [("Harness는 ", 15, False, GRAY), ("Claude Code 플러그인 마켓플레이스", 15, True, INK), ("로 배포됩니다.", 15, False, GRAY)], ]) card(s, 0.6, 2.3, 6.0, 3.9, "harness (메타 스킬)", [ "도메인 → 에이전트 팀 + 스킬 자동 생성", "6가지 아키텍처 패턴 내장", "설치: harness@ythong", "용도: 새 하네스를 처음부터 설계", ], accent=PURPLE, tsize=17) card(s, 6.8, 2.3, 5.95, 3.9, "zio-harness (도메인 플러그인)", [ "React + Spring Boot + Mobile 풀스택", "orchestrator·analyst·bot·agent 팀", "PROJECT_MAP.md로 폴더구조 기억", "용도: 이미 완성된 풀스택 하네스 즉시 사용", ], accent=CYAN, tsize=17) text(s, 0.6, 6.45, 12, 0.5, [ [("직접 도메인 플러그인 작성 → ", 12.5, False, GRAY), ("docs/plugin-guide.md", 12.5, True, PURPLE), (" 참조", 12.5, False, GRAY)], ]) footer(s) # ── 슬라이드 9: 사용법 ─────────────────────────────────────────────── s = prs.slides.add_slide(BLANK); header(s, "시작하기 — 3단계", "GET STARTED", 9) steps = [ ("1. 마켓플레이스 추가", "claude plugin marketplace add\n https://git.zioinfo.co.kr/ythong/harness"), ("2. 설치 + 플래그", "claude plugin install harness@ythong\nexport CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1"), ("3. 한 문장으로 실행", 'claude "핀테크 리스크 평가 팀용 하네스 구성해줘"'), ] y = 1.6 for title, code in steps: box(s, 0.6, y, 12.1, 1.45, fill=CARD, line=RGBColor(0xE2,0xE3,0xEE), line_w=0.75, round_=True) box(s, 0.6, y, 0.12, 1.45, fill=PURPLE) text(s, 0.95, y+0.18, 11.5, 0.5, [[(title, 15, True, INK)]]) text(s, 0.95, y+0.68, 11.5, 0.7, [[(code, 13, False, INDIGO, "Consolas")]]) y += 1.65 text(s, 0.6, 6.7, 12, 0.4, [[("요구사항: Claude Code v2.x 이상 · Agent Teams 실험 플래그", 11.5, False, GRAY)]]) footer(s) # ── 슬라이드 9: 마무리 ─────────────────────────────────────────────── s = prs.slides.add_slide(BLANK) box(s, 0, 0, 13.333, 7.5, fill=INK) box(s, 0, 0, 13.333, 0.18, fill=PURPLE) box(s, 0, 7.32, 13.333, 0.18, fill=CYAN) if os.path.exists(ICON): try: s.shapes.add_picture(ICON, Inches(5.9), Inches(0.9), height=Inches(1.7)) except Exception: pass text(s, 0.9, 2.9, 11.5, 2.2, [ [("한 문장으로, 팀을 짓는다.", 34, True, WHITE)], [("Harness — 도메인을 에이전트 팀과 스킬로.", 18, True, CYAN)], ], align=PP_ALIGN.CENTER, space=12) text(s, 0.9, 5.4, 11.5, 1.2, [ [("\"하네스 구성해줘\" · \"build a harness for this project\"", 15, False, LIGHT)], [("git.zioinfo.co.kr/ythong/harness · Apache-2.0", 12, False, RGBColor(0x9A,0x9F,0xC0))], ], align=PP_ALIGN.CENTER, space=8) out = os.path.join(HERE, "Harness_소개덱_v1.pptx") try: prs.save(out) except PermissionError: out = os.path.join(HERE, "Harness_소개덱_v1.1.pptx") prs.save(out) print("(원본 파일이 열려 있어 v1.1로 저장)") print("생성 완료:", out, "(", len(prs.slides._sldIdLst), "슬라이드 )")