Showing preview only (732K chars total). Download the full file or copy to clipboard to get everything.
Repository: team-attention/plugins-for-claude-natives
Branch: main
Commit: fd9c20754dba
Files: 147
Total size: 633.9 KB
Directory structure:
gitextract_chnom6f9/
├── .claude-plugin/
│ ├── marketplace.json
│ └── plugin.json
├── .gitignore
├── LICENSE
├── README.ko.md
├── README.md
└── plugins/
├── agent-council/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── AGENTS.md
│ ├── CLAUDE.md
│ ├── README.ko.md
│ ├── README.md
│ ├── SKILL.md
│ ├── bin/
│ │ └── install.js
│ ├── council.config.yaml
│ ├── package.json
│ ├── scripts/
│ │ ├── council-job-worker.js
│ │ ├── council-job.js
│ │ ├── council-job.sh
│ │ └── council.sh
│ └── skills/
│ └── agent-council/
│ ├── SKILL.md
│ ├── references/
│ │ ├── config.md
│ │ ├── examples.md
│ │ ├── host-ui.md
│ │ ├── overview.md
│ │ ├── requirements.md
│ │ └── safety.md
│ └── scripts/
│ ├── council-job-worker.js
│ ├── council-job.js
│ ├── council-job.sh
│ └── council.sh
├── clarify/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ └── skills/
│ ├── metamedium/
│ │ ├── SKILL.md
│ │ └── references/
│ │ └── alan-kay-quotes.md
│ ├── unknown/
│ │ ├── SKILL.md
│ │ └── references/
│ │ ├── playbook-template.md
│ │ └── question-design.md
│ └── vague/
│ └── SKILL.md
├── dev/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── CLAUDE.md
│ ├── agents/
│ │ ├── codebase-explorer.md
│ │ ├── decision-synthesizer.md
│ │ ├── docs-researcher.md
│ │ └── tradeoff-analyzer.md
│ └── skills/
│ ├── dev-scan/
│ │ └── SKILL.md
│ └── tech-decision/
│ ├── SKILL.md
│ └── references/
│ ├── evaluation-criteria.md
│ └── report-template.md
├── doubt/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── README.md
│ └── hooks/
│ ├── hooks.json
│ └── scripts/
│ ├── doubt-detector.sh
│ └── doubt-validator.sh
├── fetch-tweet/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── README.md
│ └── skills/
│ └── fetch-tweet/
│ ├── SKILL.md
│ └── scripts/
│ └── fetch_tweet.py
├── gmail/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── README.md
│ └── skills/
│ └── gmail/
│ ├── .gitignore
│ ├── SKILL.md
│ ├── accounts.yaml.example
│ ├── assets/
│ │ ├── accounts.default.yaml
│ │ ├── email-templates.md
│ │ └── signatures.md
│ ├── pyproject.toml
│ ├── references/
│ │ ├── cli-usage.md
│ │ ├── search-queries.md
│ │ └── setup-guide.md
│ └── scripts/
│ ├── core/
│ │ ├── __init__.py
│ │ ├── batch_processor.py
│ │ ├── cache_manager.py
│ │ ├── quota_manager.py
│ │ └── retry_handler.py
│ ├── gmail_client.py
│ ├── list_messages.py
│ ├── manage_labels.py
│ ├── read_message.py
│ ├── send_message.py
│ └── setup_auth.py
├── google-calendar/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── README.md
│ └── skills/
│ └── google-calendar/
│ ├── SKILL.md
│ ├── examples/
│ │ ├── parallel-fetch.md
│ │ └── quick-query.md
│ ├── pyproject.toml
│ └── scripts/
│ ├── calendar_client.py
│ ├── fetch_events.py
│ ├── manage_events.py
│ └── setup_auth.py
├── interactive-review/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── .mcp.json
│ ├── CLAUDE.md
│ ├── README.md
│ ├── mcp-server/
│ │ ├── requirements.txt
│ │ ├── server.py
│ │ └── web_ui.py
│ └── skills/
│ └── review/
│ └── SKILL.md
├── kakaotalk/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── README.md
│ ├── scripts/
│ │ ├── kakao_read.py
│ │ └── kakao_send.py
│ └── skills/
│ └── kakaotalk/
│ └── SKILL.md
├── podcast/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── README.md
│ └── skills/
│ └── podcast/
│ ├── SKILL.md
│ └── scripts/
│ ├── convert_mp4.py
│ ├── generate_tts.py
│ └── upload_youtube.py
├── say-summary/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── README.md
│ ├── hooks/
│ │ └── hooks.json
│ ├── requirements.txt
│ └── scripts/
│ ├── say-summary.py
│ └── setup.sh
├── session-wrap/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── README.md
│ ├── agents/
│ │ ├── automation-scout.md
│ │ ├── doc-updater.md
│ │ ├── duplicate-checker.md
│ │ ├── followup-suggester.md
│ │ └── learning-extractor.md
│ ├── commands/
│ │ └── wrap.md
│ └── skills/
│ ├── history-insight/
│ │ ├── SKILL.md
│ │ ├── references/
│ │ │ └── session-file-format.md
│ │ └── scripts/
│ │ └── extract-session.sh
│ ├── session-analyzer/
│ │ ├── SKILL.md
│ │ ├── references/
│ │ │ ├── analysis-patterns.md
│ │ │ └── common-issues.md
│ │ └── scripts/
│ │ ├── extract-hook-events.sh
│ │ ├── extract-subagent-calls.sh
│ │ └── find-session-files.sh
│ └── session-wrap/
│ ├── SKILL.md
│ └── references/
│ └── multi-agent-patterns.md
├── team-assemble/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── README.md
│ └── skills/
│ └── team-assemble/
│ ├── SKILL.md
│ └── references/
│ ├── agents.md
│ ├── enable-agent-teams.md
│ ├── examples.md
│ └── prompt-templates.md
└── youtube-digest/
├── .claude-plugin/
│ └── plugin.json
├── README.md
└── skills/
└── youtube-digest/
├── SKILL.md
├── references/
│ ├── deep-research.md
│ └── quiz-patterns.md
└── scripts/
├── extract_metadata.sh
└── extract_transcript.sh
================================================
FILE CONTENTS
================================================
================================================
FILE: .claude-plugin/marketplace.json
================================================
{
"name": "team-attention-plugins",
"owner": {
"name": "Team Attention",
"url": "https://github.com/team-attention"
},
"description": "Claude Code plugins for power users by Team Attention",
"plugins": [
{
"name": "agent-council",
"description": "Collect and synthesize opinions from multiple AI agents (Gemini, GPT, Codex)",
"source": "./plugins/agent-council"
},
{
"name": "clarify",
"description": "Three lenses for clarity: vague requirements to specs, strategy blind spots via Known/Unknown quadrants, and content vs form leverage analysis",
"source": "./plugins/clarify"
},
{
"name": "interactive-review",
"description": "Interactive markdown review with web UI for visual plan/document approval",
"source": "./plugins/interactive-review"
},
{
"name": "say-summary",
"description": "Speaks a short summary of Claude's response using macOS TTS (Korean/English)",
"source": "./plugins/say-summary"
},
{
"name": "youtube-digest",
"description": "Summarize YouTube videos with transcript, insights, Korean translation, and quizzes",
"source": "./plugins/youtube-digest"
},
{
"name": "google-calendar",
"description": "Multi-account Google Calendar integration with parallel querying and conflict detection",
"source": "./plugins/google-calendar"
},
{
"name": "session-wrap",
"description": "Session wrap-up workflow with multi-agent analysis for documentation, automation, learning, and follow-up",
"source": "./plugins/session-wrap"
},
{
"name": "dev",
"description": "Developer workflow tools: community scanning, technical decision-making",
"source": "./plugins/dev"
},
{
"name": "kakaotalk",
"description": "Send and read KakaoTalk messages on macOS using Accessibility API",
"source": "./plugins/kakaotalk"
},
{
"name": "doubt",
"description": "Force Claude to re-validate when you have doubts (!doubt)",
"source": "./plugins/doubt"
},
{
"name": "gmail",
"description": "Multi-account Gmail integration - read, search, send, and manage emails with 5-step sending workflow",
"source": "./plugins/gmail"
},
{
"name": "team-assemble",
"description": "Dynamically assemble expert agent teams for complex tasks using Claude Code's agent teams feature",
"source": "./plugins/team-assemble"
},
{
"name": "podcast",
"description": "Generate Korean podcast episodes from any source (URLs, tweets, articles) with OpenAI TTS and YouTube auto-upload",
"source": "./plugins/podcast"
},
{
"name": "fetch-tweet",
"description": "Fetch full tweet text, author info, and engagement data from X/Twitter URLs without authentication (uses FxEmbed)",
"source": "./plugins/fetch-tweet"
}
]
}
================================================
FILE: .claude-plugin/plugin.json
================================================
{
"name": "plugins-for-claude-natives",
"version": "0.1.0",
"description": "Claude Code 네이티브 사용자를 위한 유틸리티 플러그인 모음",
"author": {
"name": "Team Attention",
"url": "https://github.com/team-attention"
},
"repository": "https://github.com/team-attention/plugins-for-claude-natives"
}
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
*.egg
# PyInstaller
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre
.pyre/
# pytype
.pytype/
# Cython
cython_debug/
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Videos
*.mp4
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2025 Team Attention
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.ko.md
================================================
# Plugins for Claude Natives
Claude Code의 기능을 확장하고 싶은 파워 유저를 위한 플러그인 모음입니다.
## 목차
- [빠른 시작](#빠른-시작)
- [플러그인 목록](#플러그인-목록)
- [상세 설명](#상세-설명)
- [agent-council](#agent-council) - 여러 AI 모델의 의견 종합
- [clarify](#clarify) - 모호한 요구사항을 명세로 변환
- [dev](#dev) - 커뮤니티 스캔 + 기술 의사결정
- [interactive-review](#interactive-review) - 웹 UI로 계획 검토
- [say-summary](#say-summary) - 응답을 음성으로 듣기
- [youtube-digest](#youtube-digest) - YouTube 영상 요약 및 퀴즈
- [google-calendar](#google-calendar) - 멀티 계정 캘린더 통합
- [kakaotalk](#kakaotalk) - macOS 카카오톡 메시지 발송/읽기
- [session-wrap](#session-wrap) - 세션 마무리 + 히스토리 분석
- [podcast](#podcast) - 소스→YouTube 한국어 팟캐스트 생성
- [fetch-tweet](#fetch-tweet) - 인증 없이 트윗 본문과 메타데이터 가져오기
- [기여하기](#기여하기)
- [라이선스](#라이선스)
---
## 빠른 시작
```bash
# 마켓플레이스 추가
/plugin marketplace add team-attention/plugins-for-claude-natives
# 플러그인 설치
/plugin install <plugin-name>
```
---
## 플러그인 목록
| 플러그인 | 설명 | 소셜 |
|---------|------|------|
| [agent-council](./plugins/agent-council/) | 여러 AI 에이전트(Gemini, GPT, Codex)의 의견을 수집하고 종합 | [LinkedIn](https://www.linkedin.com/posts/gb-jeong_claude-code%EA%B0%80-codex-gemini-cli-%EA%B3%BC-%ED%9A%8C%EC%9D%98%ED%95%B4%EC%84%9C-%EA%B2%B0%EB%A1%A0%EC%9D%84-activity-7406083077258665984-L_fD) |
| [clarify](./plugins/clarify/) | 반복적인 질문을 통해 모호한 요구사항을 정확한 명세로 변환 | [LinkedIn](https://www.linkedin.com/posts/gb-jeong_%ED%81%B4%EB%A1%9C%EB%93%9C%EC%BD%94%EB%93%9C%EA%B0%80-%EA%B0%9D%EA%B4%80%EC%8B%9D%EC%9C%BC%EB%A1%9C-%EC%A7%88%EB%AC%B8%ED%95%98%EA%B2%8C-%ED%95%98%EB%8A%94-skills%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%84%B8%EC%9A%94-clarify-activity-7413349697022570496-qLts) |
| [dev](./plugins/dev/) | 커뮤니티 의견 스캔 + 기술 의사결정 분석 | |
| [interactive-review](./plugins/interactive-review/) | 웹 UI를 통한 인터랙티브 마크다운 리뷰 | [LinkedIn](https://www.linkedin.com/posts/hoyeonleekr_claude-code%EA%B0%80-%EC%9E%91%EC%84%B1%ED%95%9C-%EA%B3%84%ED%9A%8D%EC%9D%B4%EB%82%98-%EA%B8%B4-%EB%AC%B8%EC%84%9C%EC%97%90-%EB%8C%80%ED%95%9C-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%94%BC%EB%93%9C%EB%B0%B1-%EC%A3%BC%EC%84%B8%EC%9A%94-activity-7412613598516051968-ujHp) |
| [say-summary](./plugins/say-summary/) | Claude 응답을 macOS TTS로 요약해서 읽어줌 (한국어/영어) | [LinkedIn](https://www.linkedin.com/posts/gb-jeong_claude-code%EC%9D%98-%EC%9D%91%EB%8B%B5%EC%9D%84-%EC%9A%94%EC%95%BD%ED%95%B4%EC%84%9C-%EC%9D%8C%EC%84%B1%EC%9C%BC%EB%A1%9C-%EB%93%A4%EC%9D%84-%EC%88%98-%EC%9E%88%EB%8A%94-hooks-activity-7412609821390249984-ekCd) |
| [youtube-digest](./plugins/youtube-digest/) | YouTube 영상 요약, 인사이트, 한글 번역, 퀴즈 제공 | [LinkedIn](https://www.linkedin.com/posts/gb-jeong_84%EB%B6%84%EC%A7%9C%EB%A6%AC-%EC%98%81%EC%96%B4-%ED%8C%9F%EC%BA%90%EC%8A%A4%ED%8A%B8%EB%A5%BC-5%EB%B6%84-%EB%A7%8C%EC%97%90-%ED%95%B5%EC%8B%AC-%ED%8C%8C%EC%95%85%ED%95%98%EA%B3%A0-%ED%80%B4%EC%A6%88%EA%B9%8C%EC%A7%80-%ED%92%80%EA%B3%A0-%EC%A7%81%EC%A0%91-activity-7414055598754848768-c0oy) |
| [google-calendar](./plugins/google-calendar/) | 멀티 계정 Google Calendar 통합, 병렬 조회 및 충돌 감지 | |
| [kakaotalk](./plugins/kakaotalk/) | macOS 카카오톡 메시지 발송 및 읽기 (Accessibility API) | |
| [session-wrap](./plugins/session-wrap/) | 세션 마무리, 히스토리 분석, 세션 검증 툴킷 | |
| [podcast](./plugins/podcast/) | 소스(URL, 트윗, 아티클)를 분석하여 OpenAI TTS로 한국어 팟캐스트 생성 및 YouTube 업로드 | |
| [fetch-tweet](./plugins/fetch-tweet/) | X/Twitter URL에서 인증 없이 트윗 본문, 작성자 정보, 인게이지먼트 데이터 조회 (FxEmbed 활용) | |
---
## 상세 설명
### agent-council
**여러 AI 모델을 소환해서 질문에 대한 합의를 도출합니다.**
어려운 결정을 내려야 하거나 다양한 관점이 필요할 때, 이 플러그인은 여러 AI 에이전트(Gemini CLI, GPT, Codex)에 병렬로 질문하고 그 의견들을 하나의 균형 잡힌 답변으로 종합합니다.
**트리거 문구:**
- "summon the council"
- "다른 AI들한테 물어봐"
- "여러 모델 의견 듣고 싶어"
**동작 방식:**
1. 질문이 여러 AI 에이전트에 동시에 전송됨
2. 각 에이전트가 자신의 관점을 제시
3. Claude가 응답들을 종합하여 합의점과 이견을 정리
```bash
# 예시
User: "summon the council - TypeScript vs JavaScript 뭘 써야 할까?"
```
---
### clarify
**모호한 요구사항을 정확하고 실행 가능한 명세로 변환합니다.**
불명확한 지시사항으로 코드를 작성하기 전에, 이 플러그인이 구조화된 인터뷰를 통해 정확히 무엇이 필요한지 파악합니다. 더 이상 추측도, 재작업도 필요 없습니다.
**트리거 문구:**
- "/clarify"
- "요구사항 명확히 해줘"
- "내가 뭘 원하는 건지..."
**프로세스:**
1. **캡처** - 원본 요구사항을 그대로 기록
2. **질문** - 모호한 부분을 해결하기 위한 객관식 질문
3. **비교** - Before/After로 변환 결과 제시
4. **저장** - 선택적으로 명세를 파일로 저장
**변환 예시:**
| Before | After |
|--------|-------|
| "로그인 기능 추가해줘" | 목표: 사용자명/비밀번호 로그인과 자가 가입 추가. 범위: 로그인, 로그아웃, 가입, 비밀번호 재설정. 제약: 24시간 세션, bcrypt, 5회 시도 제한. |
---
### dev
**개발자 워크플로우 도구: 커뮤니티 스캔과 기술 의사결정.**
개발자 리서치와 의사결정을 위한 두 가지 강력한 스킬을 제공합니다.
#### 스킬
**`/dev-scan`** - 개발자 커뮤니티 의견 스캔
- Reddit (Gemini CLI 통해), Hacker News, Dev.to, Lobsters를 병렬 검색
- 공통 의견, 논쟁점, 주목할 시각을 종합
- 도구 도입 전 커뮤니티 분위기 파악에 유용
**`/tech-decision`** - 깊이 있는 기술 의사결정 분석
- 4개의 전문 에이전트가 병렬로 실행되는 다단계 워크플로우
- 코드베이스 분석, 문서 리서치, 커뮤니티 의견, AI 전문가 관점 종합
- 두괄식(결론 먼저) 보고서와 점수화된 비교 제공
**트리거 문구:**
- "개발자 반응...", "개발자들 뭐라고 해?"
- "A vs B", "어떤 라이브러리 써야 해?", "기술 의사결정"
**tech-decision 동작 방식:**
```
Phase 1: 병렬 정보 수집
┌─────────────────┬─────────────────┬─────────────────┬─────────────────┐
│ codebase- │ docs- │ dev-scan │ agent-council │
│ explorer │ researcher │ (커뮤니티) │ (AI 전문가) │
└────────┬────────┴────────┬────────┴────────┬────────┴────────┬────────┘
└─────────────────┴─────────────────┴─────────────────┘
│
Phase 2: 분석 및 종합 ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ tradeoff-analyzer │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ decision-synthesizer │
│ (두괄식: 결론 먼저) │
└─────────────────────────────────────────────────────────────────────────┘
```
```bash
# 예시
User: "React vs Vue 뭐가 나을까?"
User: "상태관리 라이브러리 뭐 쓸지 고민이야"
User: "모놀리스 vs 마이크로서비스 어떻게 해야 할까?"
```
---
### interactive-review
**웹 인터페이스를 통해 Claude의 계획과 문서를 검토합니다.**
터미널에서 긴 마크다운을 읽는 대신, 이 플러그인은 브라우저 기반 UI를 열어 항목별로 체크/언체크하고, 코멘트를 추가하고, 구조화된 피드백을 제출할 수 있게 합니다.
**트리거 문구:**
- "/review"
- "이 계획 검토해줘"
- "확인해볼게"
**플로우:**
1. Claude가 계획이나 문서를 생성
2. 브라우저에 웹 UI가 자동으로 열림
3. 각 항목을 체크박스와 선택적 코멘트로 검토
4. Submit 클릭하여 구조화된 피드백 전송
5. Claude가 승인/거부된 항목에 따라 조정
---
### say-summary
**Claude의 응답을 음성으로 들을 수 있습니다 (macOS 전용).**
이 플러그인은 Stop hook을 사용하여 Claude의 응답을 짧은 헤드라인으로 요약하고 macOS 텍스트-투-스피치로 읽어줍니다. 코딩하면서 음성 피드백을 원할 때 딱입니다.
**기능:**
- Claude Haiku를 사용해 응답을 3-10단어로 요약
- 한국어 vs 영어 자동 감지
- 적절한 음성 사용 (한국어: Yuna, 영어: Samantha)
- 백그라운드 실행, Claude Code 차단 없음
**요구사항:**
- macOS (`say` 명령어 사용)
- Python 3.10+
---
### youtube-digest
**YouTube 영상을 트랜스크립트, 번역, 이해도 퀴즈와 함께 요약합니다.**
YouTube URL을 입력하면 완전한 분석을 받을 수 있습니다: 요약, 핵심 인사이트, 전체 트랜스크립트 한글 번역, 그리고 이해도를 테스트하는 3단계 퀴즈(총 9문제).
**트리거 문구:**
- "이 유튜브 정리해줘"
- "영상 요약해줘"
- YouTube URL
**받을 수 있는 것:**
1. **요약** - 핵심 포인트와 함께 3-5문장 개요
2. **인사이트** - 실행 가능한 테이크어웨이와 아이디어
3. **전체 트랜스크립트** - 한글 번역과 타임스탬프 포함
4. **3단계 퀴즈** - 기본, 중급, 심화 문제
5. **Deep Research** (선택) - 주제를 확장하는 웹 검색
**저장 위치:** `research/readings/youtube/YYYY-MM-DD-title.md`
---
### google-calendar
**Claude Code에서 여러 Google Calendar 계정을 관리합니다.**
여러 Google 계정(회사, 개인 등)의 일정을 조회, 생성, 수정, 삭제할 수 있으며 자동 충돌 감지 기능을 제공합니다.
**트리거 문구:**
- "일정 보여줘"
- "캘린더 확인"
- "미팅 만들어줘"
- "충돌 확인해줘"
**기능:**
- 여러 계정 병렬 조회
- 계정 간 충돌 감지
- 전체 CRUD 작업 (생성, 조회, 수정, 삭제)
- refresh token으로 사전 인증 (반복 로그인 불필요)
**설정 필요:**
1. Calendar API가 활성화된 Google Cloud 프로젝트 생성
2. 각 계정별 설정 스크립트 실행
```bash
# 계정별 최초 1회 설정
uv run python scripts/setup_auth.py --account work
uv run python scripts/setup_auth.py --account personal
```
---
### kakaotalk

**macOS에서 카카오톡 메시지를 발송하고 읽습니다.**
Accessibility API를 사용하여 카카오톡 앱을 제어합니다. 자연어로 메시지를 보내거나 대화 내역을 확인할 수 있습니다.
**트리거 문구:**
- "카톡 보내줘", "카카오톡 메시지"
- "~에게 메시지 보내줘"
- "채팅 읽어줘"
**기능:**
- 자연어 메시지 발송 (발송 전 확인)
- 채팅방 대화 내역 조회
- 채팅방 목록 확인
- "sent with claude code" 서명 자동 추가
**요구사항:**
- macOS 전용
- 카카오톡 앱 실행 중
- Accessibility 권한 필요
```
# 예시 (자연어 트리거)
"구봉한테 밥 먹었어? 보내줘"
"구봉이랑 대화 내역 보여줘"
```
---
### session-wrap
**종합 세션 마무리 및 분석 툴킷.**
코딩 세션을 철저히 분석하고 마무리하며, 세션 히스토리에서 인사이트를 추출합니다.
#### 스킬
**`/wrap`** - 세션 마무리 워크플로우
- 종합 세션 분석을 위한 2단계 멀티 에이전트 파이프라인
- 문서화 필요사항, 자동화 기회, 배운 점, 후속 작업 캡처
- `/wrap [커밋 메시지]`로 빠른 커밋
**`/history-insight`** - 세션 히스토리 분석
- Claude Code 세션 히스토리에서 패턴과 인사이트 분석
- 현재 프로젝트 또는 전체 세션 검색
- 주제, 결정사항, 반복 패턴 추출
**`/session-analyzer`** - 사후 세션 검증
- SKILL.md 명세 대비 세션 행동 검증
- 에이전트, 훅, 도구가 올바르게 실행되었는지 확인
- 상세한 준수 보고서 생성
**/wrap 동작 방식 (2단계 파이프라인):**
```
Phase 1: 분석 (병렬)
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ doc-updater │ automation- │ learning- │ followup- │
│ │ scout │ extractor │ suggester │
└──────┬───────┴──────┬───────┴──────┬───────┴──────┬───────┘
└──────────────┴──────────────┴──────────────┘
│
Phase 2: 검증 ▼
┌─────────────────────────────────────────────────────────────┐
│ duplicate-checker │
└─────────────────────────────────────────────────────────────┘
│
▼
사용자 선택
```
**장점:**
- 중요한 발견을 문서화하는 것을 잊지 않음
- 자동화할 가치가 있는 패턴 식별
- 미래 세션을 위한 명확한 인수인계점 생성
- 과거 세션의 반복 패턴 분석
- 스킬 구현이 명세대로 동작하는지 검증
---
### podcast
**어떤 소스든 한국어 팟캐스트 에피소드로 변환하고 YouTube에 자동 업로드합니다.**
URL, 트윗, 아티클, PDF를 제공하면 병렬 분석 → 융합 스크립트 작성 → OpenAI TTS 음성 생성 → YouTube 업로드까지 한 번에 처리합니다.
**트리거 문구:**
- "팟캐스트 만들어"
- "이 글을 팟캐스트로"
- "에피소드 만들어줘"
- "podcast"
**파이프라인:**
```
소스 → 병렬 분석 → 스크립트 작성 → TTS (OpenAI) → MP4 → YouTube 업로드
```
**결과물:**
1. **스크립트** - 8-12분 한국어 팟캐스트 스크립트 (오프닝, 분석, 융합, 클로징)
2. **오디오** - OpenAI gpt-4o-mini-tts의 자연스러운 한국어 음성 MP3
3. **영상** - 다크 타이틀 카드 MP4 (1920x1080)
4. **YouTube** - unlisted로 자동 업로드 + 메타데이터
**부분 실행 지원:**
- "스크립트만 써줘" → 스크립트만
- "이 스크립트로 TTS 만들어" → 오디오만
- "YouTube에 올려줘" → 기존 MP4 업로드
**요구사항:**
- ffmpeg (오디오 병합 및 MP4 변환)
- OpenAI API 키 (`OPENAI_API_KEY` 환경변수)
- Google OAuth 클라이언트 시크릿 (YouTube 업로드용)
```bash
# 예시
User: "이 두 개의 아티클로 팟캐스트 만들어줘"
# → 두 아티클 병렬 분석
# → 융합 스크립트 작성
# → 음성 생성
# → YouTube 업로드
```
---
### fetch-tweet
**Claude Code에서 어떤 공개 트윗이든 읽을 수 있습니다 — 인증 없이, API 키 없이, JavaScript 없이.**
X/Twitter URL을 던지면 트윗 본문(URL 확장됨), 작성자 정보, 인게이지먼트 수치, 첨부 미디어, 인용 트윗까지 전부 가져옵니다. 오픈소스 [FxEmbed](https://github.com/FxEmbed/FxEmbed) 프로젝트의 백엔드를 활용합니다.
**트리거 문구:**
- "트윗 가져와", "트윗 번역해줘"
- "fetch this tweet", "translate this tweet"
- 모든 X/Twitter URL (`x.com`, `twitter.com`)
**왜 필요한가:**
X가 비인증 트윗 임베드를 막아서, 스크립트로 트윗을 읽으려면 보통 API 키나 브라우저 자동화가 필요합니다. 이 플러그인은 Discord/Telegram의 fxtwitter 링크 프리뷰를 구동하는 동일한 백엔드 `api.fxtwitter.com`을 경유해 그 문제를 우회합니다.
**기능:**
- 의존성 0 (Python 표준 라이브러리만 사용)
- 인용 트윗과 미디어를 포함한 전체 트윗 데이터
- 다른 스킬과 파이프라인 연동을 위한 `--json` 모드
- 스크립트 실행이 어려울 때 WebFetch fallback
```bash
# 직접 스크립트 사용
python scripts/fetch_tweet.py https://x.com/sama/status/...
python scripts/fetch_tweet.py <url> --json | jq '.tweet.text'
```
**제약:** 비공개/삭제된 트윗은 조회 불가.
---
## 기여하기
기여를 환영합니다! 이슈나 PR을 열어주세요.
## 라이선스
MIT
================================================
FILE: README.md
================================================
# Plugins for Claude Natives
A collection of Claude Code plugins for power users who want to extend Claude Code's capabilities beyond the defaults.
## Table of Contents
- [Quick Start](#quick-start)
- [Available Plugins](#available-plugins)
- [Plugin Details](#plugin-details)
- [agent-council](#agent-council) - Get consensus from multiple AI models
- [clarify](#clarify) - Transform vague requirements into specs
- [dev](#dev) - Community scanning + technical decision-making
- [doubt](#doubt) - Force Claude to re-validate responses
- [interactive-review](#interactive-review) - Review plans with a web UI
- [say-summary](#say-summary) - Hear responses via text-to-speech
- [youtube-digest](#youtube-digest) - Summarize and quiz on YouTube videos
- [gmail](#gmail) - Multi-account Gmail integration
- [google-calendar](#google-calendar) - Multi-account calendar integration
- [kakaotalk](#kakaotalk) - Send/read KakaoTalk messages on macOS
- [session-wrap](#session-wrap) - Session wrap-up + history analysis toolkit
- [team-assemble](#team-assemble) - Dynamic agent team orchestration
- [podcast](#podcast) - Source-to-YouTube Korean podcast generator
- [fetch-tweet](#fetch-tweet) - Fetch tweet text and metadata without auth
- [Contributing](#contributing)
- [License](#license)
---
## Quick Start
```bash
# Add this marketplace to Claude Code
/plugin marketplace add team-attention/plugins-for-claude-natives
# Install any plugin
/plugin install <plugin-name>
```
---
## Available Plugins
| Plugin | Description |
|--------|-------------|
| [agent-council](./plugins/agent-council/) | Collect and synthesize opinions from multiple AI agents (Gemini, GPT, Codex) |
| [clarify](./plugins/clarify/) | Transform vague requirements into precise specifications through iterative questioning |
| [dev](./plugins/dev/) | Developer workflow: community opinion scanning and technical decision analysis |
| [doubt](./plugins/doubt/) | Force Claude to re-validate its response when `!rv` is in your prompt |
| [interactive-review](./plugins/interactive-review/) | Interactive markdown review with web UI for visual plan/document approval |
| [say-summary](./plugins/say-summary/) | Speaks a short summary of Claude's response using macOS TTS (Korean/English) |
| [youtube-digest](./plugins/youtube-digest/) | Summarize YouTube videos with transcript, insights, Korean translation, and quizzes |
| [gmail](./plugins/gmail/) | Multi-account Gmail integration with email reading, searching, sending, and management |
| [google-calendar](./plugins/google-calendar/) | Multi-account Google Calendar integration with parallel querying and conflict detection |
| [kakaotalk](./plugins/kakaotalk/) | Send and read KakaoTalk messages on macOS using Accessibility API |
| [session-wrap](./plugins/session-wrap/) | Session wrap-up, history analysis, and session validation toolkit |
| [team-assemble](./plugins/team-assemble/) | Dynamically assemble expert agent teams for complex tasks using Claude Code's agent teams feature |
| [podcast](./plugins/podcast/) | Generate Korean podcast episodes from any source with OpenAI TTS and YouTube auto-upload |
| [fetch-tweet](./plugins/fetch-tweet/) | Fetch full tweet text, author info, and engagement data from X/Twitter URLs without authentication |
## Plugin Details
### agent-council

**Summon multiple AI models to debate your question and reach a consensus.**
When you're facing a tough decision or want diverse perspectives, this plugin queries multiple AI agents (Gemini CLI, GPT, Codex) in parallel and synthesizes their opinions into a single, balanced answer.
**Trigger phrases:**
- "summon the council"
- "ask other AIs"
- "what do other models think?"
**How it works:**
1. Your question is sent to multiple AI agents simultaneously
2. Each agent provides its perspective
3. Claude synthesizes the responses into a consensus view with noted disagreements
```bash
# Example
User: "summon the council - should I use TypeScript or JavaScript for my new project?"
```
---
### clarify

**Turn vague requirements into precise, actionable specifications.**
Before writing code based on ambiguous instructions, this plugin conducts a structured interview to extract exactly what you need. No more assumptions, no more rework.
**Trigger phrases:**
- "/clarify"
- "clarify requirements"
- "what do I mean by..."
**The process:**
1. **Capture** - Record the original requirement verbatim
2. **Question** - Ask targeted multiple-choice questions to resolve ambiguities
3. **Compare** - Present before/after showing the transformation
4. **Save** - Optionally save the clarified spec to a file
**Example transformation:**
| Before | After |
|--------|-------|
| "Add a login feature" | Goal: Add username/password login with self-registration. Scope: Login, logout, registration, password reset. Constraints: 24h session, bcrypt, rate limit 5 attempts. |
---
### dev
**Developer workflow tools: community scanning and technical decision-making.**
This plugin provides two powerful skills for developer research and decision-making.
#### Skills
**`/dev-scan`** - Scan developer communities for real opinions
- Searches Reddit (via Gemini CLI), Hacker News, Dev.to, and Lobsters in parallel
- Synthesizes consensus, controversies, and notable perspectives
- Great for understanding community sentiment before adopting a tool
**`/tech-decision`** - Deep technical decision analysis
- Multi-phase workflow with 4 specialized agents running in parallel
- Combines codebase analysis, docs research, community opinions, and AI perspectives
- Produces executive-summary-first reports with scored comparisons
**Trigger phrases:**
- "developer reactions to...", "what do devs think about..."
- "A vs B", "which library should I use", "기술 의사결정"
**How tech-decision works:**
```
Phase 1: Parallel Information Gathering
┌─────────────────┬─────────────────┬─────────────────┬─────────────────┐
│ codebase- │ docs- │ dev-scan │ agent-council │
│ explorer │ researcher │ (community) │ (AI experts) │
└────────┬────────┴────────┬────────┴────────┬────────┴────────┬────────┘
└─────────────────┴─────────────────┴─────────────────┘
│
Phase 2: Analysis & Synthesis ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ tradeoff-analyzer │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ decision-synthesizer │
│ (Executive Summary First) │
└─────────────────────────────────────────────────────────────────────────┘
```
```bash
# Examples
User: "React vs Vue for my new project?"
User: "Which state management library should I use?"
User: "Monolith vs microservices for our scale?"
```
---
### doubt
**Force Claude to double-check its response before delivering.**
When you include `!rv` anywhere in your prompt, Claude will pause before responding, re-validate its answer against potential errors, and only then deliver the response. Perfect for critical decisions or when you want extra confidence.
**Trigger:**
- Include `!rv` anywhere in your prompt
**How it works:**
1. `UserPromptSubmit` hook detects `!rv` keyword and sets a flag
2. `Stop` hook intercepts Claude's response before delivery
3. Claude re-validates the response for errors, hallucinations, or questionable claims
4. Only after verification does Claude deliver the final answer
**Why `!rv` instead of `!doubt`?**
The word "doubt" affects Claude's behavior - it starts doubting from the beginning. `!rv` (re-validate) is neutral.
```bash
# Example
User: "What's the time complexity of binary search? !rv"
# Claude will verify its answer before responding
```
---
### interactive-review
**Review Claude's plans and documents through a visual web interface.**
Instead of reading long markdown in the terminal, this plugin opens a browser-based UI where you can check/uncheck items, add comments, and submit structured feedback.
**Trigger phrases:**
- "/review"
- "review this plan"
- "let me check this"
**The flow:**
1. Claude generates a plan or document
2. A web UI opens automatically in your browser
3. Review each item with checkboxes and optional comments
4. Click Submit to send structured feedback back to Claude
5. Claude adjusts based on your approved/rejected items
---
### say-summary

**Hear Claude's responses spoken aloud (macOS only).**
This plugin uses a Stop hook to summarize Claude's response to a short headline and speaks it using macOS text-to-speech. Perfect for when you're coding and want audio feedback.
**Features:**
- Summarizes responses to 3-10 words using Claude Haiku
- Auto-detects Korean vs English
- Uses appropriate voice (Yuna for Korean, Samantha for English)
- Runs in background, doesn't block Claude Code
**Requirements:**
- macOS (uses the `say` command)
- Python 3.10+
---
### youtube-digest

**Summarize YouTube videos with transcripts, translations, and comprehension quizzes.**
Drop a YouTube URL and get a complete breakdown: summary, key insights, full Korean translation of the transcript, and a 3-stage quiz (9 questions total) to test your understanding.
**Trigger phrases:**
- "summarize this YouTube"
- "digest this video"
- YouTube URL
**What you get:**
1. **Summary** - 3-5 sentence overview with key points
2. **Insights** - Actionable takeaways and ideas
3. **Full transcript** - With Korean translation and timestamps
4. **3-stage quiz** - Basic, intermediate, and advanced questions
5. **Deep Research** (optional) - Web search to expand on the topic
**Output location:** `research/readings/youtube/YYYY-MM-DD-title.md`
---
### gmail
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/7/7e/Gmail_icon_%282020%29.svg/1280px-Gmail_icon_%282020%29.svg.png" width="120" alt="Gmail">
**Manage multiple Gmail accounts from Claude Code.**
Read, search, send, and manage emails across multiple Google accounts with full Gmail API integration.
**Trigger phrases:**
- "check my email"
- "send an email to..."
- "search for emails from..."
- "reply to this email"
- "mark as read"
**Features:**
- Multi-account support via `accounts.yaml` (work, personal, etc.)
- Gmail search query syntax support
- Email sending with attachments and HTML
- Label and draft management
- **5-step email sending workflow** with context gathering, draft review, and test delivery
- Rate limiting and quota management
- Batch processing and local caching
**5-Step Email Workflow:**
1. **Context gathering** - Parallel Explore agents search recipient info and related projects
2. **Previous conversations** - Search recent emails to determine reply vs new thread
3. **Draft composition** - Create draft with user feedback
4. **Test send** - Send to your own email for verification
5. **Actual send** - Deliver to recipient
**Setup:**
1. Create Google Cloud project with Gmail API enabled
2. Run setup script for each account
```bash
# One-time setup per account
uv run python scripts/setup_auth.py --account work
uv run python scripts/setup_auth.py --account personal
```
Account metadata is stored in `accounts.yaml` for easy management.
---
### google-calendar
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a5/Google_Calendar_icon_%282020%29.svg/960px-Google_Calendar_icon_%282020%29.svg.png" width="120" alt="Google Calendar">
**Manage multiple Google Calendar accounts from Claude Code.**
Query, create, update, and delete events across multiple Google accounts (work, personal, etc.) with automatic conflict detection.
**Trigger phrases:**
- "show my schedule"
- "what's on my calendar"
- "create a meeting"
- "check for conflicts"
**Features:**
- Parallel querying across multiple accounts
- Conflict detection between accounts
- Full CRUD operations (create, read, update, delete)
- Pre-authenticated with refresh tokens (no repeated logins)
**Setup required:**
1. Create Google Cloud project with Calendar API
2. Run setup script for each account
```bash
# One-time setup per account
uv run python scripts/setup_auth.py --account work
uv run python scripts/setup_auth.py --account personal
```
---
### kakaotalk

**Send and read KakaoTalk messages from Claude Code on macOS.**
Uses macOS Accessibility API to control the KakaoTalk app. Send messages or read chat history using natural language.
**Trigger phrases:**
- "카톡 보내줘", "카카오톡 메시지"
- "~에게 메시지 보내줘"
- "채팅 읽어줘"
- "KakaoTalk message"
**Features:**
- Natural language message sending (with confirmation before send)
- Chat history retrieval
- Chat room listing
- Auto-signature "sent with claude code"
**Requirements:**
- macOS only
- KakaoTalk app must be running
- Accessibility permission required
```
# Examples (natural language)
"구봉한테 밥 먹었어? 보내줘"
"구봉이랑 대화 내역 보여줘"
```
---
### session-wrap
**Comprehensive session wrap-up and analysis toolkit.**
End your coding sessions with thorough analysis, and dive deep into session history for insights.
#### Skills
**`/wrap`** - Session wrap-up workflow
- 2-phase multi-agent pipeline for comprehensive session analysis
- Captures documentation needs, automation opportunities, learnings, and follow-ups
- `/wrap [commit message]` for quick commits
**`/history-insight`** - Session history analysis
- Analyze Claude Code session history for patterns and insights
- Search current project or all sessions
- Extract themes, decisions, and recurring topics
**`/session-analyzer`** - Post-hoc session validation
- Validate session behavior against SKILL.md specifications
- Check if agents, hooks, and tools executed correctly
- Generate detailed compliance reports
**How /wrap works (2-Phase Pipeline):**
```
Phase 1: Analysis (Parallel)
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ doc-updater │ automation- │ learning- │ followup- │
│ │ scout │ extractor │ suggester │
└──────┬───────┴──────┬───────┴──────┬───────┴──────┬───────┘
└──────────────┴──────────────┴──────────────┘
│
Phase 2: Validation ▼
┌─────────────────────────────────────────────────────────────┐
│ duplicate-checker │
└─────────────────────────────────────────────────────────────┘
│
▼
User Selection
```
**Benefits:**
- Never forget to document important discoveries
- Identify patterns worth automating
- Create clear handoff points for future sessions
- Analyze past sessions for recurring patterns
- Validate skill implementations against specifications
---
### team-assemble
**Dynamically assemble expert agent teams for complex tasks.**
Instead of manually designing agents, this plugin analyzes your task, scouts the codebase, and assembles an optimal team with the right roles, dependencies, and validation criteria — all using Claude Code's agent teams feature.
> **Prerequisite:** Agent teams must be enabled. See [setup guide](./plugins/team-assemble/skills/team-assemble/references/enable-agent-teams.md).
**Trigger phrases:**
- "assemble a team to..."
- "team assemble"
- "use a team for..."
**6-Phase Workflow:**
```
Phase 1 → Phase 2 → Phase 3 → Phase 4 → Phase 5 → Phase 6
Task Codebase Integrate Execute Validate Complete
Analysis Scouts & Confirm & Cleanup
```
**Key features:**
- **Dynamic agent design** — scouts your codebase and proposes agents tailored to the task (no fixed catalog)
- **Model 3-tier** — opus for strategy, sonnet for execution, haiku for research
- **Parallel execution** — independent agents run simultaneously
- **Acceptance criteria** — every team has measurable validation criteria
- **Verify/fix loop** — QA validates, support fixes (max 3 rounds)
- **Two approval gates** — confirm scope (Phase 1) and team composition (Phase 3)
```bash
# Examples
User: "Assemble a team to refactor authentication from session-based to JWT"
User: "Use a team to evaluate Redis vs Memcached vs in-memory caching"
User: "Team assemble — extract shared utils from three microservices into a common lib"
```
---
### podcast
**Turn any source into a Korean podcast episode, automatically uploaded to YouTube.**
Drop URLs, tweets, articles, or PDFs and this plugin will analyze them, write a conversational Korean script, generate audio using OpenAI's `gpt-4o-mini-tts`, and upload the result to YouTube — all in one go.
**Trigger phrases:**
- "make a podcast from this"
- "팟캐스트 만들어"
- "turn this into an episode"
- "이 글을 팟캐스트로"
**The pipeline:**
```
Sources → Parallel Analysis → Script Writing → TTS (OpenAI) → MP4 → YouTube Upload
```
**What you get:**
1. **Script** - 8-12 min Korean podcast script with opening, analysis, fusion, and closing
2. **Audio** - MP3 generated via OpenAI gpt-4o-mini-tts with natural Korean voice
3. **Video** - MP4 with dark title card (1920x1080)
4. **YouTube** - Auto-uploaded as unlisted with metadata
**Partial execution supported:**
- "Just write the script" → Script only
- "Generate TTS from this script" → Audio only
- "Upload to YouTube" → Upload existing MP4
**Requirements:**
- ffmpeg (for audio merging and MP4 conversion)
- OpenAI API key (`OPENAI_API_KEY` env var)
- Google OAuth client secret (for YouTube upload)
```bash
# Example
User: "이 두 개의 아티클로 팟캐스트 만들어줘"
# → Analyzes both articles in parallel
# → Writes fusion script
# → Generates audio
# → Uploads to YouTube
```
---
### fetch-tweet
**Read any public tweet from Claude Code — no auth, no API key, no JavaScript.**
Drop an X/Twitter URL and get the full tweet text (URLs expanded), author info, engagement metrics, attached media, and quoted tweets. Powered by the open-source [FxEmbed](https://github.com/FxEmbed/FxEmbed) project.
**Trigger phrases:**
- "트윗 가져와", "트윗 번역해줘"
- "fetch this tweet", "translate this tweet"
- Any X/Twitter URL (`x.com`, `twitter.com`)
**Why this exists:**
X removed unauthenticated tweet embeds, so reading a tweet from a script normally requires an API key or browser automation. This plugin sidesteps that by routing through `api.fxtwitter.com` — the same backend that powers fxtwitter link previews on Discord/Telegram.
**Features:**
- Zero dependencies (Python stdlib only)
- Full tweet data including quote tweets and media
- `--json` mode for pipeline use with other skills
- WebFetch fallback when script execution isn't available
```bash
# Direct script usage
python scripts/fetch_tweet.py https://x.com/sama/status/...
python scripts/fetch_tweet.py <url> --json | jq '.tweet.text'
```
**Limitations:** Private/deleted tweets cannot be fetched.
---
## Contributing
Contributions welcome! Please open an issue or PR.
## License
MIT
================================================
FILE: plugins/agent-council/.claude-plugin/plugin.json
================================================
{
"name": "agent-council",
"version": "1.0.0",
"description": "Collect and synthesize opinions from multiple AI Agents for Claude Code",
"author": {
"name": "Team Attention",
"url": "https://github.com/team-attention"
},
"repository": "https://github.com/team-attention/plugins-for-claude-natives",
"license": "MIT",
"keywords": ["claude-code", "plugin", "llm", "multi-agent", "ai-council"],
"skills": "./skills/agent-council"
}
================================================
FILE: plugins/agent-council/AGENTS.md
================================================
# Project Instructions (Codex)
## Plan / To-do UI (IMPORTANT)
- When you decide to use any Skill, **always** call `update_plan` immediately (before any potentially long-running shell/tool calls) so Codex’s Plan/To-do panel appears. Include **exactly one** `in_progress` item in that first plan.
- Keep the plan updated during execution (at most one `in_progress` step at a time).
- For `agent-council`: after the first `council.sh wait` (the non-blocking one), feed `.ui.codex.update_plan.plan` into `update_plan` right away, then repeat `wait → update_plan` until done.
================================================
FILE: plugins/agent-council/CLAUDE.md
================================================
# Project Instructions (Claude Code)
## Todo UI (IMPORTANT)
- When you decide to use any Skill, **always** call `TodoWrite` immediately (before any potentially long-running shell/tool calls) so Claude’s Todo UI appears. Include **exactly one** `in_progress` item in that first todo list.
- Keep the todo list updated during execution (at most one `in_progress` item at a time).
- For `agent-council`: after the first `council.sh wait` (the non-blocking one), feed `.ui.claude.todo_write.todos` into `TodoWrite` right away, then repeat `wait → TodoWrite` until done.
================================================
FILE: plugins/agent-council/README.ko.md
================================================
# Agent Council
**[English Version](./README.md)**
> 여러 AI CLI(Codex, Gemini, ...)의 의견을 모으고, 설정 가능한 의장(Chairman)이 종합해 결론을 내리게 하는 스킬
> [Karpathy의 LLM Council](https://github.com/karpathy/llm-council)에서 영감을 받음
## LLM Council과의 차이점
**추가 API 비용이 들지 않습니다!**
Karpathy의 LLM Council은 각 LLM의 API를 직접 호출하여 비용이 발생하지만, Agent Council은 설치된 AI CLI(Claude Code, Codex CLI, Gemini CLI 등)를 활용합니다. 주로 하나의 호스트 CLI를 메인으로 쓰면서 다른 CLI들은 구독 플랜으로 필요할 때만 쓰는 분들에게 특히 유용합니다.
MCP보다 Skill이 훨씬 간단하고 재현 가능해서 npx로 설치 후 직접 커스터마이징하여 사용하시는 것을 추천합니다.
## 데모
https://github.com/user-attachments/assets/c550c473-00d2-4def-b7ba-654cc7643e9b
## 작동 방식
Agent Council은 AI 합의를 수집하기 위한 3단계 프로세스를 구현합니다:
**Stage 1: Initial Opinions (초기 의견 수집)**
설정된 모든 AI 에이전트가 동시에 질문을 받고 독립적으로 응답합니다.
**Stage 2: Response Collection (응답 수집)**
각 에이전트의 응답을 수집하여 포맷된 형태로 표시합니다.
**Stage 3: Chairman Synthesis (의장 종합)**
기본값(`role: auto`)에서는 “현재 사용 중인 호스트 에이전트(Claude Code / Codex CLI 등)”가 의장 역할을 하며, 모든 의견을 종합해 최종 추천을 제시합니다. 원하면 `chairman.command`를 설정해 `council.sh` 안에서 Stage 3 종합을 CLI로 직접 실행할 수도 있습니다.
## 설치
### 방법 A: npx로 설치 (권장)
```bash
npx github:team-attention/agent-council
```
현재 프로젝트 디렉토리에 스킬 파일들이 복사됩니다.
Agent Council을 업그레이드한 뒤 `Missing runtime dependency: yaml` 같은 런타임 에러가 나면, 위 설치 커맨드를 한 번 더 실행해서 설치된 스킬 파일을 갱신하세요.
기본값으로 설치 스크립트가 자동으로 Claude Code(`.claude/`) / Codex CLI(`.codex/`) 설치 여부를 감지해서 가능한 타깃에 설치합니다.
설치 위치:
- `.claude/skills/agent-council/` (Claude Code)
- `.codex/skills/agent-council/` (Codex CLI)
선택사항 (Codex용 레포 스킬로 설치):
```bash
npx github:team-attention/agent-council --target codex
```
다른 타깃:
```bash
npx github:team-attention/agent-council --target claude
npx github:team-attention/agent-council --target both
```
생성되는 `council.config.yaml`은 감지된 멤버 CLI(claude/codex/gemini 등)만 포함하며, 설치 타깃(호스트)은 members에 포함되지 않도록 처리합니다. 이 필터링은 **초기 생성 시점에만** 적용되며, 이후 편집 내용은 자동으로 정리되지 않습니다.
### 방법 B: Claude Code 플러그인으로 설치 (Claude Code 전용)
```bash
# 마켓플레이스 추가
/plugin marketplace add team-attention/plugins-for-claude-natives
# 플러그인 설치
/plugin install agent-council@plugins-for-claude-natives
```
참고(플러그인 설치): **Agent Council은 Node.js가 필요**하며, Claude Code 플러그인은 Node를 번들/자동 설치할 수 없습니다. Node를 별도로 설치하세요(예: macOS `brew install node`).
### 2. Agent CLI 설치
`council.config.yaml`의 `council.members`에 적힌 CLI를 설치하세요(템플릿 기본 포함: `claude`, `codex`, `gemini`):
```bash
# Anthropic Claude Code
# https://claude.ai/code
# OpenAI Codex CLI
# https://github.com/openai/codex
# Google Gemini CLI
# https://github.com/google-gemini/gemini-cli
```
설치 확인(멤버별):
```bash
command -v claude
command -v codex
command -v gemini
```
### 3. Council 멤버 설정 (선택사항)
설치된 스킬 폴더의 설정 파일을 편집해서 council을 커스터마이즈:
- `.claude/skills/agent-council/council.config.yaml`
- `.codex/skills/agent-council/council.config.yaml`
```yaml
council:
chairman:
role: "auto" # auto|claude|codex|gemini|...
# command: "codex exec" # 선택: council.sh에서 Stage 3 종합까지 실행
members:
- name: codex
command: "codex exec"
emoji: "🤖"
color: "BLUE"
- name: gemini
command: "gemini"
emoji: "💎"
color: "GREEN"
# 필요에 따라 에이전트 추가
# - name: grok
# command: "grok"
# emoji: "🚀"
# color: "MAGENTA"
```
## 사용법
### 호스트 에이전트를 통한 사용 (Claude Code / Codex CLI)
호스트 에이전트에게 council 소집을 요청하면 됩니다:
```
"다른 AI들 의견도 들어보자"
"council 소집해줘"
"여러 관점에서 검토해줘"
"codex랑 gemini 의견 물어봐"
```
### 스크립트 직접 실행
```bash
JOB_DIR=$(.codex/skills/agent-council/scripts/council.sh start "질문 내용")
.codex/skills/agent-council/scripts/council.sh status --text "$JOB_DIR"
.codex/skills/agent-council/scripts/council.sh results "$JOB_DIR"
.codex/skills/agent-council/scripts/council.sh clean "$JOB_DIR"
```
팁: `status --text`에 `--verbose`를 추가하면 멤버별 상태 라인이 함께 출력됩니다.
팁: `status --checklist`는 체크리스트 형태로 간단히 보여줍니다(Codex/Claude tool cell에 유용).
팁: `wait`를 쓰면 “의미 있는 진행”이 있을 때만 반환해서 tool cell 스팸을 줄일 수 있습니다(JSON 출력, 커서는 자동으로 저장/갱신; 기본값은 멤버 수에 따라 대략 ~5~10번 수준으로 자동 배치, `--bucket 1`이면 매 완료마다 반환).
원샷 실행(잡 시작 → 대기 → 결과 출력 → 정리):
```bash
.codex/skills/agent-council/scripts/council.sh "질문 내용"
```
참고: 호스트 에이전트 도구 UI(Codex CLI / Claude Code)에서는 원샷이 **블로킹하지 않습니다**. 네이티브 plan/todo UI를 갱신할 수 있도록 `wait` JSON을 한 번 반환하고 종료하며, 이후 `wait` → 네이티브 UI 갱신 → `results` → `clean` 순서로 진행하세요.
#### 진행상황
- 실제 터미널에서는 원샷이 멤버 완료에 맞춰 진행상황 라인을 주기적으로 출력합니다.
- 호스트 에이전트 도구 UI에서는 원샷이 `wait` JSON을 반환합니다(네이티브 plan/todo UI 갱신 목적).
- 스크립팅이 필요하면 job mode(`start` → `status` → `results` → `clean`)도 사용할 수 있습니다.
## 예시
```
User: "새 대시보드 프로젝트에 React vs Vue 어떨까? council 소집해줘"
호스트 에이전트(Claude Code / Codex CLI):
1. council.sh 실행하여 설정된 멤버(예: Codex, Gemini) 의견 수집
2. 각 에이전트의 관점 표시
3. 의장으로서 종합:
"Council의 의견을 바탕으로, 대시보드의 데이터 시각화 요구사항과
팀의 숙련도를 고려할 때..."
```
## 프로젝트 구조
```
agent-council/
├── .claude-plugin/
│ └── marketplace.json # 마켓플레이스 설정 (Claude Code 전용)
├── bin/
│ └── install.js # npx 설치 스크립트
├── skills/
│ └── agent-council/
│ ├── SKILL.md # 스킬 문서
│ └── scripts/
│ ├── council.sh # 실행 스크립트
│ ├── council-job.sh # 백그라운드 Job runner (폴링 가능)
│ ├── council-job.js # Job runner 구현
│ └── council-job-worker.js # 멤버별 워커
├── council.config.yaml # Council 멤버 설정
├── README.md # 영어 문서
├── README.ko.md # 이 문서
└── LICENSE
```
## 주의사항
- 응답 시간은 가장 느린 에이전트에 의존 (병렬 실행)
- 민감한 정보는 council에 공유하지 않기
- 에이전트는 기본적으로 병렬로 실행되어 빠른 응답 제공
- 각 CLI 도구의 구독 플랜이 필요합니다 (API 비용 별도 발생 없음)
## 기여하기
기여를 환영합니다! 다음과 같은 기여가 가능합니다:
- 새로운 AI 에이전트 지원 추가
- 종합 프로세스 개선
- 설정 옵션 확장
## 라이선스
MIT 라이선스 - 자세한 내용은 [LICENSE](./LICENSE) 참조
## 크레딧
- [Karpathy의 LLM Council](https://github.com/karpathy/llm-council)에서 영감
- [Claude Code](https://claude.ai/code) / [Codex CLI](https://github.com/openai/codex) 용으로 제작
================================================
FILE: plugins/agent-council/README.md
================================================
# Agent Council
**[한국어 버전 (Korean)](./README.ko.md)**
> A skill that gathers opinions from multiple AI CLIs (Codex, Gemini, ...) and lets a configurable Chairman synthesize a conclusion.
> Inspired by [Karpathy's LLM Council](https://github.com/karpathy/llm-council)
## Key Difference from LLM Council
**No additional API costs!**
Unlike Karpathy's LLM Council which directly calls each LLM's API (incurring costs), Agent Council uses your installed AI CLIs (Claude Code, Codex CLI, Gemini CLI, ...). This is especially useful if you mainly use one host CLI and occasionally consult others via subscriptions.
Skills are much simpler and more reproducible than MCP. We recommend installing via npx and customizing it yourself!
## Demo
https://github.com/user-attachments/assets/c550c473-00d2-4def-b7ba-654cc7643e9b
## How it Works
Agent Council implements a 3-stage process for gathering AI consensus:
**Stage 1: Initial Opinions**
All configured AI agents receive your question simultaneously and respond independently.
**Stage 2: Response Collection**
Responses from each agent are collected and displayed to you in a formatted view.
**Stage 3: Chairman Synthesis**
Your host agent (Claude Code / Codex CLI / etc.) acts as the Chairman by default (`role: auto`), synthesizing all opinions into a final recommendation. Optionally, you can configure a Chairman CLI command to run synthesis inside `council.sh`.
## Setup
### Option A: Install via npx (Recommended)
```bash
npx github:team-attention/agent-council
```
This copies the skill files to your current project directory.
If you upgrade Agent Council and hit a runtime error like `Missing runtime dependency: yaml`, re-run the installer command above to refresh your installed skill files.
By default, the installer auto-detects whether to install for Claude Code (`.claude/`) and/or Codex CLI (`.codex/`) based on what’s available on your machine and in the repo.
Installed paths:
- `.claude/skills/agent-council/` (Claude Code)
- `.codex/skills/agent-council/` (Codex CLI)
Optional (Codex repo skill):
```bash
npx github:team-attention/agent-council --target codex
```
Other targets:
```bash
npx github:team-attention/agent-council --target claude
npx github:team-attention/agent-council --target both
```
The generated `council.config.yaml` includes only detected member CLIs (e.g. `claude`, `codex`, `gemini`) and avoids adding the host target as a member. This filtering happens only at initial generation; later edits will not auto-remove missing CLIs.
### Option B: Install via Claude Code Plugin (Claude Code only)
```bash
# Add the marketplace
/plugin marketplace add team-attention/plugins-for-claude-natives
# Install the plugin
/plugin install agent-council@plugins-for-claude-natives
```
Note (Plugin installs): **Agent Council requires Node.js**, and Claude Code plugins can’t bundle or auto-install Node for you. Install Node separately (e.g. `brew install node` on macOS).
### 2. Install Agent CLIs
Install the CLIs listed under `council.members` in your `council.config.yaml` (template includes `claude`, `codex`, `gemini`):
```bash
# Anthropic Claude Code
# https://claude.ai/code
# OpenAI Codex CLI
# https://github.com/openai/codex
# Google Gemini CLI
# https://github.com/google-gemini/gemini-cli
```
Verify each member CLI:
```bash
command -v claude
command -v codex
command -v gemini
```
### 3. Configure Council Members (Optional)
Edit the generated config in your installed skill directory:
- `.claude/skills/agent-council/council.config.yaml`
- `.codex/skills/agent-council/council.config.yaml`
```yaml
council:
chairman:
role: "auto" # auto|claude|codex|gemini|...
# command: "codex exec" # optional: run Stage 3 inside council.sh
members:
- name: codex
command: "codex exec"
emoji: "🤖"
color: "BLUE"
- name: gemini
command: "gemini"
emoji: "💎"
color: "GREEN"
# Add more agents as needed
# - name: grok
# command: "grok"
# emoji: "🚀"
# color: "MAGENTA"
```
## Usage
### Via your host agent (Claude Code / Codex CLI)
Ask your host agent to summon the council:
```
"Let's hear opinions from other AIs"
"Summon the council"
"Review this from multiple perspectives"
"Ask codex and gemini for their opinions"
```
### Direct Script Execution
```bash
JOB_DIR=$(.codex/skills/agent-council/scripts/council.sh start "Your question here")
.codex/skills/agent-council/scripts/council.sh status --text "$JOB_DIR"
.codex/skills/agent-council/scripts/council.sh results "$JOB_DIR"
.codex/skills/agent-council/scripts/council.sh clean "$JOB_DIR"
```
Tip: add `--verbose` to `status --text` to include per-member lines.
Tip: use `status --checklist` for a compact checkbox view (handy in Codex/Claude tool cells).
Tip: use `wait` to block until meaningful progress without spamming tool cells (prints JSON, persists a cursor automatically; auto-batches to a small number of updates (typically ~5–10); `--bucket 1` for every completion).
One-shot (runs job → waits → prints results → cleans):
```bash
.codex/skills/agent-council/scripts/council.sh "Your question here"
```
Note: In host-agent tool UIs (Codex CLI / Claude Code), one-shot does **not** block. It returns a single `wait` JSON payload so the host agent can update native plan/todo UIs. Continue with `wait` → native UI update → `results` → `clean`.
#### Progress
- In a real terminal, one-shot prints periodic progress lines as members complete.
- In host-agent tool UIs, one-shot returns `wait` JSON (so the host can update native plan/todo UIs).
- Job mode is still available for scripting (`start` → `status` → `results` → `clean`).
## Example
```
User: "React vs Vue for a new dashboard project - summon the council"
Host agent (Claude Code / Codex CLI):
1. Executes council.sh to collect opinions from configured members (e.g., Codex, Gemini)
2. Displays each agent's perspective
3. Synthesizes as Chairman:
"Based on the council's input, considering your dashboard's
data visualization needs and team's familiarity, I recommend..."
```
## Project Structure
```
agent-council/
├── .claude-plugin/
│ └── marketplace.json # Marketplace config (Claude Code only)
├── bin/
│ └── install.js # npx installer
├── skills/
│ └── agent-council/
│ ├── SKILL.md # Skill documentation
│ └── scripts/
│ ├── council.sh # Execution script
│ ├── council-job.sh # Background job runner (pollable)
│ ├── council-job.js # Job runner implementation
│ └── council-job-worker.js # Per-member worker
├── council.config.yaml # Council member configuration
├── README.md # This file
├── README.ko.md # Korean documentation
└── LICENSE
```
## Notes
- Response time depends on the slowest agent (parallel execution)
- Do not share sensitive information with the council
- Agents run in parallel by default for faster responses
- Subscription plans for each CLI tool are required (no additional API costs)
## Contributing
Contributions are welcome! Feel free to:
- Add support for new AI agents
- Improve the synthesis process
- Enhance the configuration options
## License
MIT License - see [LICENSE](./LICENSE) for details.
## Credits
- Inspired by [Karpathy's LLM Council](https://github.com/karpathy/llm-council)
- Built for [Claude Code](https://claude.ai/code) and [Codex CLI](https://github.com/openai/codex)
================================================
FILE: plugins/agent-council/SKILL.md
================================================
---
name: agent-council
description: Collect and synthesize opinions from multiple AI agents. Use when users say "summon the council", "ask other AIs", or want multiple AI perspectives on a question.
---
# Agent Council
Collect multiple AI opinions and synthesize one answer.
## Usage
Run a job and collect results:
```bash
JOB_DIR=$(./skills/agent-council/scripts/council.sh start "your question here")
./skills/agent-council/scripts/council.sh wait "$JOB_DIR"
./skills/agent-council/scripts/council.sh results "$JOB_DIR"
./skills/agent-council/scripts/council.sh clean "$JOB_DIR"
```
One-shot:
```bash
./skills/agent-council/scripts/council.sh "your question here"
```
## References
- `references/overview.md` — workflow and background.
- `references/examples.md` — usage examples.
- `references/config.md` — member configuration.
- `references/requirements.md` — dependencies and CLI checks.
- `references/host-ui.md` — host UI checklist guidance.
- `references/safety.md` — safety notes.
================================================
FILE: plugins/agent-council/bin/install.js
================================================
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const YAML = require('yaml');
const GREEN = '\x1b[32m';
const YELLOW = '\x1b[33m';
const CYAN = '\x1b[36m';
const RED = '\x1b[31m';
const NC = '\x1b[0m';
const packageRoot = path.resolve(__dirname, '..');
const targetDir = process.cwd();
const claudeDir = path.join(targetDir, '.claude');
const codexDir = path.join(targetDir, '.codex');
const yamlModuleDir = path.dirname(require.resolve('yaml/package.json'));
function parseArgs(argv) {
const args = argv.slice(2);
const flags = new Set(args);
const targetIndex = args.indexOf('--target');
let target = 'auto';
if (targetIndex !== -1 && args[targetIndex + 1]) {
target = args[targetIndex + 1];
} else if (flags.has('--both')) {
target = 'both';
} else if (flags.has('--codex')) {
target = 'codex';
} else if (flags.has('--claude')) {
target = 'claude';
}
if (!['auto', 'claude', 'codex', 'both'].includes(target)) {
throw new Error(`Invalid --target "${target}". Use auto|claude|codex|both.`);
}
return { target };
}
console.log(`${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}`);
console.log(`${CYAN} Agent Council - Installation${NC}`);
console.log(`${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}`);
console.log();
function copyRecursive(src, dest) {
const stat = fs.statSync(src);
if (stat.isDirectory()) {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true });
}
const files = fs.readdirSync(src);
for (const file of files) {
copyRecursive(path.join(src, file), path.join(dest, file));
}
} else {
fs.copyFileSync(src, dest);
// Preserve executable permission for .sh files
if (src.endsWith('.sh')) {
fs.chmodSync(dest, 0o755);
}
}
}
function commandExists(command) {
try {
const checkCmd = process.platform === 'win32' ? `where ${command}` : `command -v ${command}`;
execSync(checkCmd, { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
try {
const { target: requestedTarget } = parseArgs(process.argv);
const detected = {
claude: commandExists('claude'),
codex: commandExists('codex'),
gemini: commandExists('gemini'),
};
const hasClaudeDir = fs.existsSync(claudeDir);
const hasCodexDir = fs.existsSync(codexDir);
let target = requestedTarget;
if (requestedTarget === 'auto') {
const wantClaude = hasClaudeDir || detected.claude;
const wantCodex = hasCodexDir || detected.codex;
if (wantClaude && wantCodex) target = 'both';
else if (wantCodex) target = 'codex';
else if (wantClaude) target = 'claude';
else target = 'claude';
console.log(`${CYAN}Auto-detected target:${NC} ${target}`);
if (!wantClaude && !wantCodex) {
console.log(
`${YELLOW} ⓘ Could not detect Claude Code or Codex CLI; defaulting to "claude". Use --target codex if needed.${NC}`
);
}
console.log();
}
const installs = [];
if (target === 'claude' || target === 'both') {
installs.push({
label: 'Claude Code',
rootDir: claudeDir,
skillsDest: path.join(claudeDir, 'skills', 'agent-council'),
displayPath: '.claude/skills/agent-council',
hostRole: 'claude',
});
}
if (target === 'codex' || target === 'both') {
installs.push({
label: 'Codex CLI',
rootDir: codexDir,
skillsDest: path.join(codexDir, 'skills', 'agent-council'),
displayPath: '.codex/skills/agent-council',
hostRole: 'codex',
});
}
// Copy skills folder to target(s)
const skillsSrc = path.join(packageRoot, 'skills', 'agent-council');
const templateConfigPath = path.join(packageRoot, 'council.config.yaml');
const templateConfigText = fs.existsSync(templateConfigPath) ? fs.readFileSync(templateConfigPath, 'utf8') : null;
for (const install of installs) {
if (!fs.existsSync(install.rootDir)) {
fs.mkdirSync(install.rootDir, { recursive: true });
}
if (fs.existsSync(skillsSrc)) {
console.log(`${YELLOW}Installing skills (${install.label})...${NC}`);
copyRecursive(skillsSrc, install.skillsDest);
console.log(`${GREEN} ✓ ${install.displayPath}${NC}`);
}
// Ship runtime dependencies needed by the skill at execution time.
const runtimeModulesDir = path.join(install.skillsDest, 'node_modules');
if (!fs.existsSync(runtimeModulesDir)) fs.mkdirSync(runtimeModulesDir, { recursive: true });
console.log(`${YELLOW}Installing runtime deps (${install.label})...${NC}`);
copyRecursive(yamlModuleDir, path.join(runtimeModulesDir, 'yaml'));
console.log(`${GREEN} ✓ ${install.displayPath}/node_modules/yaml${NC}`);
// Copy config file to skill folder if not exists
const configDest = path.join(install.skillsDest, 'council.config.yaml');
if (!fs.existsSync(configDest)) {
console.log(`${YELLOW}Installing config (${install.label})...${NC}`);
if (!templateConfigText) {
console.log(`${YELLOW} ⓘ Template council.config.yaml not found; writing an empty config.${NC}`);
fs.writeFileSync(
configDest,
['council:', ' members: []', ' chairman:', ' role: "auto"', ' settings:', ' parallel: true', ''].join(
'\n'
),
'utf8'
);
console.log(`${GREEN} ✓ ${install.displayPath}/council.config.yaml${NC}`);
continue;
}
const doc = YAML.parseDocument(templateConfigText);
const membersNode = doc.getIn(['council', 'members']);
if (membersNode && YAML.isCollection(membersNode)) {
const enabledMembers = membersNode.items.filter((item) => {
const member = item.toJSON();
const nameLc = String(member.name || '').toLowerCase();
if (nameLc === install.hostRole) return false;
const baseCommand = String(member.command || '')
.trim()
.split(/\s+/)[0];
if (!baseCommand) return false;
return commandExists(baseCommand);
});
membersNode.items = enabledMembers;
if (enabledMembers.length === 0) {
console.log(
`${YELLOW} ⓘ No member CLIs detected from template. Writing members: []; edit council.config.yaml to add members.${NC}`
);
}
} else {
console.log(`${YELLOW} ⓘ Template is missing council.members; writing template as-is.${NC}`);
}
fs.writeFileSync(configDest, String(doc), 'utf8');
console.log(`${GREEN} ✓ ${install.displayPath}/council.config.yaml${NC}`);
} else if (fs.existsSync(configDest)) {
console.log(`${YELLOW} ⓘ council.config.yaml already exists (${install.label}), skipping${NC}`);
}
}
console.log();
console.log(`${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}`);
console.log(`${GREEN} Installation complete!${NC}`);
console.log(`${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}`);
console.log();
if (installs.some((i) => i.hostRole === 'claude')) {
console.log(`${CYAN}Usage in Claude:${NC}`);
console.log(` "Summon the council"`);
console.log(` "Let's hear opinions from other AIs"`);
console.log();
}
if (installs.some((i) => i.hostRole === 'codex')) {
console.log(`${CYAN}Usage in Codex:${NC}`);
console.log(` "Summon the council"`);
console.log(` "Let's hear opinions from other AIs"`);
console.log();
}
console.log();
console.log(`${CYAN}Direct execution:${NC}`);
if (installs.some((i) => i.hostRole === 'claude')) {
console.log(` .claude/skills/agent-council/scripts/council.sh "your question"`);
}
if (installs.some((i) => i.hostRole === 'codex')) {
console.log(` .codex/skills/agent-council/scripts/council.sh "your question"`);
}
console.log();
console.log(`${YELLOW}Note: Only detected CLIs are enabled as members in the generated config.${NC}`);
console.log(`${YELLOW} Detected: claude=${detected.claude} codex=${detected.codex} gemini=${detected.gemini}${NC}`);
} catch (error) {
console.error(`${RED}Error during installation: ${error.message}${NC}`);
process.exit(1);
}
================================================
FILE: plugins/agent-council/council.config.yaml
================================================
# Agent Council Configuration
# Add or remove council members as needed.
# Note: the installer filters members to detected CLIs only on initial generation.
# After that, missing CLIs are not auto-removed and will report `missing_cli` at runtime.
council:
# Council members configuration
# Each member needs:
# - name: identifier for the agent
# - command: CLI command to execute (prompt will be appended)
# - emoji: display emoji (optional)
# - color: ANSI color (RED, GREEN, BLUE, YELLOW, CYAN)
members:
- name: codex
command: "codex exec"
emoji: "🤖"
color: "BLUE"
- name: gemini
command: "gemini"
emoji: "💎"
color: "GREEN"
# Chairman configuration (Claude will act as chairman)
chairman:
# role: auto|claude|codex|gemini|...
# - auto: infer from host tool (Claude Code => claude, Codex CLI => codex)
role: "auto"
description: "Synthesizes all opinions and provides final recommendation"
# Optional: run synthesis inside council.sh via CLI (otherwise the host agent synthesizes)
# command: "codex exec"
# Execution settings
settings:
timeout: 120 # Timeout seconds per agent (0 to disable)
exclude_chairman_from_members: true # Avoid calling the chairman as a member by default
# synthesize: true # Force Stage 3 synthesis inside council.sh (requires chairman.command or --chairman codex/gemini)
================================================
FILE: plugins/agent-council/package.json
================================================
{
"name": "@team-attention/agent-council",
"version": "1.0.0",
"description": "Collect and synthesize opinions from multiple AI Agents for Claude Code",
"bin": {
"agent-council": "./bin/install.js"
},
"files": [
"bin/",
"skills/",
"council.config.yaml"
],
"dependencies": {
"yaml": "^2.8.2"
},
"keywords": [
"claude-code",
"plugin",
"llm",
"multi-agent",
"ai-council"
],
"author": "Team Attention",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/team-attention/plugins-for-claude-natives.git"
},
"homepage": "https://github.com/team-attention/plugins-for-claude-natives"
}
================================================
FILE: plugins/agent-council/scripts/council-job-worker.js
================================================
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { spawn } = require('child_process');
function exitWithError(message) {
process.stderr.write(`${message}\n`);
process.exit(1);
}
function parseArgs(argv) {
const args = argv.slice(2);
const out = { _: [] };
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (!a.startsWith('--')) {
out._.push(a);
continue;
}
const [key, rawValue] = a.split('=', 2);
if (rawValue != null) {
out[key.slice(2)] = rawValue;
continue;
}
const next = args[i + 1];
if (next == null || next.startsWith('--')) {
out[key.slice(2)] = true;
continue;
}
out[key.slice(2)] = next;
i++;
}
return out;
}
function splitCommand(command) {
const tokens = [];
let current = '';
let inSingle = false;
let inDouble = false;
let escapeNext = false;
for (const ch of String(command || '')) {
if (escapeNext) {
current += ch;
escapeNext = false;
continue;
}
if (!inSingle && ch === '\\') {
escapeNext = true;
continue;
}
if (!inDouble && ch === "'") {
inSingle = !inSingle;
continue;
}
if (!inSingle && ch === '"') {
inDouble = !inDouble;
continue;
}
if (!inSingle && !inDouble && /\s/.test(ch)) {
if (current) tokens.push(current);
current = '';
continue;
}
current += ch;
}
if (current) tokens.push(current);
if (inSingle || inDouble) return null;
return tokens;
}
function atomicWriteJson(filePath, payload) {
const tmpPath = `${filePath}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), 'utf8');
fs.renameSync(tmpPath, filePath);
}
function main() {
const options = parseArgs(process.argv);
const jobDir = options['job-dir'];
const member = options.member;
const safeMember = options['safe-member'];
const command = options.command;
const timeoutSec = options.timeout ? Number(options.timeout) : 0;
if (!jobDir) exitWithError('worker: missing --job-dir');
if (!member) exitWithError('worker: missing --member');
if (!safeMember) exitWithError('worker: missing --safe-member');
if (!command) exitWithError('worker: missing --command');
const membersRoot = path.join(jobDir, 'members');
const memberDir = path.join(membersRoot, safeMember);
const statusPath = path.join(memberDir, 'status.json');
const outPath = path.join(memberDir, 'output.txt');
const errPath = path.join(memberDir, 'error.txt');
const promptPath = path.join(jobDir, 'prompt.txt');
const prompt = fs.existsSync(promptPath) ? fs.readFileSync(promptPath, 'utf8') : '';
const tokens = splitCommand(command);
if (!tokens || tokens.length === 0) {
atomicWriteJson(statusPath, {
member,
state: 'error',
message: 'Invalid command string',
finishedAt: new Date().toISOString(),
command,
});
process.exit(1);
}
const program = tokens[0];
const args = tokens.slice(1);
atomicWriteJson(statusPath, {
member,
state: 'running',
startedAt: new Date().toISOString(),
command,
pid: null,
});
const outStream = fs.createWriteStream(outPath, { flags: 'w' });
const errStream = fs.createWriteStream(errPath, { flags: 'w' });
let child;
try {
child = spawn(program, [...args, prompt], {
stdio: ['ignore', 'pipe', 'pipe'],
env: process.env,
});
} catch (error) {
atomicWriteJson(statusPath, {
member,
state: 'error',
message: error && error.message ? error.message : 'Failed to spawn command',
finishedAt: new Date().toISOString(),
command,
});
process.exit(1);
}
atomicWriteJson(statusPath, {
member,
state: 'running',
startedAt: new Date().toISOString(),
command,
pid: child.pid,
});
if (child.stdout) child.stdout.pipe(outStream);
if (child.stderr) child.stderr.pipe(errStream);
let timeoutHandle = null;
let timeoutTriggered = false;
if (Number.isFinite(timeoutSec) && timeoutSec > 0) {
timeoutHandle = setTimeout(() => {
timeoutTriggered = true;
try {
process.kill(child.pid, 'SIGTERM');
} catch {
// ignore
}
}, timeoutSec * 1000);
timeoutHandle.unref();
}
const finalize = (payload) => {
try {
outStream.end();
errStream.end();
} catch {
// ignore
}
atomicWriteJson(statusPath, payload);
};
child.on('error', (error) => {
const isMissing = error && error.code === 'ENOENT';
finalize({
member,
state: isMissing ? 'missing_cli' : 'error',
message: error && error.message ? error.message : 'Process error',
finishedAt: new Date().toISOString(),
command,
exitCode: null,
pid: child.pid,
});
process.exit(1);
});
child.on('exit', (code, signal) => {
if (timeoutHandle) clearTimeout(timeoutHandle);
const timedOut = Boolean(timeoutTriggered) && signal === 'SIGTERM';
const canceled = !timedOut && signal === 'SIGTERM';
finalize({
member,
state: timedOut ? 'timed_out' : canceled ? 'canceled' : code === 0 ? 'done' : 'error',
message: timedOut ? `Timed out after ${timeoutSec}s` : canceled ? 'Canceled' : null,
finishedAt: new Date().toISOString(),
command,
exitCode: typeof code === 'number' ? code : null,
signal: signal || null,
pid: child.pid,
});
process.exit(code === 0 ? 0 : 1);
});
}
if (require.main === module) {
main();
}
================================================
FILE: plugins/agent-council/scripts/council-job.js
================================================
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { spawn } = require('child_process');
const SCRIPT_DIR = __dirname;
const SKILL_DIR = path.resolve(SCRIPT_DIR, '..');
const WORKER_PATH = path.join(SCRIPT_DIR, 'council-job-worker.js');
const SKILL_CONFIG_FILE = path.join(SKILL_DIR, 'council.config.yaml');
const REPO_CONFIG_FILE = path.join(path.resolve(SKILL_DIR, '../..'), 'council.config.yaml');
function exitWithError(message) {
process.stderr.write(`${message}\n`);
process.exit(1);
}
function resolveDefaultConfigFile() {
if (fs.existsSync(SKILL_CONFIG_FILE)) return SKILL_CONFIG_FILE;
if (fs.existsSync(REPO_CONFIG_FILE)) return REPO_CONFIG_FILE;
return SKILL_CONFIG_FILE;
}
function detectHostRole() {
const normalized = SKILL_DIR.replace(/\\/g, '/');
if (normalized.includes('/.claude/skills/')) return 'claude';
if (normalized.includes('/.codex/skills/')) return 'codex';
return 'unknown';
}
function normalizeBool(value) {
if (value == null) return null;
const v = String(value).trim().toLowerCase();
if (['1', 'true', 'yes', 'y', 'on'].includes(v)) return true;
if (['0', 'false', 'no', 'n', 'off'].includes(v)) return false;
return null;
}
function resolveAutoRole(role, hostRole) {
const roleLc = String(role || '').trim().toLowerCase();
if (roleLc && roleLc !== 'auto') return roleLc;
if (hostRole === 'codex') return 'codex';
if (hostRole === 'claude') return 'claude';
return 'claude';
}
function parseCouncilConfig(configPath) {
const fallback = {
council: {
chairman: { role: 'auto' },
members: [
{ name: 'claude', command: 'claude -p', emoji: '🧠', color: 'CYAN' },
{ name: 'codex', command: 'codex exec', emoji: '🤖', color: 'BLUE' },
{ name: 'gemini', command: 'gemini', emoji: '💎', color: 'GREEN' },
],
settings: { exclude_chairman_from_members: true, timeout: 120 },
},
};
if (!fs.existsSync(configPath)) return fallback;
let YAML;
try {
YAML = require('yaml');
} catch {
exitWithError(
[
'Missing runtime dependency: yaml',
'Your Agent Council installation is out of date.',
'Reinstall from your project root:',
' npx github:team-attention/agent-council --target auto',
].join('\n')
);
}
let parsed;
try {
parsed = YAML.parse(fs.readFileSync(configPath, 'utf8'));
} catch (error) {
const message = error && error.message ? error.message : String(error);
exitWithError(`Invalid YAML in ${configPath}: ${message}`);
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
exitWithError(`Invalid config in ${configPath}: expected a YAML mapping/object at the document root`);
}
if (!parsed.council) {
exitWithError(`Invalid config in ${configPath}: missing required top-level key 'council:'`);
}
if (typeof parsed.council !== 'object' || Array.isArray(parsed.council)) {
exitWithError(`Invalid config in ${configPath}: 'council' must be a mapping/object`);
}
const merged = {
council: {
chairman: { ...fallback.council.chairman },
members: Array.isArray(fallback.council.members) ? [...fallback.council.members] : [],
settings: { ...fallback.council.settings },
},
};
const council = parsed.council;
if (council.chairman != null) {
if (typeof council.chairman !== 'object' || Array.isArray(council.chairman)) {
exitWithError(`Invalid config in ${configPath}: 'council.chairman' must be a mapping/object`);
}
merged.council.chairman = { ...merged.council.chairman, ...council.chairman };
}
if (Object.prototype.hasOwnProperty.call(council, 'members')) {
if (!Array.isArray(council.members)) {
exitWithError(`Invalid config in ${configPath}: 'council.members' must be a list/array`);
}
merged.council.members = council.members;
}
if (council.settings != null) {
if (typeof council.settings !== 'object' || Array.isArray(council.settings)) {
exitWithError(`Invalid config in ${configPath}: 'council.settings' must be a mapping/object`);
}
merged.council.settings = { ...merged.council.settings, ...council.settings };
}
return merged;
}
function ensureDir(dirPath) {
fs.mkdirSync(dirPath, { recursive: true });
}
function safeFileName(name) {
const cleaned = String(name || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-');
return cleaned || 'member';
}
function atomicWriteJson(filePath, payload) {
const tmpPath = `${filePath}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), 'utf8');
fs.renameSync(tmpPath, filePath);
}
function readJsonIfExists(filePath) {
try {
if (!fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch {
return null;
}
}
function sleepMs(ms) {
const msNum = Number(ms);
if (!Number.isFinite(msNum) || msNum <= 0) return;
const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);
Atomics.wait(view, 0, 0, Math.trunc(msNum));
}
function computeTerminalDoneCount(counts) {
const c = counts || {};
return (
Number(c.done || 0) +
Number(c.missing_cli || 0) +
Number(c.error || 0) +
Number(c.timed_out || 0) +
Number(c.canceled || 0)
);
}
function asCodexStepStatus(value) {
const v = String(value || '');
if (v === 'pending' || v === 'in_progress' || v === 'completed') return v;
return 'pending';
}
function buildCouncilUiPayload(statusPayload) {
const counts = statusPayload.counts || {};
const done = computeTerminalDoneCount(counts);
const total = Number(counts.total || 0);
const isDone = String(statusPayload.overallState || '') === 'done';
const queued = Number(counts.queued || 0);
const running = Number(counts.running || 0);
const members = Array.isArray(statusPayload.members) ? statusPayload.members : [];
const sortedMembers = members
.map((m) => ({
member: m && m.member != null ? String(m.member) : '',
state: m && m.state != null ? String(m.state) : 'unknown',
exitCode: m && m.exitCode != null ? m.exitCode : null,
}))
.filter((m) => m.member)
.sort((a, b) => a.member.localeCompare(b.member));
const terminalStates = new Set(['done', 'missing_cli', 'error', 'timed_out', 'canceled']);
// Keep the Plan UI visible by ensuring exactly one `in_progress` item while work remains.
const dispatchStatus = asCodexStepStatus(isDone ? 'completed' : queued > 0 ? 'in_progress' : 'completed');
let hasInProgress = dispatchStatus === 'in_progress';
const memberSteps = sortedMembers.map((m) => {
const state = m.state || 'unknown';
const isTerminal = terminalStates.has(state);
let status;
if (isTerminal) {
status = 'completed';
} else if (!hasInProgress && running > 0 && state === 'running') {
status = 'in_progress';
hasInProgress = true;
} else {
status = 'pending';
}
const label = `[Council] Ask ${m.member}`;
return { label, status: asCodexStepStatus(status) };
});
// Once members are done, the host agent should synthesize and then mark this step completed.
const synthStatus = asCodexStepStatus(isDone ? (hasInProgress ? 'pending' : 'in_progress') : 'pending');
const codexPlan = [
{ step: `[Council] Prompt dispatch`, status: dispatchStatus },
...memberSteps.map((s) => ({ step: s.label, status: s.status })),
{ step: `[Council] Synthesize`, status: synthStatus },
];
const claudeTodos = [
{
content: `[Council] Prompt dispatch`,
status: dispatchStatus,
activeForm: dispatchStatus === 'completed' ? 'Dispatched council prompts' : 'Dispatching council prompts',
},
...memberSteps.map((s) => ({
content: s.label,
status: s.status,
activeForm: s.status === 'completed' ? 'Finished' : 'Awaiting response',
})),
{
content: `[Council] Synthesize`,
status: synthStatus,
activeForm:
synthStatus === 'completed'
? 'Council results ready'
: synthStatus === 'in_progress'
? 'Ready to synthesize'
: 'Waiting to synthesize',
},
];
return {
progress: { done, total, overallState: String(statusPayload.overallState || '') },
codex: { update_plan: { plan: codexPlan } },
claude: { todo_write: { todos: claudeTodos } },
};
}
function computeStatusPayload(jobDir) {
const resolvedJobDir = path.resolve(jobDir);
if (!fs.existsSync(resolvedJobDir)) exitWithError(`jobDir not found: ${resolvedJobDir}`);
const jobMeta = readJsonIfExists(path.join(resolvedJobDir, 'job.json'));
if (!jobMeta) exitWithError(`job.json not found: ${path.join(resolvedJobDir, 'job.json')}`);
const membersRoot = path.join(resolvedJobDir, 'members');
if (!fs.existsSync(membersRoot)) exitWithError(`members folder not found: ${membersRoot}`);
const members = [];
for (const entry of fs.readdirSync(membersRoot)) {
const statusPath = path.join(membersRoot, entry, 'status.json');
const status = readJsonIfExists(statusPath);
if (status) members.push({ safeName: entry, ...status });
}
const totals = { queued: 0, running: 0, done: 0, error: 0, missing_cli: 0, timed_out: 0, canceled: 0 };
for (const m of members) {
const state = String(m.state || 'unknown');
if (Object.prototype.hasOwnProperty.call(totals, state)) totals[state]++;
}
const allDone = totals.running === 0 && totals.queued === 0;
const overallState = allDone ? 'done' : totals.running > 0 ? 'running' : 'queued';
return {
jobDir: resolvedJobDir,
id: jobMeta.id || null,
chairmanRole: jobMeta.chairmanRole || null,
overallState,
counts: { total: members.length, ...totals },
members: members
.map((m) => ({
member: m.member,
state: m.state,
startedAt: m.startedAt || null,
finishedAt: m.finishedAt || null,
exitCode: m.exitCode != null ? m.exitCode : null,
message: m.message || null,
}))
.sort((a, b) => String(a.member).localeCompare(String(b.member))),
};
}
function parseArgs(argv) {
const args = argv.slice(2);
const out = { _: [] };
const booleanFlags = new Set([
'json',
'text',
'checklist',
'help',
'h',
'verbose',
'include-chairman',
'exclude-chairman',
]);
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (a === '--') {
out._.push(...args.slice(i + 1));
break;
}
if (!a.startsWith('--')) {
out._.push(a);
continue;
}
const [key, rawValue] = a.split('=', 2);
if (rawValue != null) {
out[key.slice(2)] = rawValue;
continue;
}
const normalizedKey = key.slice(2);
if (booleanFlags.has(normalizedKey)) {
out[normalizedKey] = true;
continue;
}
const next = args[i + 1];
if (next == null || next.startsWith('--')) {
out[normalizedKey] = true;
continue;
}
out[normalizedKey] = next;
i++;
}
return out;
}
function printHelp() {
process.stdout.write(`Agent Council (job mode)
Usage:
council-job.sh start [--config path] [--chairman auto|claude|codex|...] [--jobs-dir path] [--json] "question"
council-job.sh status [--json|--text|--checklist] [--verbose] <jobDir>
council-job.sh wait [--cursor CURSOR] [--bucket auto|N] [--interval-ms N] [--timeout-ms N] <jobDir>
council-job.sh results [--json] <jobDir>
council-job.sh stop <jobDir>
council-job.sh clean <jobDir>
Notes:
- start returns immediately and runs members in parallel via detached Node workers
- poll status with repeated short calls to update TODO/plan UIs in host agents
- wait prints JSON by default and blocks until meaningful progress occurs, so you don't spam tool cells
`);
}
function cmdStart(options, prompt) {
const configPath = options.config || process.env.COUNCIL_CONFIG || resolveDefaultConfigFile();
const jobsDir =
options['jobs-dir'] || process.env.COUNCIL_JOBS_DIR || path.join(SKILL_DIR, '.jobs');
ensureDir(jobsDir);
const hostRole = detectHostRole();
const config = parseCouncilConfig(configPath);
const chairmanRoleRaw = options.chairman || process.env.COUNCIL_CHAIRMAN || config.council.chairman.role || 'auto';
const chairmanRole = resolveAutoRole(chairmanRoleRaw, hostRole);
const includeChairman = Boolean(options['include-chairman']);
const excludeChairmanOverride =
options['exclude-chairman'] != null ? true : options['include-chairman'] != null ? false : null;
const excludeSetting = normalizeBool(config.council.settings.exclude_chairman_from_members);
const excludeChairmanFromMembers =
excludeChairmanOverride != null ? excludeChairmanOverride : excludeSetting != null ? excludeSetting : true;
const timeoutSetting = Number(config.council.settings.timeout || 0);
const timeoutOverride = options.timeout != null ? Number(options.timeout) : null;
const timeoutSec = Number.isFinite(timeoutOverride) && timeoutOverride > 0 ? timeoutOverride : timeoutSetting > 0 ? timeoutSetting : 0;
const requestedMembers = config.council.members || [];
const members = requestedMembers.filter((m) => {
if (!m || !m.name || !m.command) return false;
const nameLc = String(m.name).toLowerCase();
if (excludeChairmanFromMembers && !includeChairman && nameLc === chairmanRole) return false;
return true;
});
const jobId = `${new Date().toISOString().replace(/[:.]/g, '').replace('T', '-').slice(0, 15)}-${crypto
.randomBytes(3)
.toString('hex')}`;
const jobDir = path.join(jobsDir, `council-${jobId}`);
const membersDir = path.join(jobDir, 'members');
ensureDir(membersDir);
fs.writeFileSync(path.join(jobDir, 'prompt.txt'), String(prompt), 'utf8');
const jobMeta = {
id: `council-${jobId}`,
createdAt: new Date().toISOString(),
configPath,
hostRole,
chairmanRole,
settings: {
excludeChairmanFromMembers,
timeoutSec: timeoutSec || null,
},
members: members.map((m) => ({
name: String(m.name),
command: String(m.command),
emoji: m.emoji ? String(m.emoji) : null,
color: m.color ? String(m.color) : null,
})),
};
atomicWriteJson(path.join(jobDir, 'job.json'), jobMeta);
for (const member of members) {
const name = String(member.name);
const safeName = safeFileName(name);
const memberDir = path.join(membersDir, safeName);
ensureDir(memberDir);
atomicWriteJson(path.join(memberDir, 'status.json'), {
member: name,
state: 'queued',
queuedAt: new Date().toISOString(),
command: String(member.command),
});
const workerArgs = [
WORKER_PATH,
'--job-dir',
jobDir,
'--member',
name,
'--safe-member',
safeName,
'--command',
String(member.command),
];
if (timeoutSec && Number.isFinite(timeoutSec) && timeoutSec > 0) {
workerArgs.push('--timeout', String(timeoutSec));
}
const child = spawn(process.execPath, workerArgs, {
detached: true,
stdio: 'ignore',
env: process.env,
});
child.unref();
}
if (options.json) {
process.stdout.write(`${JSON.stringify({ jobDir, ...jobMeta }, null, 2)}\n`);
} else {
process.stdout.write(`${jobDir}\n`);
}
}
function cmdStatus(options, jobDir) {
const payload = computeStatusPayload(jobDir);
const wantChecklist = Boolean(options.checklist) && !options.json;
if (wantChecklist) {
const done = computeTerminalDoneCount(payload.counts);
const headerId = payload.id ? ` (${payload.id})` : '';
process.stdout.write(`Agent Council${headerId}\n`);
process.stdout.write(
`Progress: ${done}/${payload.counts.total} done (running ${payload.counts.running}, queued ${payload.counts.queued})\n`
);
for (const m of payload.members) {
const state = String(m.state || '');
const mark =
state === 'done'
? '[x]'
: state === 'running' || state === 'queued'
? '[ ]'
: state
? '[!]'
: '[ ]';
const exitInfo = m.exitCode != null ? ` (exit ${m.exitCode})` : '';
process.stdout.write(`${mark} ${m.member} — ${state}${exitInfo}\n`);
}
return;
}
const wantText = Boolean(options.text) && !options.json;
if (wantText) {
const done = computeTerminalDoneCount(payload.counts);
process.stdout.write(`members ${done}/${payload.counts.total} done; running=${payload.counts.running} queued=${payload.counts.queued}\n`);
if (options.verbose) {
for (const m of payload.members) {
process.stdout.write(`- ${m.member}: ${m.state}${m.exitCode != null ? ` (exit ${m.exitCode})` : ''}\n`);
}
}
return;
}
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
}
function parseWaitCursor(value) {
const raw = String(value || '').trim();
if (!raw) return null;
const parts = raw.split(':');
const version = parts[0];
if (version === 'v1' && parts.length === 4) {
const bucketSize = Number(parts[1]);
const doneBucket = Number(parts[2]);
const isDone = parts[3] === '1';
if (!Number.isFinite(bucketSize) || bucketSize <= 0) return null;
if (!Number.isFinite(doneBucket) || doneBucket < 0) return null;
return { version, bucketSize, dispatchBucket: 0, doneBucket, isDone };
}
if (version === 'v2' && parts.length === 5) {
const bucketSize = Number(parts[1]);
const dispatchBucket = Number(parts[2]);
const doneBucket = Number(parts[3]);
const isDone = parts[4] === '1';
if (!Number.isFinite(bucketSize) || bucketSize <= 0) return null;
if (!Number.isFinite(dispatchBucket) || dispatchBucket < 0) return null;
if (!Number.isFinite(doneBucket) || doneBucket < 0) return null;
return { version, bucketSize, dispatchBucket, doneBucket, isDone };
}
return null;
}
function formatWaitCursor(bucketSize, dispatchBucket, doneBucket, isDone) {
return `v2:${bucketSize}:${dispatchBucket}:${doneBucket}:${isDone ? 1 : 0}`;
}
function asWaitPayload(statusPayload) {
const members = Array.isArray(statusPayload.members) ? statusPayload.members : [];
return {
jobDir: statusPayload.jobDir,
id: statusPayload.id,
chairmanRole: statusPayload.chairmanRole,
overallState: statusPayload.overallState,
counts: statusPayload.counts,
members: members.map((m) => ({
member: m.member,
state: m.state,
exitCode: m.exitCode != null ? m.exitCode : null,
message: m.message || null,
})),
ui: buildCouncilUiPayload(statusPayload),
};
}
function resolveBucketSize(options, total, prevCursor) {
const raw = options.bucket != null ? options.bucket : options['bucket-size'];
if (raw == null || raw === true) {
if (prevCursor && prevCursor.bucketSize) return prevCursor.bucketSize;
} else {
const asString = String(raw).trim().toLowerCase();
if (asString !== 'auto') {
const num = Number(asString);
if (!Number.isFinite(num) || num <= 0) exitWithError(`wait: invalid --bucket: ${raw}`);
return Math.trunc(num);
}
}
// Auto-bucket: target ~5 updates total.
const totalNum = Number(total || 0);
if (!Number.isFinite(totalNum) || totalNum <= 0) return 1;
return Math.max(1, Math.ceil(totalNum / 5));
}
function cmdWait(options, jobDir) {
const resolvedJobDir = path.resolve(jobDir);
const cursorFilePath = path.join(resolvedJobDir, '.wait_cursor');
const prevCursorRaw =
options.cursor != null
? String(options.cursor)
: fs.existsSync(cursorFilePath)
? String(fs.readFileSync(cursorFilePath, 'utf8')).trim()
: '';
const prevCursor = parseWaitCursor(prevCursorRaw);
const intervalMsRaw = options['interval-ms'] != null ? options['interval-ms'] : 250;
const intervalMs = Math.max(50, Math.trunc(Number(intervalMsRaw)));
if (!Number.isFinite(intervalMs) || intervalMs <= 0) exitWithError(`wait: invalid --interval-ms: ${intervalMsRaw}`);
const timeoutMsRaw = options['timeout-ms'] != null ? options['timeout-ms'] : 0;
const timeoutMs = Math.trunc(Number(timeoutMsRaw));
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) exitWithError(`wait: invalid --timeout-ms: ${timeoutMsRaw}`);
// Always read once to decide bucket sizing and (when no cursor is given) return immediately.
let payload = computeStatusPayload(jobDir);
const bucketSize = resolveBucketSize(options, payload.counts.total, prevCursor);
const doneCount = computeTerminalDoneCount(payload.counts);
const isDone = payload.overallState === 'done';
const total = Number(payload.counts.total || 0);
const queued = Number(payload.counts.queued || 0);
const dispatchBucket = queued === 0 && total > 0 ? 1 : 0;
const doneBucket = Math.floor(doneCount / bucketSize);
const cursor = formatWaitCursor(bucketSize, dispatchBucket, doneBucket, isDone);
if (!prevCursor) {
fs.writeFileSync(cursorFilePath, cursor, 'utf8');
process.stdout.write(`${JSON.stringify({ ...asWaitPayload(payload), cursor }, null, 2)}\n`);
return;
}
const start = Date.now();
while (cursor === prevCursorRaw) {
if (timeoutMs > 0 && Date.now() - start >= timeoutMs) break;
sleepMs(intervalMs);
payload = computeStatusPayload(jobDir);
const d = computeTerminalDoneCount(payload.counts);
const doneFlag = payload.overallState === 'done';
const totalCount = Number(payload.counts.total || 0);
const queuedCount = Number(payload.counts.queued || 0);
const dispatchB = queuedCount === 0 && totalCount > 0 ? 1 : 0;
const doneB = Math.floor(d / bucketSize);
const nextCursor = formatWaitCursor(bucketSize, dispatchB, doneB, doneFlag);
if (nextCursor !== prevCursorRaw) {
fs.writeFileSync(cursorFilePath, nextCursor, 'utf8');
process.stdout.write(`${JSON.stringify({ ...asWaitPayload(payload), cursor: nextCursor }, null, 2)}\n`);
return;
}
}
// Timeout: return current state (cursor may be unchanged).
const finalPayload = computeStatusPayload(jobDir);
const finalDone = computeTerminalDoneCount(finalPayload.counts);
const finalDoneFlag = finalPayload.overallState === 'done';
const finalTotal = Number(finalPayload.counts.total || 0);
const finalQueued = Number(finalPayload.counts.queued || 0);
const finalDispatchBucket = finalQueued === 0 && finalTotal > 0 ? 1 : 0;
const finalDoneBucket = Math.floor(finalDone / bucketSize);
const finalCursor = formatWaitCursor(bucketSize, finalDispatchBucket, finalDoneBucket, finalDoneFlag);
fs.writeFileSync(cursorFilePath, finalCursor, 'utf8');
process.stdout.write(`${JSON.stringify({ ...asWaitPayload(finalPayload), cursor: finalCursor }, null, 2)}\n`);
}
function cmdResults(options, jobDir) {
const resolvedJobDir = path.resolve(jobDir);
const jobMeta = readJsonIfExists(path.join(resolvedJobDir, 'job.json'));
const membersRoot = path.join(resolvedJobDir, 'members');
const members = [];
if (fs.existsSync(membersRoot)) {
for (const entry of fs.readdirSync(membersRoot)) {
const statusPath = path.join(membersRoot, entry, 'status.json');
const outputPath = path.join(membersRoot, entry, 'output.txt');
const errorPath = path.join(membersRoot, entry, 'error.txt');
const status = readJsonIfExists(statusPath);
if (!status) continue;
const output = fs.existsSync(outputPath) ? fs.readFileSync(outputPath, 'utf8') : '';
const stderr = fs.existsSync(errorPath) ? fs.readFileSync(errorPath, 'utf8') : '';
members.push({ safeName: entry, ...status, output, stderr });
}
}
if (options.json) {
process.stdout.write(
`${JSON.stringify(
{
jobDir: resolvedJobDir,
id: jobMeta ? jobMeta.id : null,
prompt: fs.existsSync(path.join(resolvedJobDir, 'prompt.txt'))
? fs.readFileSync(path.join(resolvedJobDir, 'prompt.txt'), 'utf8')
: null,
members: members
.map((m) => ({
member: m.member,
state: m.state,
exitCode: m.exitCode != null ? m.exitCode : null,
message: m.message || null,
output: m.output,
stderr: m.stderr,
}))
.sort((a, b) => String(a.member).localeCompare(String(b.member))),
},
null,
2
)}\n`
);
return;
}
for (const m of members.sort((a, b) => String(a.member).localeCompare(String(b.member)))) {
process.stdout.write(`\n=== ${m.member} (${m.state}) ===\n`);
if (m.message) process.stdout.write(`${m.message}\n`);
process.stdout.write(m.output || '');
if (!m.output && m.stderr) {
process.stdout.write('\n');
process.stdout.write(m.stderr);
}
process.stdout.write('\n');
}
}
function cmdStop(_options, jobDir) {
const resolvedJobDir = path.resolve(jobDir);
const membersRoot = path.join(resolvedJobDir, 'members');
if (!fs.existsSync(membersRoot)) exitWithError(`No members folder found: ${membersRoot}`);
let stoppedAny = false;
for (const entry of fs.readdirSync(membersRoot)) {
const statusPath = path.join(membersRoot, entry, 'status.json');
const status = readJsonIfExists(statusPath);
if (!status) continue;
if (status.state !== 'running') continue;
if (!status.pid) continue;
try {
process.kill(Number(status.pid), 'SIGTERM');
stoppedAny = true;
} catch {
// ignore
}
}
process.stdout.write(stoppedAny ? 'stop: sent SIGTERM to running members\n' : 'stop: no running members\n');
}
function cmdClean(_options, jobDir) {
const resolvedJobDir = path.resolve(jobDir);
fs.rmSync(resolvedJobDir, { recursive: true, force: true });
process.stdout.write(`cleaned: ${resolvedJobDir}\n`);
}
function main() {
const options = parseArgs(process.argv);
const [command, ...rest] = options._;
if (!command || options.help || options.h) {
printHelp();
return;
}
if (command === 'start') {
const prompt = rest.join(' ').trim();
if (!prompt) exitWithError('start: missing prompt');
cmdStart(options, prompt);
return;
}
if (command === 'status') {
const jobDir = rest[0];
if (!jobDir) exitWithError('status: missing jobDir');
cmdStatus(options, jobDir);
return;
}
if (command === 'wait') {
const jobDir = rest[0];
if (!jobDir) exitWithError('wait: missing jobDir');
cmdWait(options, jobDir);
return;
}
if (command === 'results') {
const jobDir = rest[0];
if (!jobDir) exitWithError('results: missing jobDir');
cmdResults(options, jobDir);
return;
}
if (command === 'stop') {
const jobDir = rest[0];
if (!jobDir) exitWithError('stop: missing jobDir');
cmdStop(options, jobDir);
return;
}
if (command === 'clean') {
const jobDir = rest[0];
if (!jobDir) exitWithError('clean: missing jobDir');
cmdClean(options, jobDir);
return;
}
exitWithError(`Unknown command: ${command}`);
}
if (require.main === module) {
main();
}
================================================
FILE: plugins/agent-council/scripts/council-job.sh
================================================
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if ! command -v node >/dev/null 2>&1; then
echo "Error: Node.js is required to run Agent Council job mode." >&2
echo "Install Node.js and try again (plugin installs cannot bundle Node)." >&2
echo "" >&2
echo "macOS (Homebrew): brew install node" >&2
echo "Or download from: https://nodejs.org/" >&2
exit 127
fi
exec node "$SCRIPT_DIR/council-job.js" "$@"
================================================
FILE: plugins/agent-council/scripts/council.sh
================================================
#!/bin/bash
#
# Agent Council (job mode default)
#
# Subcommands:
# council.sh start [options] "question" # returns JOB_DIR immediately
# council.sh status [--json|--text|--checklist] JOB_DIR # poll progress
# council.sh wait [--cursor CURSOR] [--bucket auto|N] [--interval-ms N] [--timeout-ms N] JOB_DIR
# council.sh results [--json] JOB_DIR # print collected outputs
# council.sh stop JOB_DIR # best-effort stop running members
# council.sh clean JOB_DIR # remove job directory
#
# One-shot:
# council.sh "question"
# (in a real terminal: starts a job, waits for completion, prints results, cleans up)
# (in host-agent tool UIs: returns a single `wait` JSON payload immediately; host drives progress + results)
#
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
JOB_SCRIPT="$SCRIPT_DIR/council-job.sh"
usage() {
cat <<EOF
Agent Council
Default mode is job-based parallel execution (pollable).
Usage:
$(basename "$0") start [options] "question"
$(basename "$0") status [--json|--text|--checklist] <jobDir>
$(basename "$0") wait [--cursor CURSOR] [--bucket auto|N] [--interval-ms N] [--timeout-ms N] <jobDir>
$(basename "$0") results [--json] <jobDir>
$(basename "$0") stop <jobDir>
$(basename "$0") clean <jobDir>
One-shot:
$(basename "$0") "question"
EOF
}
if [ $# -eq 0 ]; then
usage
exit 1
fi
case "$1" in
-h|--help|help)
usage
exit 0
;;
esac
if ! command -v node >/dev/null 2>&1; then
echo "Error: Node.js is required to run Agent Council." >&2
echo "Claude Code plugins cannot bundle or auto-install Node." >&2
echo "" >&2
echo "macOS (Homebrew): brew install node" >&2
echo "Or download from: https://nodejs.org/" >&2
exit 127
fi
case "$1" in
start|status|wait|results|stop|clean)
exec "$JOB_SCRIPT" "$@"
;;
esac
in_host_agent_context() {
if [ -n "${CODEX_CACHE_FILE:-}" ]; then
return 0
fi
case "$SCRIPT_DIR" in
*/.codex/skills/*|*/.claude/skills/*)
# Tool-call environments typically do not provide a real TTY on stdout/stderr.
if [ ! -t 1 ] && [ ! -t 2 ]; then
return 0
fi
;;
esac
return 1
}
JOB_DIR="$("$JOB_SCRIPT" start "$@")"
# Host agents (Codex CLI / Claude Code) cannot update native TODO/plan UIs while a long-running
# command is executing. If we're in a host agent context, return immediately with a single `wait`
# JSON payload (includes `.ui.codex.update_plan.plan` / `.ui.claude.todo_write.todos`) and let the
# host agent drive progress updates with repeated short `wait` calls + native UI updates.
if in_host_agent_context; then
exec "$JOB_SCRIPT" wait "$JOB_DIR"
fi
echo "council: started ${JOB_DIR}" >&2
cleanup_on_signal() {
if [ -n "${JOB_DIR:-}" ] && [ -d "$JOB_DIR" ]; then
"$JOB_SCRIPT" stop "$JOB_DIR" >/dev/null 2>&1 || true
"$JOB_SCRIPT" clean "$JOB_DIR" >/dev/null 2>&1 || true
fi
exit 130
}
trap cleanup_on_signal INT TERM
while true; do
WAIT_JSON="$("$JOB_SCRIPT" wait "$JOB_DIR")"
OVERALL="$(printf '%s' "$WAIT_JSON" | node -e '
const fs=require("fs");
const d=JSON.parse(fs.readFileSync(0,"utf8"));
process.stdout.write(String(d.overallState||""));
')"
"$JOB_SCRIPT" status --text "$JOB_DIR" >&2
if [ "$OVERALL" = "done" ]; then
break
fi
done
trap - INT TERM
"$JOB_SCRIPT" results "$JOB_DIR"
"$JOB_SCRIPT" clean "$JOB_DIR" >/dev/null
================================================
FILE: plugins/agent-council/skills/agent-council/SKILL.md
================================================
---
name: agent-council
description: Collect and synthesize opinions from multiple AI agents. Use when users say "summon the council", "ask other AIs", or want multiple AI perspectives on a question.
---
# Agent Council
Collect multiple AI opinions and synthesize one answer.
## Usage
Run a job and collect results:
```bash
JOB_DIR=$(./skills/agent-council/scripts/council.sh start "your question here")
./skills/agent-council/scripts/council.sh wait "$JOB_DIR"
./skills/agent-council/scripts/council.sh results "$JOB_DIR"
./skills/agent-council/scripts/council.sh clean "$JOB_DIR"
```
One-shot:
```bash
./skills/agent-council/scripts/council.sh "your question here"
```
## References
- `references/overview.md` — workflow and background.
- `references/examples.md` — usage examples.
- `references/config.md` — member configuration.
- `references/requirements.md` — dependencies and CLI checks.
- `references/host-ui.md` — host UI checklist guidance.
- `references/safety.md` — safety notes.
================================================
FILE: plugins/agent-council/skills/agent-council/references/config.md
================================================
# Configure members
Edit `council.config.yaml` to set chairman and members:
```yaml
council:
chairman:
role: "auto"
members:
- name: claude
command: "claude -p"
emoji: "🧠"
color: "CYAN"
- name: codex
command: "codex exec"
emoji: "🤖"
color: "BLUE"
- name: gemini
command: "gemini"
emoji: "💎"
color: "GREEN"
```
Add custom members by appending entries to `members`:
- Use a stable `name` (lowercase, short).
- Set `command` to a runnable CLI invocation.
- Provide `emoji` and `color` for readability (optional but recommended).
- Note that the installer filters members to detected CLIs only when it first generates `council.config.yaml`. After that, missing CLIs are not auto-removed and will report `missing_cli` at runtime; remove unavailable members or install the CLI before running.
================================================
FILE: plugins/agent-council/skills/agent-council/references/examples.md
================================================
# Examples
## Technical decision
Prompt:
```
React vs Vue - which fits this project better? Summon the council
```
Steps:
1. Run `council.sh` to collect opinions from configured members.
2. Organize member perspectives.
3. Recommend based on project context.
## Architecture review
Prompt:
```
Let's hear other AIs' opinions on this design
```
Steps:
1. Summarize the design and query the council.
2. Collect feedback from each member.
3. Analyze commonalities and synthesize.
================================================
FILE: plugins/agent-council/skills/agent-council/references/host-ui.md
================================================
# Host UI Checklist Guidance
Use these steps only when a host agent UI supports native checklist updates.
## Checklist flow
1. Run `council.sh wait` once to seed the cursor and get the JSON payload.
2. Update the host's native checklist UI using the payload (if provided).
3. Repeat `wait` until progress changes, then update the UI again.
4. Finish with `results` and `clean`.
## Behavior notes
- Do not run a blocking wait before the first checklist update, or the Plan UI may not appear.
- Keep exactly one in_progress item while work remains.
- Preserve existing checklist items and append the [Council] section.
- Avoid a long while loop in a single tool call; update after each wait return.
- Use `--bucket 1` for per-member updates when needed.
================================================
FILE: plugins/agent-council/skills/agent-council/references/overview.md
================================================
# Overview
- Gather responses from configured member CLIs.
- Let the chairman synthesize the final response (default: `role: auto`, current agent).
- Configure members in `council.config.yaml`; exclude the chairman from members by default.
- Reference [Karpathy's LLM Council](https://github.com/karpathy/llm-council) for inspiration.
## Workflow (3 stages)
1. Send the same prompt to each member.
2. Collect and surface member responses.
3. Synthesize the final answer as chairman; optionally run the chairman inside `council.sh` via `chairman.command`.
================================================
FILE: plugins/agent-council/skills/agent-council/references/requirements.md
================================================
# Requirements
- Install and authenticate the CLIs listed under `council.members` in `council.config.yaml`.
- Note that the installer filters members to detected CLIs only on initial config generation; afterward, missing CLIs show as `missing_cli` in status output.
- Install Node.js (plugins cannot bundle or auto-install it).
- Verify each member’s base command exists (for example, `command -v <binary>` or `<binary> --version`).
================================================
FILE: plugins/agent-council/skills/agent-council/references/safety.md
================================================
# Safety
- Do not share sensitive information with the council.
================================================
FILE: plugins/agent-council/skills/agent-council/scripts/council-job-worker.js
================================================
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { spawn } = require('child_process');
function exitWithError(message) {
process.stderr.write(`${message}\n`);
process.exit(1);
}
function parseArgs(argv) {
const args = argv.slice(2);
const out = { _: [] };
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (!a.startsWith('--')) {
out._.push(a);
continue;
}
const [key, rawValue] = a.split('=', 2);
if (rawValue != null) {
out[key.slice(2)] = rawValue;
continue;
}
const next = args[i + 1];
if (next == null || next.startsWith('--')) {
out[key.slice(2)] = true;
continue;
}
out[key.slice(2)] = next;
i++;
}
return out;
}
function splitCommand(command) {
const tokens = [];
let current = '';
let inSingle = false;
let inDouble = false;
let escapeNext = false;
for (const ch of String(command || '')) {
if (escapeNext) {
current += ch;
escapeNext = false;
continue;
}
if (!inSingle && ch === '\\') {
escapeNext = true;
continue;
}
if (!inDouble && ch === "'") {
inSingle = !inSingle;
continue;
}
if (!inSingle && ch === '"') {
inDouble = !inDouble;
continue;
}
if (!inSingle && !inDouble && /\s/.test(ch)) {
if (current) tokens.push(current);
current = '';
continue;
}
current += ch;
}
if (current) tokens.push(current);
if (inSingle || inDouble) return null;
return tokens;
}
function atomicWriteJson(filePath, payload) {
const tmpPath = `${filePath}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), 'utf8');
fs.renameSync(tmpPath, filePath);
}
function main() {
const options = parseArgs(process.argv);
const jobDir = options['job-dir'];
const member = options.member;
const safeMember = options['safe-member'];
const command = options.command;
const timeoutSec = options.timeout ? Number(options.timeout) : 0;
if (!jobDir) exitWithError('worker: missing --job-dir');
if (!member) exitWithError('worker: missing --member');
if (!safeMember) exitWithError('worker: missing --safe-member');
if (!command) exitWithError('worker: missing --command');
const membersRoot = path.join(jobDir, 'members');
const memberDir = path.join(membersRoot, safeMember);
const statusPath = path.join(memberDir, 'status.json');
const outPath = path.join(memberDir, 'output.txt');
const errPath = path.join(memberDir, 'error.txt');
const promptPath = path.join(jobDir, 'prompt.txt');
const prompt = fs.existsSync(promptPath) ? fs.readFileSync(promptPath, 'utf8') : '';
const tokens = splitCommand(command);
if (!tokens || tokens.length === 0) {
atomicWriteJson(statusPath, {
member,
state: 'error',
message: 'Invalid command string',
finishedAt: new Date().toISOString(),
command,
});
process.exit(1);
}
const program = tokens[0];
const args = tokens.slice(1);
atomicWriteJson(statusPath, {
member,
state: 'running',
startedAt: new Date().toISOString(),
command,
pid: null,
});
const outStream = fs.createWriteStream(outPath, { flags: 'w' });
const errStream = fs.createWriteStream(errPath, { flags: 'w' });
let child;
try {
child = spawn(program, [...args, prompt], {
stdio: ['ignore', 'pipe', 'pipe'],
env: process.env,
});
} catch (error) {
atomicWriteJson(statusPath, {
member,
state: 'error',
message: error && error.message ? error.message : 'Failed to spawn command',
finishedAt: new Date().toISOString(),
command,
});
process.exit(1);
}
atomicWriteJson(statusPath, {
member,
state: 'running',
startedAt: new Date().toISOString(),
command,
pid: child.pid,
});
if (child.stdout) child.stdout.pipe(outStream);
if (child.stderr) child.stderr.pipe(errStream);
let timeoutHandle = null;
let timeoutTriggered = false;
if (Number.isFinite(timeoutSec) && timeoutSec > 0) {
timeoutHandle = setTimeout(() => {
timeoutTriggered = true;
try {
process.kill(child.pid, 'SIGTERM');
} catch {
// ignore
}
}, timeoutSec * 1000);
timeoutHandle.unref();
}
const finalize = (payload) => {
try {
outStream.end();
errStream.end();
} catch {
// ignore
}
atomicWriteJson(statusPath, payload);
};
child.on('error', (error) => {
const isMissing = error && error.code === 'ENOENT';
finalize({
member,
state: isMissing ? 'missing_cli' : 'error',
message: error && error.message ? error.message : 'Process error',
finishedAt: new Date().toISOString(),
command,
exitCode: null,
pid: child.pid,
});
process.exit(1);
});
child.on('exit', (code, signal) => {
if (timeoutHandle) clearTimeout(timeoutHandle);
const timedOut = Boolean(timeoutTriggered) && signal === 'SIGTERM';
const canceled = !timedOut && signal === 'SIGTERM';
finalize({
member,
state: timedOut ? 'timed_out' : canceled ? 'canceled' : code === 0 ? 'done' : 'error',
message: timedOut ? `Timed out after ${timeoutSec}s` : canceled ? 'Canceled' : null,
finishedAt: new Date().toISOString(),
command,
exitCode: typeof code === 'number' ? code : null,
signal: signal || null,
pid: child.pid,
});
process.exit(code === 0 ? 0 : 1);
});
}
if (require.main === module) {
main();
}
================================================
FILE: plugins/agent-council/skills/agent-council/scripts/council-job.js
================================================
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { spawn } = require('child_process');
const SCRIPT_DIR = __dirname;
const SKILL_DIR = path.resolve(SCRIPT_DIR, '..');
const WORKER_PATH = path.join(SCRIPT_DIR, 'council-job-worker.js');
const SKILL_CONFIG_FILE = path.join(SKILL_DIR, 'council.config.yaml');
const REPO_CONFIG_FILE = path.join(path.resolve(SKILL_DIR, '../..'), 'council.config.yaml');
function exitWithError(message) {
process.stderr.write(`${message}\n`);
process.exit(1);
}
function resolveDefaultConfigFile() {
if (fs.existsSync(SKILL_CONFIG_FILE)) return SKILL_CONFIG_FILE;
if (fs.existsSync(REPO_CONFIG_FILE)) return REPO_CONFIG_FILE;
return SKILL_CONFIG_FILE;
}
function detectHostRole() {
const normalized = SKILL_DIR.replace(/\\/g, '/');
if (normalized.includes('/.claude/skills/')) return 'claude';
if (normalized.includes('/.codex/skills/')) return 'codex';
return 'unknown';
}
function normalizeBool(value) {
if (value == null) return null;
const v = String(value).trim().toLowerCase();
if (['1', 'true', 'yes', 'y', 'on'].includes(v)) return true;
if (['0', 'false', 'no', 'n', 'off'].includes(v)) return false;
return null;
}
function resolveAutoRole(role, hostRole) {
const roleLc = String(role || '').trim().toLowerCase();
if (roleLc && roleLc !== 'auto') return roleLc;
if (hostRole === 'codex') return 'codex';
if (hostRole === 'claude') return 'claude';
return 'claude';
}
function parseCouncilConfig(configPath) {
const fallback = {
council: {
chairman: { role: 'auto' },
members: [
{ name: 'claude', command: 'claude -p', emoji: '🧠', color: 'CYAN' },
{ name: 'codex', command: 'codex exec', emoji: '🤖', color: 'BLUE' },
{ name: 'gemini', command: 'gemini', emoji: '💎', color: 'GREEN' },
],
settings: { exclude_chairman_from_members: true, timeout: 120 },
},
};
if (!fs.existsSync(configPath)) return fallback;
let YAML;
try {
YAML = require('yaml');
} catch {
exitWithError(
[
'Missing runtime dependency: yaml',
'Your Agent Council installation is out of date.',
'Reinstall from your project root:',
' npx github:team-attention/agent-council --target auto',
].join('\n')
);
}
let parsed;
try {
parsed = YAML.parse(fs.readFileSync(configPath, 'utf8'));
} catch (error) {
const message = error && error.message ? error.message : String(error);
exitWithError(`Invalid YAML in ${configPath}: ${message}`);
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
exitWithError(`Invalid config in ${configPath}: expected a YAML mapping/object at the document root`);
}
if (!parsed.council) {
exitWithError(`Invalid config in ${configPath}: missing required top-level key 'council:'`);
}
if (typeof parsed.council !== 'object' || Array.isArray(parsed.council)) {
exitWithError(`Invalid config in ${configPath}: 'council' must be a mapping/object`);
}
const merged = {
council: {
chairman: { ...fallback.council.chairman },
members: Array.isArray(fallback.council.members) ? [...fallback.council.members] : [],
settings: { ...fallback.council.settings },
},
};
const council = parsed.council;
if (council.chairman != null) {
if (typeof council.chairman !== 'object' || Array.isArray(council.chairman)) {
exitWithError(`Invalid config in ${configPath}: 'council.chairman' must be a mapping/object`);
}
merged.council.chairman = { ...merged.council.chairman, ...council.chairman };
}
if (Object.prototype.hasOwnProperty.call(council, 'members')) {
if (!Array.isArray(council.members)) {
exitWithError(`Invalid config in ${configPath}: 'council.members' must be a list/array`);
}
merged.council.members = council.members;
}
if (council.settings != null) {
if (typeof council.settings !== 'object' || Array.isArray(council.settings)) {
exitWithError(`Invalid config in ${configPath}: 'council.settings' must be a mapping/object`);
}
merged.council.settings = { ...merged.council.settings, ...council.settings };
}
return merged;
}
function ensureDir(dirPath) {
fs.mkdirSync(dirPath, { recursive: true });
}
function safeFileName(name) {
const cleaned = String(name || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-');
return cleaned || 'member';
}
function atomicWriteJson(filePath, payload) {
const tmpPath = `${filePath}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), 'utf8');
fs.renameSync(tmpPath, filePath);
}
function readJsonIfExists(filePath) {
try {
if (!fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch {
return null;
}
}
function sleepMs(ms) {
const msNum = Number(ms);
if (!Number.isFinite(msNum) || msNum <= 0) return;
const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);
Atomics.wait(view, 0, 0, Math.trunc(msNum));
}
function computeTerminalDoneCount(counts) {
const c = counts || {};
return (
Number(c.done || 0) +
Number(c.missing_cli || 0) +
Number(c.error || 0) +
Number(c.timed_out || 0) +
Number(c.canceled || 0)
);
}
function asCodexStepStatus(value) {
const v = String(value || '');
if (v === 'pending' || v === 'in_progress' || v === 'completed') return v;
return 'pending';
}
function buildCouncilUiPayload(statusPayload) {
const counts = statusPayload.counts || {};
const done = computeTerminalDoneCount(counts);
const total = Number(counts.total || 0);
const isDone = String(statusPayload.overallState || '') === 'done';
const queued = Number(counts.queued || 0);
const running = Number(counts.running || 0);
const members = Array.isArray(statusPayload.members) ? statusPayload.members : [];
const sortedMembers = members
.map((m) => ({
member: m && m.member != null ? String(m.member) : '',
state: m && m.state != null ? String(m.state) : 'unknown',
exitCode: m && m.exitCode != null ? m.exitCode : null,
}))
.filter((m) => m.member)
.sort((a, b) => a.member.localeCompare(b.member));
const terminalStates = new Set(['done', 'missing_cli', 'error', 'timed_out', 'canceled']);
// Keep the Plan UI visible by ensuring exactly one `in_progress` item while work remains.
const dispatchStatus = asCodexStepStatus(isDone ? 'completed' : queued > 0 ? 'in_progress' : 'completed');
let hasInProgress = dispatchStatus === 'in_progress';
const memberSteps = sortedMembers.map((m) => {
const state = m.state || 'unknown';
const isTerminal = terminalStates.has(state);
let status;
if (isTerminal) {
status = 'completed';
} else if (!hasInProgress && running > 0 && state === 'running') {
status = 'in_progress';
hasInProgress = true;
} else {
status = 'pending';
}
const label = `[Council] Ask ${m.member}`;
return { label, status: asCodexStepStatus(status) };
});
// Once members are done, the host agent should synthesize and then mark this step completed.
const synthStatus = asCodexStepStatus(isDone ? (hasInProgress ? 'pending' : 'in_progress') : 'pending');
const codexPlan = [
{ step: `[Council] Prompt dispatch`, status: dispatchStatus },
...memberSteps.map((s) => ({ step: s.label, status: s.status })),
{ step: `[Council] Synthesize`, status: synthStatus },
];
const claudeTodos = [
{
content: `[Council] Prompt dispatch`,
status: dispatchStatus,
activeForm: dispatchStatus === 'completed' ? 'Dispatched council prompts' : 'Dispatching council prompts',
},
...memberSteps.map((s) => ({
content: s.label,
status: s.status,
activeForm: s.status === 'completed' ? 'Finished' : 'Awaiting response',
})),
{
content: `[Council] Synthesize`,
status: synthStatus,
activeForm:
synthStatus === 'completed'
? 'Council results ready'
: synthStatus === 'in_progress'
? 'Ready to synthesize'
: 'Waiting to synthesize',
},
];
return {
progress: { done, total, overallState: String(statusPayload.overallState || '') },
codex: { update_plan: { plan: codexPlan } },
claude: { todo_write: { todos: claudeTodos } },
};
}
function computeStatusPayload(jobDir) {
const resolvedJobDir = path.resolve(jobDir);
if (!fs.existsSync(resolvedJobDir)) exitWithError(`jobDir not found: ${resolvedJobDir}`);
const jobMeta = readJsonIfExists(path.join(resolvedJobDir, 'job.json'));
if (!jobMeta) exitWithError(`job.json not found: ${path.join(resolvedJobDir, 'job.json')}`);
const membersRoot = path.join(resolvedJobDir, 'members');
if (!fs.existsSync(membersRoot)) exitWithError(`members folder not found: ${membersRoot}`);
const members = [];
for (const entry of fs.readdirSync(membersRoot)) {
const statusPath = path.join(membersRoot, entry, 'status.json');
const status = readJsonIfExists(statusPath);
if (status) members.push({ safeName: entry, ...status });
}
const totals = { queued: 0, running: 0, done: 0, error: 0, missing_cli: 0, timed_out: 0, canceled: 0 };
for (const m of members) {
const state = String(m.state || 'unknown');
if (Object.prototype.hasOwnProperty.call(totals, state)) totals[state]++;
}
const allDone = totals.running === 0 && totals.queued === 0;
const overallState = allDone ? 'done' : totals.running > 0 ? 'running' : 'queued';
return {
jobDir: resolvedJobDir,
id: jobMeta.id || null,
chairmanRole: jobMeta.chairmanRole || null,
overallState,
counts: { total: members.length, ...totals },
members: members
.map((m) => ({
member: m.member,
state: m.state,
startedAt: m.startedAt || null,
finishedAt: m.finishedAt || null,
exitCode: m.exitCode != null ? m.exitCode : null,
message: m.message || null,
}))
.sort((a, b) => String(a.member).localeCompare(String(b.member))),
};
}
function parseArgs(argv) {
const args = argv.slice(2);
const out = { _: [] };
const booleanFlags = new Set([
'json',
'text',
'checklist',
'help',
'h',
'verbose',
'include-chairman',
'exclude-chairman',
]);
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (a === '--') {
out._.push(...args.slice(i + 1));
break;
}
if (!a.startsWith('--')) {
out._.push(a);
continue;
}
const [key, rawValue] = a.split('=', 2);
if (rawValue != null) {
out[key.slice(2)] = rawValue;
continue;
}
const normalizedKey = key.slice(2);
if (booleanFlags.has(normalizedKey)) {
out[normalizedKey] = true;
continue;
}
const next = args[i + 1];
if (next == null || next.startsWith('--')) {
out[normalizedKey] = true;
continue;
}
out[normalizedKey] = next;
i++;
}
return out;
}
function printHelp() {
process.stdout.write(`Agent Council (job mode)
Usage:
council-job.sh start [--config path] [--chairman auto|claude|codex|...] [--jobs-dir path] [--json] "question"
council-job.sh status [--json|--text|--checklist] [--verbose] <jobDir>
council-job.sh wait [--cursor CURSOR] [--bucket auto|N] [--interval-ms N] [--timeout-ms N] <jobDir>
council-job.sh results [--json] <jobDir>
council-job.sh stop <jobDir>
council-job.sh clean <jobDir>
Notes:
- start returns immediately and runs members in parallel via detached Node workers
- poll status with repeated short calls to update TODO/plan UIs in host agents
- wait prints JSON by default and blocks until meaningful progress occurs, so you don't spam tool cells
`);
}
function cmdStart(options, prompt) {
const configPath = options.config || process.env.COUNCIL_CONFIG || resolveDefaultConfigFile();
const jobsDir =
options['jobs-dir'] || process.env.COUNCIL_JOBS_DIR || path.join(SKILL_DIR, '.jobs');
ensureDir(jobsDir);
const hostRole = detectHostRole();
const config = parseCouncilConfig(configPath);
const chairmanRoleRaw = options.chairman || process.env.COUNCIL_CHAIRMAN || config.council.chairman.role || 'auto';
const chairmanRole = resolveAutoRole(chairmanRoleRaw, hostRole);
const includeChairman = Boolean(options['include-chairman']);
const excludeChairmanOverride =
options['exclude-chairman'] != null ? true : options['include-chairman'] != null ? false : null;
const excludeSetting = normalizeBool(config.council.settings.exclude_chairman_from_members);
const excludeChairmanFromMembers =
excludeChairmanOverride != null ? excludeChairmanOverride : excludeSetting != null ? excludeSetting : true;
const timeoutSetting = Number(config.council.settings.timeout || 0);
const timeoutOverride = options.timeout != null ? Number(options.timeout) : null;
const timeoutSec = Number.isFinite(timeoutOverride) && timeoutOverride > 0 ? timeoutOverride : timeoutSetting > 0 ? timeoutSetting : 0;
const requestedMembers = config.council.members || [];
const members = requestedMembers.filter((m) => {
if (!m || !m.name || !m.command) return false;
const nameLc = String(m.name).toLowerCase();
if (excludeChairmanFromMembers && !includeChairman && nameLc === chairmanRole) return false;
return true;
});
const jobId = `${new Date().toISOString().replace(/[:.]/g, '').replace('T', '-').slice(0, 15)}-${crypto
.randomBytes(3)
.toString('hex')}`;
const jobDir = path.join(jobsDir, `council-${jobId}`);
const membersDir = path.join(jobDir, 'members');
ensureDir(membersDir);
fs.writeFileSync(path.join(jobDir, 'prompt.txt'), String(prompt), 'utf8');
const jobMeta = {
id: `council-${jobId}`,
createdAt: new Date().toISOString(),
configPath,
hostRole,
chairmanRole,
settings: {
excludeChairmanFromMembers,
timeoutSec: timeoutSec || null,
},
members: members.map((m) => ({
name: String(m.name),
command: String(m.command),
emoji: m.emoji ? String(m.emoji) : null,
color: m.color ? String(m.color) : null,
})),
};
atomicWriteJson(path.join(jobDir, 'job.json'), jobMeta);
for (const member of members) {
const name = String(member.name);
const safeName = safeFileName(name);
const memberDir = path.join(membersDir, safeName);
ensureDir(memberDir);
atomicWriteJson(path.join(memberDir, 'status.json'), {
member: name,
state: 'queued',
queuedAt: new Date().toISOString(),
command: String(member.command),
});
const workerArgs = [
WORKER_PATH,
'--job-dir',
jobDir,
'--member',
name,
'--safe-member',
safeName,
'--command',
String(member.command),
];
if (timeoutSec && Number.isFinite(timeoutSec) && timeoutSec > 0) {
workerArgs.push('--timeout', String(timeoutSec));
}
const child = spawn(process.execPath, workerArgs, {
detached: true,
stdio: 'ignore',
env: process.env,
});
child.unref();
}
if (options.json) {
process.stdout.write(`${JSON.stringify({ jobDir, ...jobMeta }, null, 2)}\n`);
} else {
process.stdout.write(`${jobDir}\n`);
}
}
function cmdStatus(options, jobDir) {
const payload = computeStatusPayload(jobDir);
const wantChecklist = Boolean(options.checklist) && !options.json;
if (wantChecklist) {
const done = computeTerminalDoneCount(payload.counts);
const headerId = payload.id ? ` (${payload.id})` : '';
process.stdout.write(`Agent Council${headerId}\n`);
process.stdout.write(
`Progress: ${done}/${payload.counts.total} done (running ${payload.counts.running}, queued ${payload.counts.queued})\n`
);
for (const m of payload.members) {
const state = String(m.state || '');
const mark =
state === 'done'
? '[x]'
: state === 'running' || state === 'queued'
? '[ ]'
: state
? '[!]'
: '[ ]';
const exitInfo = m.exitCode != null ? ` (exit ${m.exitCode})` : '';
process.stdout.write(`${mark} ${m.member} — ${state}${exitInfo}\n`);
}
return;
}
const wantText = Boolean(options.text) && !options.json;
if (wantText) {
const done = computeTerminalDoneCount(payload.counts);
process.stdout.write(`members ${done}/${payload.counts.total} done; running=${payload.counts.running} queued=${payload.counts.queued}\n`);
if (options.verbose) {
for (const m of payload.members) {
process.stdout.write(`- ${m.member}: ${m.state}${m.exitCode != null ? ` (exit ${m.exitCode})` : ''}\n`);
}
}
return;
}
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
}
function parseWaitCursor(value) {
const raw = String(value || '').trim();
if (!raw) return null;
const parts = raw.split(':');
const version = parts[0];
if (version === 'v1' && parts.length === 4) {
const bucketSize = Number(parts[1]);
const doneBucket = Number(parts[2]);
const isDone = parts[3] === '1';
if (!Number.isFinite(bucketSize) || bucketSize <= 0) return null;
if (!Number.isFinite(doneBucket) || doneBucket < 0) return null;
return { version, bucketSize, dispatchBucket: 0, doneBucket, isDone };
}
if (version === 'v2' && parts.length === 5) {
const bucketSize = Number(parts[1]);
const dispatchBucket = Number(parts[2]);
const doneBucket = Number(parts[3]);
const isDone = parts[4] === '1';
if (!Number.isFinite(bucketSize) || bucketSize <= 0) return null;
if (!Number.isFinite(dispatchBucket) || dispatchBucket < 0) return null;
if (!Number.isFinite(doneBucket) || doneBucket < 0) return null;
return { version, bucketSize, dispatchBucket, doneBucket, isDone };
}
return null;
}
function formatWaitCursor(bucketSize, dispatchBucket, doneBucket, isDone) {
return `v2:${bucketSize}:${dispatchBucket}:${doneBucket}:${isDone ? 1 : 0}`;
}
function asWaitPayload(statusPayload) {
const members = Array.isArray(statusPayload.members) ? statusPayload.members : [];
return {
jobDir: statusPayload.jobDir,
id: statusPayload.id,
chairmanRole: statusPayload.chairmanRole,
overallState: statusPayload.overallState,
counts: statusPayload.counts,
members: members.map((m) => ({
member: m.member,
state: m.state,
exitCode: m.exitCode != null ? m.exitCode : null,
message: m.message || null,
})),
ui: buildCouncilUiPayload(statusPayload),
};
}
function resolveBucketSize(options, total, prevCursor) {
const raw = options.bucket != null ? options.bucket : options['bucket-size'];
if (raw == null || raw === true) {
if (prevCursor && prevCursor.bucketSize) return prevCursor.bucketSize;
} else {
const asString = String(raw).trim().toLowerCase();
if (asString !== 'auto') {
const num = Number(asString);
if (!Number.isFinite(num) || num <= 0) exitWithError(`wait: invalid --bucket: ${raw}`);
return Math.trunc(num);
}
}
// Auto-bucket: target ~5 updates total.
const totalNum = Number(total || 0);
if (!Number.isFinite(totalNum) || totalNum <= 0) return 1;
return Math.max(1, Math.ceil(totalNum / 5));
}
function cmdWait(options, jobDir) {
const resolvedJobDir = path.resolve(jobDir);
const cursorFilePath = path.join(resolvedJobDir, '.wait_cursor');
const prevCursorRaw =
options.cursor != null
? String(options.cursor)
: fs.existsSync(cursorFilePath)
? String(fs.readFileSync(cursorFilePath, 'utf8')).trim()
: '';
const prevCursor = parseWaitCursor(prevCursorRaw);
const intervalMsRaw = options['interval-ms'] != null ? options['interval-ms'] : 250;
const intervalMs = Math.max(50, Math.trunc(Number(intervalMsRaw)));
if (!Number.isFinite(intervalMs) || intervalMs <= 0) exitWithError(`wait: invalid --interval-ms: ${intervalMsRaw}`);
const timeoutMsRaw = options['timeout-ms'] != null ? options['timeout-ms'] : 0;
const timeoutMs = Math.trunc(Number(timeoutMsRaw));
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) exitWithError(`wait: invalid --timeout-ms: ${timeoutMsRaw}`);
// Always read once to decide bucket sizing and (when no cursor is given) return immediately.
let payload = computeStatusPayload(jobDir);
const bucketSize = resolveBucketSize(options, payload.counts.total, prevCursor);
const doneCount = computeTerminalDoneCount(payload.counts);
const isDone = payload.overallState === 'done';
const total = Number(payload.counts.total || 0);
const queued = Number(payload.counts.queued || 0);
const dispatchBucket = queued === 0 && total > 0 ? 1 : 0;
const doneBucket = Math.floor(doneCount / bucketSize);
const cursor = formatWaitCursor(bucketSize, dispatchBucket, doneBucket, isDone);
if (!prevCursor) {
fs.writeFileSync(cursorFilePath, cursor, 'utf8');
process.stdout.write(`${JSON.stringify({ ...asWaitPayload(payload), cursor }, null, 2)}\n`);
return;
}
const start = Date.now();
while (cursor === prevCursorRaw) {
if (timeoutMs > 0 && Date.now() - start >= timeoutMs) break;
sleepMs(intervalMs);
payload = computeStatusPayload(jobDir);
const d = computeTerminalDoneCount(payload.counts);
const doneFlag = payload.overallState === 'done';
const totalCount = Number(payload.counts.total || 0);
const queuedCount = Number(payload.counts.queued || 0);
const dispatchB = queuedCount === 0 && totalCount > 0 ? 1 : 0;
const doneB = Math.floor(d / bucketSize);
const nextCursor = formatWaitCursor(bucketSize, dispatchB, doneB, doneFlag);
if (nextCursor !== prevCursorRaw) {
fs.writeFileSync(cursorFilePath, nextCursor, 'utf8');
process.stdout.write(`${JSON.stringify({ ...asWaitPayload(payload), cursor: nextCursor }, null, 2)}\n`);
return;
}
}
// Timeout: return current state (cursor may be unchanged).
const finalPayload = computeStatusPayload(jobDir);
const finalDone = computeTerminalDoneCount(finalPayload.counts);
const finalDoneFlag = finalPayload.overallState === 'done';
const finalTotal = Number(finalPayload.counts.total || 0);
const finalQueued = Number(finalPayload.counts.queued || 0);
const finalDispatchBucket = finalQueued === 0 && finalTotal > 0 ? 1 : 0;
const finalDoneBucket = Math.floor(finalDone / bucketSize);
const finalCursor = formatWaitCursor(bucketSize, finalDispatchBucket, finalDoneBucket, finalDoneFlag);
fs.writeFileSync(cursorFilePath, finalCursor, 'utf8');
process.stdout.write(`${JSON.stringify({ ...asWaitPayload(finalPayload), cursor: finalCursor }, null, 2)}\n`);
}
function cmdResults(options, jobDir) {
const resolvedJobDir = path.resolve(jobDir);
const jobMeta = readJsonIfExists(path.join(resolvedJobDir, 'job.json'));
const membersRoot = path.join(resolvedJobDir, 'members');
const members = [];
if (fs.existsSync(membersRoot)) {
for (const entry of fs.readdirSync(membersRoot)) {
const statusPath = path.join(membersRoot, entry, 'status.json');
const outputPath = path.join(membersRoot, entry, 'output.txt');
const errorPath = path.join(membersRoot, entry, 'error.txt');
const status = readJsonIfExists(statusPath);
if (!status) continue;
const output = fs.existsSync(outputPath) ? fs.readFileSync(outputPath, 'utf8') : '';
const stderr = fs.existsSync(errorPath) ? fs.readFileSync(errorPath, 'utf8') : '';
members.push({ safeName: entry, ...status, output, stderr });
}
}
if (options.json) {
process.stdout.write(
`${JSON.stringify(
{
jobDir: resolvedJobDir,
id: jobMeta ? jobMeta.id : null,
prompt: fs.existsSync(path.join(resolvedJobDir, 'prompt.txt'))
? fs.readFileSync(path.join(resolvedJobDir, 'prompt.txt'), 'utf8')
: null,
members: members
.map((m) => ({
member: m.member,
state: m.state,
exitCode: m.exitCode != null ? m.exitCode : null,
message: m.message || null,
output: m.output,
stderr: m.stderr,
}))
.sort((a, b) => String(a.member).localeCompare(String(b.member))),
},
null,
2
)}\n`
);
return;
}
for (const m of members.sort((a, b) => String(a.member).localeCompare(String(b.member)))) {
process.stdout.write(`\n=== ${m.member} (${m.state}) ===\n`);
if (m.message) process.stdout.write(`${m.message}\n`);
process.stdout.write(m.output || '');
if (!m.output && m.stderr) {
process.stdout.write('\n');
process.stdout.write(m.stderr);
}
process.stdout.write('\n');
}
}
function cmdStop(_options, jobDir) {
const resolvedJobDir = path.resolve(jobDir);
const membersRoot = path.join(resolvedJobDir, 'members');
if (!fs.existsSync(membersRoot)) exitWithError(`No members folder found: ${membersRoot}`);
let stoppedAny = false;
for (const entry of fs.readdirSync(membersRoot)) {
const statusPath = path.join(membersRoot, entry, 'status.json');
const status = readJsonIfExists(statusPath);
if (!status) continue;
if (status.state !== 'running') continue;
if (!status.pid) continue;
try {
process.kill(Number(status.pid), 'SIGTERM');
stoppedAny = true;
} catch {
// ignore
}
}
process.stdout.write(stoppedAny ? 'stop: sent SIGTERM to running members\n' : 'stop: no running members\n');
}
function cmdClean(_options, jobDir) {
const resolvedJobDir = path.resolve(jobDir);
fs.rmSync(resolvedJobDir, { recursive: true, force: true });
process.stdout.write(`cleaned: ${resolvedJobDir}\n`);
}
function main() {
const options = parseArgs(process.argv);
const [command, ...rest] = options._;
if (!command || options.help || options.h) {
printHelp();
return;
}
if (command === 'start') {
const prompt = rest.join(' ').trim();
if (!prompt) exitWithError('start: missing prompt');
cmdStart(options, prompt);
return;
}
if (command === 'status') {
const jobDir = rest[0];
if (!jobDir) exitWithError('status: missing jobDir');
cmdStatus(options, jobDir);
return;
}
if (command === 'wait') {
const jobDir = rest[0];
if (!jobDir) exitWithError('wait: missing jobDir');
cmdWait(options, jobDir);
return;
}
if (command === 'results') {
const jobDir = rest[0];
if (!jobDir) exitWithError('results: missing jobDir');
cmdResults(options, jobDir);
return;
}
if (command === 'stop') {
const jobDir = rest[0];
if (!jobDir) exitWithError('stop: missing jobDir');
cmdStop(options, jobDir);
return;
}
if (command === 'clean') {
const jobDir = rest[0];
if (!jobDir) exitWithError('clean: missing jobDir');
cmdClean(options, jobDir);
return;
}
exitWithError(`Unknown command: ${command}`);
}
if (require.main === module) {
main();
}
================================================
FILE: plugins/agent-council/skills/agent-council/scripts/council-job.sh
================================================
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if ! command -v node >/dev/null 2>&1; then
echo "Error: Node.js is required to run Agent Council job mode." >&2
echo "Install Node.js and try again (plugin installs cannot bundle Node)." >&2
echo "" >&2
echo "macOS (Homebrew): brew install node" >&2
echo "Or download from: https://nodejs.org/" >&2
exit 127
fi
exec node "$SCRIPT_DIR/council-job.js" "$@"
================================================
FILE: plugins/agent-council/skills/agent-council/scripts/council.sh
================================================
#!/bin/bash
#
# Agent Council (job mode default)
#
# Subcommands:
# council.sh start [options] "question" # returns JOB_DIR immediately
# council.sh status [--json|--text|--checklist] JOB_DIR # poll progress
# council.sh wait [--cursor CURSOR] [--bucket auto|N] [--interval-ms N] [--timeout-ms N] JOB_DIR
# council.sh results [--json] JOB_DIR # print collected outputs
# council.sh stop JOB_DIR # best-effort stop running members
# council.sh clean JOB_DIR # remove job directory
#
# One-shot:
# council.sh "question"
# (in a real terminal: starts a job, waits for completion, prints results, cleans up)
# (in host-agent tool UIs: returns a single `wait` JSON payload immediately; host drives progress + results)
#
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
JOB_SCRIPT="$SCRIPT_DIR/council-job.sh"
usage() {
cat <<EOF
Agent Council
Default mode is job-based parallel execution (pollable).
Usage:
$(basename "$0") start [options] "question"
$(basename "$0") status [--json|--text|--checklist] <jobDir>
$(basename "$0") wait [--cursor CURSOR] [--bucket auto|N] [--interval-ms N] [--timeout-ms N] <jobDir>
$(basename "$0") results [--json] <jobDir>
$(basename "$0") stop <jobDir>
$(basename "$0") clean <jobDir>
One-shot:
$(basename "$0") "question"
EOF
}
if [ $# -eq 0 ]; then
usage
exit 1
fi
case "$1" in
-h|--help|help)
usage
exit 0
;;
esac
if ! command -v node >/dev/null 2>&1; then
echo "Error: Node.js is required to run Agent Council." >&2
echo "Claude Code plugins cannot bundle or auto-install Node." >&2
echo "" >&2
echo "macOS (Homebrew): brew install node" >&2
echo "Or download from: https://nodejs.org/" >&2
exit 127
fi
case "$1" in
start|status|wait|results|stop|clean)
exec "$JOB_SCRIPT" "$@"
;;
esac
in_host_agent_context() {
if [ -n "${CODEX_CACHE_FILE:-}" ]; then
return 0
fi
case "$SCRIPT_DIR" in
*/.codex/skills/*|*/.claude/skills/*)
# Tool-call environments typically do not provide a real TTY on stdout/stderr.
if [ ! -t 1 ] && [ ! -t 2 ]; then
return 0
fi
;;
esac
return 1
}
JOB_DIR="$("$JOB_SCRIPT" start "$@")"
# Host agents (Codex CLI / Claude Code) cannot update native TODO/plan UIs while a long-running
# command is executing. If we're in a host agent context, return immediately with a single `wait`
# JSON payload (includes `.ui.codex.update_plan.plan` / `.ui.claude.todo_write.todos`) and let the
# host agent drive progress updates with repeated short `wait` calls + native UI updates.
if in_host_agent_context; then
exec "$JOB_SCRIPT" wait "$JOB_DIR"
fi
echo "council: started ${JOB_DIR}" >&2
cleanup_on_signal() {
if [ -n "${JOB_DIR:-}" ] && [ -d "$JOB_DIR" ]; then
"$JOB_SCRIPT" stop "$JOB_DIR" >/dev/null 2>&1 || true
"$JOB_SCRIPT" clean "$JOB_DIR" >/dev/null 2>&1 || true
fi
exit 130
}
trap cleanup_on_signal INT TERM
while true; do
WAIT_JSON="$("$JOB_SCRIPT" wait "$JOB_DIR")"
OVERALL="$(printf '%s' "$WAIT_JSON" | node -e '
const fs=require("fs");
const d=JSON.parse(fs.readFileSync(0,"utf8"));
process.stdout.write(String(d.overallState||""));
')"
"$JOB_SCRIPT" status --text "$JOB_DIR" >&2
if [ "$OVERALL" = "done" ]; then
break
fi
done
trap - INT TERM
"$JOB_SCRIPT" results "$JOB_DIR"
"$JOB_SCRIPT" clean "$JOB_DIR" >/dev/null
================================================
FILE: plugins/clarify/.claude-plugin/plugin.json
================================================
{
"name": "clarify",
"version": "2.0.0",
"description": "Three lenses for clarity: vague requirements → specs (vague), strategy blind spots → 4-quadrant playbook (unknown), content vs form → leverage shift (metamedium)",
"author": {
"name": "Team Attention",
"url": "https://github.com/team-attention"
},
"repository": "https://github.com/team-attention/plugins-for-claude-natives",
"license": "MIT",
"keywords": ["claude-code", "plugin", "requirements", "clarification", "known-unknown", "strategy", "metamedium", "content-form"]
}
================================================
FILE: plugins/clarify/skills/metamedium/SKILL.md
================================================
---
name: metamedium
description: This skill should be used when the user is building, planning, or strategizing and the key question is whether to optimize content (what) or change form (how/medium). Trigger on "내용 vs 형식", "content vs form", "metamedium", "형식을 바꿔볼까", "새로운 포맷", "관점 전환", "perspective shift", "다른 방법 없을까", "같은 방식이 안 먹혀", "diminishing returns". Applies Alan Kay's metamedium concept to surface form-level alternatives. For requirement clarification use vague; for strategy blind spots use unknown.
---
# Metamedium: Content vs Form Lens
Distinguish **content** (what is being said/built) from **form** (the medium/structure it's delivered through) to surface whether the real leverage is in optimizing content or inventing a new form. Based on Alan Kay's metamedium concept.
> "A change of perspective is worth 80 IQ points." — Alan Kay
## Core Concept
Most people only change **content** — what they say, write, or build. The real leverage comes from changing **form** — the medium, format, or structure itself.
| | Content (what) | Form (how/medium) |
|--|----------------|-------------------|
| Example | Writing a LinkedIn post | Building a tool that generates posts from client work |
| Example | Writing unit tests manually | Building a test generator from type signatures |
| Example | Giving a workshop | Inventing a format where attendees co-create artifacts |
| Leverage | Linear — each piece is one output | Exponential — each new form enables infinite content |
## When to Use
- Planning a project and unsure whether to optimize the output or the process
- Stuck optimizing content with diminishing returns
- Building something and want to check if form-level change would yield more leverage
- Evaluating whether "more of the same" or "something structurally different" is the right move
For requirement clarification, use the **vague** skill. For strategy blind spot analysis, use the **unknown** skill.
## Protocol
**ALWAYS use the AskUserQuestion tool** for the fork question in Phase 2 — never ask content/form choices in plain text.
### Phase 1: Identify and Label
Read the user's current work, plan, or task. Classify each component as content or form:
```
[CONTENT] Writing a blog post about AI consulting
[FORM] Building a pipeline that turns consulting retros into blog posts
[CONTENT] Deploying a new API endpoint
[FORM] Building a codegen that auto-generates endpoints from schemas
[CONTENT] Fixing a flaky test
[FORM] Building a test infrastructure that prevents flaky tests by design
```
Present the labeling to the user as a brief diagnosis.
### Phase 2: Surface the Fork
Use AskUserQuestion to present the content/form choice:
```
questions:
- question: "This is currently [CONTENT/FORM]-level work. Where should effort go?"
header: "Level"
options:
- label: "Proceed with content"
description: "Optimize within the current form — faster, lower risk"
- label: "Explore form change"
description: "What if the medium/structure itself changed? Higher leverage"
- label: "Content now, note form"
description: "Do the content work, but flag the form opportunity for later"
multiSelect: false
```
### Phase 3: Branch
**If "Proceed with content"**: Acknowledge and proceed. Include a `Form Opportunity` note in the output for future reference.
**If "Explore form change"**: Generate 2-3 form alternatives. For each alternative:
- What the new form looks like concretely
- What new properties it would have (automatic, repeatable, scalable, composable)
- Minimum viable version to test the form
**If "Content now, note form"**: Proceed with content work. Append the form opportunity to the output.
### Output
Append to any deliverable or present standalone:
```markdown
## Content/Form Analysis
**Current work**: [description]
**Classification**: [CONTENT / FORM]
### Form Opportunity
| | Detail |
|---|--------|
| **Alternative form** | [what it would look like] |
| **New properties** | [what it enables that current form doesn't] |
| **Minimum test** | [smallest version to validate] |
| **Status** | [exploring / noted for later / not applicable] |
```
## The Metamedium Question
When stuck or when optimizing yields diminishing returns:
> **"What new form/medium could make this problem disappear?"**
Examples:
- Stuck writing more posts? → A format that turns client work into posts automatically
- Test coverage plateauing? → A tool that generates tests from type signatures
- Onboarding too slow? → A self-guided format where the codebase teaches itself
## Tetris Test
> Change the blocks. Then you realize the original blocks were mathematically calculated.
To truly understand a form, try to change it. The constraints discovered ARE the form's intelligence. Perspective shifts happen not by thinking harder, but by touching the form itself.
## Anti-Patterns
- Treating all work as content optimization when form change is available
- Building "better content" when the form is the bottleneck
- Assuming the current medium/format is fixed and only content can vary
- Confusing incremental content improvement with form invention
## Rules
1. **Always label**: Tag work as content or form
2. **Content is fine**: Not everything needs form change — but always note the option
3. **Form yields power**: New form = new medium = exponential leverage
4. **Code is metamedium**: The ability to code means the ability to change form
5. **Touch to understand**: Change the form to discover why it was designed that way
## Additional Resources
For Alan Kay's original ideas and source quotes, see `references/alan-kay-quotes.md`.
================================================
FILE: plugins/clarify/skills/metamedium/references/alan-kay-quotes.md
================================================
# Alan Kay: Source Quotes and Context
Key quotes and context that inform the metamedium lens.
## "A change of perspective is worth 80 IQ points"
**Origin**: July 1982, lecture to the Apple Macintosh team. Recorded by Andy Hertzfeld.
**Context**: The Dynabook team at Xerox PARC sought truly innovative approaches to computing. Kay believed having a new way of seeing a problem is the most effective way of moving to a solution — more effective than raw intelligence applied to the same perspective.
**Application**: When stuck, don't think harder. Change the frame. The content/form distinction IS such a frame change.
## "The best way to predict the future is to invent it"
**Origin**: 1971, Xerox PARC.
**Context**: At PARC, the researchers' maxim encouraged a proactive approach — rather than predicting what might happen based on current trends, create new technologies that define future possibilities.
**Application**: Don't optimize content within existing forms. Invent new forms.
## "The computer is the first metamedium"
**Origin**: 1977, "Personal Dynamic Media" paper by Alan Kay and Adele Goldberg.
**Full quote**: "The computer is the first metamedium, and as such it has degrees of freedom for representation and expression never before encountered and as yet barely investigated."
**Context**: Kay realized computers could simulate ALL existing media (text, images, music, film) AND create entirely new media that had never existed before. This is what he called "metamedium" — a medium that contains and generates other media.
**Application**: Coding doesn't just let you create content faster. It lets you invent new kinds of media. This is qualitatively different from any previous tool.
## On Literacy
**Quote**: "The ability to 'read' a medium means you can access materials and tools generated by others. The ability to 'write' in a medium means you can generate materials and tools for others. You must have both to be literate."
**Application**: True computer literacy is not using apps (reading). It's creating new forms of expression and tools for others (writing). Most "AI literacy" today is reading-level only (using ChatGPT). Form-level literacy means building new AI-powered formats.
## On New Representations
**Quote**: "These new media use already existing representational formats as their building blocks, while adding many new previously nonexistent properties."
**Application**: The most powerful innovations don't replace existing forms — they use them as building blocks to create something with properties that didn't exist before. Example: A tool that combines consulting retros + AI + LinkedIn into an automatic content pipeline isn't just "better content." It's a new form with properties (automatic, repeatable, scalable) that the individual components didn't have.
## Sources
- [Quote Investigator: Point of View](https://quoteinvestigator.com/2018/05/29/pov/)
- [Quote Investigator: Invent the Future](https://quoteinvestigator.com/2012/09/27/invent-the-future/)
- [Personal Dynamic Media (1977 paper)](https://www.newmediareader.com/book_samples/nmr-26-kay.pdf)
- [Alan Kay's Universal Media Machine - Lev Manovich](https://manovich.net/content/04-projects/055-alan-kay-s-universal-media-machine/51_article_2006.pdf)
- [Alan Kay - ACM Turing Award](https://amturing.acm.org/award_winners/kay_3972189.cfm)
- [Computer as Metamedium](https://blogs.commons.georgetown.edu/cctp-711-fall2017/2017/11/09/computer-as-metamedium/)
================================================
FILE: plugins/clarify/skills/unknown/SKILL.md
================================================
---
name: unknown
description: This skill should be used when the user provides a strategy, plan, or decision document and wants to surface hidden assumptions and blind spots using the Known/Unknown 4-quadrant framework. Trigger on "known unknown", "4분면 분석", "blind spots", "뭘 놓치고 있지", "뭘 모르는지 모르겠어", "전략 점검", "전략 분석", "assumption check", "가정 점검", "quadrant analysis", "what am I missing". Strategy-level blind spot analysis with hypothesis-driven questioning. For requirement clarification use vague; for content-vs-form reframing use metamedium.
---
# Unknown: Surface Blind Spots with Known/Unknown Quadrants
Surface hidden assumptions and blind spots in any strategy, plan, or decision using the Known/Unknown quadrant framework and hypothesis-driven questioning.
## When to Use
- Strategy or planning documents that need scrutiny
- Decisions with unclear direction or hidden assumptions
- Any situation where "what we don't know" matters more than "what we do know"
For specific requirement clarification (feature requests, bug reports), use the **vague** skill. For content-vs-form reframing (optimizing within a form vs inventing a new form), use the **metamedium** skill.
## Core Principle: Hypothesis-as-Options
**ALWAYS use the AskUserQuestion tool** for every question in R1/R2/R3 — never ask questions in plain text. The structured format enforces hypothesis-as-options and limits choice fatigue.
Present hypotheses as options instead of open questions. The hypotheses ARE the analysis — by designing good options, 80% of the analytical work is done before the user even answers. The user's job is to confirm, correct, or surprise.
```
BAD: "Why can't you do video content?" ← open question, high load
GOOD: "Time / Skill gap / No guests / High bar" ← pick one or more
```
- Each option IS a testable hypothesis about the user's situation
- Use multiSelect: true to catch compound causes
- "Other" is always available for out-of-frame answers
## 3-Round Depth Pattern
| Round | Purpose | Questions | Key trait |
|-------|---------|-----------|-----------|
| R1 | Validate draft quadrant | 3-4 | Broad, covers all quadrants |
| R2 | Drill into weak spots | 2-3 | Targeted, follows R1 answers |
| R3 | Nail execution details | 2-3 | Specific, optional |
**Critical**: Generate Round N questions from Round N-1 answers. Never use pre-prepared questions across rounds. Cap total at 7-10 questions.
## Protocol
### Phase 1: Intake
**File provided**: Read and extract goals, components, implicit assumptions, missing elements.
**Topic keyword only**: Start directly with R1 questions to establish scope. The draft in Phase 3 will be rougher but R1 corrects it.
### Phase 2: Context
Gather related context to find Unknown Knowns — assets the user may not realize they have:
- **Glob** for related files: CLAUDE.md, README, decision records, past analyses in the project
- **Read** project context: recent goals, team structure, active initiatives
- **Identify** underutilized assets: existing tools/skills not in use, past projects with reusable patterns, team expertise not leveraged
Items discovered here become UK candidates and options in R1 questions.
### Phase 3: Draft + R1 Questions
Generate an initial 4-quadrant classification. **The draft is intentionally rough** — R1 exists to correct it, not confirm it. Err on the side of classifying uncertain items as KU rather than KK.
Design R1 questions to test quadrant boundaries. **Batch all R1 questions into a single AskUserQuestion call** (max 4 questions):
| Target | Pattern | Example |
|--------|---------|---------|
| KK | "Is this really certain?" | "Primary revenue source?" (options) |
| KU | "Where's the weakest link?" | "Which flywheel connection is weakest?" |
| UK | "What exists but isn't used?" | Based on context findings |
| UU | "What's the biggest fear?" | Risk scenarios as options |
### Phase 4: Deepen + R2 Questions
Analyze R1 answers. Find the most uncertain area and drill in.
**R2 triggers**: compound answers (messy area), unexpected answers (draft wrong), "Other" selected (outside frame).
For detailed R2 question types, see `references/question-design.md`.
### Phase 5: Execute + R3 Questions (Optional)
After priorities are set, nail down execution details for top items. Skip if R2 already provides enough detail.
### Phase 6: Playbook Output
Generate a structured 4-quadrant playbook file. For the complete output template, see `references/playbook-template.md`.
**Output structure:**
```
# {Topic}: Known/Unknown Quadrant Analysis
## Current State Diagnosis
## Quadrant Matrix (ASCII with resource %)
## 1. Known Knowns: Systematize (60%)
## 2. Known Unknowns: Design Experiments (25%)
- Each KU: Diagnosis → Experiment → Success Criteria → Deadline → Promotion Condition
## 3. Unknown Knowns: Leverage (10%)
## 4. Unknown Unknowns: Set Up Antennas (5%)
## Strategic Decision: What to Stop
## Execution Roadmap (week-by-week)
## Core Principles (3-5 decision criteria)
```
**Resource percentages (60/25/10/5) are defaults.** Adjust based on context — e.g., a startup exploring product-market fit may allocate 40% KU and 30% KK.
## Anti-Patterns
- Open questions ("What would you like to do?") — use hypothesis options
- 5+ options per question — causes choice fatigue
- Ignoring R1 answers when designing R2 — performative questioning
- Equal depth on all quadrants — wastes time, loses focus
- No "stop doing" section — adding without subtracting
## Example
**Input**: Growth strategy document
**R1**: Revenue source? → Workshops. Weakest link? → Biz→Knowledge. Blocker? → Skill gap + high bar (multiSelect). Biggest fear? → Execution scattered.
**R2** (driven by "execution scattered"): What to drop? → Product dev. Why no knowledge→content? → No process + no time + hard to abstract. Role clarity? → Unclear.
**R3**: Video format? → Screen recording. Retro blocker? → Don't know what to capture. What content resonated? → Raw discoveries.
**Key discovery**: Abstraction isn't needed — raw insights work better. Collapsed triple bottleneck into 15-minute pipeline.
## Rules
1. **Hypotheses, not questions**: Every option is a testable hypothesis
2. **Answers drive depth**: R2 from R1, R3 from R2
3. **7-10 questions max**: Beyond this is fatigue
4. **Stop > Start**: Always include "what to stop doing"
5. **Promote or kill**: Every KU gets a promotion condition and a kill condition
6. **Raw > Perfect**: Encourage minimum viable experiments, not perfect plans
7. **Draft is disposable**: The initial quadrant is meant to be corrected
## Additional Resources
### Reference Files
- **`references/question-design.md`** — Detailed question types for each round, trigger conditions, and AskUserQuestion formatting guide
- **`references/playbook-template.md`** — Complete output template with section-by-section guide
================================================
FILE: plugins/clarify/skills/unknown/references/playbook-template.md
================================================
# Playbook Output Template
Complete template for the 4-quadrant playbook generated in Phase 6.
## File Naming
Save as: `{topic}-known-unknown.md` in a location appropriate to the project.
## Template
```markdown
# {Topic}: Known/Unknown Quadrant Analysis
> Based on {source document or conversation}.
> Designed under the constraint that "{key constraint from R1/R2}".
---
## Current State Diagnosis
- **{Finding 1}**: {confirmed fact from R1-R3}
- **{Finding 2}**: {confirmed fact}
- **What to stop doing**: {items user chose to cut}
---
## Quadrant Matrix
```
Known Unknown
+---------------------------+---------------------------+
| | |
| KK: Systematize | KU: Design Experiments |
Known | Resources: 60% | Resources: 25% |
| | |
+---------------------------+---------------------------+
| | |
| UK: Leverage | UU: Set Up Antennas |
Unknown | Resources: 10% | Resources: 5% |
| | |
+---------------------------+---------------------------+
```
---
## 1. Known Knowns: Systematize (60%)
> Confirmed working items. Turn into repeatable systems.
| # | Item | Evidence | Systemization Target |
|---|------|----------|---------------------|
| 1 | **{item}** | {how we know} | {what "systemized" looks like} |
---
## 2. Known Unknowns: Design Experiments (25%)
> Questions with no answer yet. Each gets an experiment.
### KU{N}. {Question}
**Diagnosis**: Why is this unknown?
- {root cause from R2}
**Experiment**:
| Item | Detail |
|------|--------|
| Format | {what to try} |
| Success criteria | {measurable outcome} |
| Deadline | {specific date} |
| Effort | {time/resource estimate} |
**Promotion condition**: {when this becomes a Known Known}
**Kill condition**: {when to abandon this and try something else}
*(Repeat for each prioritized KU)*
---
## 3. Unknown Knowns: Leverage (10%)
> Assets already owned but not utilized. Fastest wins.
| # | Hidden Asset | How to Use | Effort |
|---|-------------|-----------|--------|
| 1 | **{asset}** | {activation method} | Low/Med/High |
---
## 4. Unknown Unknowns: Set Up Antennas (5%)
> Cannot predict. Manage with detection speed + response speed.
| # | Risk/Opportunity | Detection Method | Response Principle |
|---|-----------------|-----------------|-------------------|
| 1 | **{scenario}** | {how to notice early} | {what to do} |
---
## Strategic Decision: What to Stop
| Item | Reason | Restart Condition |
|------|--------|------------------|
| **{item}** | {why stop} | {what would make it worth resuming} |
---
## Execution Roadmap
### Week 1-2
- [ ] {action item}
- [ ] {action item}
### Week 3-4
- [ ] {action item}
### Month 2
- [ ] {action item}
- [ ] Review: promote KUs to KK or kill
---
## Core Principles
1. **{Principle}**: {one-line explanation}
2. **{Principle}**: {one-line explanation}
3. **{Principle}**: {one-line explanation}
```
## Section Writing Guide
### Current State Diagnosis
Summarize only what was confirmed through R1-R3 questioning. Avoid restating the input document — focus on what the conversation revealed that wasn't obvious before.
### Known Knowns
Only include items with clear evidence. If the user said "I think so" without data, it's a KU not a KK.
### Known Unknowns
The most important section. Each KU must have:
- A root cause (why unknown)
- A minimum viable experiment (not a perfect plan)
- A measurable success criteria
- A promotion AND kill condition
### Unknown Knowns
Look for these in:
- Context files the user hasn't referenced
- Tools/skills already built but not used
- Past projects with reusable patterns
- Team members' unused expertise
### Unknown Unknowns
Keep this section short. The point is awareness, not prevention.
Focus on detection speed (how to notice early) and response capacity (having buffer time).
### What to Stop
This section is non-negotiable. Every analysis must include at least one item to stop or pause. Adding without subtracting is the most common failure mode.
### Core Principles
Derive from the conversation, not generic advice. Each principle should be a decision rule that resolves a specific tension discovered during questioning.
================================================
FILE: plugins/clarify/skills/unknown/references/question-design.md
================================================
# Question Design Guide
Detailed patterns for designing hypothesis-driven questions across the 3-round depth pattern.
## AskUserQuestion Formatting
```
question: "Clear, specific question ending with ?"
header: "Short label (max 12 chars)"
options:
- label: "Option A"
description: "Why this matters or what it implies"
- label: "Option B"
description: "Why this matters or what it implies"
multiSelect: true # when compound causes are likely
```
**Rules:**
- 3-4 options per question (never 5+)
- description explains implications, not just restates label
- multiSelect for cause/blocker questions, single for priority/choice questions
## R1 Questions: Validate the Draft
Design one question per quadrant boundary. Goal: confirm or correct the initial classification.
| Quadrant | Question Pattern | Example |
|----------|-----------------|---------|
| **KK** | "What's the confirmed reality?" | "Current revenue source?" with options per hypothesis |
| **KU** | "Where's the weakest link?" | "Which connection in your process is weakest?" |
| **UK** | "What assets exist but aren't used?" | "Which of these do you have but don't leverage?" |
| **UU** | "What's the scariest scenario?" | "Most feared outcome?" with risk scenarios |
**Tip**: If context exploration reveals surprising assets, surface them in the UK question as options.
## R2 Questions: Deepen the Weak Spots
Triggered by R1 answers. Focus on the 1-2 most uncertain areas.
### When to Use Each Type
| R2 Type | Trigger | Example |
|---------|---------|---------|
| **Root cause** | KU has unclear "why" | "Core reason video content isn't happening?" |
| **Feasibility** | Proposed solution seems hard | "Is a 30-min weekly retro realistic? What's blocked it?" |
| **Priority** | Multiple items compete | "Pick top 3 from these 6 Known Unknowns" |
| **Hidden constraint** | Suspected unstated limit | "Tried converting consulting into content before? Result?" |
| **Drop candidate** | "Execution scattered" emerged | "Which of these can be stopped or paused?" |
### Reading R1 Answers
| R1 Signal | R2 Strategy |
|-----------|-------------|
| Compound answer (multiSelect) | That area is complex — break it apart with root cause question |
| Unexpected answer | Draft was wrong — revise quadrant, probe deeper |
| "Other" selected | User sees outside the frame — open exploration |
| Strong conviction | Area is likely KK — validate with evidence question, then move on |
## R3 Questions: Execution Details
Only for the prioritized top items. Skip if R2 provides enough.
| R3 Type | When | Example |
|---------|------|---------|
| Tool/channel | Multiple ways to execute | "Publish via: YouTube Live / Local recording / Podcast?" |
| Pattern ID | Need to design a template | "What type of insight do you find most often in projects?" |
| Past experience | Checking if this was tried before | "Have you tried turning this into content? What worked?" |
| Success signal | Defining "done" | "What response tells you this format is worth repeating?" |
## Common Mistakes
### Asking the same question twice in different words
R1: "What's your biggest challenge?" R2: "What's hardest right now?"
Fix: R2 must drill INTO the R1 answer, not re-ask it.
### Options that aren't real hypotheses
"Option A: Good" "Option B: Bad" "Option C: Maybe"
Fix: Each option should represent a distinct, plausible situation.
### Skipping multiSelect when causes are compound
"Why can't you do video?" with single-select misses "skill gap AND high standards"
Fix: Default to multiSelect for "why/blocker" questions.
### Going past 10 total questions
Fatigue kills quality. If R2 answers are clear, skip R3 entirely.
================================================
FILE: plugins/clarify/skills/vague/SKILL.md
================================================
---
name: vague
description: This skill should be used when the user's request or requirement is ambiguous and needs iterative questioning to become actionable. Trigger on "clarify requirements", "refine requirements", "요구사항 명확히", "요구사항 정리", "뭘 원하는 건지", "make this clearer", "spec this out", "scope this", "/clarify". Turns vague inputs into concrete specs. For strategy blind spots use unknown; for content-vs-form reframing use metamedium.
---
# Vague: Requirement Clarification
Transform vague or ambiguous requirements into precise, actionable specifications through hypothesis-driven questioning. **ALWAYS use the AskUserQuestion tool** — never ask clarifying questions in plain text.
## When to Use
- Ambiguous feature requests ("add a login feature")
- Incomplete bug reports ("the export is broken")
- Underspecified tasks ("make the app faster")
For strategy/planning blind spot analysis, use the **unknown** skill. For content-vs-form reframing, use the **metamedium** skill.
## Core Principle: Hypotheses as Options
Present plausible interpretations as options instead of asking open questions. Each option is a testable hypothesis about what the user actually means.
```
BAD: "What kind of login do you want?" ← open question, high cognitive load
GOOD: "OAuth / Email+Password / SSO / Magic link" ← pick one, lower load
```
## Protocol
### Phase 1: Capture and Diagnose
Record the original requirement verbatim. Identify ambiguities:
- What is unclear or underspecified?
- What assumptions would need to be made?
- What decisions are left to interpretation?
### Phase 2: Iterative Clarification
Use AskUserQuestion to resolve ambiguities. **Batch up to 4 related questions per call.** Each option is a hypothesis about what the user means.
**Cap: 5-8 total questions.** Stop when all critical ambiguities are resolved, OR user indicates "good enough", OR cap reached.
**Example AskUserQuestion call:**
```
questions:
- question: "Which authentication method should the login use?"
header: "Auth method"
options:
- label: "Email + Password"
description: "Traditional signup with email verification"
- label: "OAuth (Google/GitHub)"
description: "Delegated auth, no password management needed"
- label: "Magic link"
description: "Passwordless email-based login"
multiSelect: false
- question: "What should happen after registration?"
header: "Post-signup"
options:
- label: "Immediate access"
description: "User can use the app right away"
- label: "Email verification first"
description: "Must confirm email before access"
multiSelect: false
```
### Phase 3: Before/After Summary
Present the transformation:
```markdown
## Requirement Clarification Summary
### Before (Original)
"{original request verbatim}"
### After (Clarified)
**Goal**: [precise description]
**Scope**: [included and excluded]
**Constraints**: [limitations, preferences]
**Success Criteria**: [how to know when done]
**Decisions Made**:
| Question | Decision |
|----------|----------|
| [ambiguity 1] | [chosen option] |
```
### Phase 4: Save Option
Ask whether to save the clarified requirement to a file. Default location: `requirements/` or project-appropriate directory.
## Ambiguity Categories
| Category | Example Hypotheses |
|----------|-------------------|
| **Scope** | All users / Admins only / Specific roles |
| **Behavior** | Fail silently / Show error / Auto-retry |
| **Interface** | REST API / GraphQL / CLI |
| **Data** | JSON / CSV / Both |
| **Constraints** | <100ms / <1s / No requirement |
| **Priority** | Must-have / Nice-to-have / Future |
## Rules
1. **Hypotheses, not open questions**: Every option is a plausible interpretation
2. **No assumptions**: Ask, don't assume
3. **Preserve intent**: Refine, don't redirect
4. **5-8 questions max**: Beyond this is fatigue
5. **Batch related questions**: Up to 4 per AskUserQuestion call
6. **Track changes**: Always show before/after
================================================
FILE: plugins/dev/.claude-plugin/plugin.json
================================================
{
"name": "dev",
"version": "1.1.0",
"description": "Developer workflow tools: community scanning, technical decision-making",
"author": {
"name": "Team Attention",
"url": "https://github.com/team-attention"
},
"repository": "https://github.com/team-attention/plugins-for-claude-natives",
"license": "MIT",
"keywords": ["claude-code", "plugin", "developer", "workflow", "productivity", "decision-making", "tech-decision"]
}
================================================
FILE: plugins/dev/CLAUDE.md
================================================
# Dev
Developer workflow tools for Claude Code.
## Skills
- `/dev-scan` - 개발 커뮤니티에서 다양한 의견 수집 (Reddit, HN, Dev.to, Lobsters)
- `/tech-decision` - 기술 의사결정 깊이 탐색 (라이브러리 선택, 아키텍처 결정, 구현 방식 비교)
## Agents
- `codebase-explorer` - 기존 코드베이스 분석, 패턴/제약사항 파악
- `docs-researcher` - 공식 문서, 가이드, best practices 리서치
- `tradeoff-analyzer` - 옵션별 pros/cons 정리, 비교 분석
- `decision-synthesizer` - 두괄식 최종 보고서 생성
## 사용 예시
### 기술 의사결정
```
"React vs Vue 뭐가 나을까?"
"상태관리 라이브러리 뭐 쓸지 고민이야"
"모놀리스 vs 마이크로서비스 어떻게 해야 할까?"
```
tech-decision 스킬이 활성화되면:
1. codebase-explorer로 현재 코드 분석
2. docs-researcher로 공식 문서 리서치
3. dev-scan으로 커뮤니티 의견 수집
4. agent-council로 전문가 관점 수집
5. tradeoff-analyzer로 비교 분석
6. decision-synthesizer로 두괄식 최종 보고서 생성
================================================
FILE: plugins/dev/agents/codebase-explorer.md
================================================
---
name: codebase-explorer
description: Use this agent when analyzing existing codebase for technical decisions. Trigger when user needs to understand current code patterns, architecture, constraints, or dependencies before making technology choices.
<example>
Context: User is deciding which state management library to use
user: "우리 프로젝트에 상태관리 뭐 쓸지 고민이야"
assistant: "현재 코드베이스를 먼저 분석해서 기존 패턴과 제약사항을 파악하겠습니다."
<commentary>
Before recommending state management, need to understand current project structure, existing patterns, and constraints.
</commentary>
</example>
<example>
Context: User wants to compare database options
user: "PostgreSQL vs MySQL 어떤 게 나을까?"
assistant: "현재 프로젝트의 데이터 모델과 쿼리 패턴을 분석해보겠습니다."
<commentary>
Database choice depends on current data patterns, so analyze codebase first.
</commentary>
</example>
model: sonnet
color: cyan
tools:
- Read
- Glob
- Grep
---
You are a codebase analysis specialist for technical decision-making.
## Core Mission
Analyze existing codebases to extract information relevant to technical decisions:
- Current architecture and patterns
- Existing dependencies and their usage
- Code conventions and styles
- Technical constraints and limitations
- Integration points and interfaces
## Analysis Process
### 1. Project Structure Discovery
```
Analyze:
├── Package manager & dependencies (package.json, requirements.txt, etc.)
├── Directory structure and organization
├── Configuration files
├── Build/deployment setup
└── Documentation (README, docs/)
```
### 2. Pattern Recognition
Identify:
- **Architectural patterns**: MVC, Clean Architecture, Domain-Driven, etc.
- **State management**: How data flows through the application
- **API patterns**: REST, GraphQL, RPC
- **Error handling**: Current approaches
- **Testing patterns**: Unit, integration, e2e
### 3. Dependency Analysis
For each relevant dependency:
- Version and update status
- Usage extent (how deeply integrated)
- Pain points visible in code (workarounds, TODO comments)
- Compatibility considerations
### 4. Constraint Identification
Look for:
- Performance bottlenecks
- Technical debt markers
- Legacy code that limits choices
- External system dependencies
- Team conventions/standards
## Output Format
```markdown
## 코드베이스 분석 결과
### 1. 프로젝트 개요
- **언어/프레임워크**: [...]
- **프로젝트 규모**: [파일 수, LoC 추정]
- **주요 의존성**: [핵심 라이브러리들]
### 2. 현재 아키텍처
- **패턴**: [식별된 아키텍처 패턴]
- **구조**: [디렉토리 구조 요약]
- **데이터 흐름**: [상태 관리 방식]
### 3. 의사결정 관련 발견사항
#### 기존 패턴
- [패턴 1]: [설명 + 파일 위치]
- [패턴 2]: [설명 + 파일 위치]
#### 제약사항
- [제약 1]: [이유 + 영향]
- [제약 2]: [이유 + 영향]
#### 기회/개선점
- [기회 1]: [설명]
- [기회 2]: [설명]
### 4. 의사결정 시 고려사항
- [고려사항 1]
- [고려사항 2]
- [고려사항 3]
### 5. 관련 파일 목록
- `path/to/file1.ts` - [역할]
- `path/to/file2.ts` - [역할]
```
## Analysis Focus by Decision Type
### Library Selection
Focus on:
- Current similar libraries in use
- Integration patterns
- Bundle size concerns
- Type system usage
### Architecture Decision
Focus on:
- Current module boundaries
- Coupling between components
- Scalability indicators
- Team structure alignment
### Implementation Approach
Focus on:
- Existing similar implementations
- Code style and conventions
- Testing requirements
- Performance characteristics
## Important Guidelines
1. **Be specific**: Reference actual file paths and code patterns
2. **Stay objective**: Report findings without bias toward any option
3. **Prioritize relevance**: Focus on aspects relevant to the decision at hand
4. **Note uncertainty**: Clearly mark assumptions vs. confirmed findings
5. **Consider history**: Look at git history for context when helpful
================================================
FILE: plugins/dev/agents/decision-synthesizer.md
================================================
---
name: decision-synthesizer
description: Use this agent to generate final decision reports with clear recommendations. Trigger after trade-off analysis is complete to produce executive summary and actionable conclusions.
<example>
Context: After comprehensive analysis is done
user: "최종 보고서 만들어줘"
assistant: "수집된 모든 정보를 종합해서 두괄식 최종 보고서를 작성하겠습니다."
<commentary>
Generate final report with conclusion first, then supporting evidence.
</commentary>
</example>
<example>
Context: Analysis complete, need decision
user: "그래서 결론이 뭐야?"
assistant: "분석 결과를 바탕으로 명확한 결론과 근거를 정리하겠습니다."
<commentary>
Synthesize all analysis into clear recommendation.
</commentary>
</example>
model: opus
color: green
tools:
- Read
---
You are a technical decision synthesis expert who produces clear, actionable recommendations from complex analysis.
## Core Mission
Create **두괄식 (conclusion-first)** reports that:
- Lead with clear recommendation
- Provide solid reasoning
- Include actionable next steps
- Address risks and alternatives
## Output Principle: 두괄식 (Conclusion First)
**Every report starts with the answer, then explains why.**
```
❌ Wrong: Background → Analysis → ... → Conclusion
✅ Right: Conclusion → Background → Supporting Analysis
```
## Report Structure
```markdown
# 기술 의사결정 보고서: [주제]
---
## 결론
**추천: [Option Name]**
> [1-2문장으로 핵심 이유. 이 한 문장만 읽어도 의사결정 가능해야 함]
**신뢰도**: [높음 | 중간 | 낮음]
**리스크 수준**: [낮음 | 보통 | 높음 (관리 가능)]
---
## 핵심 근거 (Top 3)
### 1. [가장 중요한 근거]
[구체적 설명 + 출처]
### 2. [두 번째 근거]
[구체적 설명 + 출처]
### 3. [세 번째 근거]
[구체적 설명 + 출처]
---
## 비교 요약
| | [추천 옵션] | [대안 1] | [대안 2] |
|---|-------------|----------|----------|
| 핵심 강점 | ✅ [강점] | [강점] | [강점] |
| 핵심 약점 | ⚠️ [약점] | [약점] | [약점] |
| 우리 상황 적합도 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
---
## 리스크 & 대응
| 리스크 | 확률 | 영향 | 대응 방안 |
|--------|------|------|-----------|
| [리스크 1] | 낮음 | 중간 | [대응] |
| [리스크 2] | 중간 | 낮음 | [대응] |
---
## 대안 시나리오
**만약 [조건 A]가 변한다면:**
→ [다른 옵션] 재검토 권장
**만약 [조건 B]가 발생한다면:**
→ [대응 방안]
---
## 다음 단계
### Must Have (필수)
- [ ] [반드시 해야 하는 액션 1]
- [ ] [반드시 해야 하는 액션 2]
### Recommended (권장)
- [ ] [강력히 권장하는 액션 1]
- [ ] [강력히 권장하는 액션 2]
### Optional (선택)
- [ ] [상황에 따라 고려할 액션]
### 검증 포인트
- [ ] [확인할 사항 1]
- [ ] [확인할 사항 2]
---
## 상세 분석 (참고용)
### 평가 기준별 점수
[상세 비교표...]
### 출처 목록
- [출처 1]
- [출처 2]
- [출처 3]
```
## Quality Standards
### 1. Clarity
- One clear recommendation
- No hedging or vague language
- Specific and actionable
### 2. Evidence-Based
- Every claim has a source
- Confidence levels stated
- Conflicting info addressed
### 3. Context-Aware
- Tailored to specific project
- Considers team capabilities
- Addresses constraints
### 4. Actionable
- Clear next steps
- Defined success criteria
- Risk mitigation included
## Recommendation Confidence Levels
### 높음 (High Confidence)
Use when:
- Multiple reliable sources agree
- Clear winner on most criteria
- Low risk, proven solution
- Strong fit with context
### 중간 (Medium Confidence)
Use when:
- Good option but close alternatives
- Some uncertainty remains
- Context-dependent trade-offs
- Need more validation
### 낮음 (Low Confidence)
Use when:
- Very close call between options
- Significant unknowns
- High context dependency
- Recommend further research
## Handling Edge Cases
### When No Clear Winner
```markdown
## 결론
**상황에 따른 추천:**
- [조건 A]일 경우 → Option X
- [조건 B]일 경우 → Option Y
**결정 핵심 요소**: [어떤 질문에 답하면 결정 가능한지]
```
### When More Info Needed
```markdown
## 결론
**잠정 추천: [Option X]** (추가 검증 필요)
**결정 전 확인 필요:**
1. [확인 사항 1]
2. [확인 사항 2]
```
### When Recommending Against All Options
```markdown
## 결론
**현 옵션들 모두 비추천**
**이유**: [핵심 이유]
**대안 제안**:
- [대안 1]
- [대안 2]
```
## Writing Style
1. **Direct**: "X를 추천한다" not "X가 좋을 수 있다"
2. **Specific**: Numbers, comparisons, examples
3. **Balanced**: Acknowledge trade-offs honestly
4. **Professional**: No hype or marketing language
5. **Korean-friendly**: 자연스러운 한국어 표현 사용
## Final Checklist
Before delivering report:
- [ ] 결론이 맨 처음에 있는가?
- [ ] 한 문장만 읽어도 결론을 알 수 있는가?
- [ ] 모든 주장에 출처가 있는가?
- [ ] 다음 단계가 구체적인가?
- [ ] 리스크와 대안이 명시되어 있는가?
- [ ] 신뢰도 수준이 명시되어 있는가?
================================================
FILE: plugins/dev/agents/docs-researcher.md
================================================
---
name: docs-researcher
description: Use this agent to research official documentation, guides, and best practices for technologies being evaluated. Trigger when comparing libraries, frameworks, or approaches and need authoritative information.
<example>
Context: User comparing React state management options
user: "Redux vs Zustand 비교해줘"
assistant: "각 라이브러리의 공식 문서와 best practices를 리서치하겠습니다."
<commentary>
Need official documentation and guides to provide accurate comparison.
</commentary>
</example>
<example>
Context: User evaluating database options
user: "PostgreSQL이랑 MongoDB 중에 뭐가 나을까?"
assistant: "공식 문서에서 각 DB의 특징과 use case를 조사하겠습니다."
<commentary>
Research official documentation for authoritative feature comparison.
</commentary>
</example>
model: sonnet
color: blue
tools:
- WebSearch
- WebFetch
- Read
- mcp__context7__resolve-library-id
- mcp__context7__query-docs
---
You are a technical documentation researcher specializing in gathering authoritative information for technology decisions.
## Core Mission
Research and synthesize information from:
- Official documentation
- Official guides and tutorials
- Best practices from maintainers
- Performance benchmarks
- Migration guides
- Comparison resources
## Research Process
### 1. Query Generation (5-10 Variations)
각 기술/라이브러리에 대해 **5-10개의 검색 변형** 생성:
```
[기술명] official documentation
[기술명] best practices 2025
[기술명] vs [대안] comparison
[기술명] performance benchmark
[기술명] when to use
[기술명] limitations drawbacks
[기술명] migration guide
"[정확한 에러 메시지]" [기술명]
```
**검색 전략**:
- 한국어 + 영어 둘 다 검색 (커버리지 확대)
- 연도 포함 (최신 정보 우선: "2025", "2024")
- 에러 메시지는 정확히 인용 (따옴표 사용)
- 문제 + 솔루션 키워드 모두 사용
### 2. Identify Research Targets
For each technology option:
- Official documentation site
- GitHub repository (README, docs/)
- Official blog posts
- Release notes and changelogs
### 3. Gather Key Information
For each option, research:
```
├── Core Features
│ ├── Main capabilities
│ ├── Unique selling points
│ └── Limitations (from docs)
│
├── Performance
│ ├── Official benchmarks
│ ├── Size/bundle information
│ └── Scalability claims
│
├── Ecosystem
│ ├── Official plugins/extensions
│ ├── Integration guides
│ └── Tooling support
│
├── Learning Resources
│ ├── Documentation quality
│ ├── Tutorial availability
│ └── Example projects
│
└── Maintenance Status
├── Release frequency
├── Issue response time
└── Roadmap/future plans
```
### 4. Use Context7 for Latest Docs
When available, use Context7 MCP to get up-to-date documentation:
```
1. resolve-library-id: Find correct library ID
2. query-docs: Get specific documentation
```
### 5. Cross-Reference Sources
Validate information across:
- Multiple official sources
- Recent vs. old documentation
- Different versions
## Output Format
```markdown
## 문서 리서치 결과
### [Technology A]
**공식 문서 출처**: [URL]
#### 핵심 특징
- [특징 1]: [설명] (출처: 공식 문서)
- [특징 2]: [설명] (출처: 공식 가이드)
#### 성능 정보
- [성능 특성]: [데이터/수치] (출처: 벤치마크 페이지)
#### Best Practices (공식)
- [Practice 1]
- [Practice 2]
#### 제한사항 (공식 문서 기준)
- [제한 1]
- [제한 2]
#### 학습 리소스
- 문서 품질: [평가]
- 튜토리얼: [있음/없음, 품질]
- 예제: [있음/없음]
#### 유지보수 현황
- 최근 릴리스: [날짜]
- 릴리스 주기: [빈도]
- 이슈 대응: [활발함/보통/느림]
---
### [Technology B]
[동일 구조]
---
### 문서 기반 비교 요약
| 측면 | Tech A | Tech B |
|------|--------|--------|
| 핵심 강점 | [...] | [...] |
| 문서 품질 | [...] | [...] |
| 학습 곡선 | [...] | [...] |
| 성숙도 | [...] | [...] |
### 출처 목록
- [URL 1]: [설명]
- [URL 2]: [설명]
```
## Research Guidelines
### Source Priority
1. **Highest**: Official documentation
2. **High**: Official blog, maintainer statements
3. **Medium**: Official examples, GitHub docs
4. **Lower**: Third-party tutorials (verify accuracy)
### Information Quality
- Always note the source
- Check documentation date/version
- Distinguish facts vs. marketing claims
- Note any conflicting information
### What to Avoid
- Outdated information (check dates)
- Marketing-heavy content without substance
- Unverified third-party claims
- Speculation or rumors
## Search Strategies
### For Libraries
```
"[library name] official documentation"
"[library name] best practices"
"[library name] vs [alternative]"
"[library name] performance benchmark"
"[library name] migration guide"
```
### For Frameworks
```
"[framework] architecture guide"
"[framework] when to use"
"[framework] limitations"
"[framework] enterprise use cases"
```
### For Databases
```
"[database] use cases"
"[database] scaling guide"
"[database] comparison"
"[database] benchmarks [year]"
```
## Important Notes
1. **Cite sources**: Always include URLs for claims
2. **Be current**: Prioritize recent documentation
3. **Be balanced**: Research all options equally thoroughly
4. **Note gaps**: If documentation is lacking, note it as a finding
5. **Version awareness**: Note which version documentation refers to
================================================
FILE: plugins/dev/agents/tradeoff-analyzer.md
================================================
---
name: tradeoff-analyzer
description: Use this agent to synthesize research findings into structured pros/cons analysis. Trigger after gathering information from multiple sources to create comprehensive trade-off comparison.
<example>
Context: User comparing state management libraries after research
user: "Redux vs Zustand vs Jotai 장단점 비교해줘"
assistant: "세 라이브러리의 장단점을 평가 기준별로 정리하고 비교 분석하겠습니다."
<commentary>
Specific libraries named - synthesize into structured comparison with pros/cons for each.
</commentary>
</example>
<example>
Context: Architecture decision after gathering information
user: "모놀리스랑 마이크로서비스 트레이드오프 분석해줘"
assistant: "두 아키텍처의 장단점을 현재 프로젝트 맥락에서 비교 분석하겠습니다."
<commentary>
Architecture comparison - analyze trade-offs considering project context from codebase analysis.
</commentary>
</example>
model: sonnet
color: yellow
tools:
- Read
---
You are a trade-off analysis specialist who synthesizes information from multiple sources into clear, actionable comparisons.
## Core Mission
Transform raw research findings into:
- Structured pros/cons for each option
- Comparative analysis across evaluation criteria
- Confidence ratings based on source quality
- Clear recommendations with reasoning
## Analysis Process
### 1. Consolidate Information
Gather findings from:
- Codebase analysis (codebase-explorer)
- Documentation research (docs-researcher)
- Community opinions (dev-scan skill)
- Expert perspectives (agent-council skill)
### 2. Identify Evaluation Criteria
Based on the decision type and context:
- Define relevant criteria
- Assign weights based on project needs
- Note any criteria requested by user
### 3. Analyze Each Option
For each option:
```
├── Strengths
│ ├── Supported by which sources?
│ ├── How significant?
│ └── Confidence level?
│
├── Weaknesses
│ ├── Supported by which sources?
│ ├── How significant?
│ └── Workarounds available?
│
├── Fit with Current Context
│ ├── Alignment with existing code
│ ├── Team familiarity
│ └── Migration complexity
│
└── Risks
├── Known issues
├── Potential problems
└── Mitigation strategies
```
### 4. Cross-Option Comparison
Compare options across each criterion:
- Score each option (1-5 scale)
- Note trade-offs between options
- Identify deal-breakers if any
### 5. Handle Conflicting Information
When sources disagree:
- Note the disagreement
- Analyze why (different contexts, versions, etc.)
- Assign confidence based on source quality
## Output Format
```markdown
## 트레이드오프 분석 결과
### 평가 기준
| 기준 | 가중치 | 근거 |
|------|--------|------|
| [기준 1] | X% | [왜 이 가중치인지] |
| [기준 2] | X% | [...] |
| [기준 3] | X% | [...] |
---
### Option A: [이름]
#### 장점 (Pros)
| 장점 | 중요도 | 출처 | 신뢰도 |
|------|--------|------|--------|
| [장점 1] | 높음 | 공식 문서 | 95% |
| [장점 2] | 중간 | Reddit + HN | 75% |
| [장점 3] | 높음 | 코드 분석 | 90% |
#### 단점 (Cons)
| 단점 | 심각도 | 출처 | 완화 가능 |
|------|--------|------|----------|
| [단점 1] | 높음 | 커뮤니티 | 부분적 |
| [단점 2] | 낮음 | 벤치마크 | 예 |
#### 리스크
- **[리스크 1]**: [설명] - 완화: [방법]
- **[리스크 2]**: [설명] - 완화: [방법]
#### 적합한 시나리오
- [시나리오 1]
- [시나리오 2]
---
### Option B: [이름]
[동일 구조]
---
### 종합 비교표
#### 기준별 점수 (5점 만점)
| 기준 (가중치) | Option A | Option B | Option C | 비고 |
|---------------|----------|----------|----------|------|
| [기준 1] (X%) | ⭐4 | ⭐3 | ⭐5 | [핵심 차이] |
| [기준 2] (X%) | ⭐3 | ⭐5 | ⭐2 | [핵심 차이] |
| [기준 3] (X%) | ⭐4 | ⭐4 | ⭐3 | [핵심 차이] |
| **가중 점수** | **X.X** | **X.X** | **X.X** | |
#### Trade-off 요약
| 선택 | 얻는 것 | 포기하는 것 |
|------|---------|-------------|
| Option A | [핵심 장점] | [핵심 단점] |
| Option B | [핵심 장점] | [핵심 단점] |
| Option C | [핵심 장점] | [핵심 단점] |
---
### 충돌하는 의견 정리
| 주제 | 의견 A | 의견 B | 분석 |
|------|--------|--------|------|
| [주제] | [의견] (출처) | [의견] (출처) | [왜 다른지, 어느 쪽이 더 신뢰할 만한지] |
---
### 분석 결론
**예비 추천**: [Option X]
**핵심 근거**:
1. [근거 1]
2. [근거 2]
3. [근거 3]
**주의사항**:
- [주의 1]
- [주의 2]
**추가 고려 필요**:
- [추가로 확인하면 좋을 사항]
```
## Confidence Rating System
| 신뢰도 | 기준 |
|--------|------|
| 90-100% | 공식 문서 + 다수 출처 일치 |
| 75-89% | 신뢰할 만한 출처 2개 이상 일치 |
| 50-74% | 단일 신뢰 출처 또는 다수 비공식 출처 |
| 25-49% | 비공식 출처, 일부 상충 |
| 0-24% | 추측성, 출처 불분명, 상충 많음 |
## Analysis Guidelines
1. **Be balanced**: Give each option fair analysis
2. **Be specific**: Use concrete examples and numbers
3. **Be honest**: Note limitations and uncertainties
4. **Be practical**: Consider real-world implementation
5. **Be contextual**: Weigh findings against project context
================================================
FILE: plugins/dev/skills/dev-scan/SKILL.md
================================================
---
name: dev-scan
description: 개발 커뮤니티에서 기술 주제에 대한 다양한 의견 수집. "개발자 반응", "커뮤니티 의견", "developer reactions" 요청에 사용. Reddit, HN, Dev.to, Lobsters 등 종합.
version: 1.0.0
---
# Dev Opinions Scan
여러 개발 커뮤니티에서 특정 주제에 대한 다양한 의견을 수집하여 종합.
## Purpose
기술 주제에 대한 **다양한 시각**을 빠르게 파악:
- 찬반 의견 분포
- 실무자들의 경험담
- 숨겨진 우려사항이나 장점
- 독특하거나 주목할 만한 시각
## Data Sources
| Platform | Method |
|----------|--------|
| Reddit | Gemini CLI |
| Hacker News | WebSearch |
| Dev.to | WebSearch |
| Lobsters | WebSearch |
## Execution
### Step 1: Topic Extraction
사용자 요청에서 핵심 주제 추출.
예시:
- "React 19에 대한 개발자들 반응" → `React 19`
- "Bun vs Deno 커뮤니티 의견" → `Bun vs Deno`
### Step 2: Parallel Search (Single Message, 4 Sources)
**Reddit** (Gemini CLI - WebFetch blocked):
```bash
# 단일 Gemini 호출로 Reddit 검색 (명시적 검색 지시 필수)
gemini -p "Search Reddit for discussions about {TOPIC}. Summarize the main opinions, debates, and insights from developers. Include Reddit post URLs where possible. Focus on: 1) Common opinions 2) Controversies 3) Notable perspectives from experienced developers."
```
**주의사항**:
- `site:reddit.com` 형식은 작동하지 않음 - Gemini가 검색 쿼리가 아닌 작업 요청으로 해석
- 반드시 "Search Reddit for..." 형태로 명시적 검색 지시 필요
- 단일 호출이 병렬 호출보다 안정적 (출력 혼재 방지)
**Other Sources** (WebSearch, parallel):
```
WebSearch: "{topic} site:news.ycombinator.com"
WebSearch: "{topic} site:dev.to"
WebSearch: "{topic} site:lobste.rs"
```
**CRITICAL**: 4개 검색을 반드시 **하나의 메시지**에서 병렬로 실행. Gemini는 단일 호출, WebSearch는 3개 병렬.
### Step 3: Synthesize & Present
수집된 데이터를 분석하여 의미 있는 인사이트를 도출한다.
#### 3-1. 의견 분류 및 패턴 파악
각 소스에서 수집된 의견들을 다음 기준으로 분류:
- **찬성/긍정**: 해당 기술/도구를 지지하는 의견
- **반대/부정**: 우려, 비판, 대안 제시
- **중립/조건부**: "~한 경우에만", "~와 함께 쓰면" 등의 조건부 의견
- **경험 기반**: 실제 프로덕션 사용 경험을 바탕으로 한 의견
#### 3-2. 공통 의견(Consensus) 도출
여러 커뮤니티에서 **반복적으로 등장하는** 의견을 식별:
- 2개 이상의 소스에서 동일한 포인트가 언급되면 공통 의견으로 분류
- 특히 Reddit과 HN에서 동시에 언급되는 의견은 신뢰도 높음
- 구체적인 수치나 사례가 포함된 의견 우선
- **최소 5개 이상의 공통 의견** 도출 목표
#### 3-3. 논쟁점(Controversy) 식별
커뮤니티 간 또는 커뮤니티 내에서 **의견이 갈리는** 지점 파악:
- 같은 주제에 대해 상반된 의견이 존재하는 경우
- 댓글에서 활발한 토론이 벌어진 스레드
- "depends on...", "but actually..." 등의 반론이 많은 주제
- **최소 3개 이상의 논쟁점** 식별 목표
#### 3-4. 주목할 시각(Notable Perspective) 선별
독특하거나 깊이 있는 인사이트 발굴:
- 다수 의견과 다르지만 논리적 근거가 탄탄한 의견
- 시니어 개발자나 해당 분야 전문가의 의견
- 실제 대규모 프로젝트 경험에서 나온 인사이트
- 다른 사람들이 놓치기 쉬운 엣지 케이스나 장기적 관점
- **최소 3개 이상의 주목할 시각** 선별 목표
## Output Format
**핵심 원칙**: 모든 의견에 출처를 인라인으로 붙인다. 출처 없는 의견은 포함하지 않는다.
```markdown
## Key Insights
### Consensus (공통 의견)
1. **[의견 제목]**
- [구체적인 내용 설명]
- [추가 맥락이나 예시]
- Sources: [Reddit](url), [HN](url)
2. **[의견 제목]**
- [구체적인 내용]
- Source: [Dev.to](url)
(최소 5개 이상)
---
### Controversy (논쟁점)
1. **[논쟁 주제]**
- 찬성측: "[인용]" - [Source](url)
- 반대측: "[인용]" - [Source](url)
- 맥락: [왜 의견이 갈리는지]
2. **[논쟁 주제]**
- ...
(최소 3개 이상)
---
### Notable Perspective (주목할 시각)
1. **[인사이트 제목]**
> "[원문 인용 또는 핵심 문장]"
- [왜 주목할 만한지 설명]
- Source: [Platform](url)
2. **[인사이트 제목]**
- ...
(최소 3개 이상)
```
### 출처 표기 규칙
- **인라인 링크 필수**: 모든 의견 끝에 `Source: [Platform](url)` 형식으로 붙임
- **복수 출처**: 동일 의견이 여러 곳에서 언급되면 `Sources: [Reddit](url), [HN](url)`
- **직접 인용**: 가능하면 원문을 `"..."` 형태로 인용
- **URL 정확성**: 실제 접근 가능한 링크만 포함 (검색 결과에서 확인된 URL)
## Error Handling
| 상황 | 대응 |
|------|------|
| 검색 결과 없음 | 해당 플랫폼 생략, 다른 소스에 집중 |
| Gemini CLI 실패 | Reddit 생략하고 나머지 3개로 진행 |
| 주제가 너무 새로움 | 결과 부족 안내, 관련 키워드 제안 |
## Examples
**단순 주제**:
```
User: "Tailwind v4 개발자들 반응 어때?"
→ topic: "Tailwind v4"
→ 4개 소스 병렬 검색
→ 종합 인사이트 제공
```
**비교 주제**:
```
User: "pnpm vs yarn vs npm 커뮤니티 의견"
→ topic: "pnpm vs yarn vs npm comparison"
→ 4개 소스 병렬 검색
→ 각 도구별 선호도 정리
```
**논쟁적 주제**:
```
User: "Claude Code Plugin 에 대한 개발자들 생각"
→ topic: "Claude Code Plugin tips"
→ 4개 소스 병렬 검색
→ 종합 인사이트 제공
```
================================================
FILE: plugins/dev/skills/tech-decision/SKILL.md
================================================
---
name: tech-decision
description: This skill should be used when the user asks to "기술 의사결정", "뭐 쓸지 고민", "A vs B", "비교 분석", "라이브러리 선택", "아키텍처 결정", "어떤 걸 써야 할지", "트레이드오프", "기술 선택", "구현 방식 고민", or needs deep analysis for technical decisions. Provides systematic multi-source research and synthesized recommendations.
version: 0.1.0
---
# Tech Decision - 기술 의사결정 깊이 탐색
기술적 의사결정을 체계적으로 분석하고 종합적인 결론을 도출하는 스킬.
## 핵심 원칙
**두괄식 결과물**: 모든 보고서는 결론을 먼저 제시하고, 그 다음에 근거를 제공한다.
## 사용 시나리오
- 라이브러리/프레임워크 선택 (React vs Vue, Prisma vs TypeORM)
- 아키텍처 패턴 결정 (Monolith vs Microservices, REST vs GraphQL)
- 구현 방식 선택 (Server-side vs Client-side, Polling vs WebSocket)
- 기술 스택 결정 (언어, 데이터베이스, 인프라 등)
## 의사결정 워크플로우
### Phase 1: 문제 정의
의사결정 주제와 맥락을 명확히 한다:
1. **주제 파악**: 무엇을 결정해야 하는가?
2. **옵션 식별**: 비교할 선택지들은 무엇인가?
3. **평가 기준 수립**: 어떤 기준으로 평가할 것인가?
- 성능, 학습 곡선, 생태계, 유지보수성, 비용 등
- 프로젝트 특성에 맞는 기준 우선순위 설정
- 상세 기준은 **`references/evaluation-criteria.md`** 참조
### Phase 2: 병렬 정보 수집
여러 소스에서 동시에 정보를 수집한다. **반드시 병렬로 실행**:
```
┌─────────────────────────────────────────────────────────────┐
│ 동시 실행 (Task tool로 병렬 실행) │
├─────────────────────────────────────────────────────────────┤
│ 1. codebase-explorer agent │
│ → 기존 코드베이스 분석, 현재 패턴/제약사항 파악 │
│ │
│ 2. docs-researcher agent │
│ → 공식 문서, 가이드, best practices 리서치 │
│ │
│ 3. Skill: dev-scan │
│ → 커뮤니티 의견 수집 (Reddit, HN, Dev.to, Lobsters) │
│ │
│ 4. Skill: agent-council │
│ → 다양한 AI 전문가 관점 수집 │
│ │
│ 5. [선택] Context7 MCP │
│ → 라이브러리별 최신 문서 조회 │
└─────────────────────────────────────────────────────────────┘
```
**실행 방법**:
```markdown
# Agents는 Task tool로 병렬 실행
Task codebase-explorer: "분석할 주제와 컨텍스트"
Task docs-researcher: "리서치할 기술/라이브러리"
# 기존 스킬은 Skill tool로 호출
Skill: dev-scan (커뮤니티 의견)
Skill: agent-council (전문가 관점)
```
### Phase 3: 종합 분석
수집된 정보를 바탕으로 tradeoff-analyzer agent를 실행:
- 각 옵션별 pros/cons 정리
- 평가 기준별 점수화
- 충돌하는 의견 정리
- 신뢰도 평가 (출처 기반)
### Phase 4: 최종 보고서 생성
decisio
gitextract_chnom6f9/
├── .claude-plugin/
│ ├── marketplace.json
│ └── plugin.json
├── .gitignore
├── LICENSE
├── README.ko.md
├── README.md
└── plugins/
├── agent-council/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── AGENTS.md
│ ├── CLAUDE.md
│ ├── README.ko.md
│ ├── README.md
│ ├── SKILL.md
│ ├── bin/
│ │ └── install.js
│ ├── council.config.yaml
│ ├── package.json
│ ├── scripts/
│ │ ├── council-job-worker.js
│ │ ├── council-job.js
│ │ ├── council-job.sh
│ │ └── council.sh
│ └── skills/
│ └── agent-council/
│ ├── SKILL.md
│ ├── references/
│ │ ├── config.md
│ │ ├── examples.md
│ │ ├── host-ui.md
│ │ ├── overview.md
│ │ ├── requirements.md
│ │ └── safety.md
│ └── scripts/
│ ├── council-job-worker.js
│ ├── council-job.js
│ ├── council-job.sh
│ └── council.sh
├── clarify/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ └── skills/
│ ├── metamedium/
│ │ ├── SKILL.md
│ │ └── references/
│ │ └── alan-kay-quotes.md
│ ├── unknown/
│ │ ├── SKILL.md
│ │ └── references/
│ │ ├── playbook-template.md
│ │ └── question-design.md
│ └── vague/
│ └── SKILL.md
├── dev/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── CLAUDE.md
│ ├── agents/
│ │ ├── codebase-explorer.md
│ │ ├── decision-synthesizer.md
│ │ ├── docs-researcher.md
│ │ └── tradeoff-analyzer.md
│ └── skills/
│ ├── dev-scan/
│ │ └── SKILL.md
│ └── tech-decision/
│ ├── SKILL.md
│ └── references/
│ ├── evaluation-criteria.md
│ └── report-template.md
├── doubt/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── README.md
│ └── hooks/
│ ├── hooks.json
│ └── scripts/
│ ├── doubt-detector.sh
│ └── doubt-validator.sh
├── fetch-tweet/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── README.md
│ └── skills/
│ └── fetch-tweet/
│ ├── SKILL.md
│ └── scripts/
│ └── fetch_tweet.py
├── gmail/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── README.md
│ └── skills/
│ └── gmail/
│ ├── .gitignore
│ ├── SKILL.md
│ ├── accounts.yaml.example
│ ├── assets/
│ │ ├── accounts.default.yaml
│ │ ├── email-templates.md
│ │ └── signatures.md
│ ├── pyproject.toml
│ ├── references/
│ │ ├── cli-usage.md
│ │ ├── search-queries.md
│ │ └── setup-guide.md
│ └── scripts/
│ ├── core/
│ │ ├── __init__.py
│ │ ├── batch_processor.py
│ │ ├── cache_manager.py
│ │ ├── quota_manager.py
│ │ └── retry_handler.py
│ ├── gmail_client.py
│ ├── list_messages.py
│ ├── manage_labels.py
│ ├── read_message.py
│ ├── send_message.py
│ └── setup_auth.py
├── google-calendar/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── README.md
│ └── skills/
│ └── google-calendar/
│ ├── SKILL.md
│ ├── examples/
│ │ ├── parallel-fetch.md
│ │ └── quick-query.md
│ ├── pyproject.toml
│ └── scripts/
│ ├── calendar_client.py
│ ├── fetch_events.py
│ ├── manage_events.py
│ └── setup_auth.py
├── interactive-review/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── .mcp.json
│ ├── CLAUDE.md
│ ├── README.md
│ ├── mcp-server/
│ │ ├── requirements.txt
│ │ ├── server.py
│ │ └── web_ui.py
│ └── skills/
│ └── review/
│ └── SKILL.md
├── kakaotalk/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── README.md
│ ├── scripts/
│ │ ├── kakao_read.py
│ │ └── kakao_send.py
│ └── skills/
│ └── kakaotalk/
│ └── SKILL.md
├── podcast/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── README.md
│ └── skills/
│ └── podcast/
│ ├── SKILL.md
│ └── scripts/
│ ├── convert_mp4.py
│ ├── generate_tts.py
│ └── upload_youtube.py
├── say-summary/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── README.md
│ ├── hooks/
│ │ └── hooks.json
│ ├── requirements.txt
│ └── scripts/
│ ├── say-summary.py
│ └── setup.sh
├── session-wrap/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── README.md
│ ├── agents/
│ │ ├── automation-scout.md
│ │ ├── doc-updater.md
│ │ ├── duplicate-checker.md
│ │ ├── followup-suggester.md
│ │ └── learning-extractor.md
│ ├── commands/
│ │ └── wrap.md
│ └── skills/
│ ├── history-insight/
│ │ ├── SKILL.md
│ │ ├── references/
│ │ │ └── session-file-format.md
│ │ └── scripts/
│ │ └── extract-session.sh
│ ├── session-analyzer/
│ │ ├── SKILL.md
│ │ ├── references/
│ │ │ ├── analysis-patterns.md
│ │ │ └── common-issues.md
│ │ └── scripts/
│ │ ├── extract-hook-events.sh
│ │ ├── extract-subagent-calls.sh
│ │ └── find-session-files.sh
│ └── session-wrap/
│ ├── SKILL.md
│ └── references/
│ └── multi-agent-patterns.md
├── team-assemble/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── README.md
│ └── skills/
│ └── team-assemble/
│ ├── SKILL.md
│ └── references/
│ ├── agents.md
│ ├── enable-agent-teams.md
│ ├── examples.md
│ └── prompt-templates.md
└── youtube-digest/
├── .claude-plugin/
│ └── plugin.json
├── README.md
└── skills/
└── youtube-digest/
├── SKILL.md
├── references/
│ ├── deep-research.md
│ └── quiz-patterns.md
└── scripts/
├── extract_metadata.sh
└── extract_transcript.sh
SYMBOL INDEX (321 symbols across 28 files)
FILE: plugins/agent-council/bin/install.js
constant YAML (line 6) | const YAML = require('yaml');
constant GREEN (line 8) | const GREEN = '\x1b[32m';
constant YELLOW (line 9) | const YELLOW = '\x1b[33m';
constant CYAN (line 10) | const CYAN = '\x1b[36m';
constant RED (line 11) | const RED = '\x1b[31m';
function parseArgs (line 20) | function parseArgs(argv) {
function copyRecursive (line 48) | function copyRecursive(src, dest) {
function commandExists (line 68) | function commandExists(command) {
FILE: plugins/agent-council/scripts/council-job-worker.js
function exitWithError (line 8) | function exitWithError(message) {
function parseArgs (line 13) | function parseArgs(argv) {
function splitCommand (line 39) | function splitCommand(command) {
function atomicWriteJson (line 82) | function atomicWriteJson(filePath, payload) {
function main (line 88) | function main() {
FILE: plugins/agent-council/scripts/council-job.js
constant SCRIPT_DIR (line 8) | const SCRIPT_DIR = __dirname;
constant SKILL_DIR (line 9) | const SKILL_DIR = path.resolve(SCRIPT_DIR, '..');
constant WORKER_PATH (line 10) | const WORKER_PATH = path.join(SCRIPT_DIR, 'council-job-worker.js');
constant SKILL_CONFIG_FILE (line 12) | const SKILL_CONFIG_FILE = path.join(SKILL_DIR, 'council.config.yaml');
constant REPO_CONFIG_FILE (line 13) | const REPO_CONFIG_FILE = path.join(path.resolve(SKILL_DIR, '../..'), 'co...
function exitWithError (line 15) | function exitWithError(message) {
function resolveDefaultConfigFile (line 20) | function resolveDefaultConfigFile() {
function detectHostRole (line 26) | function detectHostRole() {
function normalizeBool (line 33) | function normalizeBool(value) {
function resolveAutoRole (line 41) | function resolveAutoRole(role, hostRole) {
function parseCouncilConfig (line 49) | function parseCouncilConfig(configPath) {
function ensureDir (line 130) | function ensureDir(dirPath) {
function safeFileName (line 134) | function safeFileName(name) {
function atomicWriteJson (line 139) | function atomicWriteJson(filePath, payload) {
function readJsonIfExists (line 145) | function readJsonIfExists(filePath) {
function sleepMs (line 154) | function sleepMs(ms) {
function computeTerminalDoneCount (line 162) | function computeTerminalDoneCount(counts) {
function asCodexStepStatus (line 173) | function asCodexStepStatus(value) {
function buildCouncilUiPayload (line 179) | function buildCouncilUiPayload(statusPayload) {
function computeStatusPayload (line 260) | function computeStatusPayload(jobDir) {
function parseArgs (line 305) | function parseArgs(argv) {
function printHelp (line 352) | function printHelp() {
function cmdStart (line 370) | function cmdStart(options, prompt) {
function cmdStatus (line 473) | function cmdStatus(options, jobDir) {
function parseWaitCursor (line 515) | function parseWaitCursor(value) {
function formatWaitCursor (line 541) | function formatWaitCursor(bucketSize, dispatchBucket, doneBucket, isDone) {
function asWaitPayload (line 545) | function asWaitPayload(statusPayload) {
function resolveBucketSize (line 563) | function resolveBucketSize(options, total, prevCursor) {
function cmdWait (line 583) | function cmdWait(options, jobDir) {
function cmdResults (line 652) | function cmdResults(options, jobDir) {
function cmdStop (line 710) | function cmdStop(_options, jobDir) {
function cmdClean (line 734) | function cmdClean(_options, jobDir) {
function main (line 740) | function main() {
FILE: plugins/agent-council/skills/agent-council/scripts/council-job-worker.js
function exitWithError (line 8) | function exitWithError(message) {
function parseArgs (line 13) | function parseArgs(argv) {
function splitCommand (line 39) | function splitCommand(command) {
function atomicWriteJson (line 82) | function atomicWriteJson(filePath, payload) {
function main (line 88) | function main() {
FILE: plugins/agent-council/skills/agent-council/scripts/council-job.js
constant SCRIPT_DIR (line 8) | const SCRIPT_DIR = __dirname;
constant SKILL_DIR (line 9) | const SKILL_DIR = path.resolve(SCRIPT_DIR, '..');
constant WORKER_PATH (line 10) | const WORKER_PATH = path.join(SCRIPT_DIR, 'council-job-worker.js');
constant SKILL_CONFIG_FILE (line 12) | const SKILL_CONFIG_FILE = path.join(SKILL_DIR, 'council.config.yaml');
constant REPO_CONFIG_FILE (line 13) | const REPO_CONFIG_FILE = path.join(path.resolve(SKILL_DIR, '../..'), 'co...
function exitWithError (line 15) | function exitWithError(message) {
function resolveDefaultConfigFile (line 20) | function resolveDefaultConfigFile() {
function detectHostRole (line 26) | function detectHostRole() {
function normalizeBool (line 33) | function normalizeBool(value) {
function resolveAutoRole (line 41) | function resolveAutoRole(role, hostRole) {
function parseCouncilConfig (line 49) | function parseCouncilConfig(configPath) {
function ensureDir (line 130) | function ensureDir(dirPath) {
function safeFileName (line 134) | function safeFileName(name) {
function atomicWriteJson (line 139) | function atomicWriteJson(filePath, payload) {
function readJsonIfExists (line 145) | function readJsonIfExists(filePath) {
function sleepMs (line 154) | function sleepMs(ms) {
function computeTerminalDoneCount (line 162) | function computeTerminalDoneCount(counts) {
function asCodexStepStatus (line 173) | function asCodexStepStatus(value) {
function buildCouncilUiPayload (line 179) | function buildCouncilUiPayload(statusPayload) {
function computeStatusPayload (line 260) | function computeStatusPayload(jobDir) {
function parseArgs (line 305) | function parseArgs(argv) {
function printHelp (line 352) | function printHelp() {
function cmdStart (line 370) | function cmdStart(options, prompt) {
function cmdStatus (line 473) | function cmdStatus(options, jobDir) {
function parseWaitCursor (line 515) | function parseWaitCursor(value) {
function formatWaitCursor (line 541) | function formatWaitCursor(bucketSize, dispatchBucket, doneBucket, isDone) {
function asWaitPayload (line 545) | function asWaitPayload(statusPayload) {
function resolveBucketSize (line 563) | function resolveBucketSize(options, total, prevCursor) {
function cmdWait (line 583) | function cmdWait(options, jobDir) {
function cmdResults (line 652) | function cmdResults(options, jobDir) {
function cmdStop (line 710) | function cmdStop(_options, jobDir) {
function cmdClean (line 734) | function cmdClean(_options, jobDir) {
function main (line 740) | function main() {
FILE: plugins/fetch-tweet/skills/fetch-tweet/scripts/fetch_tweet.py
function parse_x_url (line 21) | def parse_x_url(url: str) -> tuple[str, str] | None:
function fetch_tweet (line 33) | def fetch_tweet(screen_name: str, status_id: str) -> dict:
function format_number (line 41) | def format_number(n: int) -> str:
function format_tweet (line 50) | def format_tweet(data: dict) -> str:
function main (line 115) | def main():
FILE: plugins/gmail/skills/gmail/scripts/core/batch_processor.py
class BatchResult (line 29) | class BatchResult:
class BatchProcessor (line 39) | class BatchProcessor:
method __init__ (line 62) | def __init__(
method batch_get_messages (line 88) | def batch_get_messages(
method batch_modify_labels (line 158) | def batch_modify_labels(
method batch_trash_messages (line 219) | def batch_trash_messages(
method batch_delete_messages (line 279) | def batch_delete_messages(
method batch_get_threads (line 345) | def batch_get_threads(
method mark_all_as_read (line 413) | def mark_all_as_read(
method archive_all (line 459) | def archive_all(
FILE: plugins/gmail/skills/gmail/scripts/core/cache_manager.py
class CacheConfig (line 32) | class CacheConfig:
class EmailCache (line 46) | class EmailCache:
method __init__ (line 70) | def __init__(
method get_message (line 96) | def get_message(
method set_message (line 137) | def set_message(
method get_list (line 170) | def get_list(
method set_list (line 206) | def set_list(
method get_labels (line 242) | def get_labels(self, account: str) -> Optional[list[dict]]:
method set_labels (line 269) | def set_labels(self, account: str, labels: list[dict]) -> None:
method invalidate_message (line 293) | def invalidate_message(self, account: str, message_id: str) -> None:
method invalidate_lists (line 304) | def invalidate_lists(self, account: str) -> None:
method invalidate_labels (line 315) | def invalidate_labels(self, account: str) -> None:
method invalidate_account (line 325) | def invalidate_account(self, account: str) -> None:
method invalidate_all (line 336) | def invalidate_all(self) -> None:
method get_stats (line 347) | def get_stats(self, account: Optional[str] = None) -> dict:
method _message_path (line 403) | def _message_path(self, account: str, message_id: str) -> Path:
method _list_path (line 407) | def _list_path(self, account: str, cache_key: str) -> Path:
method _list_cache_key (line 411) | def _list_cache_key(
method _is_fresh (line 421) | def _is_fresh(
method _get_cached_accounts (line 436) | def _get_cached_accounts(self) -> list[str]:
method _cleanup_if_needed (line 447) | def _cleanup_if_needed(self, account: str) -> None:
function get_cache (line 469) | def get_cache(cache_dir: Optional[str] = None) -> EmailCache:
FILE: plugins/gmail/skills/gmail/scripts/core/quota_manager.py
class QuotaUnit (line 30) | class QuotaUnit(IntEnum):
class QuotaUsage (line 71) | class QuotaUsage:
class QuotaManager (line 80) | class QuotaManager:
method __init__ (line 106) | def __init__(
method can_execute (line 126) | def can_execute(self, user: str, units: int) -> bool:
method record_usage (line 141) | def record_usage(self, user: str, units: int) -> None:
method wait_for_quota (line 154) | def wait_for_quota(
method get_usage (line 187) | def get_usage(self, user: str) -> dict:
method get_remaining_rate (line 209) | def get_remaining_rate(self, user: str) -> int:
method is_daily_limit_reached (line 223) | def is_daily_limit_reached(self, user: str) -> bool:
method reset_user (line 236) | def reset_user(self, user: str) -> None:
method _get_or_create_usage (line 246) | def _get_or_create_usage(self, user: str) -> QuotaUsage:
method _reset_if_needed (line 252) | def _reset_if_needed(self, user: str) -> None:
function get_quota_manager (line 275) | def get_quota_manager(
FILE: plugins/gmail/skills/gmail/scripts/core/retry_handler.py
class RetryConfig (line 40) | class RetryConfig:
function calculate_delay (line 50) | def calculate_delay(
function is_retryable_error (line 78) | def is_retryable_error(error: Exception) -> bool:
function exponential_backoff (line 92) | def exponential_backoff(
class RetryableOperation (line 185) | class RetryableOperation:
method __init__ (line 209) | def __init__(
method __enter__ (line 228) | def __enter__(self) -> "RetryableOperation":
method __exit__ (line 234) | def __exit__(self, exc_type, exc_val, exc_tb):
method should_retry (line 237) | def should_retry(self) -> bool:
method success (line 241) | def success(self) -> None:
method handle_error (line 245) | def handle_error(self, error: Exception) -> None:
method execute (line 278) | def execute(self, func: Callable[..., T], *args, **kwargs) -> T:
function retry_api_call (line 307) | def retry_api_call(
function always_succeeds (line 344) | def always_succeeds():
function succeed_on_third_try (line 353) | def succeed_on_third_try():
FILE: plugins/gmail/skills/gmail/scripts/gmail_client.py
class GmailClient (line 69) | class GmailClient:
method __init__ (line 85) | def __init__(
method service (line 131) | def service(self):
method cache (line 138) | def cache(self) -> Optional[EmailCache]:
method quota_manager (line 143) | def quota_manager(self) -> Optional[QuotaManager]:
method batch_processor (line 148) | def batch_processor(self) -> BatchProcessor:
method _record_quota (line 158) | def _record_quota(self, units: int) -> None:
method _wait_for_quota (line 163) | def _wait_for_quota(self, units: int) -> None:
method _load_credentials (line 168) | def _load_credentials(self):
method list_messages (line 206) | def list_messages(
method get_message (line 274) | def get_message(
method _parse_message (line 326) | def _parse_message(self, msg: dict) -> dict:
method _extract_body_and_attachments (line 357) | def _extract_body_and_attachments(
method get_attachment (line 393) | def get_attachment(self, message_id: str, attachment_id: str) -> bytes:
method send_message (line 412) | def send_message(
method _attach_file (line 489) | def _attach_file(self, message: MIMEMultipart, filepath: str) -> None:
method modify_message (line 518) | def modify_message(
method mark_as_read (line 568) | def mark_as_read(self, message_id: str) -> dict:
method mark_as_unread (line 572) | def mark_as_unread(self, message_id: str) -> dict:
method star_message (line 576) | def star_message(self, message_id: str) -> dict:
method unstar_message (line 580) | def unstar_message(self, message_id: str) -> dict:
method archive_message (line 584) | def archive_message(self, message_id: str) -> dict:
method trash_message (line 588) | def trash_message(self, message_id: str) -> dict:
method untrash_message (line 613) | def untrash_message(self, message_id: str) -> dict:
method delete_message (line 638) | def delete_message(self, message_id: str) -> dict:
method list_threads (line 662) | def list_threads(
method get_thread (line 695) | def get_thread(self, thread_id: str, format: str = "full") -> dict:
method trash_thread (line 712) | def trash_thread(self, thread_id: str) -> dict:
method list_labels (line 729) | def list_labels(self, use_cache: bool = True) -> list[dict]:
method get_label (line 771) | def get_label(self, label_id: str) -> dict:
method create_label (line 786) | def create_label(
method update_label (line 818) | def update_label(
method delete_label (line 850) | def delete_label(self, label_id: str) -> dict:
method list_drafts (line 862) | def list_drafts(self, max_results: int = 20) -> list[dict]:
method get_draft (line 886) | def get_draft(self, draft_id: str) -> dict:
method create_draft (line 900) | def create_draft(
method send_draft (line 945) | def send_draft(self, draft_id: str) -> dict:
method delete_draft (line 961) | def delete_draft(self, draft_id: str) -> dict:
method get_profile (line 973) | def get_profile(self) -> dict:
method batch_get_messages (line 994) | def batch_get_messages(
method batch_modify_labels (line 1010) | def batch_modify_labels(
method batch_trash_messages (line 1038) | def batch_trash_messages(self, message_ids: list[str]) -> dict:
method batch_delete_messages (line 1057) | def batch_delete_messages(self, message_ids: list[str]) -> dict:
method mark_all_as_read (line 1078) | def mark_all_as_read(
method archive_all (line 1100) | def archive_all(
method get_quota_status (line 1126) | def get_quota_status(self) -> dict:
method get_cache_stats (line 1136) | def get_cache_stats(self) -> dict:
method clear_cache (line 1146) | def clear_cache(self) -> None:
class ADCGmailClient (line 1153) | class ADCGmailClient:
method __init__ (line 1165) | def __init__(self, account_name: str = "default", timeout: int = DEFAU...
method service (line 1172) | def service(self):
method list_messages (line 1177) | def list_messages(
method get_profile (line 1211) | def get_profile(self) -> dict:
function get_all_accounts (line 1220) | def get_all_accounts(base_path: Optional[Path] = None) -> list[str]:
function get_client (line 1233) | def get_client(
FILE: plugins/gmail/skills/gmail/scripts/list_messages.py
function format_message_summary (line 27) | def format_message_summary(client: GmailClient, msg_id: str) -> dict:
function main (line 40) | def main():
FILE: plugins/gmail/skills/gmail/scripts/manage_labels.py
function main (line 47) | def main():
FILE: plugins/gmail/skills/gmail/scripts/read_message.py
function main (line 25) | def main():
FILE: plugins/gmail/skills/gmail/scripts/send_message.py
function main (line 48) | def main():
FILE: plugins/gmail/skills/gmail/scripts/setup_auth.py
function load_accounts_config (line 27) | def load_accounts_config(base_path: Path) -> dict:
function save_accounts_config (line 36) | def save_accounts_config(base_path: Path, config: dict) -> None:
function setup_auth (line 52) | def setup_auth(
function list_accounts (line 135) | def list_accounts(base_path: Path) -> None:
function main (line 176) | def main():
FILE: plugins/google-calendar/skills/google-calendar/scripts/calendar_client.py
class CalendarClient (line 27) | class CalendarClient:
method __init__ (line 32) | def __init__(
method _load_credentials (line 57) | def _load_credentials(self):
method get_events (line 98) | def get_events(
method list_calendars (line 167) | def list_calendars(self) -> list[dict]:
method create_event (line 195) | def create_event(
method update_event (line 255) | def update_event(
method delete_event (line 323) | def delete_event(
class ADCCalendarClient (line 347) | class ADCCalendarClient:
method __init__ (line 356) | def __init__(self, account_name: str = "default", timeout: int = DEFAU...
method get_events (line 366) | def get_events(
method list_calendars (line 427) | def list_calendars(self) -> list[dict]:
method create_event (line 453) | def create_event(
method update_event (line 495) | def update_event(
method delete_event (line 547) | def delete_event(
function get_all_accounts (line 563) | def get_all_accounts(base_path: Optional[Path] = None) -> list[str]:
function fetch_all_events (line 578) | def fetch_all_events(days: int = 7, base_path: Optional[Path] = None) ->...
function detect_conflicts (line 621) | def detect_conflicts(events: list[dict]) -> list[dict]:
FILE: plugins/google-calendar/skills/google-calendar/scripts/fetch_events.py
function format_event_for_display (line 30) | def format_event_for_display(event: dict, tz: ZoneInfo = None) -> str:
function main (line 54) | def main():
FILE: plugins/google-calendar/skills/google-calendar/scripts/manage_events.py
function cmd_create (line 48) | def cmd_create(args):
function cmd_update (line 81) | def cmd_update(args):
function cmd_delete (line 112) | def cmd_delete(args):
function main (line 132) | def main():
FILE: plugins/google-calendar/skills/google-calendar/scripts/setup_auth.py
function setup_auth (line 22) | def setup_auth(account_name: str, base_path: Path) -> None:
function list_accounts (line 74) | def list_accounts(base_path: Path) -> None:
function main (line 93) | def main():
FILE: plugins/interactive-review/mcp-server/server.py
function find_free_port (line 44) | def find_free_port() -> int:
class ReviewHTTPHandler (line 52) | class ReviewHTTPHandler(SimpleHTTPRequestHandler):
method __init__ (line 55) | def __init__(self, *args, review_dir: str, **kwargs):
method do_POST (line 59) | def do_POST(self):
method do_OPTIONS (line 85) | def do_OPTIONS(self):
method log_message (line 93) | def log_message(self, format, *args):
function make_handler (line 98) | def make_handler(review_dir: str):
function start_review_impl (line 105) | async def start_review_impl(content: str, title: str = "Review") -> dict...
function list_tools (line 224) | async def list_tools() -> list[Tool]:
function call_tool (line 257) | async def call_tool(name: str, arguments: dict) -> list[TextContent]:
function setup_signal_handlers (line 276) | def setup_signal_handlers():
function main (line 286) | async def main():
FILE: plugins/interactive-review/mcp-server/web_ui.py
class Block (line 15) | class Block:
function parse_markdown (line 24) | def parse_markdown(content: str) -> List[Block]:
function generate_html (line 44) | def generate_html(title: str, content: str, blocks: List[Block], server_...
FILE: plugins/kakaotalk/scripts/kakao_read.py
function run_applescript (line 46) | def run_applescript(script: str) -> str:
function key_code (line 51) | def key_code(code: int, modifiers: str = ""):
function get_kakao_app (line 64) | def get_kakao_app():
function find_main_window (line 75) | def find_main_window(kakao_app):
function find_open_chat (line 83) | def find_open_chat(kakao_app, chat_name: str):
function get_all_chat_windows (line 92) | def get_all_chat_windows(kakao_app) -> list:
function ensure_main_window_focused (line 96) | def ensure_main_window_focused():
function clear_search_and_go_main (line 115) | def clear_search_and_go_main():
function search_and_open_chat (line 129) | def search_and_open_chat(chat_name: str):
function close_chat (line 148) | def close_chat():
function is_date_pattern (line 158) | def is_date_pattern(val: str) -> bool:
function is_time_pattern (line 171) | def is_time_pattern(val: str) -> bool:
function is_valid_sender_name (line 178) | def is_valid_sender_name(val: str) -> bool:
function safe_get_attr (line 205) | def safe_get_attr(elem, attr_name, default=None):
function get_window_width (line 213) | def get_window_width(chat_window) -> int:
function extract_messages (line 226) | def extract_messages(chat_window, limit: int = 100) -> list[dict]:
function _parse_static_text (line 286) | def _parse_static_text(elem, row_sender, row_time, current_date, partner...
function _parse_message (line 316) | def _parse_message(elem, cell_pos, win_width, row_sender, row_time,
function list_chats (line 365) | def list_chats(kakao_app, limit: int = 30) -> list[str]:
function search_chats (line 395) | def search_chats(query: str, limit: int = 20) -> list[str]:
function _extract_row_texts (line 436) | def _extract_row_texts(row) -> list[str]:
function read_chat (line 457) | def read_chat(chat_name: str, limit: int = 100) -> tuple[str | None, lis...
function main (line 488) | def main():
FILE: plugins/kakaotalk/scripts/kakao_send.py
function run_applescript (line 29) | def run_applescript(script: str) -> str:
function key_code (line 34) | def key_code(code: int, modifiers: str = ""):
function type_text (line 43) | def type_text(text: str):
function get_kakao_app (line 49) | def get_kakao_app():
function find_main_window (line 60) | def find_main_window(kakao_app):
function raise_main_window (line 68) | def raise_main_window(kakao_app):
function find_open_chat (line 80) | def find_open_chat(kakao_app, chat_name: str):
function get_all_chat_windows (line 91) | def get_all_chat_windows(kakao_app) -> list:
function search_and_open_chat (line 95) | def search_and_open_chat(chat_name: str):
function open_chat (line 117) | def open_chat(chat_name: str):
function send_message_via_keyboard (line 154) | def send_message_via_keyboard(message: str):
function close_chat (line 165) | def close_chat():
function send_message (line 171) | def send_message(chat_name: str, message: str, close_after: bool = False...
function main (line 202) | def main():
FILE: plugins/podcast/skills/podcast/scripts/convert_mp4.py
function escape_drawtext (line 14) | def escape_drawtext(text):
function convert (line 27) | def convert(input_path, output_path, title, subtitle=""):
function main (line 72) | def main():
FILE: plugins/podcast/skills/podcast/scripts/generate_tts.py
function extract_speech_text (line 25) | def extract_speech_text(md_path):
function split_paragraph_by_sentences (line 62) | def split_paragraph_by_sentences(para, max_chars=MAX_CHARS):
function split_into_chunks (line 84) | def split_into_chunks(text, max_chars=MAX_CHARS):
function generate_tts_chunk (line 113) | def generate_tts_chunk(text, output_path, api_key, model, voice, instruc...
function merge_audio (line 164) | def merge_audio(chunk_files, output_path):
function get_duration (line 187) | def get_duration(path):
function main (line 197) | def main():
FILE: plugins/podcast/skills/podcast/scripts/upload_youtube.py
function find_client_secret (line 20) | def find_client_secret():
function load_client_config (line 33) | def load_client_config(path):
function get_auth_code (line 40) | def get_auth_code(client_id):
function exchange_code (line 76) | def exchange_code(client_id, client_secret, code, token_path):
function refresh_token (line 95) | def refresh_token(client_id, client_secret, token_path):
function get_access_token (line 116) | def get_access_token(client_secret_path, token_path):
function upload_video (line 132) | def upload_video(access_token, video_path, title, description, tags, pri...
function main (line 186) | def main():
FILE: plugins/say-summary/scripts/say-summary.py
function log (line 24) | def log(message: str) -> None:
function get_project_dir (line 31) | def get_project_dir() -> Path | None:
function get_latest_transcript (line 43) | def get_latest_transcript(project_dir: Path) -> Path | None:
function extract_last_assistant_message (line 52) | def extract_last_assistant_message(transcript_path: Path) -> str | None:
function summarize_with_haiku (line 87) | async def summarize_with_haiku(text: str) -> str:
function detect_korean (line 121) | def detect_korean(text: str) -> bool:
function speak (line 131) | def speak(text: str) -> None:
function async_main (line 154) | async def async_main() -> None:
function main (line 189) | def main() -> None:
Condensed preview — 147 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (747K chars).
[
{
"path": ".claude-plugin/marketplace.json",
"chars": 2961,
"preview": "{\n \"name\": \"team-attention-plugins\",\n \"owner\": {\n \"name\": \"Team Attention\",\n \"url\": \"https://github.com/team-att"
},
{
"path": ".claude-plugin/plugin.json",
"chars": 299,
"preview": "{\n \"name\": \"plugins-for-claude-natives\",\n \"version\": \"0.1.0\",\n \"description\": \"Claude Code 네이티브 사용자를 위한 유틸리티 플러그인 모음\""
},
{
"path": ".gitignore",
"chars": 759,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
},
{
"path": "LICENSE",
"chars": 1071,
"preview": "MIT License\n\nCopyright (c) 2025 Team Attention\n\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "README.ko.md",
"chars": 11049,
"preview": "# Plugins for Claude Natives\n\nClaude Code의 기능을 확장하고 싶은 파워 유저를 위한 플러그인 모음입니다.\n\n## 목차\n\n- [빠른 시작](#빠른-시작)\n- [플러그인 목록](#플러그인"
},
{
"path": "README.md",
"chars": 19262,
"preview": "# Plugins for Claude Natives\n\nA collection of Claude Code plugins for power users who want to extend Claude Code's capab"
},
{
"path": "plugins/agent-council/.claude-plugin/plugin.json",
"chars": 455,
"preview": "{\n \"name\": \"agent-council\",\n \"version\": \"1.0.0\",\n \"description\": \"Collect and synthesize opinions from multiple AI Ag"
},
{
"path": "plugins/agent-council/AGENTS.md",
"chars": 573,
"preview": "# Project Instructions (Codex)\n\n## Plan / To-do UI (IMPORTANT)\n\n- When you decide to use any Skill, **always** call `upd"
},
{
"path": "plugins/agent-council/CLAUDE.md",
"chars": 569,
"preview": "# Project Instructions (Claude Code)\n\n## Todo UI (IMPORTANT)\n\n- When you decide to use any Skill, **always** call `TodoW"
},
{
"path": "plugins/agent-council/README.ko.md",
"chars": 5729,
"preview": "# Agent Council\n\n**[English Version](./README.md)**\n\n> 여러 AI CLI(Codex, Gemini, ...)의 의견을 모으고, 설정 가능한 의장(Chairman)이 종합해 "
},
{
"path": "plugins/agent-council/README.md",
"chars": 7494,
"preview": "# Agent Council\n\n**[한국어 버전 (Korean)](./README.ko.md)**\n\n> A skill that gathers opinions from multiple AI CLIs (Codex, Ge"
},
{
"path": "plugins/agent-council/SKILL.md",
"chars": 1001,
"preview": "---\nname: agent-council\ndescription: Collect and synthesize opinions from multiple AI agents. Use when users say \"summon"
},
{
"path": "plugins/agent-council/bin/install.js",
"chars": 8279,
"preview": "#!/usr/bin/env node\n\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process"
},
{
"path": "plugins/agent-council/council.config.yaml",
"chars": 1425,
"preview": "# Agent Council Configuration\n# Add or remove council members as needed.\n# Note: the installer filters members to detect"
},
{
"path": "plugins/agent-council/package.json",
"chars": 682,
"preview": "{\n \"name\": \"@team-attention/agent-council\",\n \"version\": \"1.0.0\",\n \"description\": \"Collect and synthesize opinions fro"
},
{
"path": "plugins/agent-council/scripts/council-job-worker.js",
"chars": 5658,
"preview": "#!/usr/bin/env node\n\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst { s"
},
{
"path": "plugins/agent-council/scripts/council-job.js",
"chars": 27170,
"preview": "#!/usr/bin/env node\n\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst { s"
},
{
"path": "plugins/agent-council/scripts/council-job.sh",
"chars": 449,
"preview": "#!/bin/bash\n\nset -e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n\nif ! command -v node >/dev/null 2>&1; t"
},
{
"path": "plugins/agent-council/scripts/council.sh",
"chars": 3413,
"preview": "#!/bin/bash\n#\n# Agent Council (job mode default)\n#\n# Subcommands:\n# council.sh start [options] \"question\" # return"
},
{
"path": "plugins/agent-council/skills/agent-council/SKILL.md",
"chars": 1001,
"preview": "---\nname: agent-council\ndescription: Collect and synthesize opinions from multiple AI agents. Use when users say \"summon"
},
{
"path": "plugins/agent-council/skills/agent-council/references/config.md",
"chars": 865,
"preview": "# Configure members\n\nEdit `council.config.yaml` to set chairman and members:\n\n```yaml\ncouncil:\n chairman:\n role: \"au"
},
{
"path": "plugins/agent-council/skills/agent-council/references/examples.md",
"chars": 483,
"preview": "# Examples\n\n## Technical decision\n\nPrompt:\n```\nReact vs Vue - which fits this project better? Summon the council\n```\n\nSt"
},
{
"path": "plugins/agent-council/skills/agent-council/references/host-ui.md",
"chars": 757,
"preview": "# Host UI Checklist Guidance\n\nUse these steps only when a host agent UI supports native checklist updates.\n\n## Checklist"
},
{
"path": "plugins/agent-council/skills/agent-council/references/overview.md",
"chars": 558,
"preview": "# Overview\n\n- Gather responses from configured member CLIs.\n- Let the chairman synthesize the final response (default: `"
},
{
"path": "plugins/agent-council/skills/agent-council/references/requirements.md",
"chars": 434,
"preview": "# Requirements\n\n- Install and authenticate the CLIs listed under `council.members` in `council.config.yaml`.\n- Note that"
},
{
"path": "plugins/agent-council/skills/agent-council/references/safety.md",
"chars": 65,
"preview": "# Safety\n\n- Do not share sensitive information with the council.\n"
},
{
"path": "plugins/agent-council/skills/agent-council/scripts/council-job-worker.js",
"chars": 5658,
"preview": "#!/usr/bin/env node\n\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst { s"
},
{
"path": "plugins/agent-council/skills/agent-council/scripts/council-job.js",
"chars": 27170,
"preview": "#!/usr/bin/env node\n\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst { s"
},
{
"path": "plugins/agent-council/skills/agent-council/scripts/council-job.sh",
"chars": 449,
"preview": "#!/bin/bash\n\nset -e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n\nif ! command -v node >/dev/null 2>&1; t"
},
{
"path": "plugins/agent-council/skills/agent-council/scripts/council.sh",
"chars": 3413,
"preview": "#!/bin/bash\n#\n# Agent Council (job mode default)\n#\n# Subcommands:\n# council.sh start [options] \"question\" # return"
},
{
"path": "plugins/clarify/.claude-plugin/plugin.json",
"chars": 557,
"preview": "{\n \"name\": \"clarify\",\n \"version\": \"2.0.0\",\n \"description\": \"Three lenses for clarity: vague requirements → specs (vag"
},
{
"path": "plugins/clarify/skills/metamedium/SKILL.md",
"chars": 5669,
"preview": "---\nname: metamedium\ndescription: This skill should be used when the user is building, planning, or strategizing and the"
},
{
"path": "plugins/clarify/skills/metamedium/references/alan-kay-quotes.md",
"chars": 3492,
"preview": "# Alan Kay: Source Quotes and Context\n\nKey quotes and context that inform the metamedium lens.\n\n## \"A change of perspect"
},
{
"path": "plugins/clarify/skills/unknown/SKILL.md",
"chars": 6879,
"preview": "---\nname: unknown\ndescription: This skill should be used when the user provides a strategy, plan, or decision document a"
},
{
"path": "plugins/clarify/skills/unknown/references/playbook-template.md",
"chars": 4544,
"preview": "# Playbook Output Template\n\nComplete template for the 4-quadrant playbook generated in Phase 6.\n\n## File Naming\n\nSave as"
},
{
"path": "plugins/clarify/skills/unknown/references/question-design.md",
"chars": 3693,
"preview": "# Question Design Guide\n\nDetailed patterns for designing hypothesis-driven questions across the 3-round depth pattern.\n\n"
},
{
"path": "plugins/clarify/skills/vague/SKILL.md",
"chars": 4023,
"preview": "---\nname: vague\ndescription: This skill should be used when the user's request or requirement is ambiguous and needs ite"
},
{
"path": "plugins/dev/.claude-plugin/plugin.json",
"chars": 448,
"preview": "{\n \"name\": \"dev\",\n \"version\": \"1.1.0\",\n \"description\": \"Developer workflow tools: community scanning, technical decis"
},
{
"path": "plugins/dev/CLAUDE.md",
"chars": 708,
"preview": "# Dev\n\nDeveloper workflow tools for Claude Code.\n\n## Skills\n\n- `/dev-scan` - 개발 커뮤니티에서 다양한 의견 수집 (Reddit, HN, Dev.to, Lo"
},
{
"path": "plugins/dev/agents/codebase-explorer.md",
"chars": 3770,
"preview": "---\r\nname: codebase-explorer\r\ndescription: Use this agent when analyzing existing codebase for technical decisions. Trig"
},
{
"path": "plugins/dev/agents/decision-synthesizer.md",
"chars": 4326,
"preview": "---\r\nname: decision-synthesizer\r\ndescription: Use this agent to generate final decision reports with clear recommendatio"
},
{
"path": "plugins/dev/agents/docs-researcher.md",
"chars": 5085,
"preview": "---\r\nname: docs-researcher\r\ndescription: Use this agent to research official documentation, guides, and best practices f"
},
{
"path": "plugins/dev/agents/tradeoff-analyzer.md",
"chars": 4627,
"preview": "---\r\nname: tradeoff-analyzer\r\ndescription: Use this agent to synthesize research findings into structured pros/cons anal"
},
{
"path": "plugins/dev/skills/dev-scan/SKILL.md",
"chars": 3708,
"preview": "---\nname: dev-scan\ndescription: 개발 커뮤니티에서 기술 주제에 대한 다양한 의견 수집. \"개발자 반응\", \"커뮤니티 의견\", \"developer reactions\" 요청에 사용. Reddit"
},
{
"path": "plugins/dev/skills/tech-decision/SKILL.md",
"chars": 5062,
"preview": "---\r\nname: tech-decision\r\ndescription: This skill should be used when the user asks to \"기술 의사결정\", \"뭐 쓸지 고민\", \"A vs B\", \""
},
{
"path": "plugins/dev/skills/tech-decision/references/evaluation-criteria.md",
"chars": 2825,
"preview": "# 평가 기준 가이드\r\n\r\n의사결정 유형별 권장 평가 기준.\r\n\r\n## 라이브러리/프레임워크 선택\r\n\r\n| 기준 | 설명 | 측정 방법 |\r\n|------|------|-----------|\r\n| **성능** | 속"
},
{
"path": "plugins/dev/skills/tech-decision/references/report-template.md",
"chars": 3166,
"preview": "# 기술 의사결정 보고서 템플릿\r\n\r\n## 전체 구조\r\n\r\n```markdown\r\n# 기술 의사결정 보고서: [주제]\r\n\r\n**작성일**: YYYY-MM-DD\r\n**의사결정 유형**: [라이브러리 선택 | 아키텍처 "
},
{
"path": "plugins/doubt/.claude-plugin/plugin.json",
"chars": 260,
"preview": "{\r\n \"name\": \"doubt\",\r\n \"version\": \"1.0.0\",\r\n \"description\": \"Force Claude to re-validate when you have doubts (!doubt"
},
{
"path": "plugins/doubt/README.md",
"chars": 821,
"preview": "# doubt\r\n\r\nForce Claude to re-validate its responses when you have doubts.\r\n\r\n## Usage\r\n\r\nAdd `!rv` anywhere in your pro"
},
{
"path": "plugins/doubt/hooks/hooks.json",
"chars": 593,
"preview": "{\r\n \"description\": \"Doubt mode - Type !doubt to force Claude re-validation\",\r\n \"hooks\": {\r\n \"UserPromptSubmit\": [\r\n"
},
{
"path": "plugins/doubt/hooks/scripts/doubt-detector.sh",
"chars": 827,
"preview": "#!/bin/bash\n# !rv keyword detection -> activate doubt mode\n\nSTATE_DIR=\"$HOME/.claude/.hook-state\"\nmkdir -p \"$STATE_DIR\"\n"
},
{
"path": "plugins/doubt/hooks/scripts/doubt-validator.sh",
"chars": 789,
"preview": "#!/bin/bash\n# If doubt mode is active, request Claude to re-validate\n\nSTATE_DIR=\"$HOME/.claude/.hook-state\"\n\n# Read JSON"
},
{
"path": "plugins/fetch-tweet/.claude-plugin/plugin.json",
"chars": 265,
"preview": "{\n \"name\": \"fetch-tweet\",\n \"version\": \"0.1.0\",\n \"description\": \"Fetch full tweet text, author info, and engagement da"
},
{
"path": "plugins/fetch-tweet/README.md",
"chars": 1898,
"preview": "# Fetch Tweet\n\nFetch full tweet text, author info, and engagement data from X/Twitter URLs — no authentication, no JavaS"
},
{
"path": "plugins/fetch-tweet/skills/fetch-tweet/SKILL.md",
"chars": 2069,
"preview": "---\nname: fetch-tweet\ndescription: This skill should be used when the user asks to \"트윗 가져와\", \"트윗 번역\", \"X 게시글 읽어줘\", \"twee"
},
{
"path": "plugins/fetch-tweet/skills/fetch-tweet/scripts/fetch_tweet.py",
"chars": 4583,
"preview": "#!/usr/bin/env python3\n\"\"\"Fetch tweet content via FxEmbed API (api.fxtwitter.com).\n\nUsage:\n python fetch_tweet.py <x_"
},
{
"path": "plugins/gmail/.claude-plugin/plugin.json",
"chars": 149,
"preview": "{\n \"name\": \"gmail\",\n \"version\": \"1.0.0\",\n \"description\": \"Gmail integration with multi-account support - read, search"
},
{
"path": "plugins/gmail/README.md",
"chars": 15476,
"preview": "# Gmail Plugin for Claude Code\n\nA comprehensive Gmail integration plugin for Claude Code that enables multi-account emai"
},
{
"path": "plugins/gmail/skills/gmail/.gitignore",
"chars": 171,
"preview": "# OAuth tokens (contain refresh tokens)\naccounts/\n\n# OAuth client credentials\nreferences/credentials.json\n\n# Virtual env"
},
{
"path": "plugins/gmail/skills/gmail/SKILL.md",
"chars": 3868,
"preview": "---\nname: gmail\ndescription: This skill should be used when the user asks to \"check email\", \"read emails\", \"send email\","
},
{
"path": "plugins/gmail/skills/gmail/accounts.yaml.example",
"chars": 244,
"preview": "# Gmail 계정 설정\n# 계정별로 이메일 주소와 설명을 관리합니다.\n# 토큰 파일은 accounts/{name}.json에 별도 저장됩니다.\n\naccounts:\n personal:\n email: bongb"
},
{
"path": "plugins/gmail/skills/gmail/assets/accounts.default.yaml",
"chars": 757,
"preview": "# Gmail Account Settings Default Template\n# Copy this file to ../accounts.yaml for use\n#\n# Usage:\n# cp assets/accounts"
},
{
"path": "plugins/gmail/skills/gmail/assets/email-templates.md",
"chars": 2344,
"preview": "# Email Body Templates\n\nSelect an appropriate template based on the situation when composing emails.\n\n---\n\n## 1. Meeting"
},
{
"path": "plugins/gmail/skills/gmail/assets/signatures.md",
"chars": 1607,
"preview": "# Email Signature Templates\n\n---\n\n## Default Signature (Plain Text)\n\nAutomatically added to all outgoing emails:\n\n```\n--"
},
{
"path": "plugins/gmail/skills/gmail/pyproject.toml",
"chars": 421,
"preview": "[project]\nname = \"gmail-skill\"\nversion = \"0.1.0\"\ndescription = \"Gmail sync skill for Claude Code\"\nrequires-python = \">=3"
},
{
"path": "plugins/gmail/skills/gmail/references/cli-usage.md",
"chars": 3210,
"preview": "# Gmail CLI Detailed Usage\n\n## List Messages\n\n```bash\n# Recent 10 emails\nuv run python scripts/list_messages.py --accoun"
},
{
"path": "plugins/gmail/skills/gmail/references/search-queries.md",
"chars": 1209,
"preview": "# Gmail Search Query Reference\n\n## Basic Queries\n\n| Query | Description |\n|-------|-------------|\n| `from:user@example.c"
},
{
"path": "plugins/gmail/skills/gmail/references/setup-guide.md",
"chars": 3638,
"preview": "# Gmail Skill Initial Setup Guide\n\nFollow this guide if accounts.yaml is missing or no accounts are registered.\n\n---\n\n##"
},
{
"path": "plugins/gmail/skills/gmail/scripts/core/__init__.py",
"chars": 436,
"preview": "\"\"\"Gmail Core Components.\n\nRate limiting, caching, retry logic, and batch processing for Gmail API.\n\"\"\"\n\nfrom .quota_man"
},
{
"path": "plugins/gmail/skills/gmail/scripts/core/batch_processor.py",
"chars": 15833,
"preview": "\"\"\"Gmail API Batch Processor.\n\n여러 API 요청을 일괄 처리하여 효율성을 높입니다.\n\nBatch Request Guidelines:\n- 최대 50개 요청을 하나의 배치로\n- 각 요청은 개별적"
},
{
"path": "plugins/gmail/skills/gmail/scripts/core/cache_manager.py",
"chars": 14958,
"preview": "\"\"\"Gmail Local Cache Manager.\n\nAPI 호출을 최소화하기 위한 로컬 캐시 레이어.\n\n캐시 전략:\n- 메시지 내용: 24시간 유효 (메시지는 불변)\n- 메시지 메타데이터: 1시간 유효 (라벨 변"
},
{
"path": "plugins/gmail/skills/gmail/scripts/core/quota_manager.py",
"chars": 8012,
"preview": "\"\"\"Gmail API Quota Management.\n\nGmail API 할당량 관리를 위한 모듈.\n\nQuota Units (Gmail API Reference):\n- messages.list: 5 units\n- "
},
{
"path": "plugins/gmail/skills/gmail/scripts/core/retry_handler.py",
"chars": 9426,
"preview": "\"\"\"Exponential Backoff Retry Handler.\n\nGmail API 오류 처리를 위한 지수 백오프 재시도 로직.\n\nRetry-able Errors:\n- 429: Rate Limit Exceeded"
},
{
"path": "plugins/gmail/skills/gmail/scripts/gmail_client.py",
"chars": 38136,
"preview": "\"\"\"Gmail API 클라이언트.\n\n여러 Google 계정의 Gmail을 조회/발송하기 위한 클라이언트.\n저장된 refresh token을 사용하여 매번 인증 없이 API 호출.\n\nFeatures:\n - 다중"
},
{
"path": "plugins/gmail/skills/gmail/scripts/list_messages.py",
"chars": 4046,
"preview": "#!/usr/bin/env python3\n\"\"\"Gmail 메시지 목록 조회 CLI.\n\nUsage:\n # 받은편지함 최근 10개\n uv run python list_messages.py --account w"
},
{
"path": "plugins/gmail/skills/gmail/scripts/manage_labels.py",
"chars": 6893,
"preview": "#!/usr/bin/env python3\n\"\"\"Gmail 라벨 및 메시지 관리 CLI.\n\nUsage:\n # 라벨 목록\n uv run python manage_labels.py --account work l"
},
{
"path": "plugins/gmail/skills/gmail/scripts/read_message.py",
"chars": 3704,
"preview": "#!/usr/bin/env python3\n\"\"\"Gmail 메시지 읽기 CLI.\n\nUsage:\n # 메시지 읽기\n uv run python read_message.py --account work --id <"
},
{
"path": "plugins/gmail/skills/gmail/scripts/send_message.py",
"chars": 3378,
"preview": "#!/usr/bin/env python3\n\"\"\"Gmail 메시지 발송 CLI.\n\nUsage:\n # 새 메일 발송\n uv run python send_message.py --account work \\\n "
},
{
"path": "plugins/gmail/skills/gmail/scripts/setup_auth.py",
"chars": 6142,
"preview": "#!/usr/bin/env python3\n\"\"\"Gmail OAuth 인증 설정.\n\n최초 1회 실행하여 계정별 refresh token을 저장.\n이후에는 저장된 token으로 자동 인증됨.\n\nUsage:\n uv "
},
{
"path": "plugins/google-calendar/.claude-plugin/plugin.json",
"chars": 162,
"preview": "{\n \"name\": \"google-calendar\",\n \"version\": \"1.0.0\",\n \"description\": \"Multi-account Google Calendar integration with pa"
},
{
"path": "plugins/google-calendar/README.md",
"chars": 1009,
"preview": "# Google Calendar Plugin\n\nMulti-account Google Calendar integration with parallel querying and conflict detection.\n\n## F"
},
{
"path": "plugins/google-calendar/skills/google-calendar/SKILL.md",
"chars": 4687,
"preview": "---\nname: google-calendar\ndescription: Google 캘린더 일정 조회/생성/수정/삭제. \"오늘 일정\", \"이번 주 일정\", \"미팅 추가해줘\" 요청에 사용. 여러 계정(work, pers"
},
{
"path": "plugins/google-calendar/skills/google-calendar/examples/parallel-fetch.md",
"chars": 1530,
"preview": "# 병렬 조회 예시\n\n## Subagent 병렬 실행\n\n여러 계정의 캘린더를 동시에 조회하려면 Task 도구를 병렬로 호출:\n\n```\n# 단일 메시지에 여러 Task 호출 (병렬 실행)\n\nTask(\n subag"
},
{
"path": "plugins/google-calendar/skills/google-calendar/examples/quick-query.md",
"chars": 945,
"preview": "# 빠른 조회 예시\n\n## 오늘 일정\n\n```bash\nuv run python .claude/skills/google-calendar/scripts/fetch_events.py \\\n --all --days 1 --"
},
{
"path": "plugins/google-calendar/skills/google-calendar/pyproject.toml",
"chars": 420,
"preview": "[project]\nname = \"google-calendar-skill\"\nversion = \"0.1.0\"\ndescription = \"Google Calendar sync skill for Claude Code\"\nre"
},
{
"path": "plugins/google-calendar/skills/google-calendar/scripts/calendar_client.py",
"chars": 20805,
"preview": "\"\"\"Google Calendar API 클라이언트.\n\n여러 Google 계정의 캘린더를 조회하기 위한 클라이언트.\n저장된 refresh token을 사용하여 매번 인증 없이 API 호출.\n\nEnvironment V"
},
{
"path": "plugins/google-calendar/skills/google-calendar/scripts/fetch_events.py",
"chars": 7849,
"preview": "#!/usr/bin/env python3\n\"\"\"Google Calendar 이벤트 조회 CLI.\n\nSubagent에서 호출하여 특정 계정의 이벤트를 JSON으로 반환.\n\nUsage:\n # ADC(Applicat"
},
{
"path": "plugins/google-calendar/skills/google-calendar/scripts/manage_events.py",
"chars": 6064,
"preview": "#!/usr/bin/env python3\n\"\"\"Google Calendar 이벤트 관리 CLI.\n\n일정 생성, 수정, 삭제를 위한 CLI 도구.\n\nUsage:\n # 일정 생성 (시간 지정)\n uv run "
},
{
"path": "plugins/google-calendar/skills/google-calendar/scripts/setup_auth.py",
"chars": 3187,
"preview": "#!/usr/bin/env python3\n\"\"\"Google Calendar OAuth 인증 설정.\n\n최초 1회 실행하여 계정별 refresh token을 저장.\n이후에는 저장된 token으로 자동 인증됨.\n\nUsag"
},
{
"path": "plugins/interactive-review/.claude-plugin/plugin.json",
"chars": 393,
"preview": "{\n \"name\": \"interactive-review\",\n \"description\": \"Interactive markdown review with web UI - review plans and documents"
},
{
"path": "plugins/interactive-review/.mcp.json",
"chars": 169,
"preview": "{\n \"mcpServers\": {\n \"interactive_review\": {\n \"command\": \"uv\",\n \"args\": [\"run\", \"--directory\", \"${CLAUDE_PL"
},
{
"path": "plugins/interactive-review/CLAUDE.md",
"chars": 1509,
"preview": "# Interactive Review Plugin\n\nClaude Code plugin for interactive markdown review with web UI.\n\n## Directory Structure\n\n``"
},
{
"path": "plugins/interactive-review/README.md",
"chars": 1618,
"preview": "# Interactive Review Plugin\n\nInteractive markdown review with web UI for Claude Code.\n\n## Features\n\n- **Visual Review UI"
},
{
"path": "plugins/interactive-review/mcp-server/requirements.txt",
"chars": 11,
"preview": "mcp>=1.0.0\n"
},
{
"path": "plugins/interactive-review/mcp-server/server.py",
"chars": 8953,
"preview": "#!/usr/bin/env python3\n# /// script\n# dependencies = [\"mcp>=1.0.0\"]\n# ///\n\"\"\"\nInteractive Review MCP Server\n\nProvides th"
},
{
"path": "plugins/interactive-review/mcp-server/web_ui.py",
"chars": 41703,
"preview": "\"\"\"\nWeb UI Generator for Interactive Review\n\nGenerates a self-contained HTML file with embedded CSS and JavaScript\nfor r"
},
{
"path": "plugins/interactive-review/skills/review/SKILL.md",
"chars": 2706,
"preview": "---\nname: review\ndescription: Interactive markdown review with web UI. Use when user says \"review this\", \"check this pla"
},
{
"path": "plugins/kakaotalk/.claude-plugin/plugin.json",
"chars": 398,
"preview": "{\n \"name\": \"kakaotalk\",\n \"version\": \"1.0.0\",\n \"description\": \"Send and read KakaoTalk messages on macOS using Accessi"
},
{
"path": "plugins/kakaotalk/README.md",
"chars": 2198,
"preview": "# KakaoTalk Plugin for Claude Code\n\nmacOS에서 카카오톡 메시지를 발송하고 읽는 Claude Code 플러그인.\n\n## Demo\n\n — analyz"
},
{
"path": "plugins/podcast/skills/podcast/scripts/convert_mp4.py",
"chars": 2524,
"preview": "#!/usr/bin/env python3\n\"\"\"MP3 → MP4 conversion (dark background + title overlay)\"\"\"\n\nimport argparse\nimport os\nimport su"
},
{
"path": "plugins/podcast/skills/podcast/scripts/generate_tts.py",
"chars": 7559,
"preview": "#!/usr/bin/env python3\n\"\"\"OpenAI gpt-4o-mini-tts로 팟캐스트 음성 생성 — 청크 분할 + ffmpeg 병합\"\"\"\n\nimport argparse\nimport json\nimport "
},
{
"path": "plugins/podcast/skills/podcast/scripts/upload_youtube.py",
"chars": 7643,
"preview": "#!/usr/bin/env python3\n\"\"\"YouTube Data API v3 resumable upload — standard library only\"\"\"\n\nimport argparse\nimport glob\ni"
},
{
"path": "plugins/say-summary/.claude-plugin/plugin.json",
"chars": 425,
"preview": "{\n \"name\": \"say-summary\",\n \"version\": \"1.0.0\",\n \"description\": \"Speaks a short summary of Claude's response using mac"
},
{
"path": "plugins/say-summary/README.md",
"chars": 1349,
"preview": "# say-summary\n\nA Claude Code plugin that speaks a short summary of Claude's response using macOS text-to-speech.\n\n## Fea"
},
{
"path": "plugins/say-summary/hooks/hooks.json",
"chars": 307,
"preview": "{\n \"description\": \"Summarize and speak last Claude response\",\n \"hooks\": {\n \"Stop\": [\n {\n \"hooks\": [\n "
},
{
"path": "plugins/say-summary/requirements.txt",
"chars": 24,
"preview": "claude-agent-sdk>=0.1.0\n"
},
{
"path": "plugins/say-summary/scripts/say-summary.py",
"chars": 5697,
"preview": "#!/usr/bin/env python3\n\"\"\"\nStop hook: Summarizes and speaks the last Claude response.\n\n- Extracts the last assistant mes"
},
{
"path": "plugins/say-summary/scripts/setup.sh",
"chars": 492,
"preview": "#!/bin/bash\n# Setup script for say-summary plugin\n# Installs required Python dependencies\n\nset -e\n\necho \"Installing say-"
},
{
"path": "plugins/session-wrap/.claude-plugin/plugin.json",
"chars": 529,
"preview": "{\r\n \"name\": \"session-wrap\",\r\n \"version\": \"1.0.0\",\r\n \"description\": \"Session wrap-up workflow with multi-agent analysi"
},
{
"path": "plugins/session-wrap/README.md",
"chars": 5317,
"preview": "# Session Wrap Plugin\r\n\r\nA Claude Code plugin for comprehensive session wrap-up with multi-agent analysis.\r\n\r\n## Feature"
},
{
"path": "plugins/session-wrap/agents/automation-scout.md",
"chars": 8079,
"preview": "---\r\nname: automation-scout\r\ndescription: |\r\n Analyze automation patterns. Detect opportunities to automate repetitive "
},
{
"path": "plugins/session-wrap/agents/doc-updater.md",
"chars": 4864,
"preview": "---\r\nname: doc-updater\r\ndescription: |\r\n Analyze documentation update needs for CLAUDE.md and context.md. Use during se"
},
{
"path": "plugins/session-wrap/agents/duplicate-checker.md",
"chars": 7353,
"preview": "---\r\nname: duplicate-checker\r\ndescription: |\r\n Phase 2 validation agent. Receives Phase 1 analysis results (doc-updater"
},
{
"path": "plugins/session-wrap/agents/followup-suggester.md",
"chars": 9277,
"preview": "---\r\nname: followup-suggester\r\ndescription: |\r\n Suggest follow-up tasks. Identify incomplete work, improvement points, "
},
{
"path": "plugins/session-wrap/agents/learning-extractor.md",
"chars": 9047,
"preview": "---\r\nname: learning-extractor\r\ndescription: |\r\n Extract learnings, mistakes, and new discoveries from session. Summariz"
},
{
"path": "plugins/session-wrap/commands/wrap.md",
"chars": 914,
"preview": "---\r\ndescription: Session wrap-up - analyze session, suggest documentation updates, automation opportunities, and follow"
},
{
"path": "plugins/session-wrap/skills/history-insight/SKILL.md",
"chars": 3333,
"preview": "---\nname: history-insight\ndescription: This skill should be used when user wants to access, capture, or reference Claude"
},
{
"path": "plugins/session-wrap/skills/history-insight/references/session-file-format.md",
"chars": 1612,
"preview": "# Session File Format (.jsonl)\n\nClaude Code 세션 파일의 상세 구조 및 파싱 방법\n\n## JSONL Type 분류\n\n| type | 설명 | 필요 여부 |\n|------|------"
},
{
"path": "plugins/session-wrap/skills/history-insight/scripts/extract-session.sh",
"chars": 1980,
"preview": "#!/bin/bash\n# extract-session.sh\n# Extract essential conversation from Claude Code session JSONL files\n#\n# Usage: ./extr"
},
{
"path": "plugins/session-wrap/skills/session-analyzer/SKILL.md",
"chars": 7375,
"preview": "---\nname: session-analyzer\ndescription: This skill should be used when the user asks to \"analyze session\", \"세션 분석\", \"eva"
},
{
"path": "plugins/session-wrap/skills/session-analyzer/references/analysis-patterns.md",
"chars": 5636,
"preview": "# Analysis Patterns for Session Analyzer\n\nDetailed grep/search patterns for extracting information from Claude Code debu"
},
{
"path": "plugins/session-wrap/skills/session-analyzer/references/common-issues.md",
"chars": 5977,
"preview": "# Common Issues and Troubleshooting\n\nKnown issues when analyzing Claude Code sessions and how to diagnose them.\n\n---\n\n##"
},
{
"path": "plugins/session-wrap/skills/session-analyzer/scripts/extract-hook-events.sh",
"chars": 3840,
"preview": "#!/bin/bash\n# extract-hook-events.sh - Extract Hook events from debug log\n#\n# Usage: extract-hook-events.sh <debug-log-p"
},
{
"path": "plugins/session-wrap/skills/session-analyzer/scripts/extract-subagent-calls.sh",
"chars": 2272,
"preview": "#!/bin/bash\n# extract-subagent-calls.sh - Extract SubAgent invocations from debug log\n#\n# Usage: extract-subagent-calls."
},
{
"path": "plugins/session-wrap/skills/session-analyzer/scripts/find-session-files.sh",
"chars": 1465,
"preview": "#!/bin/bash\n# find-session-files.sh - Locate all files related to a Claude Code session\n#\n# Usage: find-session-files.sh"
},
{
"path": "plugins/session-wrap/skills/session-wrap/SKILL.md",
"chars": 5463,
"preview": "---\r\nname: session-wrap\r\ndescription: This skill should be used when the user asks to \"wrap up session\", \"end session\", "
},
{
"path": "plugins/session-wrap/skills/session-wrap/references/multi-agent-patterns.md",
"chars": 6497,
"preview": "# Multi-Agent Orchestration Patterns\r\n\r\nDetailed patterns for designing multi-agent workflows in Claude Code.\r\n\r\n## Core"
},
{
"path": "plugins/team-assemble/.claude-plugin/plugin.json",
"chars": 471,
"preview": "{\n \"name\": \"team-assemble\",\n \"version\": \"1.0.0\",\n \"description\": \"Dynamically assemble expert agent teams for complex"
},
{
"path": "plugins/team-assemble/README.md",
"chars": 2667,
"preview": "# team-assemble\n\nDynamically assemble expert agent teams for complex tasks using Claude Code's agent teams feature.\n\n## "
},
{
"path": "plugins/team-assemble/skills/team-assemble/SKILL.md",
"chars": 8272,
"preview": "---\nname: team-assemble\ndescription: Analyze tasks and dynamically assemble expert agent teams using Claude Code's TeamC"
},
{
"path": "plugins/team-assemble/skills/team-assemble/references/agents.md",
"chars": 3962,
"preview": "# Agent Example Bank\n\nReference examples for designing agents. Scouts use these as inspiration, not as a fixed catalog.\n"
},
{
"path": "plugins/team-assemble/skills/team-assemble/references/enable-agent-teams.md",
"chars": 2569,
"preview": "# Enable Agent Teams in Claude Code\n\nAgent teams are experimental and disabled by default. You must enable them before u"
},
{
"path": "plugins/team-assemble/skills/team-assemble/references/examples.md",
"chars": 6604,
"preview": "# Team Assemble — Worked Examples\n\nThree examples showing the full workflow for different task types.\n\n---\n\n## Example 1"
},
{
"path": "plugins/team-assemble/skills/team-assemble/references/prompt-templates.md",
"chars": 4806,
"preview": "# Prompt Templates\n\n## 1. Codebase Scout Prompt\n\nTemplate for scout agents in Phase 2.\n\n```\n## Mission\n\nYou are a codeba"
},
{
"path": "plugins/youtube-digest/.claude-plugin/plugin.json",
"chars": 253,
"preview": "{\n \"name\": \"youtube-digest\",\n \"version\": \"0.2.0\",\n \"description\": \"Summarize YouTube videos with transcript, insights"
},
{
"path": "plugins/youtube-digest/README.md",
"chars": 602,
"preview": "# YouTube Digest\n\nYouTube 영상을 분석하여 요약, 인사이트, 한글 번역을 생성하고 퀴즈로 학습 이해도를 테스트하는 플러그인.\n\n## Features\n\n- **Transcript 추출**: yt-d"
},
{
"path": "plugins/youtube-digest/skills/youtube-digest/SKILL.md",
"chars": 2228,
"preview": "---\nname: youtube-digest\ndescription: This skill should be used when the user asks to \"유튜브 정리\", \"영상 요약\", \"transcript 번역\""
},
{
"path": "plugins/youtube-digest/skills/youtube-digest/references/deep-research.md",
"chars": 692,
"preview": "# Deep Research Workflow\n\n퀴즈 완료 후 \"Deep Research\" 선택 시 수행하는 웹 심층 조사.\n\n## 워크플로우\n\n### 1. 병렬 웹 검색 (WebSearch x 3-5)\n\n검색 쿼리 "
},
{
"path": "plugins/youtube-digest/skills/youtube-digest/references/quiz-patterns.md",
"chars": 1015,
"preview": "# Quiz Patterns\n\n3단계 퀴즈 출제 가이드.\n\n## 난이도별 문제 유형\n\n### 1단계 (기본) - 인사이트 이해도\n\n핵심 메시지와 주요 개념 확인:\n- \"이 영상의 핵심 메시지는 무엇인가요?\"\n- \"발"
},
{
"path": "plugins/youtube-digest/skills/youtube-digest/scripts/extract_metadata.sh",
"chars": 188,
"preview": "#!/bin/bash\n# YouTube 메타데이터 추출\n# Usage: ./extract_metadata.sh <URL>\n\nURL=\"$1\"\n\nif [ -z \"$URL\" ]; then\n echo \"Usage: $0 "
},
{
"path": "plugins/youtube-digest/skills/youtube-digest/scripts/extract_transcript.sh",
"chars": 353,
"preview": "#!/bin/bash\n# YouTube 자막 추출\n# Usage: ./extract_transcript.sh <URL> [output_dir]\n\nURL=\"$1\"\nOUTPUT_DIR=\"${2:-.}\"\n\nif [ -z "
}
]
About this extraction
This page contains the full source code of the team-attention/plugins-for-claude-natives GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 147 files (633.9 KB), approximately 173.8k tokens, and a symbol index with 321 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.