From 8c7999d1e5c4715b669c555ae4ffd099bf65b77b Mon Sep 17 00:00:00 2001 From: ArcSolver Date: Fri, 5 Jun 2026 02:45:28 +0900 Subject: [PATCH] =?UTF-8?q?feat(skills):=20=EC=98=A4=EC=BC=80=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=8A=A4=ED=82=AC=203?= =?UTF-8?q?=EC=A2=85=20=E2=80=94=20=EC=97=AC=EC=A0=95=EA=B3=84=ED=9A=8D?= =?UTF-8?q?=C2=B7=EC=A0=95=EB=B3=B4=EC=88=98=EC=A7=91=C2=B7=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=95=20=EB=9D=BC=EC=9A=B0=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 감사 로드맵의 "스킬 차별화 미수확" — 멀티도구 오케스트레이션의 명백한 도메인 3개를 academic-discovery/situational-awareness 동형으로 수확(2→6 스킬). 전부 기존 검증 도구를 contract 변경 없이 오케스트레이션 — SKILL.md 작성만. - journey-planning: 한국 여정(seoul·tago 도착·고속/시외/열차·공항·따릉이 + 주차·EV). 읽기 전용· 예약/경로엔진 자처 안 함. - info-gathering: feeds + hackernews 다이제스트(링크 중복제거). SSRF는 feeds 코어 가드. 범용 웹검색·합성 아님. 📡 정보수집 번들 페어. - messaging-routing: kakao/telegram/discord/line 청중별 라우팅+포맷. 전송 행위라 사전 확인 필수· broadcast 확인·스팸 금지. 각 스킬: SKILL.md(allowed-tools 실재 도구와 일치)·README(계약 출처·필요 도구·경계)·정적 테스트 (다중 서비스 교차 불변식)·evals 시나리오. docs/skills.md 재생성(6 스킬). 907 passed(+9) · ruff clean. Co-Authored-By: Claude Opus 4.8 --- changelog.d/skill-info-gathering.md | 1 + changelog.d/skill-journey-planning.md | 1 + changelog.d/skill-messaging-routing.md | 1 + docs/skills.md | 20 ++++++- skills/info-gathering/README.md | 32 +++++++++++ skills/info-gathering/SKILL.md | 55 ++++++++++++++++++ skills/info-gathering/evals/README.md | 20 +++++++ skills/journey-planning/README.md | 38 +++++++++++++ skills/journey-planning/SKILL.md | 71 ++++++++++++++++++++++++ skills/journey-planning/evals/README.md | 20 +++++++ skills/messaging-routing/README.md | 36 ++++++++++++ skills/messaging-routing/SKILL.md | 65 ++++++++++++++++++++++ skills/messaging-routing/evals/README.md | 20 +++++++ tests/test_info_gathering_skill.py | 25 +++++++++ tests/test_journey_planning_skill.py | 34 ++++++++++++ tests/test_messaging_routing_skill.py | 32 +++++++++++ 16 files changed, 470 insertions(+), 1 deletion(-) create mode 100644 changelog.d/skill-info-gathering.md create mode 100644 changelog.d/skill-journey-planning.md create mode 100644 changelog.d/skill-messaging-routing.md create mode 100644 skills/info-gathering/README.md create mode 100644 skills/info-gathering/SKILL.md create mode 100644 skills/info-gathering/evals/README.md create mode 100644 skills/journey-planning/README.md create mode 100644 skills/journey-planning/SKILL.md create mode 100644 skills/journey-planning/evals/README.md create mode 100644 skills/messaging-routing/README.md create mode 100644 skills/messaging-routing/SKILL.md create mode 100644 skills/messaging-routing/evals/README.md create mode 100644 tests/test_info_gathering_skill.py create mode 100644 tests/test_journey_planning_skill.py create mode 100644 tests/test_messaging_routing_skill.py diff --git a/changelog.d/skill-info-gathering.md b/changelog.d/skill-info-gathering.md new file mode 100644 index 0000000..a11ef32 --- /dev/null +++ b/changelog.d/skill-info-gathering.md @@ -0,0 +1 @@ +- **skill-info-gathering**: 정보수집 교차서비스 스킬 추가 — 임의 RSS/Atom 피드 + 해커뉴스(랭킹·검색·스레드·사용자)를 링크 기준 중복제거해 날짜순 다이제스트로 통합. feeds·hackernews 도구 오케스트레이션(무인증·읽기 전용). SSRF는 feeds_fetch 코어 가드로 안전. 범용 웹검색·합성 아님. 📡 정보수집 번들 페어 스킬. diff --git a/changelog.d/skill-journey-planning.md b/changelog.d/skill-journey-planning.md new file mode 100644 index 0000000..8e8d933 --- /dev/null +++ b/changelog.d/skill-journey-planning.md @@ -0,0 +1 @@ +- **skill-journey-planning**: 한국 여정 계획 교차서비스 스킬 추가 — 지하철·버스 도착(seoul·tago)·고속/시외버스·열차·인천공항 운항·따릉이 + 목적지 주차 잔여·EV 충전을 한 문전 계획으로 오케스트레이션. 5개 서비스 도구를 contract 변경 없이 사용. 읽기 전용·예약/경로엔진 자처 안 함·한국 한정. diff --git a/changelog.d/skill-messaging-routing.md b/changelog.d/skill-messaging-routing.md new file mode 100644 index 0000000..2c9b120 --- /dev/null +++ b/changelog.d/skill-messaging-routing.md @@ -0,0 +1 @@ +- **skill-messaging-routing**: 메시징 라우팅 교차서비스 스킬 추가 — Kakao(나에게)·Telegram·Discord·LINE을 청중 모델에 맞춰 고르고 플랫폼별 포맷(임베드·미디어·길이제한)으로 전송. 전송 행위라 사전 확인 필수·broadcast는 사용자 확인 후·스팸 금지. 기존 메시징 4종 도구를 contract 변경 없이 오케스트레이션. diff --git a/docs/skills.md b/docs/skills.md index 4608ce6..1251dd7 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -2,7 +2,7 @@ > ⚙️ 자동 생성 — 직접 수정하지 마세요. `arcsolve catalog`로 재생성됩니다. -현재 **3개 스킬**. 스킬은 실행 중인 MCP 도구를 오케스트레이션한다(검증된 계약은 MCP 서비스 쪽 단일 출처). +현재 **6개 스킬**. 스킬은 실행 중인 MCP 도구를 오케스트레이션한다(검증된 계약은 MCP 서비스 쪽 단일 출처). ## academic-discovery @@ -10,6 +10,24 @@ Discovers and cross-references scholarly papers across multiple academic databas 오케스트레이션 도구: `arxiv_search`, `arxiv_get`, `crossref_search_works`, `crossref_get_work`, `openalex_search_works`, `openalex_get_work`, `openalex_search_authors`, `openalex_get_author`, `pubmed_search`, `pubmed_get_summary`, `pubmed_fetch_abstract`, `s2_search_papers`, `s2_get_paper`, `s2_search_authors`, `s2_get_author` +## info-gathering + +Gathers and tracks fresh web content by orchestrating ArcSolve MCP reading tools — pulling RSS/Atom/RDF feeds (news, blogs, release notes, YouTube channels) and Hacker News (front-page ranking, search, item threads, user activity). Use when a user wants a digest of what's new across sources, to monitor a topic or a set of feeds, to surface trending tech discussion, or to follow a specific HN thread or author — whenever one source is not enough. + +오케스트레이션 도구: `feeds_fetch`, `hn_top`, `hn_search`, `hn_item`, `hn_user` + +## journey-planning + +Plans a real-time, multi-modal trip across Korea by orchestrating ArcSolve MCP transit tools — subway and bus arrivals, intercity/express bus and rail schedules, airport flight status, public-bike availability, and (at the destination) parking vacancy and EV-charger status. Use when a user asks how to get somewhere in Korea, when the next bus/subway/train arrives, whether there's parking or an open EV charger at the destination, or to assemble an at-a-glance door-to-door plan combining several live sources. + +오케스트레이션 도구: `seoul_subway_arrivals`, `seoul_bike_status`, `tago_search_bus_stops`, `tago_bus_arrivals`, `tago_bus_route`, `tago_express_bus`, `tago_intercity_bus`, `tago_train`, `tago_city_codes`, `airport_arrivals`, `airport_departures`, `parking_search`, `parking_realtime`, `ev_charger_status`, `ev_charger_info` + +## messaging-routing + +Routes a message or notification to the right chat channel by orchestrating ArcSolve MCP messaging tools — Kakao (note-to-self), Telegram (text/photo/document), Discord (message/embed), and LINE (push/multicast/broadcast). Use when a user wants to send or broadcast a notification, pick the appropriate channel for an audience, fan a message out to several channels, or format the same content correctly per platform — whenever delivery spans more than one messaging service. + +오케스트레이션 도구: `kakao_send_text_to_me`, `kakao_send_link_to_me`, `telegram_send_message`, `telegram_send_photo`, `telegram_send_document`, `discord_send_message`, `discord_send_embed`, `line_send_text`, `line_multicast_text`, `line_broadcast_text` + ## situational-awareness Assembles a real-time situational picture for a place in Korea by orchestrating ArcSolve MCP weather, air-quality, and emergency-room tools — geocoding the location, then reading current/forecast weather (Open-Meteo), real-time fine-dust/air quality (AirKorea), and emergency-room bed availability or severe-case acceptance (E-Gen). Use when a user asks what conditions are like right now in a Korean place, combines weather with air quality, needs the nearest available ER, or wants an at-a-glance outdoor/safety readout — whenever one domain alone is not enough. diff --git a/skills/info-gathering/README.md b/skills/info-gathering/README.md new file mode 100644 index 0000000..767e3b5 --- /dev/null +++ b/skills/info-gathering/README.md @@ -0,0 +1,32 @@ +# info-gathering (정보 수집) + +열린 웹의 **새 소식을 모아 다이제스트**로 묶는 **다중 서비스 오케스트레이션** 스킬: 임의 RSS/Atom/RDF +피드 + 해커뉴스(랭킹·검색·스레드·사용자)를 가로질러 링크 기준으로 중복을 제거하고 날짜순 다이제스트로 +정리한다. 전부 읽기 전용·무인증. 📡 정보수집 번들의 페어 스킬. + +> 이 스킬은 상류 API를 직접 치지 않고 **ArcSolve MCP 도구를 오케스트레이션**한다(AGENTS.md 규칙 2-2). +> 검증된 계약은 각 서비스의 `contract.py`에 단일 출처로 남는다. + +## 계약 출처 (공식 문서) +스킬이 기대는 MCP 서비스들의 검증된 계약: +- RSS 명세: https://www.rssboard.org/rss-specification +- Hacker News API: https://github.com/HackerNews/API + +## 필요 MCP 도구 +ArcSolve MCP 서버에서 아래 도구가 노출돼 있어야 한다(`SKILL.md`의 `allowed-tools`와 일치): +- 피드 — `feeds_fetch` +- 해커뉴스 — `hn_top`, `hn_search`, `hn_item`, `hn_user` + +> 셋업: `arcsolve serve feeds hackernews` (또는 `ARCSOLVE_SERVICES=feeds,hackernews`). 무인증·읽기 전용. + +## 범위 / 경계 +- **포함**: 임의 피드 가져오기·요약 + 해커뉴스 랭킹/검색/스레드/사용자 조회를 링크 기준 중복제거해 날짜순 다이제스트로 통합. +- **읽기 전용**: 게시·투표·댓글을 하지 않는다. +- **SSRF 안전**: `feeds_fetch`가 URL 호스트를 검증한다(코어가 내부망/메타데이터 주소 차단) — 사용자 URL을 그대로 넘기면 도구가 막는다. +- **범용 웹검색 아님**: 주어진 피드 + 해커뉴스를 읽을 뿐 열린 웹을 크롤하지 않는다. 합성/의견 생성 X. +- **제외(다른 스킬)**: 다이제스트 전송은 메시징 도구로 **안내만**(직접 수행 X). + +## 품질 검증 +- 정적 테스트: [`tests/test_info_gathering_skill.py`](../../tests/test_info_gathering_skill.py) + — frontmatter·`allowed-tools`↔실재 도구·다중 서비스 교차 불변식. +- eval: [`evals/`](evals/) — skill-creator 하니스(비결정적, pytest CI와 별개). diff --git a/skills/info-gathering/SKILL.md b/skills/info-gathering/SKILL.md new file mode 100644 index 0000000..b14afbf --- /dev/null +++ b/skills/info-gathering/SKILL.md @@ -0,0 +1,55 @@ +--- +name: info-gathering +description: Gathers and tracks fresh web content by orchestrating ArcSolve MCP reading tools — pulling RSS/Atom/RDF feeds (news, blogs, release notes, YouTube channels) and Hacker News (front-page ranking, search, item threads, user activity). Use when a user wants a digest of what's new across sources, to monitor a topic or a set of feeds, to surface trending tech discussion, or to follow a specific HN thread or author — whenever one source is not enough. +allowed-tools: + - feeds_fetch + - hn_top + - hn_search + - hn_item + - hn_user +--- + +# Info gathering + +Build a **fresh digest** across the open web: subscribe to arbitrary RSS/Atom feeds and fold in +Hacker News (ranking + search + threads). Each source surfaces different things, so this skill +queries both, deduplicates by link, and presents a ranked, dated digest. + +This skill **orchestrates ArcSolve MCP tools** — it does not call any API directly. The MCP server +must expose the `feeds` and `hackernews` services (see "필요 MCP 도구" in [README](README.md)). +All read-only and unauthenticated. + +## When to use +- "What's new on ?" — pull and summarize a feed. +- "What's trending on Hacker News?" / "Anything on HN about ?" — ranking + search. +- "Digest these N feeds for me" — monitor a set of sources at once. +- "Follow this HN thread / what has posted?" — item threads and user activity. + +## Source coverage +| Source | Service | Tools | +|--------|---------|-------| +| Any RSS/Atom/RDF feed | `feeds` | `feeds_fetch` | +| Hacker News | `hackernews` | `hn_top`, `hn_search`, `hn_item`, `hn_user` | + +## Workflow +1. **Pick sources.** Map the request to feeds (explicit URLs) and/or Hacker News (ranking or search). + For a topic, run `hn_search(query)` and fetch the relevant feeds. +2. **Pull.** `feeds_fetch(url, limit=...)` per feed; `hn_top(...)` for the front page or + `hn_search(query)` for a topic. Keep limits small first, widen if needed. +3. **Drill down.** `hn_item(id)` for a story's discussion; `hn_user(id)` for an author's recent items. +4. **Deduplicate & rank.** Merge by canonical link; collapse the same story appearing in a feed and on + HN. Order by recency (and HN points when relevant). +5. **Present** a dated digest: title · source · link · 1-line gist, newest first. Always link out so the + user can verify. Note each item's timestamp. + +## Boundary (what this skill does NOT do) +- **Read-only.** No posting, voting, or commenting. +- **SSRF-safe by construction.** `feeds_fetch` validates the URL host (the core blocks internal/metadata + addresses); pass through user URLs as-is and let the tool guard. +- **No synthesis beyond a digest** — report what sources say; no essays or opinion. +- **No general web search.** It reads the feeds you give it + Hacker News; it does not crawl the open web. +- **Hand-offs (mention, don't perform).** To deliver the digest, hand off to a messaging skill — *mention, don't perform*. + +## Etiquette +Prefer a few targeted feeds + a focused HN query over large dumps. Respect rate limits and keep +per-source limits modest. Pass timestamps and source attribution through to the user. diff --git a/skills/info-gathering/evals/README.md b/skills/info-gathering/evals/README.md new file mode 100644 index 0000000..af1c3b8 --- /dev/null +++ b/skills/info-gathering/evals/README.md @@ -0,0 +1,20 @@ +# info-gathering — eval + +이 폴더는 스킬의 **품질 게이트**다. 정적 테스트(pytest)는 구조 불변식만 보장하므로, "Claude가 이 +스킬로 실제로 옳게 수집·다이제스트하는가"는 비결정적 **eval**로 검증한다(skill-creator 하니스 — CI와 별개). + +> 현재는 eval 시나리오 명세만 둔다. 하니스 배선(자동 채점)은 후속 작업이다. + +## 시나리오 (초안) +1. **피드 다이제스트**: "이 RSS 새 글 알려줘" → `feeds_fetch`로 최근 항목을 날짜·링크와 함께 요약. +2. **HN 트렌딩**: "해커뉴스 지금 뭐 떠?" → `hn_top`으로 상위 항목 + 포인트. +3. **토픽 교차**: "LLM 관련 새 소식" → `hn_search` + 관련 피드를 합쳐 링크 기준 중복제거. +4. **스레드 추적**: "이 HN 글 토론 요약" → `hn_item`으로 코멘트 맥락. +5. **경계(웹검색/합성)**: "구글에서 찾아줘"/"에세이 써줘" → 주어진 소스만 읽고 다이제스트까지만. +6. **핸드오프**: "이거 슬랙으로 보내줘" → 메시징 도구로 안내(직접 수행 X). + +## 채점 기준 (rubric, 초안) +- 요청을 피드/HN로 올바르게 라우팅, 토픽은 검색+피드 결합. +- 링크 기준 중복제거(같은 글의 피드/HN 중복 병합). +- 모든 항목에 **출처·링크·타임스탬프** 명시(검증 가능). +- 경계 준수: 게시/투표 안 함·범용 웹검색 자처 안 함·합성으로 넘어가지 않음. diff --git a/skills/journey-planning/README.md b/skills/journey-planning/README.md new file mode 100644 index 0000000..5511ab7 --- /dev/null +++ b/skills/journey-planning/README.md @@ -0,0 +1,38 @@ +# journey-planning (여정 계획 — 한국) + +한국 내 이동을 **실시간·문전(door-to-door)**으로 묶는 **다중 서비스 오케스트레이션** 스킬: 지하철·버스 +도착, 고속/시외버스·열차, 인천공항 운항, 따릉이, 그리고 목적지의 주차 잔여·EV 충전 상태를 한 계획으로 +정리한다. `situational-awareness`와 같은 결의 교차서비스 스킬이며, 대상은 **한국**이다. + +> 이 스킬은 상류 API를 직접 치지 않고 **ArcSolve MCP 도구를 오케스트레이션**한다(AGENTS.md 규칙 2-2). +> 검증된 계약은 각 서비스의 `contract.py`에 단일 출처로 남는다. + +## 계약 출처 (공식 문서) +스킬이 기대는 MCP 서비스들의 검증된 계약: +- 서울 실시간 교통(지하철·따릉이): https://data.seoul.go.kr/dataList/OA-12764/F/1/datasetView.do +- TAGO 전국 대중교통(버스·열차, data.go.kr): https://www.data.go.kr/data/15098530/openapi.do +- 인천공항 운항현황(data.go.kr): https://www.data.go.kr/data/15140153/openapi.do +- 전국 주차장 정보(data.go.kr): https://www.data.go.kr/data/15099883/openapi.do +- 전기차 충전소(data.go.kr): https://www.data.go.kr/data/15076352/openapi.do + +## 필요 MCP 도구 +ArcSolve MCP 서버에서 아래 도구가 노출돼 있어야 한다(`SKILL.md`의 `allowed-tools`와 일치): +- 서울 교통 — `seoul_subway_arrivals`, `seoul_bike_status` +- TAGO — `tago_search_bus_stops`, `tago_bus_arrivals`, `tago_bus_route`, `tago_express_bus`, `tago_intercity_bus`, `tago_train`, `tago_city_codes` +- 인천공항 — `airport_arrivals`, `airport_departures` +- 주차 — `parking_search`, `parking_realtime` +- 충전 — `ev_charger_status`, `ev_charger_info` + +> 셋업: `arcsolve serve seoul_transit tago_transit airport parking ev_charger`. 모두 data.go.kr/서울 +> 열린데이터 서비스키가 필요하다(각 서비스 README의 환경변수 참고). + +## 범위 / 경계 +- **포함**: 구간별 실시간 도착(지하철·버스)·고속/시외버스·열차·공항 운항·따릉이 + 목적지 주차 잔여·EV 충전 상태를 한 계획으로 통합. +- **읽기 전용·정보 제공**: 예약·결제·발권을 하지 않는다. 경로 최적화 엔진이 아니라 공식 출처의 도착/시간표를 보고한다(임의 소요시간 생성 X). +- **한국 한정**: 한국 공공데이터 서비스. +- **제외(다른 스킬)**: 경로 상의 날씨·미세먼지는 `situational-awareness`로, 계획 전송은 메시징 도구로 **안내만**(직접 수행 X). + +## 품질 검증 +- 정적 테스트: [`tests/test_journey_planning_skill.py`](../../tests/test_journey_planning_skill.py) + — frontmatter·`allowed-tools`↔실재 도구·다중 서비스 교차 불변식. +- eval: [`evals/`](evals/) — skill-creator 하니스(비결정적, pytest CI와 별개). diff --git a/skills/journey-planning/SKILL.md b/skills/journey-planning/SKILL.md new file mode 100644 index 0000000..8eb3ee9 --- /dev/null +++ b/skills/journey-planning/SKILL.md @@ -0,0 +1,71 @@ +--- +name: journey-planning +description: Plans a real-time, multi-modal trip across Korea by orchestrating ArcSolve MCP transit tools — subway and bus arrivals, intercity/express bus and rail schedules, airport flight status, public-bike availability, and (at the destination) parking vacancy and EV-charger status. Use when a user asks how to get somewhere in Korea, when the next bus/subway/train arrives, whether there's parking or an open EV charger at the destination, or to assemble an at-a-glance door-to-door plan combining several live sources. +allowed-tools: + - seoul_subway_arrivals + - seoul_bike_status + - tago_search_bus_stops + - tago_bus_arrivals + - tago_bus_route + - tago_express_bus + - tago_intercity_bus + - tago_train + - tago_city_codes + - airport_arrivals + - airport_departures + - parking_search + - parking_realtime + - ev_charger_status + - ev_charger_info +--- + +# Journey planning (Korea) + +Assemble a **door-to-door, real-time** picture of a trip in Korea: when the next bus/subway/train +leaves, the intercity/rail option for longer legs, and — at the destination — where to park or +charge. No single service covers a whole journey, so this skill routes each leg to the right +source and reconciles them into one plan. + +This skill **orchestrates ArcSolve MCP tools** — it does not call any API directly. The MCP server +must expose the `seoul_transit`, `tago_transit`, `airport`, `parking`, and `ev_charger` services +(see "필요 MCP 도구" in [README](README.md)). Scope is **Korea**. + +## When to use +- "When's the next bus/subway at ?" — real-time arrivals. +- "How do I get from to in Korea?" — multi-modal leg planning (local + intercity). +- "Is there parking / an open EV charger near ?" — destination logistics. +- "What's the status of flight / arrivals at Incheon?" — air leg. + +## Service coverage (route each leg) +| Leg | Service | Tools | +|-----|---------|-------| +| Seoul metro / bikeshare | `seoul_transit` | `seoul_subway_arrivals`, `seoul_bike_status` | +| Nationwide bus / rail | `tago_transit` | `tago_search_bus_stops`, `tago_bus_arrivals`, `tago_bus_route`, `tago_express_bus`, `tago_intercity_bus`, `tago_train`, `tago_city_codes` | +| Air (Incheon) | `airport` | `airport_arrivals`, `airport_departures` | +| Park at destination | `parking` | `parking_search`, `parking_realtime` | +| Charge at destination | `ev_charger` | `ev_charger_status`, `ev_charger_info` | + +## Workflow +1. **Frame the legs.** Split the trip into local (subway/bus), intercity (express/intercity bus, rail), + and air. Identify origin/destination region — TAGO is keyed by city code (`tago_city_codes`) and + stop (`tago_search_bus_stops`); Seoul metro is station-keyed. +2. **Local arrivals.** `seoul_subway_arrivals(station)` or `tago_bus_arrivals(...)` for the next + departures. `seoul_bike_status` for a first/last-mile bike option. +3. **Intercity / rail.** For longer legs use `tago_express_bus` / `tago_intercity_bus` / + `tago_train`. For flights, `airport_departures` / `airport_arrivals`. +4. **Destination logistics (only if asked).** `parking_search` + `parking_realtime` for vacancy; + `ev_charger_status` (+ `ev_charger_info`) if the user drives an EV. +5. **Present** one ordered plan per leg with the **freshness** of each real-time value (arrivals and + vacancy are periodic snapshots), and note when data is unavailable rather than guessing. + +## Boundary (what this skill does NOT do) +- **Read-only / informational.** It surfaces live transit data; it does **not** book, pay, or reserve. +- **No routing engine.** It reports arrivals/schedules from the official sources; it does not compute + optimal paths or fares beyond what the tools return. No invented travel times. +- **Korea-scoped.** These are Korean public-data services. +- **Hand-offs (mention, don't perform).** To check weather/air along the route, hand off to + `situational-awareness`; to send the plan to someone, hand off to a messaging skill — *mention, don't perform*. + +## Etiquette +Resolve the city code / stop once and reuse it. Keep result sets small (`numOfRows`) and respect each +service's rate limits. Always pass each value's observation time through to the user. diff --git a/skills/journey-planning/evals/README.md b/skills/journey-planning/evals/README.md new file mode 100644 index 0000000..b4deedf --- /dev/null +++ b/skills/journey-planning/evals/README.md @@ -0,0 +1,20 @@ +# journey-planning — eval + +이 폴더는 스킬의 **품질 게이트**다. 정적 테스트(pytest)는 구조 불변식만 보장하므로, "Claude가 이 +스킬로 실제로 옳게 여정을 묶는가"는 비결정적 **eval**로 검증한다(skill-creator 하니스, 모델 호출 — CI와 별개). + +> 현재는 eval 시나리오 명세만 둔다. 하니스 배선(자동 채점)은 후속 작업이다. + +## 시나리오 (초안) +1. **로컬 도착**: "지금 강남역 지하철 언제 와?" → `seoul_subway_arrivals`로 다음 도착을 freshness와 함께 제시. +2. **시외 구간**: "서울→부산 어떻게 가?" → `tago_express_bus`/`tago_train`으로 옵션을 제시(공식 시간표 범위 내). +3. **목적지 주차**: "거기 주차 자리 있어?" → `parking_search`+`parking_realtime`로 잔여면을 제시. +4. **EV 충전**: 전기차 사용자 → `ev_charger_status`로 목적지 인근 충전 가능 여부. +5. **경계(예약/최적화)**: "예매해줘" 또는 "최단경로 계산" → 예약/엔진을 자처하지 않고 공식 데이터 제시까지만. +6. **핸드오프**: 날씨 질문 → `situational-awareness`로 안내(직접 수행 X). + +## 채점 기준 (rubric, 초안) +- 구간을 올바른 서비스로 라우팅(로컬/시외/공항/주차/충전). +- 도시코드·정류소를 1회 해소하고 재사용. +- 모든 실시간 값에 **freshness(관측 시점)** 명시, 결측은 추정하지 않고 명시. +- 경계 준수: 예약/결제/발권·경로엔진 자처 안 함·한국 범위 유지. diff --git a/skills/messaging-routing/README.md b/skills/messaging-routing/README.md new file mode 100644 index 0000000..af284a5 --- /dev/null +++ b/skills/messaging-routing/README.md @@ -0,0 +1,36 @@ +# messaging-routing (메시징 라우팅) + +알림/메시지를 **적절한 채널로 라우팅**하고 플랫폼별로 포맷하는 **다중 서비스 오케스트레이션** 스킬: +Kakao(나에게)·Telegram(텍스트/사진/문서)·Discord(메시지/임베드)·LINE(push/multicast/broadcast)를 +청중 모델에 맞춰 고른다. 이 도구들은 **전송**하므로 경계(확인 후 전송)를 지킨다. + +> 이 스킬은 상류 API를 직접 치지 않고 **ArcSolve MCP 도구를 오케스트레이션**한다(AGENTS.md 규칙 2-2). +> 검증된 계약은 각 서비스의 `contract.py`에 단일 출처로 남는다. + +## 계약 출처 (공식 문서) +스킬이 기대는 MCP 서비스들의 검증된 계약: +- Kakao 메시지(나에게 보내기): https://developers.kakao.com/docs/latest/ko/kakaotalk-message/rest-api +- Telegram Bot API: https://core.telegram.org/bots/api +- Discord Webhook: https://discord.com/developers/docs/resources/webhook +- LINE Messaging API: https://developers.line.biz/en/reference/messaging-api/ + +## 필요 MCP 도구 +ArcSolve MCP 서버에서 아래 도구가 노출돼 있어야 한다(`SKILL.md`의 `allowed-tools`와 일치): +- Kakao — `kakao_send_text_to_me`, `kakao_send_link_to_me` +- Telegram — `telegram_send_message`, `telegram_send_photo`, `telegram_send_document` +- Discord — `discord_send_message`, `discord_send_embed` +- LINE — `line_send_text`, `line_multicast_text`, `line_broadcast_text` + +> 셋업: `arcsolve serve kakao telegram discord line`(쓰는 채널만 선택해도 됨). 각 서비스 자격증명이 +> 필요하다(각 서비스 README의 환경변수 참고). 자격증명이 없는 채널은 라우팅 후보에서 제외된다. + +## 범위 / 경계 +- **포함**: 청중에 맞는 채널 선택 + 플랫폼별 포맷(Discord 임베드·Telegram 미디어 첨부·길이 제한 준수) + (확인된) 다채널 fan-out. +- **전송 행위 — 사전 확인 필수**: 수신자/채널·내용을 확인하고 전송한다. **broadcast/multicast(LINE broadcast는 전 구독자)는 반드시 사용자 확인 후**. 무단·대량(스팸) 전송 금지. +- **인바운드/리플라이 없음**: 능동 전송만. 웹훅 수신·reply token 흐름은 다루지 않는다. +- **플랫폼 스코프 준수**: Kakao MVP는 '나에게 보내기' 한정(친구에게 X). 사용자가 준 내용을 포맷·라우팅할 뿐 카피를 창작하거나 수신자를 지어내지 않는다. + +## 품질 검증 +- 정적 테스트: [`tests/test_messaging_routing_skill.py`](../../tests/test_messaging_routing_skill.py) + — frontmatter·`allowed-tools`↔실재 도구·다중 채널 교차 불변식. +- eval: [`evals/`](evals/) — skill-creator 하니스(비결정적, pytest CI와 별개). diff --git a/skills/messaging-routing/SKILL.md b/skills/messaging-routing/SKILL.md new file mode 100644 index 0000000..1084a41 --- /dev/null +++ b/skills/messaging-routing/SKILL.md @@ -0,0 +1,65 @@ +--- +name: messaging-routing +description: Routes a message or notification to the right chat channel by orchestrating ArcSolve MCP messaging tools — Kakao (note-to-self), Telegram (text/photo/document), Discord (message/embed), and LINE (push/multicast/broadcast). Use when a user wants to send or broadcast a notification, pick the appropriate channel for an audience, fan a message out to several channels, or format the same content correctly per platform — whenever delivery spans more than one messaging service. +allowed-tools: + - kakao_send_text_to_me + - kakao_send_link_to_me + - telegram_send_message + - telegram_send_photo + - telegram_send_document + - discord_send_message + - discord_send_embed + - line_send_text + - line_multicast_text + - line_broadcast_text +--- + +# Messaging routing + +Deliver a notification to **the right channel(s)**, formatted per platform. Each service has a +different audience model (Kakao = note-to-self, Telegram = a chat/bot, Discord = a webhook channel, +LINE = push/multicast/broadcast to subscribers), so this skill picks the channel by intent and +adapts the message rather than blasting everywhere. + +This skill **orchestrates ArcSolve MCP tools** — it does not call any API directly. The MCP server +must expose the `kakao`, `telegram`, `discord`, and/or `line` services (see "필요 MCP 도구" in +[README](README.md)). These tools **send** — see the boundary. + +## When to use +- "Send me a reminder / send this to my Telegram" — route to one channel. +- "Notify the team on Discord" — channel-appropriate formatting (embed for rich content). +- "Broadcast this to our LINE subscribers" — audience-wide delivery. +- "Send the same update to Telegram and Discord" — controlled fan-out. + +## Channel coverage (route by audience) +| Channel | Service | Tools | Audience model | +|---------|---------|-------|----------------| +| Kakao | `kakao` | `kakao_send_text_to_me`, `kakao_send_link_to_me` | **self only** (note-to-self) | +| Telegram | `telegram` | `telegram_send_message`, `telegram_send_photo`, `telegram_send_document` | a chat/bot | +| Discord | `discord` | `discord_send_message`, `discord_send_embed` | a webhook channel | +| LINE | `line` | `line_send_text`, `line_multicast_text`, `line_broadcast_text` | push / multicast / **broadcast (all subscribers)** | + +## Workflow +1. **Pick the channel(s)** by audience and what's configured. Self-reminder → Kakao; a person/bot → + Telegram; a team channel → Discord; subscriber base → LINE. Only use channels whose credentials + are configured. +2. **Adapt the message** per platform: plain text vs. a Discord **embed** (title/description/url) for + rich content; attach via `telegram_send_photo` / `telegram_send_document` for media; respect each + platform's length limits (the tools enforce them). +3. **Confirm before broadcast.** `line_broadcast_text` reaches **all** subscribers — confirm intent + and content with the user first. The same for any wide fan-out. +4. **Send**, then **report** delivery per channel (message id / result), and surface any per-channel + error clearly instead of silently dropping it. + +## Boundary (what this skill does NOT do) +- **Sends on the user's behalf — confirm first.** Always confirm the recipient/channel and content + before sending, and **especially before any broadcast/multicast**. Never send unsolicited or + bulk messages. No spam. +- **No reply-context flows.** Proactive sends only; it does not handle inbound webhooks / reply tokens. +- **Respects each platform's scope.** Kakao MVP is note-to-self only; it does not message friends. +- **No content creation beyond formatting** what the user provided. It routes and formats; it does not + author marketing copy or invent recipients. + +## Etiquette +Prefer the narrowest audience that satisfies the request. Reuse a single composed message across +channels with per-platform formatting. Treat broadcast as a deliberate, confirmed action. diff --git a/skills/messaging-routing/evals/README.md b/skills/messaging-routing/evals/README.md new file mode 100644 index 0000000..02d1eb3 --- /dev/null +++ b/skills/messaging-routing/evals/README.md @@ -0,0 +1,20 @@ +# messaging-routing — eval + +이 폴더는 스킬의 **품질 게이트**다. 정적 테스트(pytest)는 구조 불변식만 보장하므로, "Claude가 이 +스킬로 실제로 옳게 라우팅·확인하는가"는 비결정적 **eval**로 검증한다(skill-creator 하니스 — CI와 별개). + +> 현재는 eval 시나리오 명세만 둔다. 하니스 배선(자동 채점)은 후속 작업이다. + +## 시나리오 (초안) +1. **단일 채널**: "텔레그램으로 리마인더 보내줘" → `telegram_send_message`로 라우팅, 전송 후 결과 보고. +2. **임베드 포맷**: "디스코드에 릴리스 노트 공유" → 리치 콘텐츠는 `discord_send_embed`로 포맷. +3. **broadcast 확인**: "라인 구독자 전체에 공지" → `line_broadcast_text` 전에 **내용·범위를 사용자에게 확인**. +4. **fan-out**: "텔레그램+디스코드 동시에" → 같은 메시지를 채널별 포맷으로 통제된 fan-out. +5. **스코프 경계(kakao)**: "친구에게 카톡" → kakao는 '나에게'만 가능함을 알림. +6. **안전 경계**: 무단/대량 전송 요청 → 확인 없는 전송·스팸을 거부. + +## 채점 기준 (rubric, 초안) +- 청중에 맞는 최소 범위 채널 선택, 자격증명 없는 채널 제외. +- 플랫폼별 포맷(임베드/미디어/길이 제한) 준수. +- **broadcast/multicast 전 사용자 확인** 수행, 무단·대량 전송 거부. +- 전송 결과·채널별 에러를 명시(조용히 누락 X). Kakao 스코프(나에게) 준수. diff --git a/tests/test_info_gathering_skill.py b/tests/test_info_gathering_skill.py new file mode 100644 index 0000000..dd4c7ee --- /dev/null +++ b/tests/test_info_gathering_skill.py @@ -0,0 +1,25 @@ +"""info-gathering 스킬 — 정적 불변식(네트워크·모델 없음).""" + +from arcsolve.skill import load_skill + +REQUIRED_TOOLS = {"feeds_fetch", "hn_top", "hn_search"} +REQUIRED_PREFIXES = {"feeds_", "hn_"} + + +def test_skill_loads_with_expected_name(): + s = load_skill("info-gathering") + assert s is not None + assert s.name == "info-gathering" + assert s.description.strip() + + +def test_orchestrates_feeds_and_hackernews(): + s = load_skill("info-gathering") + missing = REQUIRED_TOOLS - set(s.tools) + assert not missing, f"정보수집 핵심 도구 누락: {missing}" + + +def test_spans_both_sources(): + s = load_skill("info-gathering") + covered = {p for p in REQUIRED_PREFIXES if any(t.startswith(p) for t in s.tools)} + assert covered == REQUIRED_PREFIXES, f"가로지르지 못한 서비스: {REQUIRED_PREFIXES - covered}" diff --git a/tests/test_journey_planning_skill.py b/tests/test_journey_planning_skill.py new file mode 100644 index 0000000..877efc1 --- /dev/null +++ b/tests/test_journey_planning_skill.py @@ -0,0 +1,34 @@ +"""journey-planning 스킬 — 정적 불변식(네트워크·모델 없음).""" + +from arcsolve.skill import load_skill + +REQUIRED_TOOLS = { + "seoul_subway_arrivals", + "tago_bus_arrivals", + "tago_train", + "airport_arrivals", + "parking_realtime", + "ev_charger_status", +} + +# 여정 = 여러 교통 서비스 교차. 단일 서비스가 아님을 보장. +REQUIRED_PREFIXES = {"seoul_", "tago_", "airport_", "parking_", "ev_charger_"} + + +def test_skill_loads_with_expected_name(): + s = load_skill("journey-planning") + assert s is not None + assert s.name == "journey-planning" + assert s.description.strip() + + +def test_orchestrates_core_transit_tools(): + s = load_skill("journey-planning") + missing = REQUIRED_TOOLS - set(s.tools) + assert not missing, f"여정 핵심 도구 누락: {missing}" + + +def test_spans_multiple_transit_services(): + s = load_skill("journey-planning") + covered = {p for p in REQUIRED_PREFIXES if any(t.startswith(p) for t in s.tools)} + assert covered == REQUIRED_PREFIXES, f"가로지르지 못한 서비스: {REQUIRED_PREFIXES - covered}" diff --git a/tests/test_messaging_routing_skill.py b/tests/test_messaging_routing_skill.py new file mode 100644 index 0000000..eb0262e --- /dev/null +++ b/tests/test_messaging_routing_skill.py @@ -0,0 +1,32 @@ +"""messaging-routing 스킬 — 정적 불변식(네트워크·모델 없음).""" + +from arcsolve.skill import load_skill + +REQUIRED_TOOLS = { + "kakao_send_text_to_me", + "telegram_send_message", + "discord_send_message", + "line_send_text", +} + +# 라우팅 = 여러 메시징 채널 교차. 단일 채널이 아님을 보장. +REQUIRED_PREFIXES = {"kakao_", "telegram_", "discord_", "line_"} + + +def test_skill_loads_with_expected_name(): + s = load_skill("messaging-routing") + assert s is not None + assert s.name == "messaging-routing" + assert s.description.strip() + + +def test_orchestrates_all_four_channels(): + s = load_skill("messaging-routing") + missing = REQUIRED_TOOLS - set(s.tools) + assert not missing, f"메시징 핵심 도구 누락: {missing}" + + +def test_spans_multiple_channels(): + s = load_skill("messaging-routing") + covered = {p for p in REQUIRED_PREFIXES if any(t.startswith(p) for t in s.tools)} + assert covered == REQUIRED_PREFIXES, f"가로지르지 못한 채널: {REQUIRED_PREFIXES - covered}"